Skip to main content

write_fonts/tables/
layout.rs

1//! OpenType layout.
2
3use std::{collections::HashSet, hash::Hash};
4
5pub use read_fonts::tables::layout::LookupFlag;
6use read_fonts::FontRead;
7
8pub mod builders;
9#[cfg(test)]
10mod extended;
11#[cfg(test)]
12mod lookup_variations;
13#[cfg(test)]
14mod spec_tests;
15
16include!("../../generated/generated_layout.rs");
17
18impl FeatureVariations {
19    fn compute_version(&self) -> MajorMinor {
20        if self.lookup_variation_records.is_some() {
21            MajorMinor::VERSION_1_1
22        } else {
23            MajorMinor::VERSION_1_0
24        }
25    }
26
27    fn validate_lookup_variations(&self, ctx: &mut ValidationCtx) {
28        if let Some(records) = &self.lookup_variation_records {
29            if records
30                .windows(2)
31                .any(|pair| pair[0].feature_index >= pair[1].feature_index)
32            {
33                ctx.report("lookup variation records must have increasing, unique feature indices");
34            }
35        }
36    }
37}
38
39/// A macro to implement the [LookupSubtable] trait.
40macro_rules! lookup_type {
41    (gpos, $ty:ty, $val:expr) => {
42        impl LookupSubtable for $ty {
43            const TYPE: LookupType = LookupType::Gpos($val);
44        }
45    };
46
47    (gsub, $ty:ty, $val:expr) => {
48        impl LookupSubtable for $ty {
49            const TYPE: LookupType = LookupType::Gsub($val);
50        }
51    };
52}
53
54/// Converts a lookup of an original subtable format into the lookup enum,
55/// wrapping each subtable in the enum of all its formats.
56macro_rules! original_lookup {
57    ($group:ident, $variant:ident, $old:ty, $new:ty) => {
58        impl From<Lookup<$old>> for $group {
59            fn from(lookup: Lookup<$old>) -> Self {
60                Self::$variant(Lookup {
61                    lookup_flag: lookup.lookup_flag,
62                    subtables: lookup
63                        .subtables
64                        .into_iter()
65                        .map(|subtable| OffsetMarker::new(<$new>::from(subtable.into_inner())))
66                        .collect(),
67                    mark_filtering_set: lookup.mark_filtering_set,
68                })
69            }
70        }
71    };
72}
73
74pub(crate) use original_lookup;
75
76/// A macro to define a newtype around an existing table, that defers all
77/// impls to that table.
78///
79/// We use this to ensure that shared lookup types (Sequence/Chain
80/// lookups) can be given different lookup ids for each of GSUB/GPOS.
81macro_rules! table_newtype {
82    ($name:ident, $inner:ident, $read_type:path) => {
83        /// A typed wrapper around a shared table.
84        ///
85        /// This is used so that we can associate the correct lookup ids for
86        /// lookups that are shared between GPOS/GSUB.
87        ///
88        /// You can access the inner type via `Deref` or the `as_inner` method.
89        #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
90        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91        pub struct $name($inner);
92
93        impl $name {
94            /// Return a reference to the inner type.
95            pub fn as_inner(&self) -> &$inner {
96                &self.0
97            }
98        }
99
100        impl std::ops::Deref for $name {
101            type Target = $inner;
102            fn deref(&self) -> &Self::Target {
103                &self.0
104            }
105        }
106
107        impl std::ops::DerefMut for $name {
108            fn deref_mut(&mut self) -> &mut Self::Target {
109                &mut self.0
110            }
111        }
112
113        impl FontWrite for $name {
114            fn write_into(&self, writer: &mut TableWriter) {
115                self.0.write_into(writer)
116            }
117
118            fn table_type(&self) -> crate::table_type::TableType {
119                self.0.table_type()
120            }
121        }
122
123        impl Validate for $name {
124            fn validate_impl(&self, ctx: &mut ValidationCtx) {
125                self.0.validate_impl(ctx)
126            }
127        }
128
129        impl<'a> FromObjRef<$read_type> for $name {
130            fn from_obj_ref(obj: &$read_type, _data: FontData) -> Self {
131                Self(FromObjRef::from_obj_ref(obj, _data))
132            }
133        }
134
135        impl<'a> FromTableRef<$read_type> for $name {}
136
137        impl From<$inner> for $name {
138            fn from(src: $inner) -> $name {
139                $name(src)
140            }
141        }
142    };
143}
144
145pub(crate) use lookup_type;
146pub(crate) use table_newtype;
147
148impl FontWrite for LookupFlag {
149    fn write_into(&self, writer: &mut TableWriter) {
150        self.to_bits().write_into(writer)
151    }
152}
153
154impl<T: LookupSubtable + FontWrite> FontWrite for Lookup<T> {
155    fn write_into(&self, writer: &mut TableWriter) {
156        T::TYPE.write_into(writer);
157        self.lookup_flag.write_into(writer);
158        u16::try_from(self.subtables.len())
159            .unwrap()
160            .write_into(writer);
161        self.subtables.write_into(writer);
162        self.mark_filtering_set.write_into(writer);
163    }
164
165    fn table_type(&self) -> crate::table_type::TableType {
166        T::TYPE.into()
167    }
168}
169
170impl Lookup<SequenceContext> {
171    /// Convert this untyped SequenceContext into its GSUB or GPOS specific version
172    pub fn into_concrete<T: From<SequenceContext>>(self) -> Lookup<T> {
173        let Lookup {
174            lookup_flag,
175            subtables,
176            mark_filtering_set,
177        } = self;
178        let subtables = subtables
179            .into_iter()
180            .map(|offset| OffsetMarker::new(offset.into_inner().into()))
181            .collect();
182        Lookup {
183            lookup_flag,
184            subtables,
185            mark_filtering_set,
186        }
187    }
188}
189
190impl Lookup<ChainedSequenceContext> {
191    /// Convert this untyped SequenceContext into its GSUB or GPOS specific version
192    pub fn into_concrete<T: From<ChainedSequenceContext>>(self) -> Lookup<T> {
193        let Lookup {
194            lookup_flag,
195            subtables,
196            mark_filtering_set,
197        } = self;
198        let subtables = subtables
199            .into_iter()
200            .map(|offset| OffsetMarker::new(offset.into_inner().into()))
201            .collect();
202        Lookup {
203            lookup_flag,
204            subtables,
205            mark_filtering_set,
206        }
207    }
208}
209
210/// A utility trait for writing lookup tables.
211///
212/// This allows us to attach the numerical lookup type to the appropriate concrete
213/// types, so that we can write it as needed without passing it around.
214pub trait LookupSubtable {
215    /// The lookup type of this layout subtable.
216    const TYPE: LookupType;
217}
218
219/// Raw values for the different layout subtables
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum LookupType {
222    Gpos(u16),
223    Gsub(u16),
224}
225
226impl LookupType {
227    pub(crate) const GSUB_EXT_TYPE: u16 = 7;
228    pub(crate) const GPOS_EXT_TYPE: u16 = 9;
229    pub(crate) const PAIR_POS: u16 = 2;
230    pub(crate) const MARK_TO_BASE: u16 = 4;
231
232    pub(crate) fn to_raw(self) -> u16 {
233        match self {
234            LookupType::Gpos(val) => val,
235            LookupType::Gsub(val) => val,
236        }
237    }
238
239    pub(crate) fn promote(self) -> Self {
240        match self {
241            LookupType::Gpos(Self::GPOS_EXT_TYPE) | LookupType::Gsub(Self::GSUB_EXT_TYPE) => {
242                panic!("should never be promoting an extension subtable")
243            }
244            LookupType::Gpos(_) => LookupType::Gpos(Self::GPOS_EXT_TYPE),
245            LookupType::Gsub(_) => LookupType::Gsub(Self::GSUB_EXT_TYPE),
246        }
247    }
248}
249
250impl FontWrite for LookupType {
251    fn write_into(&self, writer: &mut TableWriter) {
252        self.to_raw().write_into(writer)
253    }
254}
255
256#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub enum FeatureParams {
259    StylisticSet(StylisticSetParams),
260    Size(SizeParams),
261    CharacterVariant(CharacterVariantParams),
262}
263
264impl FontWrite for FeatureParams {
265    fn write_into(&self, writer: &mut TableWriter) {
266        match self {
267            FeatureParams::StylisticSet(table) => table.write_into(writer),
268            FeatureParams::Size(table) => table.write_into(writer),
269            FeatureParams::CharacterVariant(table) => table.write_into(writer),
270        }
271    }
272}
273
274impl Validate for FeatureParams {
275    fn validate_impl(&self, ctx: &mut ValidationCtx) {
276        match self {
277            Self::StylisticSet(table) => table.validate_impl(ctx),
278            Self::Size(table) => table.validate_impl(ctx),
279            Self::CharacterVariant(table) => table.validate_impl(ctx),
280        }
281    }
282}
283
284impl FromObjRef<read_fonts::tables::layout::FeatureParams<'_>> for FeatureParams {
285    fn from_obj_ref(from: &read_fonts::tables::layout::FeatureParams, data: FontData) -> Self {
286        use read_fonts::tables::layout::FeatureParams as FromType;
287        match from {
288            FromType::Size(thing) => Self::Size(SizeParams::from_obj_ref(thing, data)),
289            FromType::StylisticSet(thing) => {
290                Self::StylisticSet(FromObjRef::from_obj_ref(thing, data))
291            }
292            FromType::CharacterVariant(thing) => {
293                Self::CharacterVariant(FromObjRef::from_obj_ref(thing, data))
294            }
295        }
296    }
297}
298
299impl FromTableRef<read_fonts::tables::layout::FeatureParams<'_>> for FeatureParams {}
300
301impl ClassDefFormat1 {
302    fn iter(&self) -> impl Iterator<Item = (GlyphId16, u16)> + '_ {
303        self.class_value_array.iter().enumerate().map(|(i, cls)| {
304            (
305                GlyphId16::new(self.start_glyph_id.to_u16().saturating_add(i as u16)),
306                *cls,
307            )
308        })
309    }
310}
311
312impl ClassRangeRecord {
313    fn validate_glyph_range(&self, ctx: &mut ValidationCtx) {
314        if self.start_glyph_id > self.end_glyph_id {
315            ctx.report(format!(
316                "start_glyph_id {} larger than end_glyph_id {}",
317                self.start_glyph_id, self.end_glyph_id
318            ));
319        }
320    }
321}
322
323impl ClassDefFormat2 {
324    fn iter(&self) -> impl Iterator<Item = (GlyphId16, u16)> + '_ {
325        self.class_range_records.iter().flat_map(|rcd| {
326            (rcd.start_glyph_id.to_u16()..=rcd.end_glyph_id.to_u16())
327                .map(|gid| (GlyphId16::new(gid), rcd.class))
328        })
329    }
330}
331
332impl ClassDef {
333    pub fn iter(&self) -> impl Iterator<Item = (GlyphId, u32)> + '_ {
334        let (one, two, three, four) = match self {
335            Self::Format1(t) => (
336                Some(t.iter().map(|(g, c)| (g.into(), u32::from(c)))),
337                None,
338                None,
339                None,
340            ),
341            Self::Format2(t) => (
342                None,
343                Some(t.iter().map(|(g, c)| (g.into(), u32::from(c)))),
344                None,
345                None,
346            ),
347            Self::Format3(t) => (
348                None,
349                None,
350                Some(
351                    t.class_value_array
352                        .iter()
353                        .enumerate()
354                        .filter_map(move |(i, c)| {
355                            let g = t.start_glyph_id.to_u32().checked_add(i as u32)?;
356                            (g <= Uint24::MAX.to_u32()).then_some((GlyphId::new(g), c.to_u32()))
357                        }),
358                ),
359                None,
360            ),
361            Self::Format4(t) => (
362                None,
363                None,
364                None,
365                Some(t.class_range_records.iter().flat_map(|r| {
366                    (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32())
367                        .map(move |g| (GlyphId::new(g), u32::from(r.class)))
368                })),
369            ),
370        };
371        one.into_iter()
372            .flatten()
373            .chain(two.into_iter().flatten())
374            .chain(three.into_iter().flatten())
375            .chain(four.into_iter().flatten())
376    }
377
378    /// Returns the glyph's class, or zero if it has not been assigned one.
379    pub fn get(&self, glyph: impl Into<GlyphId>) -> u32 {
380        self.get_raw(glyph.into()).unwrap_or(0)
381    }
382
383    fn get_raw(&self, glyph: impl Into<GlyphId>) -> Option<u32> {
384        let glyph = glyph.into().to_u32();
385        match self {
386            Self::Format1(t) => glyph
387                .checked_sub(t.start_glyph_id.to_u32())
388                .and_then(|i| t.class_value_array.get(i as usize))
389                .map(|c| u32::from(*c)),
390            Self::Format2(t) => t
391                .class_range_records
392                .iter()
393                .find(|r| (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32()).contains(&glyph))
394                .map(|r| u32::from(r.class)),
395            Self::Format3(t) => glyph
396                .checked_sub(t.start_glyph_id.to_u32())
397                .filter(|_| glyph <= Uint24::MAX.to_u32())
398                .and_then(|i| t.class_value_array.get(i as usize))
399                .map(|c| c.to_u32()),
400            Self::Format4(t) => t
401                .class_range_records
402                .iter()
403                .find(|r| (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32()).contains(&glyph))
404                .map(|r| u32::from(r.class)),
405        }
406    }
407
408    pub fn class_count(&self) -> u32 {
409        self.iter()
410            .map(|(_, c)| c)
411            .chain(std::iter::once(0))
412            .collect::<HashSet<_>>()
413            .len() as u32
414    }
415
416    /// Returns whether the table explicitly assigns no glyphs.
417    pub fn is_empty(&self) -> bool {
418        match self {
419            Self::Format1(t) => t.class_value_array.is_empty(),
420            Self::Format2(t) => t.class_range_records.is_empty(),
421            Self::Format3(t) => t.class_value_array.is_empty(),
422            Self::Format4(t) => t.class_range_records.is_empty(),
423        }
424    }
425}
426
427impl CoverageFormat1 {
428    fn iter(&self) -> impl Iterator<Item = GlyphId16> + '_ {
429        self.glyph_array.iter().copied()
430    }
431
432    fn len(&self) -> usize {
433        self.glyph_array.len()
434    }
435}
436
437impl CoverageFormat2 {
438    fn iter(&self) -> impl Iterator<Item = GlyphId16> + '_ {
439        self.range_records
440            .iter()
441            .flat_map(|rcd| iter_gids(rcd.start_glyph_id, rcd.end_glyph_id))
442    }
443
444    fn len(&self) -> usize {
445        self.range_records
446            .iter()
447            .map(|rcd| {
448                rcd.end_glyph_id
449                    .to_u16()
450                    .saturating_sub(rcd.start_glyph_id.to_u16()) as usize
451                    + 1
452            })
453            .sum()
454    }
455}
456
457impl CoverageTable {
458    pub fn iter(&self) -> impl Iterator<Item = GlyphId> + '_ {
459        let (one, two, three, four) = match self {
460            Self::Format1(t) => (Some(t.iter().map(GlyphId::from)), None, None, None),
461            Self::Format2(t) => (None, Some(t.iter().map(GlyphId::from)), None, None),
462            Self::Format3(t) => (
463                None,
464                None,
465                Some(t.glyph_array.iter().copied().map(GlyphId::from)),
466                None,
467            ),
468            Self::Format4(t) => (
469                None,
470                None,
471                None,
472                Some(t.range_records.iter().flat_map(|r| {
473                    (r.start_glyph_id.to_u32()..=r.end_glyph_id.to_u32()).map(GlyphId::new)
474                })),
475            ),
476        };
477        one.into_iter()
478            .flatten()
479            .chain(two.into_iter().flatten())
480            .chain(three.into_iter().flatten())
481            .chain(four.into_iter().flatten())
482    }
483
484    pub fn len(&self) -> usize {
485        match self {
486            Self::Format1(t) => t.len(),
487            Self::Format2(t) => t.len(),
488            Self::Format3(t) => t.glyph_array.len(),
489            Self::Format4(t) => t
490                .range_records
491                .iter()
492                .map(|r| {
493                    r.end_glyph_id
494                        .to_u32()
495                        .checked_sub(r.start_glyph_id.to_u32())
496                        .map_or(0, |n| n as usize + 1)
497                })
498                .sum(),
499        }
500    }
501
502    pub fn is_empty(&self) -> bool {
503        self.len() == 0
504    }
505}
506
507impl FromIterator<GlyphId16> for CoverageTable {
508    fn from_iter<T: IntoIterator<Item = GlyphId16>>(iter: T) -> Self {
509        let glyphs = iter.into_iter().collect::<Vec<_>>();
510        builders::CoverageTableBuilder::from_glyphs(glyphs).build()
511    }
512}
513
514impl From<Vec<GlyphId16>> for CoverageTable {
515    fn from(value: Vec<GlyphId16>) -> Self {
516        builders::CoverageTableBuilder::from_glyphs(value).build()
517    }
518}
519
520impl FromIterator<(GlyphId16, u16)> for ClassDef {
521    fn from_iter<T: IntoIterator<Item = (GlyphId16, u16)>>(iter: T) -> Self {
522        builders::ClassDefBuilderImpl::from_iter(iter).build()
523    }
524}
525
526impl RangeRecord {
527    /// An iterator over records for this array of glyphs.
528    ///
529    /// # Note
530    ///
531    /// this function expects that glyphs are already sorted.
532    pub fn iter_for_glyphs(glyphs: &[GlyphId16]) -> impl Iterator<Item = RangeRecord> + '_ {
533        let mut cur_range = glyphs.first().copied().map(|g| (g, g));
534        let mut len = 0u16;
535        let mut iter = glyphs.iter().skip(1).copied();
536
537        #[allow(clippy::while_let_on_iterator)]
538        std::iter::from_fn(move || {
539            while let Some(glyph) = iter.next() {
540                match cur_range {
541                    None => return None,
542                    Some((a, b)) if are_sequential(b, glyph) => cur_range = Some((a, glyph)),
543                    Some((a, b)) => {
544                        let result = RangeRecord {
545                            start_glyph_id: a,
546                            end_glyph_id: b,
547                            start_coverage_index: len,
548                        };
549                        cur_range = Some((glyph, glyph));
550                        len += 1 + b.to_u16().saturating_sub(a.to_u16());
551                        return Some(result);
552                    }
553                }
554            }
555            cur_range
556                .take()
557                .map(|(start_glyph_id, end_glyph_id)| RangeRecord {
558                    start_glyph_id,
559                    end_glyph_id,
560                    start_coverage_index: len,
561                })
562        })
563    }
564}
565
566fn iter_gids(gid1: GlyphId16, gid2: GlyphId16) -> impl Iterator<Item = GlyphId16> {
567    (gid1.to_u16()..=gid2.to_u16()).map(GlyphId16::new)
568}
569
570fn are_sequential(gid1: GlyphId16, gid2: GlyphId16) -> bool {
571    gid2.to_u16().saturating_sub(gid1.to_u16()) == 1
572}
573
574impl Device {
575    pub fn new(start_size: u16, end_size: u16, values: &[i8]) -> Self {
576        debug_assert_eq!(
577            (start_size..=end_size).count(),
578            values.len(),
579            "device range and values must match"
580        );
581        let delta_format: DeltaFormat = values
582            .iter()
583            .map(|val| match val {
584                -2..=1 => DeltaFormat::Local2BitDeltas,
585                -8..=7 => DeltaFormat::Local4BitDeltas,
586                _ => DeltaFormat::Local8BitDeltas,
587            })
588            .max()
589            .unwrap_or_default();
590        let delta_value = encode_delta(delta_format, values);
591
592        Device {
593            start_size,
594            end_size,
595            delta_format,
596            delta_value,
597        }
598    }
599}
600
601impl DeviceOrVariationIndex {
602    /// Create a new [`Device`] subtable
603    pub fn device(start_size: u16, end_size: u16, values: &[i8]) -> Self {
604        DeviceOrVariationIndex::Device(Device::new(start_size, end_size, values))
605    }
606}
607
608impl FontWrite for PendingVariationIndex {
609    fn write_into(&self, _writer: &mut TableWriter) {
610        panic!(
611            "Attempted to write PendingVariationIndex.\n\
612            VariationIndex tables should always be resolved before compilation.\n\
613            Please report this bug at <https://github.com/googlefonts/fontations/issues>"
614        )
615    }
616}
617
618fn encode_delta(format: DeltaFormat, values: &[i8]) -> Vec<u16> {
619    let (chunk_size, mask, bits) = match format {
620        DeltaFormat::Local2BitDeltas => (8, 0b11, 2),
621        DeltaFormat::Local4BitDeltas => (4, 0b1111, 4),
622        DeltaFormat::Local8BitDeltas => (2, 0b11111111, 8),
623        _ => panic!("invalid format"),
624    };
625    values
626        .chunks(chunk_size)
627        .map(|chunk| encode_chunk(chunk, mask, bits))
628        .collect()
629}
630
631fn encode_chunk(chunk: &[i8], mask: u8, bits: usize) -> u16 {
632    let mut out = 0u16;
633    for (i, val) in chunk.iter().enumerate() {
634        out |= ((val.to_be_bytes()[0] & mask) as u16) << ((16 - bits) - i * bits);
635    }
636    out
637}
638
639impl From<VariationIndex> for u32 {
640    fn from(value: VariationIndex) -> Self {
641        ((value.delta_set_outer_index as u32) << 16) | value.delta_set_inner_index as u32
642    }
643}
644
645impl ClassRangeRecord2 {
646    fn validate_glyph_range(&self, ctx: &mut ValidationCtx) {
647        if self.start_glyph_id > self.end_glyph_id {
648            ctx.report("start_glyph_id larger than end_glyph_id");
649        }
650    }
651}
652
653impl FromIterator<GlyphId> for CoverageTable {
654    fn from_iter<T: IntoIterator<Item = GlyphId>>(iter: T) -> Self {
655        let mut glyphs: Vec<_> = iter.into_iter().collect();
656        glyphs.sort_unstable();
657        glyphs.dedup();
658        if glyphs.iter().all(|g| g.to_u32() <= u16::MAX as u32) {
659            return glyphs
660                .into_iter()
661                .map(|g| GlyphId16::new(g.to_u32() as u16))
662                .collect();
663        }
664        let glyphs: Vec<_> = glyphs
665            .into_iter()
666            .map(|g| GlyphId24::checked_new(g.to_u32()).expect("glyph ID exceeds 24 bits"))
667            .collect();
668        let mut ranges: Vec<RangeRecord2> = Vec::new();
669        for (index, glyph) in glyphs.iter().enumerate() {
670            if let Some(last) = ranges.last_mut() {
671                if last.end_glyph_id.to_u32() + 1 == glyph.to_u32() {
672                    last.end_glyph_id = *glyph;
673                    continue;
674                }
675            }
676            ranges.push(RangeRecord2::new(*glyph, *glyph, Uint24::new(index as u32)));
677        }
678        // A range record is the size of three glyph IDs.
679        if ranges.len() * 3 < glyphs.len() {
680            return Self::Format4(CoverageFormat4::new(ranges));
681        }
682        Self::Format3(CoverageFormat3::new(glyphs))
683    }
684}
685
686impl FromIterator<(GlyphId, u32)> for ClassDef {
687    fn from_iter<T: IntoIterator<Item = (GlyphId, u32)>>(iter: T) -> Self {
688        let items: std::collections::BTreeMap<_, _> = iter.into_iter().collect();
689        if items
690            .iter()
691            .all(|(g, c)| g.to_u32() <= u16::MAX as u32 && *c <= u16::MAX as u32)
692        {
693            return items
694                .into_iter()
695                .map(|(g, c)| (GlyphId16::new(g.to_u32() as u16), c as u16))
696                .collect();
697        }
698        if items.values().all(|c| *c <= u16::MAX as u32) {
699            let mut ranges: Vec<ClassRangeRecord2> = Vec::new();
700            for (g, c) in items {
701                let g = GlyphId24::checked_new(g.to_u32()).expect("glyph ID exceeds 24 bits");
702                if let Some(last) = ranges.last_mut() {
703                    if last.class == c as u16 && last.end_glyph_id.to_u32() + 1 == g.to_u32() {
704                        last.end_glyph_id = g;
705                        continue;
706                    }
707                }
708                ranges.push(ClassRangeRecord2::new(g, g, c as u16));
709            }
710            return Self::Format4(ClassDefFormat4::new(ranges));
711        }
712        let first = items.keys().next().unwrap().to_u32();
713        let last = items.keys().next_back().unwrap().to_u32();
714        let start = GlyphId24::checked_new(first).expect("glyph ID exceeds 24 bits");
715        assert!(last <= Uint24::MAX.to_u32(), "glyph ID exceeds 24 bits");
716        let values = (first..=last)
717            .map(|g| {
718                Uint24::checked_new(items.get(&GlyphId::new(g)).copied().unwrap_or(0))
719                    .expect("class exceeds 24 bits")
720            })
721            .collect();
722        Self::Format3(ClassDefFormat3::new(start, values))
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729
730    #[test]
731    #[should_panic(expected = "array exceeds max length")]
732    fn array_len_smoke_test() {
733        let table = ScriptList {
734            script_records: vec![ScriptRecord {
735                script_tag: Tag::new(b"hihi"),
736                script: OffsetMarker::new(Script {
737                    default_lang_sys: NullableOffsetMarker::new(None),
738                    lang_sys_records: vec![LangSysRecord {
739                        lang_sys_tag: Tag::new(b"coco"),
740                        lang_sys: OffsetMarker::new(LangSys {
741                            required_feature_index: 0xffff,
742                            feature_indices: vec![69; (u16::MAX) as usize + 5],
743                        }),
744                    }],
745                }),
746            }],
747        };
748
749        table.validate().unwrap();
750    }
751
752    #[test]
753    #[should_panic(expected = "larger than end_glyph_id")]
754    fn validate_classdef_ranges() {
755        let classdef = ClassDefFormat2::new(vec![ClassRangeRecord::new(
756            GlyphId16::new(12),
757            GlyphId16::new(3),
758            7,
759        )]);
760
761        classdef.validate().unwrap();
762    }
763
764    #[test]
765    fn delta_encode() {
766        let inp = [1i8, 2, 3, -1];
767        let result = encode_delta(DeltaFormat::Local4BitDeltas, &inp);
768        assert_eq!(result.len(), 1);
769        assert_eq!(result[0], 0x123f_u16);
770
771        let inp = [1i8, 1, 1, 1, 1];
772        let result = encode_delta(DeltaFormat::Local2BitDeltas, &inp);
773        assert_eq!(result.len(), 1);
774        assert_eq!(result[0], 0x5540_u16);
775    }
776}