use pdfrum_common::{DiagKind, Diagnostics, Severity};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AxisKind {
Weight,
Width,
Other(Box<str>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct MmAxis {
pub min: f32,
pub default: f32,
pub max: f32,
pub kind: AxisKind,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct DesignMap {
pub knots: Vec<(f32, f32)>,
}
impl DesignMap {
fn map(&self, design: f32) -> f32 {
let Some(&(first_d, first_n)) = self.knots.first() else {
return 0.0;
};
if design <= first_d {
return first_n;
}
for pair in self.knots.windows(2) {
let (Some(&(d0, n0)), Some(&(d1, n1))) = (pair.first(), pair.get(1)) else {
break;
};
if design <= d1 {
let span = d1 - d0;
if span.abs() < f32::EPSILON {
return n1;
}
return n0 + (n1 - n0) * (design - d0) / span;
}
}
self.knots.last().map_or(0.0, |&(_, n)| n)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Blend {
pub axes: Vec<MmAxis>,
pub design_positions: Vec<Vec<f32>>,
pub design_maps: Vec<DesignMap>,
pub default_weights: Vec<f32>,
}
impl Blend {
pub fn new(
axis_kinds: Vec<AxisKind>,
design_positions: Vec<Vec<f32>>,
design_maps: Vec<DesignMap>,
default_weights: Vec<f32>,
diags: &mut Diagnostics,
) -> Option<Self> {
let num_axes = axis_kinds.len();
let num_masters = default_weights.len();
let consistent = num_axes > 0
&& num_masters > 1
&& design_maps.len() == num_axes
&& design_positions.len() == num_masters
&& design_positions.iter().all(|row| row.len() == num_axes)
&& design_maps.iter().all(|m| m.knots.len() >= 2);
if !consistent {
diags.record(Severity::Suspicious, DiagKind::Type1BlendInconsistent, None);
return None;
}
let axes = axis_kinds
.into_iter()
.enumerate()
.map(|(i, kind)| {
let map = design_maps.get(i);
let knots = map.map_or(&[][..], |m| m.knots.as_slice());
let (min, max) = (
knots.first().map_or(0.0, |k| k.0),
knots.last().map_or(1.0, |k| k.0),
);
let normalized_default = default_normalized(&design_positions, &default_weights, i);
MmAxis {
min,
max,
default: map.map_or(normalized_default, |m| unmap(m, normalized_default)),
kind,
}
})
.collect();
Some(Self {
axes,
design_positions,
design_maps,
default_weights,
})
}
pub fn weights_for(&self, coords: &[f32]) -> Vec<f32> {
let normalized: Vec<f32> = self
.axes
.iter()
.enumerate()
.map(|(i, axis)| {
let design = coords
.get(i)
.copied()
.unwrap_or(axis.default)
.clamp(axis.min.min(axis.max), axis.max.max(axis.min));
self.design_maps.get(i).map_or(0.0, |m| m.map(design))
})
.collect();
self.design_positions
.iter()
.map(|corner| {
corner
.iter()
.zip(&normalized)
.map(|(&c, &n)| if c > 0.5 { n } else { 1.0 - n })
.product()
})
.collect()
}
}
fn unmap(map: &DesignMap, target: f32) -> f32 {
let Some(&(first_d, first_n)) = map.knots.first() else {
return 0.0;
};
if target <= first_n {
return first_d;
}
for pair in map.knots.windows(2) {
let (Some(&(d0, n0)), Some(&(d1, n1))) = (pair.first(), pair.get(1)) else {
break;
};
if target <= n1 {
let span = n1 - n0;
if span.abs() < f32::EPSILON {
return d1;
}
return d0 + (d1 - d0) * (target - n0) / span;
}
}
map.knots.last().map_or(0.0, |&(d, _)| d)
}
fn default_normalized(positions: &[Vec<f32>], weights: &[f32], axis: usize) -> f32 {
positions
.iter()
.zip(weights)
.filter(|(corner, _)| corner.get(axis).is_some_and(|&c| c > 0.5))
.map(|(_, &w)| w)
.sum::<f32>()
.clamp(0.0, 1.0)
}
#[cfg(test)]
#[allow(
clippy::indexing_slicing,
clippy::float_cmp,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names
)]
mod tests {
use super::{AxisKind, Blend, DesignMap};
use pdfrum_common::{DiagKind, Diagnostics};
fn foxit_sans() -> Blend {
let mut d = Diagnostics::default();
Blend::new(
vec![AxisKind::Weight, AxisKind::Width],
vec![
vec![0.0, 0.0],
vec![1.0, 0.0],
vec![0.0, 1.0],
vec![1.0, 1.0],
],
vec![
DesignMap {
knots: vec![(50.0, 0.0), (1450.0, 1.0)],
},
DesignMap {
knots: vec![(50.0, 0.0), (1450.0, 1.0)],
},
],
vec![0.3158, 0.1349, 0.3849, 0.1644],
&mut d,
)
.expect("consistent declaration")
}
#[test]
fn corners_select_a_single_master() {
let b = foxit_sans();
assert_eq!(b.weights_for(&[50.0, 50.0]), vec![1.0, 0.0, 0.0, 0.0]);
assert_eq!(b.weights_for(&[1450.0, 50.0]), vec![0.0, 1.0, 0.0, 0.0]);
assert_eq!(b.weights_for(&[50.0, 1450.0]), vec![0.0, 0.0, 1.0, 0.0]);
assert_eq!(b.weights_for(&[1450.0, 1450.0]), vec![0.0, 0.0, 0.0, 1.0]);
}
#[test]
fn weights_always_partition_unity() {
let b = foxit_sans();
for w in [50.0f32, 300.0, 750.0, 1200.0, 1450.0] {
for x in [50.0f32, 400.0, 900.0, 1450.0] {
let sum: f32 = b.weights_for(&[w, x]).iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "w={w} x={x} sum={sum}");
}
}
}
#[test]
fn the_default_coordinates_reproduce_the_shipped_vector() {
let b = foxit_sans();
let defaults: Vec<f32> = b.axes.iter().map(|a| a.default).collect();
for (got, want) in b.weights_for(&defaults).iter().zip(&b.default_weights) {
assert!((got - want).abs() < 1e-4, "{got} vs {want}");
}
}
#[test]
fn axis_ranges_come_from_the_design_map() {
let b = foxit_sans();
assert_eq!(b.axes.len(), 2);
assert_eq!((b.axes[0].min, b.axes[0].max), (50.0, 1450.0));
assert_eq!(b.axes[0].kind, AxisKind::Weight);
assert_eq!(b.axes[1].kind, AxisKind::Width);
}
#[test]
fn out_of_range_coordinates_clamp() {
let b = foxit_sans();
assert_eq!(
b.weights_for(&[-999.0, -999.0]),
b.weights_for(&[50.0, 50.0])
);
assert_eq!(b.weights_for(&[9e9, 9e9]), b.weights_for(&[1450.0, 1450.0]));
}
#[test]
fn a_short_coordinate_list_leaves_the_rest_at_default() {
let b = foxit_sans();
let full = b.weights_for(&[1450.0, b.axes[1].default]);
assert_eq!(b.weights_for(&[1450.0]), full);
}
#[test]
fn inconsistent_declarations_are_refused_with_a_diagnostic() {
let mut d = Diagnostics::default();
assert!(
Blend::new(
vec![AxisKind::Weight, AxisKind::Width],
vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]],
vec![
DesignMap {
knots: vec![(0.0, 0.0), (1.0, 1.0)]
},
DesignMap {
knots: vec![(0.0, 0.0), (1.0, 1.0)]
},
],
vec![0.25, 0.25, 0.25, 0.25],
&mut d,
)
.is_none()
);
assert!(d.contains(&DiagKind::Type1BlendInconsistent));
}
#[test]
fn a_three_knot_design_map_is_piecewise() {
let mut d = Diagnostics::default();
let b = Blend::new(
vec![AxisKind::Weight],
vec![vec![0.0], vec![1.0]],
vec![DesignMap {
knots: vec![(100.0, 0.0), (400.0, 0.75), (900.0, 1.0)],
}],
vec![0.5, 0.5],
&mut d,
)
.expect("consistent");
let w = b.weights_for(&[400.0]);
assert!((w[1] - 0.75).abs() < 1e-6, "{w:?}");
let w = b.weights_for(&[250.0]);
assert!((w[1] - 0.375).abs() < 1e-6, "{w:?}");
}
}