write_fonts/tables/layout/
builders.rs

1//! Common utilities and helpers for constructing layout tables
2
3use std::collections::{BTreeMap, HashMap, HashSet};
4
5use read_fonts::collections::IntSet;
6use types::GlyphId16;
7
8use super::{
9    ClassDef, ClassDefFormat1, ClassDefFormat2, ClassRangeRecord, CoverageFormat1, CoverageFormat2,
10    CoverageTable, Device, DeviceOrVariationIndex, Lookup, LookupFlag, PendingVariationIndex,
11    RangeRecord,
12};
13use crate::tables::{
14    gdef::CaretValue,
15    variations::{ivs_builder::VariationStoreBuilder, VariationRegion},
16};
17
18/// A simple trait for building GPOS/GSUB lookups and subtables.
19///
20// This exists because we use it to implement `LookupBuilder<T>`
21pub trait Builder {
22    /// The type produced by this builder.
23    ///
24    /// In the case of lookups, this is always a `Vec<Subtable>`, because a single
25    /// builder may produce multiple subtables in some instances.
26    type Output;
27    /// Finalize the builder, producing the output.
28    ///
29    /// # Note:
30    ///
31    /// The var_store is only used in GPOS, but we pass it everywhere.
32    /// This is annoying but feels like the lesser of two evils. It's easy to
33    /// ignore this argument where it isn't used, and this makes the logic
34    /// in LookupBuilder simpler, since it is identical for GPOS/GSUB.
35    ///
36    /// It would be nice if this could then be Option<&mut T>, but that type is
37    /// annoying to work with, as Option<&mut _> doesn't impl Copy, so you need
38    /// to do a dance anytime you use it.
39    fn build(self, var_store: &mut VariationStoreBuilder) -> Self::Output;
40}
41
42pub(crate) type FilterSetId = u16;
43
44#[derive(Clone, Debug, Default)]
45pub struct LookupBuilder<T> {
46    pub flags: LookupFlag,
47    pub mark_set: Option<FilterSetId>,
48    pub subtables: Vec<T>,
49}
50
51/// An opinionated builder for `ClassDef`s.
52///
53/// This ensures that class ids are assigned based on the size of the class.
54///
55/// If you need to know the values assigned to particular classes, call the
56/// [`ClassDefBuilder::build_with_mapping`] method, which will build the final
57/// [`ClassDef`] table, and will also return a map from the original class sets
58/// to the final assigned class id values.
59///
60/// If you don't care about this, you can also construct a `ClassDef` from any
61/// iterator over `(GlyphId16, u16)` tuples, using collect:
62///
63/// ```
64/// # use write_fonts::{types::GlyphId16, tables::layout::ClassDef};
65/// let gid1 = GlyphId16::new(1);
66/// let gid2 = GlyphId16::new(2);
67/// let gid3 = GlyphId16::new(2);
68/// let my_class: ClassDef = [(gid1, 2), (gid2, 3), (gid3, 4)].into_iter().collect();
69/// ```
70#[derive(Clone, Debug, Default, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct ClassDefBuilder {
73    classes: HashSet<IntSet<GlyphId16>>,
74    all_glyphs: IntSet<GlyphId16>,
75    use_class_0: bool,
76}
77
78/// A builder for [CoverageTable] tables.
79///
80/// This will choose the best format based for the included glyphs.
81#[derive(Debug, Default, PartialEq, Eq)]
82pub struct CoverageTableBuilder {
83    // invariant: is always sorted
84    glyphs: Vec<GlyphId16>,
85}
86
87/// A value with a default position and optionally variations or a device table.
88///
89/// This is used in the API for types like [`ValueRecordBuilder`] and
90/// [`AnchorBuilder`].
91///
92/// [`ValueRecordBuilder`]: crate::tables::gpos::builders::ValueRecordBuilder
93/// [`AnchorBuilder`]: crate::tables::gpos::builders::AnchorBuilder
94#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct Metric {
97    /// The value at the default location
98    pub default: i16,
99    /// An optional device table or delta set
100    pub device_or_deltas: DeviceOrDeltas,
101}
102
103/// Either a `Device` table or a set of deltas.
104///
105/// This stores deltas directly; during compilation, the deltas are bundled
106/// into some [`ItemVariationStore`], and referenced by a [`VariationIndex`].
107///
108/// [`ItemVariationStore`]: crate::tables::variations::ItemVariationStore
109/// [`VariationIndex`]: super::VariationIndex
110#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112#[allow(missing_docs)]
113pub enum DeviceOrDeltas {
114    Device(Device),
115    Deltas(Vec<(VariationRegion, i16)>),
116    #[default]
117    None,
118}
119
120/// A value in the GDEF ligature caret list
121#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
123pub enum CaretValueBuilder {
124    /// An X or Y value (in design units) with optional deltas
125    Coordinate {
126        /// The value at the default location
127        default: i16,
128        /// An optional device table or delta set
129        deltas: DeviceOrDeltas,
130    },
131    /// The index of a contour point to be used as the caret location.
132    ///
133    /// This format is rarely used.
134    PointIndex(u16),
135}
136
137impl ClassDefBuilder {
138    /// Create a new `ClassDefBuilder`.
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// Create a new `ClassDefBuilder` that will assign glyphs to class 0.
144    ///
145    /// In general, class 0 is a sentinel value returned when a glyph is not
146    /// assigned to any other class; however in some cases (specifically in
147    /// GPOS type two lookups) the `ClassDef` has an accompanying [`CoverageTable`],
148    /// which means that class 0 can be used, since it is known that the class
149    /// is only checked if a glyph is known to have _some_ class.
150    pub fn new_using_class_0() -> Self {
151        Self {
152            use_class_0: true,
153            ..Default::default()
154        }
155    }
156
157    pub(crate) fn can_add(&self, cls: &IntSet<GlyphId16>) -> bool {
158        self.classes.contains(cls) || cls.iter().all(|gid| !self.all_glyphs.contains(gid))
159    }
160
161    /// Check that this class can be added to this classdef, and add it if so.
162    ///
163    /// returns `true` if the class is added, and `false` otherwise.
164    pub fn checked_add(&mut self, cls: IntSet<GlyphId16>) -> bool {
165        if self.can_add(&cls) {
166            self.all_glyphs.extend(cls.iter());
167            self.classes.insert(cls);
168            true
169        } else {
170            false
171        }
172    }
173
174    /// Returns a compiled [`ClassDef`], as well as a mapping from our glyph sets
175    /// to the final class ids.
176    ///
177    /// This sorts the classes, ensuring that larger classes are first.
178    ///
179    /// (This is needed when subsequent structures are ordered based on the
180    /// final order of class assignments.)
181    pub fn build_with_mapping(self) -> (ClassDef, HashMap<IntSet<GlyphId16>, u16>) {
182        let mut classes = self.classes.into_iter().collect::<Vec<_>>();
183        // we match the sort order used by fonttools, see:
184        // <https://github.com/fonttools/fonttools/blob/9a46f9d3ab01e3/Lib/fontTools/otlLib/builder.py#L2677>
185        classes.sort_unstable_by_key(|cls| {
186            (
187                std::cmp::Reverse(cls.len()),
188                cls.iter().next().unwrap_or_default().to_u16(),
189            )
190        });
191        classes.dedup();
192        let add_one = u16::from(!self.use_class_0);
193        let mapping = classes
194            .into_iter()
195            .enumerate()
196            .map(|(i, cls)| (cls, i as u16 + add_one))
197            .collect::<HashMap<_, _>>();
198        let class_def = mapping
199            .iter()
200            .flat_map(|(cls, id)| cls.iter().map(move |gid| (gid, *id)))
201            .collect();
202
203        (class_def, mapping)
204    }
205
206    /// Build a final [`ClassDef`] table.
207    pub fn build(self) -> ClassDef {
208        self.build_with_mapping().0
209    }
210}
211
212/// Builder logic for classdefs.
213///
214/// This handles the actual serialization, picking the best format based on the
215/// included glyphs.
216///
217/// This will choose the best format based for the included glyphs.
218#[derive(Debug, PartialEq, Eq)]
219pub(super) struct ClassDefBuilderImpl {
220    items: BTreeMap<GlyphId16, u16>,
221}
222
223impl ClassDefBuilderImpl {
224    fn prefer_format_1(&self) -> bool {
225        const U16_LEN: usize = std::mem::size_of::<u16>();
226        const FORMAT1_HEADER_LEN: usize = U16_LEN * 3;
227        const FORMAT2_HEADER_LEN: usize = U16_LEN * 2;
228        const CLASS_RANGE_RECORD_LEN: usize = U16_LEN * 3;
229        // format 2 is the most efficient way to represent an empty classdef
230        if self.items.is_empty() {
231            return false;
232        }
233        // calculate our format2 size:
234        let first = self.items.keys().next().map(|g| g.to_u16()).unwrap();
235        let last = self.items.keys().next_back().map(|g| g.to_u16()).unwrap();
236        let format1_array_len = (last - first) as usize + 1;
237        let len_format1 = FORMAT1_HEADER_LEN + format1_array_len * U16_LEN;
238        let len_format2 =
239            FORMAT2_HEADER_LEN + iter_class_ranges(&self.items).count() * CLASS_RANGE_RECORD_LEN;
240
241        len_format1 < len_format2
242    }
243
244    pub fn build(&self) -> ClassDef {
245        if self.prefer_format_1() {
246            let first = self.items.keys().next().map(|g| g.to_u16()).unwrap_or(0);
247            let last = self.items.keys().next_back().map(|g| g.to_u16());
248            let class_value_array = (first..=last.unwrap_or_default())
249                .map(|g| self.items.get(&GlyphId16::new(g)).copied().unwrap_or(0))
250                .collect();
251            ClassDef::Format1(ClassDefFormat1 {
252                start_glyph_id: self
253                    .items
254                    .keys()
255                    .next()
256                    .copied()
257                    .unwrap_or(GlyphId16::NOTDEF),
258                class_value_array,
259            })
260        } else {
261            ClassDef::Format2(ClassDefFormat2 {
262                class_range_records: iter_class_ranges(&self.items).collect(),
263            })
264        }
265    }
266}
267
268impl CoverageTableBuilder {
269    /// Create a new builder from a vec of `GlyphId`.
270    pub fn from_glyphs(mut glyphs: Vec<GlyphId16>) -> Self {
271        glyphs.sort_unstable();
272        glyphs.dedup();
273        CoverageTableBuilder { glyphs }
274    }
275
276    /// Add a `GlyphId` to this coverage table.
277    ///
278    /// Returns the coverage index of the added glyph.
279    ///
280    /// If the glyph already exists, this returns its current index.
281    pub fn add(&mut self, glyph: GlyphId16) -> u16 {
282        match self.glyphs.binary_search(&glyph) {
283            Ok(ix) => ix as u16,
284            Err(ix) => {
285                self.glyphs.insert(ix, glyph);
286                // if we're over u16::MAX glyphs, crash
287                ix.try_into().unwrap()
288            }
289        }
290    }
291
292    //NOTE: it would be nice if we didn't do this intermediate step and instead
293    //wrote out bytes directly, but the current approach is simpler.
294    /// Convert this builder into the appropriate [CoverageTable] variant.
295    pub fn build(self) -> CoverageTable {
296        if should_choose_coverage_format_2(&self.glyphs) {
297            CoverageTable::Format2(CoverageFormat2 {
298                range_records: RangeRecord::iter_for_glyphs(&self.glyphs).collect(),
299            })
300        } else {
301            CoverageTable::Format1(CoverageFormat1 {
302                glyph_array: self.glyphs,
303            })
304        }
305    }
306}
307
308impl<T: Default> LookupBuilder<T> {
309    pub fn new(flags: LookupFlag, mark_set: Option<FilterSetId>) -> Self {
310        LookupBuilder {
311            flags,
312            mark_set,
313            subtables: vec![Default::default()],
314        }
315    }
316
317    pub fn new_with_lookups(
318        flags: LookupFlag,
319        mark_set: Option<FilterSetId>,
320        subtables: Vec<T>,
321    ) -> Self {
322        Self {
323            flags,
324            mark_set,
325            subtables,
326        }
327    }
328
329    //TODO: if we keep this, make it unwrap and ensure we always have a subtable
330    pub fn last_mut(&mut self) -> Option<&mut T> {
331        self.subtables.last_mut()
332    }
333
334    pub fn force_subtable_break(&mut self) {
335        self.subtables.push(Default::default())
336    }
337
338    pub fn iter_subtables(&self) -> impl Iterator<Item = &T> + '_ {
339        self.subtables.iter()
340    }
341}
342
343impl<U> LookupBuilder<U> {
344    /// A helper method for converting from (say) ContextBuilder to PosContextBuilder
345    pub fn convert<T: From<U>>(self) -> LookupBuilder<T> {
346        let LookupBuilder {
347            flags,
348            mark_set,
349            subtables,
350        } = self;
351        LookupBuilder {
352            flags,
353            mark_set,
354            subtables: subtables.into_iter().map(Into::into).collect(),
355        }
356    }
357}
358
359impl<U, T> Builder for LookupBuilder<T>
360where
361    T: Builder<Output = Vec<U>>,
362    U: Default,
363{
364    type Output = Lookup<U>;
365
366    fn build(self, var_store: &mut VariationStoreBuilder) -> Self::Output {
367        let subtables = self
368            .subtables
369            .into_iter()
370            .flat_map(|b| b.build(var_store).into_iter())
371            .collect();
372        let mut out = Lookup::new(self.flags, subtables);
373        out.mark_filtering_set = self.mark_set;
374        out
375    }
376}
377
378impl Metric {
379    /// Returns `true` if the default value is `0` and there is no device or deltas
380    pub fn is_zero(&self) -> bool {
381        self.default == 0 && !self.has_device_or_deltas()
382    }
383
384    /// `true` if this metric has either a device table or deltas
385    pub fn has_device_or_deltas(&self) -> bool {
386        !self.device_or_deltas.is_none()
387    }
388}
389
390impl DeviceOrDeltas {
391    /// Returns `true` if there is no device table or variation index
392    pub fn is_none(&self) -> bool {
393        *self == DeviceOrDeltas::None
394    }
395
396    /// Compile the device or deltas into their final form.
397    ///
398    /// In the case of a device, this generates a [`Device`] table; in the
399    /// case of deltas this adds them to the `VariationStoreBuilder`, and returns
400    /// a [`PendingVariationIndex`] that must be remapped after the builder is
401    /// finished, using the returned [`VariationIndexRemapping`].
402    ///
403    /// [`PendingVariationIndex`]: super::PendingVariationIndex
404    /// [`VariationIndexRemapping`]: crate::tables::variations::ivs_builder::VariationIndexRemapping
405    pub fn build(self, var_store: &mut VariationStoreBuilder) -> Option<DeviceOrVariationIndex> {
406        match self {
407            DeviceOrDeltas::Device(dev) => Some(DeviceOrVariationIndex::Device(dev)),
408            DeviceOrDeltas::Deltas(deltas) => {
409                let temp_id = var_store.add_deltas(deltas);
410                Some(DeviceOrVariationIndex::PendingVariationIndex(
411                    PendingVariationIndex::new(temp_id),
412                ))
413            }
414            DeviceOrDeltas::None => None,
415        }
416    }
417}
418
419impl CaretValueBuilder {
420    /// Build the final [`CaretValue`] table.
421    pub fn build(self, var_store: &mut VariationStoreBuilder) -> CaretValue {
422        match self {
423            Self::Coordinate { default, deltas } => match deltas.build(var_store) {
424                Some(deltas) => CaretValue::format_3(default, deltas),
425                None => CaretValue::format_1(default),
426            },
427            Self::PointIndex(index) => CaretValue::format_2(index),
428        }
429    }
430}
431
432impl From<i16> for Metric {
433    fn from(src: i16) -> Metric {
434        Metric {
435            default: src,
436            device_or_deltas: DeviceOrDeltas::None,
437        }
438    }
439}
440
441impl From<Option<Device>> for DeviceOrDeltas {
442    fn from(src: Option<Device>) -> DeviceOrDeltas {
443        src.map(DeviceOrDeltas::Device).unwrap_or_default()
444    }
445}
446
447impl From<Device> for DeviceOrDeltas {
448    fn from(value: Device) -> Self {
449        DeviceOrDeltas::Device(value)
450    }
451}
452
453impl From<Vec<(VariationRegion, i16)>> for DeviceOrDeltas {
454    fn from(src: Vec<(VariationRegion, i16)>) -> DeviceOrDeltas {
455        if src.is_empty() {
456            DeviceOrDeltas::None
457        } else {
458            DeviceOrDeltas::Deltas(src)
459        }
460    }
461}
462impl FromIterator<(GlyphId16, u16)> for ClassDefBuilderImpl {
463    fn from_iter<T: IntoIterator<Item = (GlyphId16, u16)>>(iter: T) -> Self {
464        Self {
465            items: iter.into_iter().filter(|(_, cls)| *cls != 0).collect(),
466        }
467    }
468}
469
470impl FromIterator<GlyphId16> for CoverageTableBuilder {
471    fn from_iter<T: IntoIterator<Item = GlyphId16>>(iter: T) -> Self {
472        let glyphs = iter.into_iter().collect::<Vec<_>>();
473        CoverageTableBuilder::from_glyphs(glyphs)
474    }
475}
476
477fn iter_class_ranges(
478    values: &BTreeMap<GlyphId16, u16>,
479) -> impl Iterator<Item = ClassRangeRecord> + '_ {
480    let mut iter = values.iter();
481    let mut prev = None;
482
483    #[allow(clippy::while_let_on_iterator)]
484    std::iter::from_fn(move || {
485        while let Some((gid, class)) = iter.next() {
486            match prev.take() {
487                None => prev = Some((*gid, *gid, *class)),
488                Some((start, end, pclass))
489                    if super::are_sequential(end, *gid) && pclass == *class =>
490                {
491                    prev = Some((start, *gid, pclass))
492                }
493                Some((start_glyph_id, end_glyph_id, pclass)) => {
494                    prev = Some((*gid, *gid, *class));
495                    return Some(ClassRangeRecord {
496                        start_glyph_id,
497                        end_glyph_id,
498                        class: pclass,
499                    });
500                }
501            }
502        }
503        prev.take()
504            .map(|(start_glyph_id, end_glyph_id, class)| ClassRangeRecord {
505                start_glyph_id,
506                end_glyph_id,
507                class,
508            })
509    })
510}
511
512fn should_choose_coverage_format_2(glyphs: &[GlyphId16]) -> bool {
513    let format2_len = 4 + RangeRecord::iter_for_glyphs(glyphs).count() * 6;
514    let format1_len = 4 + glyphs.len() * 2;
515    format2_len < format1_len
516}
517
518#[cfg(test)]
519mod tests {
520    use std::ops::RangeInclusive;
521
522    use read_fonts::collections::IntSet;
523
524    use crate::tables::layout::DeltaFormat;
525
526    use super::*;
527
528    #[test]
529    fn classdef_format() {
530        let builder: ClassDefBuilderImpl = [(3u16, 4u16), (4, 6), (5, 1), (9, 5), (10, 2), (11, 3)]
531            .map(|(gid, cls)| (GlyphId16::new(gid), cls))
532            .into_iter()
533            .collect();
534
535        assert!(builder.prefer_format_1());
536
537        let builder: ClassDefBuilderImpl = [(1u16, 1u16), (3, 4), (9, 5), (10, 2), (11, 3)]
538            .map(|(gid, cls)| (GlyphId16::new(gid), cls))
539            .into_iter()
540            .collect();
541
542        assert!(builder.prefer_format_1());
543    }
544
545    #[test]
546    fn classdef_prefer_format2() {
547        fn iter_class_items(
548            start: u16,
549            end: u16,
550            cls: u16,
551        ) -> impl Iterator<Item = (GlyphId16, u16)> {
552            (start..=end).map(move |gid| (GlyphId16::new(gid), cls))
553        }
554
555        // 3 ranges of 4 glyphs at 6 bytes a range should be smaller than writing
556        // out the 3 * 4 classes directly
557        let builder: ClassDefBuilderImpl = iter_class_items(5, 8, 3)
558            .chain(iter_class_items(9, 12, 4))
559            .chain(iter_class_items(13, 16, 5))
560            .collect();
561
562        assert!(!builder.prefer_format_1());
563    }
564
565    #[test]
566    fn delta_format_dflt() {
567        let some: DeltaFormat = Default::default();
568        assert_eq!(some, DeltaFormat::Local2BitDeltas);
569    }
570
571    fn make_glyph_vec<const N: usize>(gids: [u16; N]) -> Vec<GlyphId16> {
572        gids.into_iter().map(GlyphId16::new).collect()
573    }
574
575    #[test]
576    fn coverage_builder() {
577        let coverage = make_glyph_vec([1u16, 2, 9, 3, 6, 9])
578            .into_iter()
579            .collect::<CoverageTableBuilder>();
580        assert_eq!(coverage.glyphs, make_glyph_vec([1, 2, 3, 6, 9]));
581    }
582
583    fn make_class<const N: usize>(gid_class_pairs: [(u16, u16); N]) -> ClassDef {
584        gid_class_pairs
585            .iter()
586            .map(|(gid, cls)| (GlyphId16::new(*gid), *cls))
587            .collect::<ClassDefBuilderImpl>()
588            .build()
589    }
590
591    #[test]
592    fn class_def_builder_zero() {
593        // even if class 0 is provided, we don't need to assign explicit entries for it
594        let class = make_class([(4, 0), (5, 1)]);
595        assert!(class.get_raw(GlyphId16::new(4)).is_none());
596        assert_eq!(class.get_raw(GlyphId16::new(5)), Some(1));
597        assert!(class.get_raw(GlyphId16::new(100)).is_none());
598    }
599
600    // https://github.com/googlefonts/fontations/issues/923
601    // an empty classdef should always be format 2
602    #[test]
603    fn class_def_builder_empty() {
604        let builder = ClassDefBuilderImpl::from_iter([]);
605        let built = builder.build();
606
607        assert_eq!(
608            built,
609            ClassDef::Format2(ClassDefFormat2 {
610                class_range_records: vec![]
611            })
612        )
613    }
614
615    #[test]
616    fn class_def_small() {
617        let class = make_class([(1, 1), (2, 1), (3, 1)]);
618
619        assert_eq!(
620            class,
621            ClassDef::Format2(ClassDefFormat2 {
622                class_range_records: vec![ClassRangeRecord {
623                    start_glyph_id: GlyphId16::new(1),
624                    end_glyph_id: GlyphId16::new(3),
625                    class: 1
626                }]
627            })
628        )
629    }
630
631    #[test]
632    fn classdef_f2_get() {
633        fn make_f2_class<const N: usize>(range: [RangeInclusive<u16>; N]) -> ClassDef {
634            ClassDefFormat2::new(
635                range
636                    .into_iter()
637                    .enumerate()
638                    .map(|(i, range)| {
639                        ClassRangeRecord::new(
640                            GlyphId16::new(*range.start()),
641                            GlyphId16::new(*range.end()),
642                            (1 + i) as _,
643                        )
644                    })
645                    .collect(),
646            )
647            .into()
648        }
649
650        let cls = make_f2_class([1..=1, 2..=9]);
651        assert_eq!(cls.get(GlyphId16::new(2)), 2);
652        assert_eq!(cls.get(GlyphId16::new(20)), 0);
653    }
654
655    fn make_glyph_class<const N: usize>(glyphs: [u16; N]) -> IntSet<GlyphId16> {
656        glyphs.into_iter().map(GlyphId16::new).collect()
657    }
658
659    #[test]
660    fn smoke_test_class_builder() {
661        let mut builder = ClassDefBuilder::new();
662        builder.checked_add(make_glyph_class([6, 10]));
663        let cls = builder.build();
664        assert_eq!(cls.get(GlyphId16::new(6)), 1);
665
666        let mut builder = ClassDefBuilder::new_using_class_0();
667        builder.checked_add(make_glyph_class([6, 10]));
668        let cls = builder.build();
669        assert_eq!(cls.get(GlyphId16::new(6)), 0);
670        assert_eq!(cls.get(GlyphId16::new(10)), 0);
671    }
672
673    #[test]
674    fn classdef_assign_order() {
675        // - longer classes before short ones
676        // - if tied, lowest glyph id first
677
678        let mut builder = ClassDefBuilder::default();
679        builder.checked_add(make_glyph_class([7, 8, 9]));
680        builder.checked_add(make_glyph_class([1, 12]));
681        builder.checked_add(make_glyph_class([3, 4]));
682        let cls = builder.build();
683        assert_eq!(cls.get(GlyphId16::new(9)), 1);
684        assert_eq!(cls.get(GlyphId16::new(1)), 2);
685        assert_eq!(cls.get(GlyphId16::new(4)), 3);
686        // notdef
687        assert_eq!(cls.get(GlyphId16::new(5)), 0);
688    }
689
690    #[test]
691    fn we_handle_dupes() {
692        let mut builder = ClassDefBuilder::default();
693        let c1 = make_glyph_class([1, 2, 3, 4]);
694        let c2 = make_glyph_class([4, 3, 2, 1, 1]);
695        let c3 = make_glyph_class([1, 5, 6, 7]);
696        assert!(builder.checked_add(c1.clone()));
697        assert!(builder.checked_add(c2.clone()));
698        assert!(!builder.checked_add(c3.clone()));
699
700        let (_, map) = builder.build_with_mapping();
701        assert_eq!(map.get(&c1), map.get(&c2));
702        assert!(!map.contains_key(&c3));
703    }
704}