read_fonts/tables/postscript/
blend.rs

1//! Support for the dictionary and charstring blend operator.
2
3use font_types::{BigEndian, F2Dot14, Fixed};
4
5use super::Error;
6use crate::tables::variations::{ItemVariationData, ItemVariationStore};
7
8/// The maximum number of region scalars that we precompute.
9///
10/// Completely made up number chosen to balance size with trying to capture as
11/// many precomputed regions as possible.
12///
13/// TODO: measure with a larger set of CFF2 fonts and adjust accordingly.
14const MAX_PRECOMPUTED_SCALARS: usize = 16;
15
16/// State for processing the blend operator for DICTs and charstrings.
17///
18/// To avoid allocation, scalars are computed on demand but this can be
19/// prohibitively expensive in charstrings where blends are applied to
20/// large numbers of elements. To amortize the cost, a fixed number of
21/// precomputed scalars are stored internally and the overflow is computed
22/// as needed.
23///
24/// The `MAX_PRECOMPUTED_SCALARS` constant determines the size of the
25/// internal buffer (currently 16).
26///
27/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#45-variation-data-operators>
28pub struct BlendState<'a> {
29    store: ItemVariationStore<'a>,
30    coords: &'a [F2Dot14],
31    store_index: u16,
32    // The following are dependent on the current `store_index`
33    data: Option<ItemVariationData<'a>>,
34    region_indices: &'a [BigEndian<u16>],
35    scalars: [Fixed; MAX_PRECOMPUTED_SCALARS],
36}
37
38impl<'a> BlendState<'a> {
39    pub fn new(
40        store: ItemVariationStore<'a>,
41        coords: &'a [F2Dot14],
42        store_index: u16,
43    ) -> Result<Self, Error> {
44        let mut state = Self {
45            store,
46            coords,
47            store_index,
48            data: None,
49            region_indices: &[],
50            scalars: Default::default(),
51        };
52        state.update_precomputed_scalars()?;
53        Ok(state)
54    }
55
56    /// Sets the active variation store index.
57    ///
58    /// This should be called with the operand of the `vsindex` operator
59    /// for both DICTs and charstrings.
60    pub fn set_store_index(&mut self, store_index: u16) -> Result<(), Error> {
61        if self.store_index != store_index {
62            self.store_index = store_index;
63            self.update_precomputed_scalars()?;
64        }
65        Ok(())
66    }
67
68    /// Returns the number of variation regions for the currently active
69    /// variation store index.
70    pub fn region_count(&self) -> Result<usize, Error> {
71        Ok(self.region_indices.len())
72    }
73
74    /// Returns an iterator yielding scalars for each variation region of
75    /// the currently active variation store index.
76    pub fn scalars(&self) -> Result<impl Iterator<Item = Result<Fixed, Error>> + '_, Error> {
77        let total_count = self.region_indices.len();
78        let cached = &self.scalars[..MAX_PRECOMPUTED_SCALARS.min(total_count)];
79        let remaining_regions = if total_count > MAX_PRECOMPUTED_SCALARS {
80            &self.region_indices[MAX_PRECOMPUTED_SCALARS..]
81        } else {
82            &[]
83        };
84        Ok(cached.iter().copied().map(Ok).chain(
85            remaining_regions
86                .iter()
87                .map(|region_ix| self.region_scalar(region_ix.get())),
88        ))
89    }
90
91    fn update_precomputed_scalars(&mut self) -> Result<(), Error> {
92        self.data = None;
93        self.region_indices = &[];
94        let store = &self.store;
95        let variation_data = store.item_variation_data();
96        let data = variation_data
97            .get(self.store_index as usize)
98            .ok_or(Error::InvalidVariationStoreIndex(self.store_index))??;
99        let region_indices = data.region_indexes();
100        let regions = self.store.variation_region_list()?.variation_regions();
101        // Precompute scalars for all regions up to MAX_PRECOMPUTED_SCALARS
102        for (region_ix, scalar) in region_indices
103            .iter()
104            .take(MAX_PRECOMPUTED_SCALARS)
105            .zip(&mut self.scalars)
106        {
107            // We can't use region_scalar here because self is already borrowed
108            // as mutable above
109            let region = regions.get(region_ix.get() as usize)?;
110            *scalar = region.compute_scalar(self.coords);
111        }
112        self.data = Some(data);
113        self.region_indices = region_indices;
114        Ok(())
115    }
116
117    fn region_scalar(&self, index: u16) -> Result<Fixed, Error> {
118        Ok(self
119            .store
120            .variation_region_list()?
121            .variation_regions()
122            .get(index as usize)
123            .map_err(Error::Read)?
124            .compute_scalar(self.coords))
125    }
126}
127
128#[cfg(test)]
129mod test {
130    use super::*;
131    use crate::{FontData, FontRead};
132
133    #[test]
134    fn example_blends() {
135        // args are (coords, expected_scalars)
136        example_test(&[-1.0], &[0.0, 1.0]);
137        example_test(&[-0.25], &[0.5, 0.0]);
138        example_test(&[-0.5], &[1.0, 0.0]);
139        example_test(&[-0.75], &[0.5, 0.5]);
140        example_test(&[0.0], &[0.0, 0.0]);
141        example_test(&[0.5], &[0.0, 0.0]);
142        example_test(&[1.0], &[0.0, 0.0]);
143    }
144
145    fn example_test(coords: &[f32], expected: &[f64]) {
146        let scalars = example_scalars_for_coords(coords);
147        let expected: Vec<_> = expected.iter().copied().map(Fixed::from_f64).collect();
148        assert_eq!(scalars, expected);
149    }
150
151    fn example_scalars_for_coords(coords: &[f32]) -> Vec<Fixed> {
152        let ivs = example_ivs();
153        let coords: Vec<_> = coords
154            .iter()
155            .map(|coord| F2Dot14::from_f32(*coord))
156            .collect();
157        let blender = BlendState::new(ivs, &coords, 0).unwrap();
158        blender.scalars().unwrap().map(|res| res.unwrap()).collect()
159    }
160
161    fn example_ivs() -> ItemVariationStore<'static> {
162        // ItemVariationStore is at offset 18 in the CFF example table
163        let ivs_data = &font_test_data::cff2::EXAMPLE[18..];
164        ItemVariationStore::read(FontData::new(ivs_data)).unwrap()
165    }
166}