1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//! Support for the dictionary and charstring blend operator.

use font_types::{BigEndian, F2Dot14, Fixed};

use super::Error;
use crate::tables::variations::{ItemVariationData, ItemVariationStore};

/// The maximum number of region scalars that we precompute.
///
/// Completely made up number chosen to balance size with trying to capture as
/// many precomputed regions as possible.
///
/// TODO: measure with a larger set of CFF2 fonts and adjust accordingly.
const MAX_PRECOMPUTED_SCALARS: usize = 16;

/// State for processing the blend operator for DICTs and charstrings.
///
/// To avoid allocation, scalars are computed on demand but this can be
/// prohibitively expensive in charstrings where blends are applied to
/// large numbers of elements. To amortize the cost, a fixed number of
/// precomputed scalars are stored internally and the overflow is computed
/// as needed.
///
/// The `MAX_PRECOMPUTED_SCALARS` constant determines the size of the
/// internal buffer (currently 16).
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2charstr#45-variation-data-operators>
pub struct BlendState<'a> {
    store: ItemVariationStore<'a>,
    coords: &'a [F2Dot14],
    store_index: u16,
    // The following are dependent on the current `store_index`
    data: Option<ItemVariationData<'a>>,
    region_indices: &'a [BigEndian<u16>],
    scalars: [Fixed; MAX_PRECOMPUTED_SCALARS],
}

impl<'a> BlendState<'a> {
    pub fn new(
        store: ItemVariationStore<'a>,
        coords: &'a [F2Dot14],
        store_index: u16,
    ) -> Result<Self, Error> {
        let mut state = Self {
            store,
            coords,
            store_index,
            data: None,
            region_indices: &[],
            scalars: Default::default(),
        };
        state.update_precomputed_scalars()?;
        Ok(state)
    }

    /// Sets the active variation store index.
    ///
    /// This should be called with the operand of the `vsindex` operator
    /// for both DICTs and charstrings.
    pub fn set_store_index(&mut self, store_index: u16) -> Result<(), Error> {
        if self.store_index != store_index {
            self.store_index = store_index;
            self.update_precomputed_scalars()?;
        }
        Ok(())
    }

    /// Returns the number of variation regions for the currently active
    /// variation store index.
    pub fn region_count(&self) -> Result<usize, Error> {
        Ok(self.region_indices.len())
    }

    /// Returns an iterator yielding scalars for each variation region of
    /// the currently active variation store index.
    pub fn scalars(&self) -> Result<impl Iterator<Item = Result<Fixed, Error>> + '_, Error> {
        let total_count = self.region_indices.len();
        let cached = &self.scalars[..MAX_PRECOMPUTED_SCALARS.min(total_count)];
        let remaining_regions = if total_count > MAX_PRECOMPUTED_SCALARS {
            &self.region_indices[MAX_PRECOMPUTED_SCALARS..]
        } else {
            &[]
        };
        Ok(cached.iter().copied().map(Ok).chain(
            remaining_regions
                .iter()
                .map(|region_ix| self.region_scalar(region_ix.get())),
        ))
    }

    fn update_precomputed_scalars(&mut self) -> Result<(), Error> {
        self.data = None;
        self.region_indices = &[];
        let store = &self.store;
        let variation_data = store.item_variation_data();
        let data = variation_data
            .get(self.store_index as usize)
            .ok_or(Error::InvalidVariationStoreIndex(self.store_index))??;
        let region_indices = data.region_indexes();
        let regions = self.store.variation_region_list()?.variation_regions();
        // Precompute scalars for all regions up to MAX_PRECOMPUTED_SCALARS
        for (region_ix, scalar) in region_indices
            .iter()
            .take(MAX_PRECOMPUTED_SCALARS)
            .zip(&mut self.scalars)
        {
            // We can't use region_scalar here because self is already borrowed
            // as mutable above
            let region = regions.get(region_ix.get() as usize)?;
            *scalar = region.compute_scalar(self.coords);
        }
        self.data = Some(data);
        self.region_indices = region_indices;
        Ok(())
    }

    fn region_scalar(&self, index: u16) -> Result<Fixed, Error> {
        Ok(self
            .store
            .variation_region_list()?
            .variation_regions()
            .get(index as usize)
            .map_err(Error::Read)?
            .compute_scalar(self.coords))
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::{FontData, FontRead};

    #[test]
    fn example_blends() {
        // args are (coords, expected_scalars)
        example_test(&[-1.0], &[0.0, 1.0]);
        example_test(&[-0.25], &[0.5, 0.0]);
        example_test(&[-0.5], &[1.0, 0.0]);
        example_test(&[-0.75], &[0.5, 0.5]);
        example_test(&[0.0], &[0.0, 0.0]);
        example_test(&[0.5], &[0.0, 0.0]);
        example_test(&[1.0], &[0.0, 0.0]);
    }

    fn example_test(coords: &[f32], expected: &[f64]) {
        let scalars = example_scalars_for_coords(coords);
        let expected: Vec<_> = expected.iter().copied().map(Fixed::from_f64).collect();
        assert_eq!(scalars, expected);
    }

    fn example_scalars_for_coords(coords: &[f32]) -> Vec<Fixed> {
        let ivs = example_ivs();
        let coords: Vec<_> = coords
            .iter()
            .map(|coord| F2Dot14::from_f32(*coord))
            .collect();
        let blender = BlendState::new(ivs, &coords, 0).unwrap();
        blender.scalars().unwrap().map(|res| res.unwrap()).collect()
    }

    fn example_ivs() -> ItemVariationStore<'static> {
        // ItemVariationStore is at offset 18 in the CFF example table
        let ivs_data = &font_test_data::cff2::EXAMPLE[18..];
        ItemVariationStore::read(FontData::new(ivs_data)).unwrap()
    }
}