read_fonts/tables/
aat.rs

1//! Apple Advanced Typography common tables.
2//!
3//! See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html>
4
5include!("../../generated/generated_aat.rs");
6
7/// Predefined classes.
8///
9/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html>
10pub mod class {
11    pub const END_OF_TEXT: u8 = 0;
12    pub const OUT_OF_BOUNDS: u8 = 1;
13    pub const DELETED_GLYPH: u8 = 2;
14}
15
16impl Lookup0<'_> {
17    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
18        let data = self.values_data();
19        let data_len = data.len();
20        let n_elems = data_len / T::RAW_BYTE_LEN;
21        let len_in_bytes = n_elems * T::RAW_BYTE_LEN;
22        FontData::new(&data[..len_in_bytes])
23            .cursor()
24            .read_array::<BigEndian<T>>(n_elems)?
25            .get(index as usize)
26            .map(|val| val.get())
27            .ok_or(ReadError::OutOfBounds)
28    }
29}
30
31/// Lookup segment for format 2.
32#[derive(Copy, Clone, bytemuck::AnyBitPattern)]
33#[repr(C, packed)]
34pub struct LookupSegment2<T>
35where
36    T: LookupValue,
37{
38    /// Last glyph index in this segment.
39    pub last_glyph: BigEndian<u16>,
40    /// First glyph index in this segment.
41    pub first_glyph: BigEndian<u16>,
42    /// The lookup value.
43    pub value: BigEndian<T>,
44}
45
46/// Note: this requires `LookupSegment2` to be `repr(packed)`.
47impl<T: LookupValue> FixedSize for LookupSegment2<T> {
48    const RAW_BYTE_LEN: usize = std::mem::size_of::<Self>();
49}
50
51impl Lookup2<'_> {
52    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
53        let segments = self.segments::<T>()?;
54        let ix = match segments.binary_search_by(|segment| segment.first_glyph.get().cmp(&index)) {
55            Ok(ix) => ix,
56            Err(ix) => ix.saturating_sub(1),
57        };
58        let segment = segments.get(ix).ok_or(ReadError::OutOfBounds)?;
59        if (segment.first_glyph.get()..=segment.last_glyph.get()).contains(&index) {
60            let value = segment.value;
61            return Ok(value.get());
62        }
63        Err(ReadError::OutOfBounds)
64    }
65
66    fn segments<T: LookupValue>(&self) -> Result<&[LookupSegment2<T>], ReadError> {
67        FontData::new(self.segments_data())
68            .cursor()
69            .read_array(self.n_units() as usize)
70    }
71}
72
73impl Lookup4<'_> {
74    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
75        let segments = self.segments();
76        let ix = match segments.binary_search_by(|segment| segment.first_glyph.get().cmp(&index)) {
77            Ok(ix) => ix,
78            Err(ix) => ix.saturating_sub(1),
79        };
80        let segment = segments.get(ix).ok_or(ReadError::OutOfBounds)?;
81        if (segment.first_glyph.get()..=segment.last_glyph.get()).contains(&index) {
82            let base_offset = segment.value_offset() as usize;
83            let offset = base_offset
84                + index
85                    .checked_sub(segment.first_glyph())
86                    .ok_or(ReadError::OutOfBounds)? as usize
87                    * T::RAW_BYTE_LEN;
88            return self.offset_data().read_at(offset);
89        }
90        Err(ReadError::OutOfBounds)
91    }
92}
93
94/// Lookup single record for format 6.
95#[derive(Copy, Clone, bytemuck::AnyBitPattern)]
96#[repr(C, packed)]
97pub struct LookupSingle<T>
98where
99    T: LookupValue,
100{
101    /// The glyph index.
102    pub glyph: BigEndian<u16>,
103    /// The lookup value.
104    pub value: BigEndian<T>,
105}
106
107/// Note: this requires `LookupSingle` to be `repr(packed)`.
108impl<T: LookupValue> FixedSize for LookupSingle<T> {
109    const RAW_BYTE_LEN: usize = std::mem::size_of::<Self>();
110}
111
112impl Lookup6<'_> {
113    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
114        let entries = self.entries::<T>()?;
115        if let Ok(ix) = entries.binary_search_by_key(&index, |entry| entry.glyph.get()) {
116            let entry = &entries[ix];
117            let value = entry.value;
118            return Ok(value.get());
119        }
120        Err(ReadError::OutOfBounds)
121    }
122
123    fn entries<T: LookupValue>(&self) -> Result<&[LookupSingle<T>], ReadError> {
124        FontData::new(self.entries_data())
125            .cursor()
126            .read_array(self.n_units() as usize)
127    }
128}
129
130impl Lookup8<'_> {
131    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
132        index
133            .checked_sub(self.first_glyph())
134            .and_then(|ix| {
135                self.value_array()
136                    .get(ix as usize)
137                    .map(|val| T::from_u16(val.get()))
138            })
139            .ok_or(ReadError::OutOfBounds)
140    }
141}
142
143impl Lookup10<'_> {
144    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
145        let ix = index
146            .checked_sub(self.first_glyph())
147            .ok_or(ReadError::OutOfBounds)? as usize;
148        let unit_size = self.unit_size() as usize;
149        let offset = ix * unit_size;
150        let mut cursor = FontData::new(self.values_data()).cursor();
151        cursor.advance_by(offset);
152        let val = match unit_size {
153            1 => cursor.read::<u8>()? as u32,
154            2 => cursor.read::<u16>()? as u32,
155            4 => cursor.read::<u32>()?,
156            _ => {
157                return Err(ReadError::MalformedData(
158                    "invalid unit_size in format 10 AAT lookup table",
159                ))
160            }
161        };
162        Ok(T::from_u32(val))
163    }
164}
165
166impl Lookup<'_> {
167    pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
168        match self {
169            Lookup::Format0(lookup) => lookup.value::<T>(index),
170            Lookup::Format2(lookup) => lookup.value::<T>(index),
171            Lookup::Format4(lookup) => lookup.value::<T>(index),
172            Lookup::Format6(lookup) => lookup.value::<T>(index),
173            Lookup::Format8(lookup) => lookup.value::<T>(index),
174            Lookup::Format10(lookup) => lookup.value::<T>(index),
175        }
176    }
177}
178
179pub struct TypedLookup<'a, T> {
180    lookup: Lookup<'a>,
181    _marker: std::marker::PhantomData<fn() -> T>,
182}
183
184impl<T: LookupValue> TypedLookup<'_, T> {
185    /// Returns the value associated with the given index.
186    pub fn value(&self, index: u16) -> Result<T, ReadError> {
187        self.lookup.value::<T>(index)
188    }
189}
190
191impl<'a, T> FontRead<'a> for TypedLookup<'a, T> {
192    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
193        Ok(Self {
194            lookup: Lookup::read(data)?,
195            _marker: std::marker::PhantomData,
196        })
197    }
198}
199
200#[cfg(feature = "experimental_traverse")]
201impl<'a, T> SomeTable<'a> for TypedLookup<'a, T> {
202    fn type_name(&self) -> &str {
203        "TypedLookup"
204    }
205
206    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
207        self.lookup.get_field(idx)
208    }
209}
210
211/// Trait for values that can be read from lookup tables.
212pub trait LookupValue: Copy + Scalar + bytemuck::AnyBitPattern {
213    fn from_u16(v: u16) -> Self;
214    fn from_u32(v: u32) -> Self;
215}
216
217impl LookupValue for u16 {
218    fn from_u16(v: u16) -> Self {
219        v
220    }
221
222    fn from_u32(v: u32) -> Self {
223        // intentionally truncates
224        v as _
225    }
226}
227
228impl LookupValue for u32 {
229    fn from_u16(v: u16) -> Self {
230        v as _
231    }
232
233    fn from_u32(v: u32) -> Self {
234        v
235    }
236}
237
238impl LookupValue for GlyphId16 {
239    fn from_u16(v: u16) -> Self {
240        GlyphId16::from(v)
241    }
242
243    fn from_u32(v: u32) -> Self {
244        // intentionally truncates
245        GlyphId16::from(v as u16)
246    }
247}
248
249pub type LookupU16<'a> = TypedLookup<'a, u16>;
250pub type LookupU32<'a> = TypedLookup<'a, u32>;
251pub type LookupGlyphId<'a> = TypedLookup<'a, GlyphId16>;
252
253/// Empty data type for a state table entry with no payload.
254///
255/// Note: this type is only intended for use as the type parameter for
256/// `StateEntry`. The inner field is private and this type cannot be
257/// constructed outside of this module.
258#[derive(Copy, Clone, bytemuck::AnyBitPattern)]
259pub struct NoPayload(());
260
261impl FixedSize for NoPayload {
262    const RAW_BYTE_LEN: usize = 0;
263}
264
265/// Entry in an (extended) state table.
266pub struct StateEntry<T = NoPayload> {
267    /// Index of the next state.
268    pub new_state: u16,
269    /// Flag values are table specific.
270    pub flags: u16,
271    /// Payload is table specific.
272    pub payload: T,
273}
274
275impl<'a, T: bytemuck::AnyBitPattern + FixedSize> FontRead<'a> for StateEntry<T> {
276    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
277        let mut cursor = data.cursor();
278        let new_state = cursor.read()?;
279        let flags = cursor.read()?;
280        let remaining = cursor.remaining().ok_or(ReadError::OutOfBounds)?;
281        let payload = *remaining.read_ref_at(0)?;
282        Ok(Self {
283            new_state,
284            flags,
285            payload,
286        })
287    }
288}
289
290impl<T> FixedSize for StateEntry<T>
291where
292    T: FixedSize,
293{
294    // Two u16 fields + payload
295    const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + u16::RAW_BYTE_LEN + T::RAW_BYTE_LEN;
296}
297
298pub struct StateTable<'a> {
299    header: StateHeader<'a>,
300}
301
302impl StateTable<'_> {
303    /// Returns the class table entry for the given glyph identifier.
304    pub fn class(&self, glyph_id: GlyphId16) -> Result<u8, ReadError> {
305        let glyph_id = glyph_id.to_u16();
306        if glyph_id == 0xFFFF {
307            return Ok(class::DELETED_GLYPH);
308        }
309        let class_table = self.header.class_table()?;
310        glyph_id
311            .checked_sub(class_table.first_glyph())
312            .and_then(|ix| class_table.class_array().get(ix as usize).copied())
313            .ok_or(ReadError::OutOfBounds)
314    }
315
316    /// Returns the entry for the given state and class.
317    pub fn entry(&self, state: u16, class: u8) -> Result<StateEntry, ReadError> {
318        // Each state has a 1-byte entry per class so state_size == n_classes
319        let n_classes = self.header.state_size() as usize;
320        if n_classes == 0 {
321            // Avoid potential divide by zero below
322            return Err(ReadError::MalformedData("empty AAT state table"));
323        }
324        let mut class = class as usize;
325        if class >= n_classes {
326            class = class::OUT_OF_BOUNDS as usize;
327        }
328        let state_array = self.header.state_array()?.data();
329        let entry_ix = state_array
330            .get(
331                (state as usize)
332                    .checked_mul(n_classes)
333                    .ok_or(ReadError::OutOfBounds)?
334                    + class,
335            )
336            .copied()
337            .ok_or(ReadError::OutOfBounds)? as usize;
338        let entry_offset = entry_ix * 4;
339        let entry_data = self
340            .header
341            .entry_table()?
342            .data()
343            .get(entry_offset..)
344            .ok_or(ReadError::OutOfBounds)?;
345        let mut entry = StateEntry::read(FontData::new(entry_data))?;
346        // For legacy state tables, the newState is a byte offset into
347        // the state array. Convert this to an index for consistency.
348        let new_state = (entry.new_state as i32)
349            .checked_sub(self.header.state_array_offset().to_u32() as i32)
350            .ok_or(ReadError::OutOfBounds)?
351            / n_classes as i32;
352        entry.new_state = new_state.try_into().map_err(|_| ReadError::OutOfBounds)?;
353        Ok(entry)
354    }
355}
356
357impl<'a> FontRead<'a> for StateTable<'a> {
358    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
359        Ok(Self {
360            header: StateHeader::read(data)?,
361        })
362    }
363}
364
365#[cfg(feature = "experimental_traverse")]
366impl<'a> SomeTable<'a> for StateTable<'a> {
367    fn type_name(&self) -> &str {
368        "StateTable"
369    }
370
371    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
372        self.header.get_field(idx)
373    }
374}
375
376pub struct ExtendedStateTable<'a, T = ()> {
377    header: StxHeader<'a>,
378    _marker: std::marker::PhantomData<fn() -> T>,
379}
380
381impl<T: bytemuck::AnyBitPattern + FixedSize> ExtendedStateTable<'_, T> {
382    /// Returns the class table entry for the given glyph identifier.
383    pub fn class(&self, glyph_id: GlyphId16) -> Result<u16, ReadError> {
384        let glyph_id = glyph_id.to_u16();
385        if glyph_id == 0xFFFF {
386            return Ok(class::DELETED_GLYPH as u16);
387        }
388        self.header.class_table()?.value(glyph_id)
389    }
390
391    /// Returns the entry for the given state and class.
392    pub fn entry(&self, state: u16, class: u16) -> Result<StateEntry<T>, ReadError> {
393        let n_classes = self.header.n_classes() as usize;
394        let mut class = class as usize;
395        if class >= n_classes {
396            class = class::OUT_OF_BOUNDS as usize;
397        }
398        let state_array = self.header.state_array()?.data();
399        let state_ix = state as usize * n_classes + class;
400        let entry_ix = state_array
401            .get(state_ix)
402            .copied()
403            .ok_or(ReadError::OutOfBounds)?
404            .get() as usize;
405        let entry_offset = entry_ix * StateEntry::<T>::RAW_BYTE_LEN;
406        let entry_data = self
407            .header
408            .entry_table()?
409            .data()
410            .get(entry_offset..)
411            .ok_or(ReadError::OutOfBounds)?;
412        StateEntry::read(FontData::new(entry_data))
413    }
414}
415
416impl<'a, T> FontRead<'a> for ExtendedStateTable<'a, T> {
417    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
418        Ok(Self {
419            header: StxHeader::read(data)?,
420            _marker: std::marker::PhantomData,
421        })
422    }
423}
424
425#[cfg(feature = "experimental_traverse")]
426impl<'a, T> SomeTable<'a> for ExtendedStateTable<'a, T> {
427    fn type_name(&self) -> &str {
428        "ExtendedStateTable"
429    }
430
431    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
432        self.header.get_field(idx)
433    }
434}
435
436pub type ExtendedStateTableU16<'a> = ExtendedStateTable<'a, u16>;
437
438#[cfg(test)]
439mod tests {
440    use font_test_data::bebuffer::BeBuffer;
441
442    use super::*;
443
444    #[test]
445    fn lookup_format_0() {
446        #[rustfmt::skip]
447        let words = [
448            0_u16, // format
449            0, 2, 4, 6, 8, 10, 12, 14, 16, // maps all glyphs to gid * 2
450        ];
451        let mut buf = BeBuffer::new();
452        buf = buf.extend(words);
453        let lookup = LookupU16::read(buf.data().into()).unwrap();
454        for gid in 0..=8 {
455            assert_eq!(lookup.value(gid).unwrap(), gid * 2);
456        }
457        assert!(lookup.value(9).is_err());
458    }
459
460    // Taken from example 2 at https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
461    #[test]
462    fn lookup_format_2() {
463        #[rustfmt::skip]
464        let words = [
465            2_u16, // format
466            6,     // unit size (6 bytes)
467            3,     // number of units
468            12,    // search range
469            1,     // entry selector
470            6,     // range shift
471            22, 20, 4, // First segment, mapping glyphs 20 through 22 to class 4
472            24, 23, 5, // Second segment, mapping glyph 23 and 24 to class 5
473            28, 25, 6, // Third segment, mapping glyphs 25 through 28 to class 6
474        ];
475        let mut buf = BeBuffer::new();
476        buf = buf.extend(words);
477        let lookup = LookupU16::read(buf.data().into()).unwrap();
478        let expected = [(20..=22, 4), (23..=24, 5), (25..=28, 6)];
479        for (range, class) in expected {
480            for gid in range {
481                assert_eq!(lookup.value(gid).unwrap(), class);
482            }
483        }
484        for fail in [0, 10, 19, 29, 0xFFFF] {
485            assert!(lookup.value(fail).is_err());
486        }
487    }
488
489    #[test]
490    fn lookup_format_4() {
491        #[rustfmt::skip]
492        let words = [
493            4_u16, // format
494            6,     // unit size (6 bytes)
495            3,     // number of units
496            12,    // search range
497            1,     // entry selector
498            6,     // range shift
499            22, 20, 30, // First segment, mapping glyphs 20 through 22 to mapped data at offset 30
500            24, 23, 36, // Second segment, mapping glyph 23 and 24 to mapped data at offset 36
501            28, 25, 40, // Third segment, mapping glyphs 25 through 28 to mapped data at offset 40
502            // mapped data
503            3, 2, 1,
504            100, 150,
505            8, 6, 7, 9
506        ];
507        let mut buf = BeBuffer::new();
508        buf = buf.extend(words);
509        let lookup = LookupU16::read(buf.data().into()).unwrap();
510        let expected = [
511            (20, 3),
512            (21, 2),
513            (22, 1),
514            (23, 100),
515            (24, 150),
516            (25, 8),
517            (26, 6),
518            (27, 7),
519            (28, 9),
520        ];
521        for (in_glyph, out_glyph) in expected {
522            assert_eq!(lookup.value(in_glyph).unwrap(), out_glyph);
523        }
524        for fail in [0, 10, 19, 29, 0xFFFF] {
525            assert!(lookup.value(fail).is_err());
526        }
527    }
528
529    // Taken from example 1 at https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
530    #[test]
531    fn lookup_format_6() {
532        #[rustfmt::skip]
533        let words = [
534            6_u16, // format
535            4,     // unit size (4 bytes)
536            4,     // number of units
537            16,    // search range
538            2,     // entry selector
539            0,     // range shift
540            50, 600, // Input glyph 50 maps to glyph 600
541            51, 601, // Input glyph 51 maps to glyph 601
542            201, 602, // Input glyph 201 maps to glyph 602
543            202, 900, // Input glyph 202 maps to glyph 900
544        ];
545        let mut buf = BeBuffer::new();
546        buf = buf.extend(words);
547        let lookup = LookupU16::read(buf.data().into()).unwrap();
548        let expected = [(50, 600), (51, 601), (201, 602), (202, 900)];
549        for (in_glyph, out_glyph) in expected {
550            assert_eq!(lookup.value(in_glyph).unwrap(), out_glyph);
551        }
552        for fail in [0, 10, 49, 52, 203, 0xFFFF] {
553            assert!(lookup.value(fail).is_err());
554        }
555    }
556
557    #[test]
558    fn lookup_format_8() {
559        #[rustfmt::skip]
560        let words = [
561            8_u16, // format
562            201,   // first glyph
563            8,     // glyph count
564            3, 8, 2, 9, 1, 200, 60, // glyphs 201..209 mapped to these values
565        ];
566        let mut buf = BeBuffer::new();
567        buf = buf.extend(words);
568        let lookup = LookupU16::read(buf.data().into()).unwrap();
569        let expected = &words[3..];
570        for (gid, expected) in (201..209).zip(expected) {
571            assert_eq!(lookup.value(gid).unwrap(), *expected);
572        }
573        for fail in [0, 10, 200, 210, 0xFFFF] {
574            assert!(lookup.value(fail).is_err());
575        }
576    }
577
578    #[test]
579    fn lookup_format_10() {
580        #[rustfmt::skip]
581        let words = [
582            10_u16, // format
583            4,      // unit size, use 4 byte values
584            201,   // first glyph
585            8,     // glyph count
586        ];
587        // glyphs 201..209 mapped to these values
588        let mapped = [3_u32, 8, 2902384, 9, 1, u32::MAX, 60];
589        let mut buf = BeBuffer::new();
590        buf = buf.extend(words).extend(mapped);
591        let lookup = LookupU32::read(buf.data().into()).unwrap();
592        for (gid, expected) in (201..209).zip(mapped) {
593            assert_eq!(lookup.value(gid).unwrap(), expected);
594        }
595        for fail in [0, 10, 200, 210, 0xFFFF] {
596            assert!(lookup.value(fail).is_err());
597        }
598    }
599
600    #[test]
601    fn extended_state_table() {
602        #[rustfmt::skip]
603        let header = [
604            6_u32, // number of classes
605            20, // byte offset to class table
606            56, // byte offset to state array
607            92, // byte offset to entry array
608            0, // padding
609        ];
610        #[rustfmt::skip]
611        let class_table = [
612            6_u16, // format
613            4,     // unit size (4 bytes)
614            5,     // number of units
615            16,    // search range
616            2,     // entry selector
617            0,     // range shift
618            50, 4, // Input glyph 50 maps to class 4
619            51, 4, // Input glyph 51 maps to class 4
620            80, 5, // Input glyph 80 maps to class 5
621            201, 4, // Input glyph 201 maps to class 4
622            202, 4, // Input glyph 202 maps to class 4
623            !0, !0
624        ];
625        #[rustfmt::skip]
626        let state_array: [u16; 18] = [
627            0, 0, 0, 0, 0, 1,
628            0, 0, 0, 0, 0, 1,
629            0, 0, 0, 0, 2, 1,
630        ];
631        #[rustfmt::skip]
632        let entry_table: [u16; 12] = [
633            0, 0, u16::MAX, u16::MAX,
634            2, 0, u16::MAX, u16::MAX,
635            0, 0, u16::MAX, 0,
636        ];
637        let buf = BeBuffer::new()
638            .extend(header)
639            .extend(class_table)
640            .extend(state_array)
641            .extend(entry_table);
642        let table = ExtendedStateTable::<ContextualData>::read(buf.data().into()).unwrap();
643        // check class lookups
644        let [class_50, class_80, class_201] =
645            [50, 80, 201].map(|gid| table.class(GlyphId16::from(gid)).unwrap());
646        assert_eq!(class_50, 4);
647        assert_eq!(class_80, 5);
648        assert_eq!(class_201, 4);
649        // initial state
650        let entry = table.entry(0, 4).unwrap();
651        assert_eq!(entry.new_state, 0);
652        assert_eq!(entry.payload.current_index, !0);
653        // entry (state 0, class 5) should transition to state 2
654        let entry = table.entry(0, 5).unwrap();
655        assert_eq!(entry.new_state, 2);
656        // from state 2, we transition back to state 0 when class is not 5
657        // this also enables an action (payload.current_index != -1)
658        let entry = table.entry(2, 4).unwrap();
659        assert_eq!(entry.new_state, 0);
660        assert_eq!(entry.payload.current_index, 0);
661    }
662
663    #[derive(Copy, Clone, Debug, bytemuck::AnyBitPattern)]
664    #[repr(C, packed)]
665    struct ContextualData {
666        _mark_index: BigEndian<u16>,
667        current_index: BigEndian<u16>,
668    }
669
670    impl FixedSize for ContextualData {
671        const RAW_BYTE_LEN: usize = 4;
672    }
673
674    // Take from example at <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html>
675    // with class table trimmed to 4 glyphs
676    #[test]
677    fn state_table() {
678        #[rustfmt::skip]
679        let header = [
680            7_u16, // number of classes
681            10, // byte offset to class table
682            18, // byte offset to state array
683            40, // byte offset to entry array
684            64, // byte offset to value array (unused here)
685        ];
686        #[rustfmt::skip]
687        let class_table = [
688            3_u16, // first glyph
689            4, // number of glyphs
690        ];
691        let classes = [1u8, 2, 3, 4];
692        #[rustfmt::skip]
693        let state_array: [u8; 22] = [
694            2, 0, 0, 2, 1, 0, 0,
695            2, 0, 0, 2, 1, 0, 0,
696            2, 3, 3, 2, 3, 4, 5,
697            0, // padding
698        ];
699        #[rustfmt::skip]
700        let entry_table: [u16; 10] = [
701            // The first column are offsets from the beginning of the state
702            // table to some position in the state array
703            18, 0x8112,
704            32, 0x8112,
705            18, 0x0000,
706            32, 0x8114,
707            18, 0x8116,
708        ];
709        let buf = BeBuffer::new()
710            .extend(header)
711            .extend(class_table)
712            .extend(classes)
713            .extend(state_array)
714            .extend(entry_table);
715        let table = StateTable::read(buf.data().into()).unwrap();
716        // check class lookups
717        for i in 0..4u8 {
718            assert_eq!(table.class(GlyphId16::from(i as u16 + 3)).unwrap(), i + 1);
719        }
720        // (state, class) -> (new_state, flags)
721        let cases = [
722            ((0, 4), (2, 0x8112)),
723            ((2, 1), (2, 0x8114)),
724            ((1, 3), (0, 0x0000)),
725            ((2, 5), (0, 0x8116)),
726        ];
727        for ((state, class), (new_state, flags)) in cases {
728            let entry = table.entry(state, class).unwrap();
729            assert_eq!(
730                entry.new_state, new_state,
731                "state {state}, class {class} should map to new state {new_state} (got {})",
732                entry.new_state
733            );
734            assert_eq!(
735                entry.flags, flags,
736                "state {state}, class {class} should map to flags 0x{flags:X} (got 0x{:X})",
737                entry.flags
738            );
739        }
740    }
741}