Skip to main content

write_fonts/tables/variations/
ivs_builder.rs

1//! Building the ItemVariationStore
2
3use std::{
4    cmp::Reverse,
5    collections::{BinaryHeap, HashMap, HashSet},
6    fmt::{Debug, Display},
7};
8
9use crate::tables::{
10    layout::VariationIndex,
11    variations::{
12        common_builder::{TemporaryDeltaSetId, VarStoreRemapping},
13        ItemVariationData, ItemVariationStore, VariationRegion, VariationRegionList,
14    },
15};
16use indexmap::IndexMap;
17
18pub type VariationIndexRemapping = VarStoreRemapping<VariationIndex>;
19
20/// A builder for the [ItemVariationStore].
21///
22/// This handles assigning VariationIndex values to unique sets of deltas and
23/// grouping delta sets into [ItemVariationData] subtables.
24#[derive(Clone, Debug)]
25pub struct VariationStoreBuilder {
26    // region -> index map
27    all_regions: HashMap<VariationRegion, usize>,
28    delta_sets: DeltaSetStorage,
29    // must match fvar. We require the user to pass this in because we cannot
30    // infer it in the case where no deltas are added to the builder.
31    axis_count: u16,
32}
33
34/// A collection of delta sets.
35#[derive(Clone, Debug)]
36enum DeltaSetStorage {
37    // only for hvar: we do not deduplicate deltas, and store one per glyph id
38    Direct(Vec<DeltaSet>),
39    // the general case, where each delta gets a unique id
40    Deduplicated(IndexMap<DeltaSet, TemporaryDeltaSetId>),
41}
42
43/// Always sorted, so we can ensure equality
44///
45/// Each tuple is (region index, delta value)
46#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
47struct DeltaSet(Vec<(u16, i32)>);
48
49impl VariationStoreBuilder {
50    /// Create a builder that will optimize delta storage.
51    ///
52    /// This is the general case. For HVAR, it is also possible to use the
53    /// glyph ids as implicit indices, which may be more efficient for some
54    /// data. To use implicit indices, use [`new_with_implicit_indices`] instead.
55    ///
56    /// [`new_with_implicit_indices`]: VariationStoreBuilder::new_with_implicit_indices
57    pub fn new(axis_count: u16) -> Self {
58        Self {
59            axis_count,
60            delta_sets: DeltaSetStorage::Deduplicated(Default::default()),
61            all_regions: Default::default(),
62        }
63    }
64
65    /// Returns `true` if no deltas have been added to this builder
66    pub fn is_empty(&self) -> bool {
67        match &self.delta_sets {
68            DeltaSetStorage::Direct(val) => val.is_empty(),
69            DeltaSetStorage::Deduplicated(val) => val.is_empty(),
70        }
71    }
72
73    /// Create a builder that does not share deltas between entries.
74    ///
75    /// This is used in HVAR, where it is possible to use glyph ids as the
76    /// 'inner index', and to generate a single ItemVariationData subtable
77    /// with one entry per item.
78    pub fn new_with_implicit_indices(axis_count: u16) -> Self {
79        VariationStoreBuilder {
80            axis_count,
81            all_regions: Default::default(),
82            delta_sets: DeltaSetStorage::Direct(Default::default()),
83        }
84    }
85
86    pub fn add_deltas<T: Into<i32>>(
87        &mut self,
88        deltas: Vec<(VariationRegion, T)>,
89    ) -> TemporaryDeltaSetId {
90        let mut delta_set = Vec::with_capacity(deltas.len());
91        for (region, delta) in deltas {
92            let region_idx = self.canonical_index_for_region(region) as u16;
93            delta_set.push((region_idx, delta.into()));
94        }
95        delta_set.sort_unstable();
96        // treat a deltaset containing all zeros the same as an empty one;
97        // e.g. a glyph that only has one instance at the default location (no deltas)
98        // vs another that defines multiple instances but all of them are at the
99        // default location (all deltas are zero).
100        if delta_set.iter().all(|(_, delta)| *delta == 0) {
101            delta_set.clear();
102        }
103        self.delta_sets.add(DeltaSet(delta_set))
104    }
105
106    fn canonical_index_for_region(&mut self, region: VariationRegion) -> usize {
107        let next_idx = self.all_regions.len();
108        *self.all_regions.entry(region).or_insert(next_idx)
109    }
110
111    fn make_region_list(&self, subtables: &mut [Option<ItemVariationData>]) -> VariationRegionList {
112        // collect the set of region indices actually used by each ItemVariationData
113        let used_regions = subtables
114            .iter()
115            .flatten()
116            .flat_map(|var_data| var_data.region_indexes.iter())
117            .map(|idx| *idx as usize)
118            .collect::<HashSet<_>>();
119        // prune unused regions and keep track of old index to new index
120        let mut region_list = self
121            .all_regions
122            .iter()
123            .filter_map(|(reg, idx)| {
124                if used_regions.contains(idx) {
125                    Some((idx, reg.to_owned()))
126                } else {
127                    None
128                }
129            })
130            .collect::<Vec<_>>();
131        region_list.sort_unstable();
132        let mut new_regions = Vec::new();
133        let mut region_map = HashMap::new();
134        for (old_idx, reg) in region_list.into_iter() {
135            region_map.insert(*old_idx as u16, new_regions.len() as u16);
136            new_regions.push(reg);
137        }
138        // remap the region indexes in each subtable
139        for var_data in subtables.iter_mut().flatten() {
140            var_data.region_indexes = var_data
141                .region_indexes
142                .iter()
143                .map(|idx| region_map[idx])
144                .collect();
145        }
146        VariationRegionList::new(self.axis_count, new_regions)
147    }
148
149    fn encoder(&self) -> Encoder<'_> {
150        Encoder::new(&self.delta_sets, self.all_regions.len() as u16)
151    }
152
153    /// Build the `ItemVariationStore` table
154    ///
155    /// This also returns a structure that can be used to remap the temporarily
156    /// assigned delta set Ids to their final `VariationIndex` values.
157    pub fn build(self) -> (ItemVariationStore, VariationIndexRemapping) {
158        let mut key_map = VariationIndexRemapping::default();
159        let mut subtables = if matches!(self.delta_sets, DeltaSetStorage::Direct(_)) {
160            vec![self.build_unoptimized(&mut key_map)]
161        } else {
162            let mut encoder = self.encoder();
163            encoder.optimize();
164            encoder.encode(&mut key_map)
165        };
166        let region_list = self.make_region_list(&mut subtables);
167        (ItemVariationStore::new(region_list, subtables), key_map)
168    }
169
170    /// Build a single ItemVariationData subtable
171    fn build_unoptimized(
172        &self,
173        key_map: &mut VariationIndexRemapping,
174    ) -> Option<ItemVariationData> {
175        // first pick an encoding capable of representing all items:
176        let n_regions = self.all_regions.len() as u16;
177        let mut shape = RowShape(vec![ColumnBits::None; n_regions as usize]);
178        let mut temp = RowShape::default();
179
180        for (delta, _) in self.delta_sets.iter() {
181            temp.reuse(delta, n_regions);
182            if !shape.can_cover(&temp) {
183                shape = shape.merge(&temp);
184            }
185        }
186
187        // then encode everything with that encoding.
188        let encoding = Encoding {
189            shape,
190            deltas: self.delta_sets.iter().collect(),
191        };
192        debug_assert!(
193            encoding.deltas.len() <= u16::MAX as usize,
194            "unmapped variation store supports at most u16::MAX items"
195        );
196        encoding.encode(key_map, 0)
197    }
198}
199
200/// A context for encoding deltas into the final [`ItemVariationStore`].
201///
202/// This mostly exists so that we can write better tests.
203struct Encoder<'a> {
204    encodings: Vec<Encoding<'a>>,
205}
206
207/// A set of deltas that share a shape.
208struct Encoding<'a> {
209    shape: RowShape,
210    deltas: Vec<(&'a DeltaSet, TemporaryDeltaSetId)>,
211}
212
213/// A type for remapping delta sets during encoding.
214///
215/// This mapping applies to a single ItemVariationData table, with the regions
216/// defined in the VariationRegionList in the parent table.
217struct RegionMap {
218    /// A map from the canonical region indices (represented in the sorted
219    /// order of the map) to the ordering of the deltas in a particular
220    /// ItemVariationData table.
221    ///
222    /// For each canonical index, we store the local (column) index and the bits
223    /// required to store that column.
224    regions_to_columns: Vec<(u16, ColumnBits)>,
225    n_active_regions: u16,
226    n_long_regions: u16,
227    long_words: bool,
228}
229
230/// Describes the compressability of a row of deltas across all variation regions.
231///
232/// fonttools calls this the 'characteristic' of a row.
233///
234/// We could get much fancier about how we represent this type, and avoid
235/// allocation in most cases; but this is simple and works, so :shrug:
236#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
237struct RowShape(Vec<ColumnBits>);
238
239//NOTE: we could do fancier bit packing here (fonttools uses four bits per
240//column but I think the gains will be marginal)
241/// The number of bits required to represent a given delta column.
242#[repr(u8)]
243#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
244enum ColumnBits {
245    /// i.e. the value is zero
246    None = 0,
247    /// an i8
248    One = 1,
249    /// an i16
250    Two = 2,
251    /// an i32
252    Four = 4,
253}
254
255impl<'a> Encoder<'a> {
256    fn new(delta_map: &'a DeltaSetStorage, total_regions: u16) -> Self {
257        let mut shape = RowShape::default();
258        let mut encodings: IndexMap<_, Vec<_>> = Default::default();
259
260        for (delta, idx) in delta_map.iter() {
261            shape.reuse(delta, total_regions);
262            match encodings.get_mut(&shape) {
263                Some(items) => items.push((delta, idx)),
264                None => {
265                    encodings.insert(shape.clone(), vec![(delta, idx)]);
266                }
267            }
268        }
269        let encodings = encodings
270            .into_iter()
271            .map(|(shape, deltas)| Encoding { shape, deltas })
272            .collect();
273
274        Encoder { encodings }
275    }
276
277    fn cost(&self) -> usize {
278        self.encodings.iter().map(Encoding::cost).sum()
279    }
280
281    /// Recursively combine encodings where doing so provides space savings.
282    ///
283    /// This is a reimplementation of the [VarStore_optimize][fonttools] function
284    /// in fonttools, although it is not a direct port.
285    ///
286    /// [fonttools]: https://github.com/fonttools/fonttools/blob/fb56e7b7c9715895b81708904c840875008adb9c/Lib/fontTools/varLib/varStore.py#L471
287    fn optimize(&mut self) {
288        let cost = self.cost();
289        log::trace!("optimizing {} encodings, {cost}B", self.encodings.len(),);
290        // a little helper for pretty-printing our todo list
291        struct DebugTodoList<'a>(&'a [Option<Encoding<'a>>]);
292        impl Debug for DebugTodoList<'_> {
293            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294                write!(f, "Todo({} items", self.0.len())?;
295                for (i, enc) in self.0.iter().enumerate() {
296                    if let Some(enc) = enc {
297                        write!(f, "\n    {i:>4}: {enc:?}")?;
298                    }
299                }
300                writeln!(f, ")")
301            }
302        }
303        // temporarily take ownership of all the encodings
304        let mut to_process = std::mem::take(&mut self.encodings);
305        // pre-sort them like fonttools does, for stability
306        to_process.sort_unstable_by(Encoding::ord_matching_fonttools);
307        // convert to a vec of Option<Encoding>;
308        // we will replace items with None as they are combined
309        let mut to_process = to_process.into_iter().map(Option::Some).collect::<Vec<_>>();
310
311        // build up a priority list of the space savings from combining each pair
312        // of encodings
313        let mut queue = BinaryHeap::with_capacity(to_process.len());
314
315        for (i, red) in to_process.iter().enumerate() {
316            for (j, blue) in to_process.iter().enumerate().skip(i + 1) {
317                let gain = red.as_ref().unwrap().compute_gain(blue.as_ref().unwrap());
318                if gain > 0 {
319                    log::trace!("adding ({i}, {j} ({gain})) to queue");
320                    // negate gain to match fonttools and use std::cmp::Reverse to
321                    // mimic Python heapq's "min heap"
322                    queue.push(Reverse((-gain, i, j)));
323                }
324            }
325        }
326
327        // iteratively process each item in the queue
328        while let Some(Reverse((_gain, i, j))) = queue.pop() {
329            if to_process[i].is_none() || to_process[j].is_none() {
330                continue;
331            }
332            // as items are combined, we leave `None` in the to_process list.
333            // This ensures that indices are stable.
334            let (Some(mut to_update), Some(to_add)) = (
335                to_process.get_mut(i).and_then(Option::take),
336                to_process.get_mut(j).and_then(Option::take),
337            ) else {
338                unreachable!("checked above")
339            };
340            log::trace!(
341                "combining {i}+{j} ({}, {} {_gain})",
342                to_update.shape,
343                to_add.shape
344            );
345
346            //NOTE: it is now possible that we have duplicate data. I'm not sure
347            //how likely this is? Not very likely? it would require one deltaset's
348            //regions to be a subset of another, with zeros for the missing axes?
349            to_update.merge_with(to_add);
350            let n = to_process.len(); // index we assign the combined encoding
351            let mut maybe_existing_encoding = None;
352            for (ii, opt_encoding) in to_process.iter_mut().enumerate() {
353                // does two things: skips empty indices, and also temporarily
354                // removes the item (we'll put it back unless we merge, below)
355                let Some(encoding) = opt_encoding.take() else {
356                    continue;
357                };
358
359                if encoding.shape == to_update.shape {
360                    // if an identical encoding exists in the list, we will just
361                    // merge it with the newly created one. We do this after
362                    // calculating the new gains, though, so we aren't changing
363                    // anything mid-stream
364                    maybe_existing_encoding = Some(encoding);
365                    continue;
366                }
367                let gain = to_update.compute_gain(&encoding);
368                if gain > 0 {
369                    log::trace!("adding ({n}, {ii} ({gain})) to queue");
370                    queue.push(Reverse((-gain, ii, n)));
371                }
372                *opt_encoding = Some(encoding);
373            }
374            if let Some(existing) = maybe_existing_encoding.take() {
375                to_update.deltas.extend(existing.deltas);
376            }
377            to_process.push(Some(to_update));
378            log::trace!("{:?}", DebugTodoList(&to_process));
379        }
380        self.encodings = to_process.into_iter().flatten().collect();
381        // now sort the items in each individual encoding
382        self.encodings
383            .iter_mut()
384            .for_each(|enc| enc.deltas.sort_unstable());
385        // and then sort the encodings themselves; order doesn't matter,
386        // but we want to match fonttools output when comparing ttx
387        self.encodings
388            .sort_unstable_by(Encoding::ord_matching_fonttools);
389        log::trace!(
390            "optimized {} encodings, {}B, ({}B saved)",
391            self.encodings.len(),
392            self.cost(),
393            cost.saturating_sub(self.cost()),
394        );
395    }
396
397    /// Encode the `Encoding` sets into [`ItemVariationData`] subtables.
398    ///
399    /// In general, each encoding ends up being one subtable, except:
400    /// - if the encoding is empty, we get a `NULL` subtable (aka None)
401    /// - if an encoding contains more than 0xFFFF rows, it is split into
402    ///   multiple subtables.
403    fn encode(self, key_map: &mut VariationIndexRemapping) -> Vec<Option<ItemVariationData>> {
404        self.encodings
405            .into_iter()
406            .flat_map(Encoding::iter_split_into_table_size_chunks)
407            .enumerate()
408            .map(|(i, encoding)| encoding.encode(key_map, i as u16))
409            .collect()
410    }
411}
412
413impl ColumnBits {
414    fn for_val(val: i32) -> Self {
415        if val == 0 {
416            Self::None
417        } else if i8::try_from(val).is_ok() {
418            Self::One
419        } else if i16::try_from(val).is_ok() {
420            Self::Two
421        } else {
422            Self::Four
423        }
424    }
425
426    /// The number of bytes required to store this column
427    fn cost(self) -> usize {
428        self as u8 as _
429    }
430}
431
432impl RowShape {
433    /// Reuse this types storage for a new delta set.
434    ///
435    /// This might be premature optimization.
436    ///
437    /// The rationale is that many of these are identical, so this saves us
438    /// from constantly allocating and throwing away.
439    fn reuse(&mut self, deltas: &DeltaSet, n_regions: u16) {
440        self.0.clear();
441        self.0.resize(n_regions as _, ColumnBits::None);
442        for (region, delta) in &deltas.0 {
443            self.0[*region as usize] = ColumnBits::for_val(*delta);
444        }
445    }
446
447    /// Returns a shape that can fit both self and other.
448    ///
449    /// In practice this means taking the max of each column.
450    fn merge(&self, other: &Self) -> Self {
451        Self(
452            self.0
453                .iter()
454                .zip(other.0.iter())
455                .map(|(us, them)| *us.max(them))
456                .collect(),
457        )
458    }
459
460    /// `true` if each value in this shape is >= the same value in `other`.
461    fn can_cover(&self, other: &Self) -> bool {
462        debug_assert_eq!(self.0.len(), other.0.len());
463        self.0
464            .iter()
465            .zip(other.0.iter())
466            .all(|(us, them)| us >= them)
467    }
468
469    /// the cost in bytes of a row in this encoding
470    fn row_cost(&self) -> usize {
471        self.0.iter().copied().map(ColumnBits::cost).sum()
472    }
473
474    fn overhead(&self) -> usize {
475        /// the minimum number of bytes in an ItemVariationData table
476        const SUBTABLE_FIXED_COST: usize = 10;
477        const COST_PER_REGION: usize = 2;
478        SUBTABLE_FIXED_COST + (self.n_non_zero_regions() * COST_PER_REGION)
479    }
480
481    fn n_non_zero_regions(&self) -> usize {
482        self.0.iter().map(|x| (*x as u8).min(1) as usize).sum()
483    }
484
485    /// return a tuple for the counts of (1, 2, 3) byte-encoded items in self
486    fn count_lengths(&self) -> (u16, u16, u16) {
487        self.0
488            .iter()
489            .fold((0, 0, 0), |(byte, short, long), this| match this {
490                ColumnBits::One => (byte + 1, short, long),
491                ColumnBits::Two => (byte, short + 1, long),
492                ColumnBits::Four => (byte, short, long + 1),
493                _ => (byte, short, long),
494            })
495    }
496
497    /// Returns a struct that maps the canonical regions to the column indices
498    /// used in this ItemVariationData.
499    fn region_map(&self) -> RegionMap {
500        let mut with_idx = self.0.iter().copied().enumerate().collect::<Vec<_>>();
501        // sort in descending order of bit size, e.g. big first
502        with_idx.sort_unstable_by_key(|(idx, bit)| (std::cmp::Reverse(*bit), *idx));
503        // now build a map of indexes from the original positions to the new ones.
504        let mut map = vec![(0u16, ColumnBits::None); with_idx.len()];
505        for (new_idx, (canonical_idx, bits)) in with_idx.iter().enumerate() {
506            map[*canonical_idx] = (new_idx as _, *bits);
507        }
508
509        let (count_8, count_16, count_32) = self.count_lengths();
510        let long_words = count_32 > 0;
511        let n_long_regions = if long_words { count_32 } else { count_16 };
512        let n_active_regions = count_8 + count_16 + count_32;
513        RegionMap {
514            regions_to_columns: map,
515            n_active_regions,
516            n_long_regions,
517            long_words,
518        }
519    }
520
521    // for verifying our sorting behaviour.
522    // ported from https://github.com/fonttools/fonttools/blob/ec9986d3b863d/Lib/fontTools/varLib/varStore.py#L441
523    #[cfg(test)]
524    fn to_fonttools_repr(&self) -> u128 {
525        assert!(
526            self.0.len() <= u128::BITS as usize / 4,
527            "we can only pack 128 bits"
528        );
529
530        let has_long_word = self.0.iter().any(|bits| *bits == ColumnBits::Four);
531        let mut chars = 0;
532        let mut i = 1;
533
534        if !has_long_word {
535            for v in &self.0 {
536                if *v != ColumnBits::None {
537                    chars += i;
538                }
539                if *v == ColumnBits::Two {
540                    chars += i * 0b0010;
541                }
542                i <<= 4;
543            }
544        } else {
545            for v in &self.0 {
546                if *v != ColumnBits::None {
547                    chars += i * 0b0011;
548                }
549                if *v == ColumnBits::Four {
550                    chars += i * 0b1100;
551                }
552                i <<= 4;
553            }
554        }
555        chars
556    }
557}
558
559impl<'a> Encoding<'a> {
560    fn cost(&self) -> usize {
561        self.shape.overhead() + (self.shape.row_cost() * self.deltas.len())
562    }
563
564    fn compute_gain(&self, other: &Encoding) -> i64 {
565        let current_cost = self.cost() + other.cost();
566
567        let combined = self.shape.merge(&other.shape);
568        let combined_cost =
569            combined.overhead() + (combined.row_cost() * (self.deltas.len() + other.deltas.len()));
570        current_cost as i64 - combined_cost as i64
571    }
572
573    fn merge_with(&mut self, other: Encoding<'a>) {
574        self.shape = self.shape.merge(&other.shape);
575        self.deltas.extend(other.deltas);
576    }
577
578    /// Split this item into chunks that fit in an ItemVariationData subtable.
579    ///
580    /// we can only encode up to u16::MAX items in a single subtable, so if we
581    /// have more items than that we split them off now.
582    fn iter_split_into_table_size_chunks(self) -> impl Iterator<Item = Encoding<'a>> {
583        let mut next = Some(self);
584        std::iter::from_fn(move || {
585            let mut this = next.take()?;
586            next = this.split_off_back();
587            Some(this)
588        })
589    }
590
591    /// If we contain more than the max allowed items, split the extra items off
592    ///
593    /// This ensures `self` can be encoded.
594    fn split_off_back(&mut self) -> Option<Self> {
595        const MAX_ITEMS: usize = 0xFFFF;
596        if self.deltas.len() <= MAX_ITEMS {
597            return None;
598        }
599        let deltas = self.deltas.split_off(MAX_ITEMS);
600        Some(Self {
601            shape: self.shape.clone(),
602            deltas,
603        })
604    }
605
606    fn encode(
607        self,
608        key_map: &mut VariationIndexRemapping,
609        subtable_idx: u16,
610    ) -> Option<ItemVariationData> {
611        log::trace!(
612            "encoding subtable {subtable_idx} ({} rows, {}B)",
613            self.deltas.len(),
614            self.cost()
615        );
616        assert!(self.deltas.len() <= 0xffff, "call split_off_back first");
617        let item_count = self.deltas.len() as u16;
618        if item_count == 0 {
619            //TODO: figure out when a null subtable is useful?
620            return None;
621        }
622
623        let region_map = self.shape.region_map();
624        let n_regions = self.shape.n_non_zero_regions();
625        let total_n_delta_values = self.deltas.len() * n_regions;
626        let mut raw_deltas = vec![0i32; total_n_delta_values];
627
628        // first we generate a vec of i32s, which represents an uncompressed
629        // 2d array where rows are items and columns are per-region values.
630        for (i, (delta, raw_key)) in self.deltas.iter().enumerate() {
631            let pos = i * n_regions;
632            for (region, val) in &delta.0 {
633                let Some(column_idx) = region_map.column_index_for_region(*region) else {
634                    continue;
635                };
636                let idx = pos + column_idx as usize;
637                raw_deltas[idx] = *val;
638            }
639            let final_key = VariationIndex::new(subtable_idx, i as u16);
640            key_map.set(*raw_key, final_key);
641        }
642
643        // then we convert the correctly-ordered i32s into the final compressed
644        // representation.
645        let delta_sets = region_map.encode_raw_delta_values(raw_deltas);
646        let word_delta_count = region_map.word_delta_count();
647        let region_indexes = region_map.indices();
648
649        Some(ItemVariationData::new(
650            item_count,
651            word_delta_count,
652            region_indexes,
653            delta_sets,
654        ))
655    }
656
657    /// match the sorting behaviour that fonttools uses for the final sorting.
658    ///
659    /// fonttools's behaviour is particular, because they store the 'rowshape' as
660    /// a packed bitvec with the least significant bits storing the first item,
661    /// e.g. it's the inverse of our default order. Also we don't want to include
662    /// our temporary ids.
663    fn ord_matching_fonttools(&self, other: &Self) -> std::cmp::Ordering {
664        // first just compare the cost
665        let cost_ord = self.shape.row_cost().cmp(&other.shape.row_cost());
666        if cost_ord != std::cmp::Ordering::Equal {
667            return cost_ord;
668        }
669
670        debug_assert_eq!(
671            self.shape.0.len(),
672            other.shape.0.len(),
673            "all shapes have same # of regions"
674        );
675
676        // if cost is equal, compare each column, in reverse
677        for (a, b) in self.shape.0.iter().rev().zip(other.shape.0.iter().rev()) {
678            match a.cmp(b) {
679                std::cmp::Ordering::Equal => (), // continue
680                not_eq => return not_eq,
681            }
682        }
683        std::cmp::Ordering::Equal
684    }
685}
686
687impl RegionMap {
688    /// Takes the delta data as a vec of i32s, writes a vec of BigEndian bytes.
689    ///
690    /// This is mostly boilerplate around whether we are writing i16 and i8, or
691    /// i32 and i16.
692    ///
693    /// Invariant: the raw deltas are sorted based on the region ordering of this
694    /// RegionMap.
695    fn encode_raw_delta_values(&self, raw_deltas: Vec<i32>) -> Vec<u8> {
696        // handles the branching logic of whether long words are 32 or 16 bits.
697        fn encode_words<'a>(
698            long: &'a [i32],
699            short: &'a [i32],
700            long_words: bool,
701        ) -> impl Iterator<Item = u8> + 'a {
702            // dumb trick: the two branches have different concrete types,
703            // so we need to unify them
704            let left = long_words.then(|| {
705                long.iter()
706                    .flat_map(|x| x.to_be_bytes().into_iter())
707                    .chain(short.iter().flat_map(|x| (*x as i16).to_be_bytes()))
708            });
709            let right = (!long_words).then(|| {
710                long.iter()
711                    .flat_map(|x| (*x as i16).to_be_bytes().into_iter())
712                    .chain(short.iter().flat_map(|x| (*x as i8).to_be_bytes()))
713            });
714
715            // combine the two branches into a single type
716            left.into_iter()
717                .flatten()
718                .chain(right.into_iter().flatten())
719        }
720
721        if self.n_active_regions == 0 {
722            return Default::default();
723        }
724
725        raw_deltas
726            .chunks(self.n_active_regions as usize)
727            .flat_map(|delta_set| {
728                let (long, short) = delta_set.split_at(self.n_long_regions as usize);
729                encode_words(long, short, self.long_words)
730            })
731            .collect()
732    }
733
734    /// Compute the 'wordDeltaCount' field
735    ///
736    /// This is a packed field, with the high bit indicating if we have 2-or-4-bit
737    /// words, and the low 15 bits indicating the number of 'long' types
738    fn word_delta_count(&self) -> u16 {
739        let long_flag = if self.long_words { 0x8000 } else { 0 };
740        self.n_long_regions | long_flag
741    }
742
743    /// For the provided canonical region index, returns the column index used
744    /// in this encoding, or None if the region is ignored.
745    fn column_index_for_region(&self, region: u16) -> Option<u16> {
746        let (column, bits) = self.regions_to_columns[region as usize];
747        (bits != ColumnBits::None).then_some(column)
748    }
749
750    /// the indexes into the canonical region list of the active columns
751    fn indices(&self) -> Vec<u16> {
752        let mut result: Vec<_> = self
753            .regions_to_columns
754            .iter()
755            .enumerate()
756            .filter_map(|(i, (_, bits))| (*bits as u8 > 0).then_some(i as _))
757            .collect();
758        // we need this result to be sorted based on the local order:
759        result.sort_unstable_by_key(|region_idx| {
760            self.regions_to_columns
761                .get(*region_idx as usize)
762                .map(|(column, _)| *column)
763                // this can't fail since we got the indexes from this array
764                // immediately previously, but this probably generates better
765                // code than an unwrap
766                .unwrap_or(u16::MAX)
767        });
768        result
769    }
770}
771
772impl VariationIndexRemapping {
773    /// convert to tuple for easier comparisons in tests
774    #[cfg(test)]
775    fn get_raw(&self, from: TemporaryDeltaSetId) -> Option<(u16, u16)> {
776        self.map
777            .get(&from)
778            .map(|var| (var.delta_set_outer_index, var.delta_set_inner_index))
779    }
780}
781
782impl DeltaSetStorage {
783    fn add(&mut self, delta_set: DeltaSet) -> TemporaryDeltaSetId {
784        match self {
785            DeltaSetStorage::Direct(deltas) => {
786                let next_id = deltas.len() as u32;
787                deltas.push(delta_set);
788                next_id
789            }
790            DeltaSetStorage::Deduplicated(deltas) => {
791                let next_id = deltas.len() as u32;
792                *deltas.entry(delta_set).or_insert(next_id)
793            }
794        }
795    }
796
797    fn iter(&self) -> impl Iterator<Item = (&DeltaSet, TemporaryDeltaSetId)> + '_ {
798        // a dumb trick so that we are returning a single concrete type regardless
799        // of which variant this is (which is required when returning impl Trait)
800        let (a_vec, a_map) = match self {
801            DeltaSetStorage::Direct(deltas) => (Some(deltas), None),
802            DeltaSetStorage::Deduplicated(deltas) => (None, Some(deltas)),
803        };
804        a_vec
805            .into_iter()
806            .flat_map(|x| x.iter().enumerate().map(|(i, val)| (val, i as u32)))
807            .chain(
808                a_map
809                    .into_iter()
810                    .flat_map(|map| map.iter().map(|(val, idx)| (val, *idx))),
811            )
812    }
813}
814
815// a custom impl so that we match the behaviour of fonttools:
816//
817// - fonttools stores this densely, as just a tuple of deltas in region-order.
818// - we store this sparsely, with explicit region indices.
819// - this means that we need to handle the case where we are eliding a delta,
820//   in one deltaset where we have a negative value in the other.
821//   For example:
822//   # fonttools rep (1, 5, -10), (1, 5, 0)
823//   # fontations   [(0, 1), (1, 5), (2, -10), (0, 1), (1, 5)]
824//
825// in this case fonttools will sort the first set before the second, and we would
826// do the opposite.
827impl PartialOrd for DeltaSet {
828    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
829        Some(self.cmp(other))
830    }
831}
832
833impl Ord for DeltaSet {
834    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
835        let max_region_idx = self
836            .0
837            .iter()
838            .chain(other.0.iter())
839            .map(|(idx, _)| *idx)
840            .max()
841            .unwrap_or(0);
842
843        let left = DenseDeltaIter::new(&self.0, max_region_idx);
844        let right = DenseDeltaIter::new(&other.0, max_region_idx);
845
846        for (l, r) in left.zip(right) {
847            match l.cmp(&r) {
848                std::cmp::Ordering::Equal => (),
849                non_eq => return non_eq,
850            }
851        }
852        std::cmp::Ordering::Equal
853    }
854}
855
856// a helper that iterates our sparse deltas, inserting explicit 0s for any missing
857// regions.
858//
859// // this is only used in our partial ord impl
860struct DenseDeltaIter<'a> {
861    total_len: u16,
862    cur_pos: u16,
863    deltas: &'a [(u16, i32)],
864}
865
866impl<'a> DenseDeltaIter<'a> {
867    fn new(deltas: &'a [(u16, i32)], max_idx: u16) -> Self {
868        DenseDeltaIter {
869            total_len: max_idx,
870            deltas,
871            cur_pos: 0,
872        }
873    }
874}
875
876impl Iterator for DenseDeltaIter<'_> {
877    type Item = i32;
878
879    fn next(&mut self) -> Option<Self::Item> {
880        if self.cur_pos > self.total_len {
881            return None;
882        }
883        let result = if self.deltas.first().map(|(idx, _)| *idx) == Some(self.cur_pos) {
884            let result = self.deltas.first().unwrap().1;
885            self.deltas = &self.deltas[1..];
886            result
887        } else {
888            0
889        };
890        self.cur_pos += 1;
891        Some(result)
892    }
893}
894
895impl Default for DeltaSetStorage {
896    fn default() -> Self {
897        Self::Deduplicated(Default::default())
898    }
899}
900
901impl Display for RowShape {
902    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
903        for col in &self.0 {
904            match col {
905                ColumnBits::None => write!(f, "-"),
906                ColumnBits::One => write!(f, "B"),
907                ColumnBits::Two => write!(f, "S"),
908                ColumnBits::Four => write!(f, "L"),
909            }?
910        }
911        Ok(())
912    }
913}
914
915impl Debug for Encoding<'_> {
916    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
917        write!(
918            f,
919            "Encoding({}, {} items {} bytes)",
920            self.shape,
921            self.deltas.len(),
922            self.cost()
923        )
924    }
925}
926
927impl Debug for Encoder<'_> {
928    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
929        f.debug_struct("Encoder")
930            .field("encodings", &self.encodings)
931            .finish_non_exhaustive()
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use crate::tables::variations::RegionAxisCoordinates;
938    use font_types::F2Dot14;
939    use read_fonts::{FontData, FontRead};
940
941    use super::*;
942
943    fn reg_coords(min: f32, default: f32, max: f32) -> RegionAxisCoordinates {
944        RegionAxisCoordinates {
945            start_coord: F2Dot14::from_f32(min),
946            peak_coord: F2Dot14::from_f32(default),
947            end_coord: F2Dot14::from_f32(max),
948        }
949    }
950
951    fn test_regions() -> [VariationRegion; 3] {
952        [
953            VariationRegion::new(vec![reg_coords(0.0, 0.2, 1.0), reg_coords(0.0, 0.0, 1.0)]),
954            VariationRegion::new(vec![reg_coords(0.0, 0.1, 0.3), reg_coords(0.0, 0.1, 0.3)]),
955            VariationRegion::new(vec![reg_coords(0.0, 0.1, 0.5), reg_coords(0.0, 0.1, 0.3)]),
956        ]
957    }
958
959    #[test]
960    #[allow(clippy::redundant_clone)]
961    fn smoke_test() {
962        let [r1, r2, r3] = test_regions();
963
964        let mut builder = VariationStoreBuilder::new(2);
965        builder.add_deltas(vec![(r1.clone(), 512), (r2, 266), (r3.clone(), 1115)]);
966        builder.add_deltas(vec![(r3.clone(), 20)]);
967        builder.add_deltas(vec![(r3.clone(), 21)]);
968        builder.add_deltas(vec![(r3, 22)]);
969
970        // we should have three regions, and two subtables
971        let (store, _) = builder.build();
972        assert_eq!(store.variation_region_list.variation_regions.len(), 3);
973        assert_eq!(store.item_variation_data.len(), 2);
974        assert_eq!(
975            store.item_variation_data[0]
976                .as_ref()
977                .unwrap()
978                .region_indexes,
979            vec![2]
980        );
981        assert_eq!(
982            store.item_variation_data[1]
983                .as_ref()
984                .unwrap()
985                .region_indexes,
986            vec![0, 1, 2]
987        );
988    }
989
990    #[test]
991    fn key_mapping() {
992        let [r1, r2, r3] = test_regions();
993
994        let mut builder = VariationStoreBuilder::new(2);
995        let k1 = builder.add_deltas(vec![(r1.clone(), 5), (r2, 1000), (r3.clone(), 1500)]);
996        let k2 = builder.add_deltas(vec![(r1.clone(), -3), (r3.clone(), 20)]);
997        let k3 = builder.add_deltas(vec![(r1.clone(), -12), (r3.clone(), 7)]);
998        // add enough items so that the optimizer doesn't merge these two encodings
999        let _ = builder.add_deltas(vec![(r1.clone(), -10), (r3.clone(), 7)]);
1000        let _ = builder.add_deltas(vec![(r1.clone(), -9), (r3.clone(), 7)]);
1001        let _ = builder.add_deltas(vec![(r1, -11), (r3, 7)]);
1002
1003        // let encoder = builder.encoder();
1004        // eprintln!("{encoder:?}");
1005        // we should have three regions, and two subtables
1006        let (_, key_lookup) = builder.build();
1007
1008        // first subtable has only one item
1009        // first item gets mapped into second subtable, because of how we sort
1010        assert_eq!(key_lookup.get_raw(k1).unwrap(), (1, 0),);
1011        // next two items are in the same (first) subtable
1012        // inner indexes are based on sort order within the subtable:
1013        assert_eq!(key_lookup.get_raw(k2).unwrap(), (0, 4),); // largest r1 value
1014        assert_eq!(key_lookup.get_raw(k3).unwrap(), (0, 0),); // smallest r1 value
1015
1016        assert_eq!(key_lookup.map.len(), 6);
1017    }
1018
1019    // really just here to check my own understanding of what's going on
1020    #[test]
1021    fn fontools_rowshape_repr() {
1022        use ColumnBits as C;
1023        let shape1 = RowShape(vec![C::None, C::One, C::One, C::Two]);
1024        assert_eq!(shape1.to_fonttools_repr(), 0b0011_0001_0001_0000);
1025        let shape2 = RowShape(vec![C::Two, C::One, C::One, C::None]);
1026        assert_eq!(shape2.to_fonttools_repr(), 0b0000_0001_0001_0011);
1027
1028        assert!(shape1.to_fonttools_repr() > shape2.to_fonttools_repr());
1029    }
1030
1031    #[test]
1032    fn encoding_sort_order() {
1033        let _ = env_logger::builder().is_test(true).try_init();
1034        let [r1, r2, r3] = test_regions();
1035
1036        // make two encodings that have the same total cost, but different shape
1037
1038        let mut builder = VariationStoreBuilder::new(2);
1039        // shape (2, 1, 0)
1040        builder.add_deltas(vec![(r1.clone(), 1000), (r2.clone(), 5)]);
1041        builder.add_deltas(vec![(r1.clone(), 1013), (r2.clone(), 20)]);
1042        builder.add_deltas(vec![(r1.clone(), 1014), (r2.clone(), 21)]);
1043        // shape (0, 2, 1)
1044        builder.add_deltas(vec![(r2.clone(), 1212), (r3.clone(), 7)]);
1045        builder.add_deltas(vec![(r2.clone(), 1213), (r3.clone(), 8)]);
1046        builder.add_deltas(vec![(r2.clone(), 1214), (r3.clone(), 8)]);
1047
1048        //shape (1, 0, 1)
1049        builder.add_deltas(vec![(r1.clone(), 12), (r3.clone(), 7)]);
1050        builder.add_deltas(vec![(r1.clone(), 13), (r3.clone(), 9)]);
1051        builder.add_deltas(vec![(r1.clone(), 14), (r3.clone(), 10)]);
1052        builder.add_deltas(vec![(r1.clone(), 15), (r3.clone(), 11)]);
1053        builder.add_deltas(vec![(r1.clone(), 16), (r3.clone(), 12)]);
1054
1055        let (var_store, key_lookup) = builder.build();
1056        assert_eq!(var_store.item_variation_data.len(), 3);
1057        assert_eq!(key_lookup.map.len(), 11);
1058
1059        // encoding (1, 0, 1) will be sorted first, since it has the lowest cost
1060        assert_eq!(
1061            var_store.item_variation_data[0]
1062                .as_ref()
1063                .unwrap()
1064                .region_indexes,
1065            vec![0, 2]
1066        );
1067
1068        // then encoding with shape (2, 1, 0) since the costs are equal and we
1069        // compare backwards, to match fonttools
1070        assert_eq!(
1071            var_store.item_variation_data[1]
1072                .as_ref()
1073                .unwrap()
1074                .region_indexes,
1075            vec![0, 1]
1076        );
1077    }
1078
1079    #[test]
1080    #[allow(clippy::redundant_clone)]
1081    fn to_binary() {
1082        let [r1, r2, r3] = test_regions();
1083
1084        let mut builder = VariationStoreBuilder::new(2);
1085        builder.add_deltas(vec![(r1.clone(), 512), (r2, 1000), (r3.clone(), 265)]);
1086        builder.add_deltas(vec![(r1.clone(), -3), (r3.clone(), 20)]);
1087        builder.add_deltas(vec![(r1.clone(), -12), (r3.clone(), 7)]);
1088        builder.add_deltas(vec![(r1.clone(), -11), (r3.clone(), 8)]);
1089        builder.add_deltas(vec![(r1.clone(), -10), (r3.clone(), 9)]);
1090        let (table, _) = builder.build();
1091        let bytes = crate::dump_table(&table).unwrap();
1092        let data = FontData::new(&bytes);
1093
1094        let reloaded = read_fonts::tables::variations::ItemVariationStore::read(data).unwrap();
1095
1096        assert_eq!(reloaded.item_variation_data_count(), 2);
1097        let var_data_array = reloaded.item_variation_data();
1098
1099        let var_data = var_data_array.get(0).unwrap().unwrap();
1100        assert_eq!(var_data.region_indexes(), &[0, 2]);
1101        assert_eq!(var_data.item_count(), 4);
1102        assert_eq!(var_data.delta_set(0).collect::<Vec<_>>(), vec![-12, 7]);
1103        assert_eq!(var_data.delta_set(1).collect::<Vec<_>>(), vec![-11, 8]);
1104        assert_eq!(var_data.delta_set(2).collect::<Vec<_>>(), vec![-10, 9]);
1105        assert_eq!(var_data.delta_set(3).collect::<Vec<_>>(), vec![-3, 20]);
1106
1107        let var_data = var_data_array.get(1).unwrap().unwrap();
1108        assert_eq!(var_data.region_indexes(), &[0, 1, 2]);
1109        assert_eq!(var_data.item_count(), 1);
1110        assert_eq!(
1111            var_data.delta_set(0).collect::<Vec<_>>(),
1112            vec![512, 1000, 265]
1113        );
1114    }
1115
1116    #[test]
1117    fn reuse_identical_variation_data() {
1118        let _ = env_logger::builder().is_test(true).try_init();
1119        let [r1, r2, r3] = test_regions();
1120
1121        let mut builder = VariationStoreBuilder::new(2);
1122        let k1 = builder.add_deltas(vec![(r1.clone(), 5), (r2, 10), (r3.clone(), 15)]);
1123        let k2 = builder.add_deltas(vec![(r1.clone(), -12), (r3.clone(), 7)]);
1124        let k3 = builder.add_deltas(vec![(r1.clone(), -12), (r3.clone(), 7)]);
1125        let k4 = builder.add_deltas(vec![(r1, 322), (r3, 532)]);
1126
1127        // we should have three regions, and two subtables
1128        let (_, key_lookup) = builder.build();
1129        assert_eq!(k2, k3);
1130        assert_ne!(k1, k2);
1131        assert_ne!(k1, k4);
1132        assert_eq!(key_lookup.map.len(), 3);
1133    }
1134
1135    /// if we have a single region set, where some deltas are 32-bit, the
1136    /// smaller deltas should get their own subtable IFF we save enough bytes
1137    /// to justify this
1138    #[test]
1139    #[allow(clippy::redundant_clone)]
1140    fn long_deltas_split() {
1141        let [r1, r2, _] = test_regions();
1142        let mut builder = VariationStoreBuilder::new(2);
1143        // short
1144        builder.add_deltas(vec![(r1.clone(), 1), (r2.clone(), 2)]);
1145        builder.add_deltas(vec![(r1.clone(), 3), (r2.clone(), 4)]);
1146        builder.add_deltas(vec![(r1.clone(), 5), (r2.clone(), 6)]);
1147        // long
1148        builder.add_deltas(vec![(r1.clone(), 0xffff + 1), (r2.clone(), 0xffff + 2)]);
1149        let mut encoder = builder.encoder();
1150        assert_eq!(encoder.encodings.len(), 2);
1151        encoder.optimize();
1152        assert_eq!(encoder.encodings.len(), 2);
1153    }
1154
1155    /// combine smaller deltas into larger when there aren't many of them
1156    #[test]
1157    #[allow(clippy::redundant_clone)]
1158    fn long_deltas_combine() {
1159        let [r1, r2, _] = test_regions();
1160        let mut builder = VariationStoreBuilder::new(2);
1161        // short
1162        builder.add_deltas(vec![(r1.clone(), 1), (r2.clone(), 2)]);
1163        builder.add_deltas(vec![(r1.clone(), 3), (r2.clone(), 4)]);
1164        // long
1165        builder.add_deltas(vec![(r1.clone(), 0xffff + 1), (r2.clone(), 0xffff + 2)]);
1166
1167        let mut encoder = builder.encoder();
1168        assert_eq!(encoder.encodings.len(), 2);
1169        assert_eq!(encoder.encodings[0].shape.overhead(), 14); // 10 base, 2 * 2 columns
1170        assert_eq!(encoder.encodings[0].cost(), 14 + 4); // overhead + 2 * 2 bytes/row
1171        assert_eq!(encoder.encodings[1].shape.overhead(), 14);
1172        assert_eq!(encoder.encodings[1].cost(), 14 + 8); // overhead + 1 * 8 bytes/rows
1173        encoder.optimize();
1174        assert_eq!(encoder.encodings.len(), 1);
1175    }
1176
1177    // ensure that we are merging as expected
1178    #[test]
1179    #[allow(clippy::redundant_clone)]
1180    fn combine_many_shapes() {
1181        let _ = env_logger::builder().is_test(true).try_init();
1182        let [r1, r2, r3] = test_regions();
1183        let mut builder = VariationStoreBuilder::new(2);
1184        // orchestrate a failure case:
1185        // - we want to combine
1186        builder.add_deltas(vec![(r1.clone(), 0xffff + 5)]); // (L--)
1187        builder.add_deltas(vec![(r1.clone(), 2)]); // (B--)
1188        builder.add_deltas(vec![(r1.clone(), 300)]); // (S--)
1189        builder.add_deltas(vec![(r2.clone(), 0xffff + 5)]); // (-L-)
1190        builder.add_deltas(vec![(r2.clone(), 2)]); // (-B-)
1191        builder.add_deltas(vec![(r2.clone(), 300)]); // (-S-)
1192        builder.add_deltas(vec![(r3.clone(), 0xffff + 5)]); // (--L)
1193        builder.add_deltas(vec![(r3.clone(), 2)]); // (--B)
1194        builder.add_deltas(vec![(r3.clone(), 300)]); // (--S)
1195        let mut encoder = builder.encoder();
1196        encoder.optimize();
1197        // we compile down to three subtables, each with one column
1198        assert_eq!(encoder.encodings.len(), 3);
1199        assert!(encoder.encodings[0]
1200            .compute_gain(&encoder.encodings[1])
1201            .is_negative());
1202    }
1203
1204    #[test]
1205    #[allow(clippy::redundant_clone)]
1206    fn combine_two_big_fellas() {
1207        let _ = env_logger::builder().is_test(true).try_init();
1208        let [r1, r2, r3] = test_regions();
1209        let mut builder = VariationStoreBuilder::new(2);
1210        // we only combine two of these, since that saves 2 bytes, but adding
1211        // the third is too expensive
1212        builder.add_deltas(vec![(r1.clone(), 0xffff + 5)]); // (L--)
1213        builder.add_deltas(vec![(r2.clone(), 0xffff + 5)]); // (-L-)
1214        builder.add_deltas(vec![(r3.clone(), 0xffff + 5)]); // (--L)
1215
1216        let mut encoder = builder.encoder();
1217        assert_eq!(encoder.encodings[0].cost(), 16);
1218        let merge_cost = 2 // extra column
1219            + 4 // existing encoding gets extra column
1220            + 8; // two columns for new row
1221        assert_eq!(
1222            encoder.encodings[0].compute_gain(&encoder.encodings[1]),
1223            16 - merge_cost
1224        );
1225        encoder.optimize();
1226
1227        // we didn't merge any further because it's too expensive
1228        let next_merge_cost = 2
1229            + 2 * 4 // two existing rows get extra column
1230            + 12; // three columns for new row
1231        assert_eq!(encoder.encodings.len(), 2);
1232        assert_eq!(encoder.encodings[0].cost(), 16);
1233        assert_eq!(
1234            encoder.encodings[0].compute_gain(&encoder.encodings[1]),
1235            16 - next_merge_cost
1236        );
1237    }
1238
1239    /// we had a crash here where we were trying to write zeros when they should
1240    /// be getting ignored.
1241    #[test]
1242    fn ensure_zero_deltas_dont_write() {
1243        let _ = env_logger::builder().is_test(true).try_init();
1244        let [r1, r2, _] = test_regions();
1245        let mut builder = VariationStoreBuilder::new(2);
1246        builder.add_deltas(vec![(r1.clone(), 0), (r2.clone(), 4)]);
1247        let _ = builder.build();
1248    }
1249
1250    // we had another crash here where when *all* deltas were zero we would
1251    // call 'slice.chunks()' with '0', which is not allowed
1252    #[test]
1253    fn ensure_all_zeros_dont_write() {
1254        let _ = env_logger::builder().is_test(true).try_init();
1255        let [r1, r2, _] = test_regions();
1256        let mut builder = VariationStoreBuilder::new(2);
1257        builder.add_deltas(vec![(r1.clone(), 0), (r2.clone(), 0)]);
1258        let _ = builder.build();
1259    }
1260
1261    #[test]
1262    fn vardata_region_indices_order() {
1263        let r0 = VariationRegion::new(vec![reg_coords(0.0, 0.5, 1.0)]);
1264        let r1 = VariationRegion::new(vec![reg_coords(0.5, 1.0, 1.0)]);
1265
1266        let mut builder = VariationStoreBuilder::new(1);
1267        builder.add_deltas(vec![(r0.clone(), 1), (r1.clone(), 2)]);
1268        // 256 won't fit in a u8 thus we expect the deltas for the column corresponding
1269        // to r1 will be packed as u16
1270        builder.add_deltas(vec![(r0.clone(), 3), (r1.clone(), 256)]);
1271
1272        let (store, _varidx_map) = builder.build();
1273
1274        assert_eq!(store.variation_region_list.variation_regions.len(), 2);
1275        assert_eq!(store.item_variation_data.len(), 1);
1276
1277        let var_data = store.item_variation_data[0].as_ref().unwrap();
1278
1279        assert_eq!(var_data.item_count, 2);
1280        assert_eq!(var_data.word_delta_count, 1);
1281        // this should be [1, 0] and not [0, 1] because the regions with wider
1282        // deltas should be packed first.
1283        // var_data.region_indexes is an array of indices into the variation region list
1284        // in the order of the columns of the variation data. So it maps from column index
1285        // to region index, not the other way around.
1286        assert_eq!(var_data.region_indexes, vec![1, 0]);
1287        assert_eq!(
1288            var_data.delta_sets,
1289            // ItemVariationData packs deltas as two-dimensional [u8] array
1290            // with item_count rows and region_index_count columns.
1291            // In this particular case (word_count=1) the first column contains 'words'
1292            // with 2-byte deltas, followed by the second column with 1-byte deltas.
1293            vec![
1294                // item[0]
1295                0, 2, // 2: delta for r1
1296                1, //    1: delta for r0
1297                // item[1]
1298                1, 0, // 256: delta for r1
1299                3, //    3: delta for r0
1300            ],
1301        );
1302    }
1303
1304    #[test]
1305    fn unoptimized_version() {
1306        let r0 = VariationRegion::new(vec![reg_coords(0.0, 0.5, 1.0)]);
1307        let r1 = VariationRegion::new(vec![reg_coords(0.5, 1.0, 1.0)]);
1308
1309        let mut builder = VariationStoreBuilder::new_with_implicit_indices(1);
1310        builder.add_deltas(vec![(r0.clone(), 1), (r1.clone(), 2)]);
1311        // 256 won't fit in a u8 thus we expect the deltas for the column corresponding
1312        // to r1 will be packed as u16
1313        builder.add_deltas(vec![(r0.clone(), 1), (r1.clone(), 2)]);
1314        builder.add_deltas(vec![(r0.clone(), 3), (r1.clone(), 256)]);
1315        builder.add_deltas(vec![(r0.clone(), 3), (r1.clone(), 256)]);
1316
1317        let (ivs, key_map) = builder.build();
1318        // we should get an ivs with one subtable, containing four deltas
1319        assert_eq!(ivs.item_variation_data.len(), 1);
1320
1321        let var_data = ivs.item_variation_data[0].as_ref().unwrap();
1322        assert_eq!(var_data.item_count, 4);
1323        assert_eq!(var_data.region_indexes, vec![1, 0]);
1324
1325        assert_eq!(
1326            var_data.delta_sets,
1327            &[
1328                0x0, 0x2, // item 1, region 2
1329                0x1, // item 1, region 1
1330                0x0, 0x2, // item 2, region 2
1331                0x1, // item 2, region 1
1332                0x1, 0x0, // item 3, region 2
1333                0x3, // item 3, region 1
1334                0x1, 0x0, // item 4, region 2
1335                0x3, // item 4, region 1
1336            ]
1337        );
1338
1339        // assert that keymap entries are identity mapping
1340        assert_eq!(key_map.map.len(), 4);
1341        assert!(key_map
1342            .map
1343            .iter()
1344            .all(|(key, idx)| *key == idx.delta_set_inner_index as u32))
1345    }
1346
1347    #[test]
1348    fn delta_set_ordering() {
1349        let left = DeltaSet(vec![(0, 1), (1, 2), (2, -11)]);
1350        let right = DeltaSet(vec![(0, 1), (1, 2)]);
1351
1352        // although the vec ord impl thinks that the left is 'bigger'
1353        // (it having more items):
1354        assert!(left.0 > right.0);
1355        // our custom impl treats it as smaller, matching fonttools
1356        assert!(left < right);
1357
1358        // but this is only the case because the delta is negative
1359        let left = DeltaSet(vec![(0, 1), (1, 2), (2, 11)]);
1360        let right = DeltaSet(vec![(0, 1), (1, 2)]);
1361
1362        assert!(left > right);
1363        let left = DeltaSet(vec![(0, 1), (1, 2), (2, -11)]);
1364        let right = DeltaSet(vec![(0, 1), (1, 2), (3, 0)]);
1365
1366        assert!(left < right);
1367
1368        // also true in the middle
1369        let left = DeltaSet(vec![(0, 1), (1, -2), (2, -11)]);
1370        let right = DeltaSet(vec![(0, 1), (2, -11)]);
1371        assert!(left < right)
1372    }
1373
1374    #[test]
1375    fn no_duplicate_zero_delta_sets() {
1376        let r0 = VariationRegion::new(vec![reg_coords(0.0, 5.0, 1.0)]);
1377        let r1 = VariationRegion::new(vec![reg_coords(0.5, 1.0, 1.0)]);
1378        let mut builder = VariationStoreBuilder::new(1);
1379        let varidxes = vec![
1380            // first glyph has no variations (e.g. .notdef only defined at default location)
1381            // but we still need to add it to the variation store to reserve an index so
1382            // we add an empty delta set
1383            builder.add_deltas::<i32>(Vec::new()),
1384            builder.add_deltas(vec![(r0.clone(), 50), (r1.clone(), 100)]),
1385            // this glyph has explicit masters that are *all* the same as the default (delta is 0);
1386            // we expect the builder to reuse the same no-op delta set as the first glyph
1387            builder.add_deltas(vec![(r0.clone(), 0), (r1.clone(), 0)]),
1388            // this glyph repeats the same delta set as the second glyph, thus we expect
1389            // the builder to map it to the same delta set index
1390            builder.add_deltas(vec![(r0.clone(), 50), (r1.clone(), 100)]),
1391            // this glyph happens to have one master that's the same as the default (delta is 0);
1392            // nothing special here, we expect a new delta set to be created
1393            builder.add_deltas(vec![(r0.clone(), 0), (r1.clone(), 100)]),
1394        ];
1395        let (store, key_map) = builder.build();
1396
1397        let varidx_map: Vec<u32> = varidxes
1398            .into_iter()
1399            .map(|idx| key_map.get(idx).unwrap().into())
1400            .collect::<Vec<_>>();
1401
1402        assert_eq!(store.variation_region_list.variation_regions.len(), 2);
1403        assert_eq!(store.item_variation_data.len(), 1);
1404
1405        let var_data = store.item_variation_data[0].as_ref().unwrap();
1406
1407        assert_eq!(var_data.item_count, 3);
1408        assert_eq!(var_data.word_delta_count, 0);
1409        assert_eq!(var_data.region_indexes, vec![0, 1]);
1410        assert_eq!(var_data.delta_sets, vec![0, 0, 0, 100, 50, 100],);
1411        // glyph 0 and 2 should map to the same no-op [0, 0] deltaset, while
1412        // glyph 1 and 3 should map to deltaset [50, 100];
1413        // glyph 4 should map to deltaset [0, 100]
1414        assert_eq!(varidx_map, vec![0, 2, 0, 2, 1]);
1415    }
1416
1417    #[test]
1418    fn prune_unused_regions() {
1419        // https://github.com/googlefonts/fontations/issues/733
1420        let r0 = VariationRegion::new(vec![reg_coords(-1.0, -0.5, 0.0)]);
1421        let r1 = VariationRegion::new(vec![reg_coords(-1.0, -1.0, 0.0)]);
1422        let r2 = VariationRegion::new(vec![reg_coords(0.0, 0.5, 1.0)]);
1423        let r3 = VariationRegion::new(vec![reg_coords(0.0, 1.0, 1.0)]);
1424        let mut builder = VariationStoreBuilder::new(1);
1425        builder.add_deltas(vec![
1426            (r0.clone(), 0),
1427            (r1.clone(), 50),
1428            (r2.clone(), 0),
1429            (r3.clone(), 100),
1430        ]);
1431        let (store, _) = builder.build();
1432
1433        // not 4 regions, since only 2 are actually used
1434        assert_eq!(store.variation_region_list.variation_regions.len(), 2);
1435        assert_eq!(store.item_variation_data.len(), 1);
1436
1437        let var_data = store.item_variation_data[0].as_ref().unwrap();
1438        assert_eq!(var_data.item_count, 1);
1439        assert_eq!(var_data.word_delta_count, 0);
1440        assert_eq!(var_data.region_indexes, vec![0, 1]); // not 1, 3
1441        assert_eq!(var_data.delta_sets, vec![50, 100]);
1442    }
1443
1444    #[test]
1445    fn we_match_fonttools_stable_order() {
1446        use rand::{seq::SliceRandom, thread_rng};
1447
1448        let mut builder = VariationStoreBuilder::new(1);
1449        let r1 = VariationRegion::new(vec![reg_coords(-1.0, -1.0, 0.0)]);
1450        let r2 = VariationRegion::new(vec![reg_coords(0.0, 1.0, 1.0)]);
1451
1452        let mut delta_sets = vec![
1453            // 7 delta sets with row shape "BB"
1454            vec![(r1.clone(), 1), (r2.clone(), 2)],
1455            vec![(r1.clone(), 3), (r2.clone(), 4)],
1456            vec![(r1.clone(), 5), (r2.clone(), 6)],
1457            vec![(r1.clone(), 7), (r2.clone(), 8)],
1458            vec![(r1.clone(), 9), (r2.clone(), 10)],
1459            vec![(r1.clone(), 11), (r2.clone(), 12)],
1460            vec![(r1.clone(), 13), (r2.clone(), 14)],
1461            // 4 delta sets with row shape "-S"
1462            vec![(r1.clone(), 0), (r2.clone(), -130)],
1463            vec![(r1.clone(), 0), (r2.clone(), -129)],
1464            vec![(r1.clone(), 0), (r2.clone(), 128)],
1465            vec![(r1.clone(), 0), (r2.clone(), 129)],
1466            // 1 delta set with row shape "-B".
1467            // The gain from merging the following into either one of the previous
1468            // encodings happens to be the same so the order in which the winning pair
1469            // gets popped from the heap (sorted by relative gain) depends on the order
1470            // in which the delta sets were pushed; the sort key that fonttools uses to
1471            // sort the inputs (for stability) is such that the encoding with row shape
1472            // "-B" will be merged with the first encoding with row shape "BB" and not
1473            // with the second one with row shape "-S".
1474            vec![(r1.clone(), 0), (r2.clone(), -1)],
1475        ];
1476
1477        // Add delta sets in random order and test that the algorithm is stable
1478        let mut rng = thread_rng();
1479        delta_sets.shuffle(&mut rng);
1480        for deltas in delta_sets {
1481            builder.add_deltas(deltas);
1482        }
1483
1484        let (store, _) = builder.build();
1485
1486        let bytes = crate::dump_table(&store).unwrap();
1487        let data = FontData::new(&bytes);
1488        let reloaded = read_fonts::tables::variations::ItemVariationStore::read(data).unwrap();
1489
1490        assert_eq!(reloaded.item_variation_data_count(), 2);
1491        let var_data_array = reloaded.item_variation_data();
1492
1493        let var_data = var_data_array.get(0).unwrap().unwrap();
1494        assert_eq!(var_data.region_indexes(), &[0, 1]);
1495        // count must be 8, not 7, because [0, -1] should be in here
1496        assert_eq!(var_data.item_count(), 8);
1497        assert_eq!(var_data.delta_set(0).collect::<Vec<_>>(), vec![0, -1]);
1498        assert_eq!(var_data.delta_set(1).collect::<Vec<_>>(), vec![1, 2]);
1499        assert_eq!(var_data.delta_set(2).collect::<Vec<_>>(), vec![3, 4]);
1500        assert_eq!(var_data.delta_set(3).collect::<Vec<_>>(), vec![5, 6]);
1501        assert_eq!(var_data.delta_set(4).collect::<Vec<_>>(), vec![7, 8]);
1502        assert_eq!(var_data.delta_set(5).collect::<Vec<_>>(), vec![9, 10]);
1503        assert_eq!(var_data.delta_set(6).collect::<Vec<_>>(), vec![11, 12]);
1504        assert_eq!(var_data.delta_set(7).collect::<Vec<_>>(), vec![13, 14]);
1505
1506        let var_data = var_data_array.get(1).unwrap().unwrap();
1507        assert_eq!(var_data.region_indexes(), &[1]);
1508        assert_eq!(var_data.item_count(), 4);
1509        // ... and not in here
1510        assert_eq!(var_data.delta_set(0).collect::<Vec<_>>(), vec![-130]);
1511        assert_eq!(var_data.delta_set(1).collect::<Vec<_>>(), vec![-129]);
1512        assert_eq!(var_data.delta_set(2).collect::<Vec<_>>(), vec![128]);
1513        assert_eq!(var_data.delta_set(3).collect::<Vec<_>>(), vec![129]);
1514    }
1515}