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