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
179#[derive(Clone)]
180pub struct TypedLookup<'a, T> {
181    lookup: Lookup<'a>,
182    _marker: std::marker::PhantomData<fn() -> T>,
183}
184
185impl<T: LookupValue> TypedLookup<'_, T> {
186    /// Returns the value associated with the given index.
187    pub fn value(&self, index: u16) -> Result<T, ReadError> {
188        self.lookup.value::<T>(index)
189    }
190}
191
192impl<'a, T> FontRead<'a> for TypedLookup<'a, T> {
193    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
194        Ok(Self {
195            lookup: Lookup::read(data)?,
196            _marker: std::marker::PhantomData,
197        })
198    }
199}
200
201#[cfg(feature = "experimental_traverse")]
202impl<'a, T> SomeTable<'a> for TypedLookup<'a, T> {
203    fn type_name(&self) -> &str {
204        "TypedLookup"
205    }
206
207    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
208        self.lookup.get_field(idx)
209    }
210}
211
212/// Trait for values that can be read from lookup tables.
213pub trait LookupValue: Copy + Scalar + bytemuck::AnyBitPattern {
214    fn from_u16(v: u16) -> Self;
215    fn from_u32(v: u32) -> Self;
216}
217
218impl LookupValue for u16 {
219    fn from_u16(v: u16) -> Self {
220        v
221    }
222
223    fn from_u32(v: u32) -> Self {
224        // intentionally truncates
225        v as _
226    }
227}
228
229impl LookupValue for u32 {
230    fn from_u16(v: u16) -> Self {
231        v as _
232    }
233
234    fn from_u32(v: u32) -> Self {
235        v
236    }
237}
238
239impl LookupValue for GlyphId16 {
240    fn from_u16(v: u16) -> Self {
241        GlyphId16::from(v)
242    }
243
244    fn from_u32(v: u32) -> Self {
245        // intentionally truncates
246        GlyphId16::from(v as u16)
247    }
248}
249
250pub type LookupU16<'a> = TypedLookup<'a, u16>;
251pub type LookupU32<'a> = TypedLookup<'a, u32>;
252pub type LookupGlyphId<'a> = TypedLookup<'a, GlyphId16>;
253
254/// Empty data type for a state table entry with no payload.
255///
256/// Note: this type is only intended for use as the type parameter for
257/// `StateEntry`. The inner field is private and this type cannot be
258/// constructed outside of this module.
259#[derive(Copy, Clone, bytemuck::AnyBitPattern, Debug)]
260pub struct NoPayload(());
261
262impl FixedSize for NoPayload {
263    const RAW_BYTE_LEN: usize = 0;
264}
265
266/// Entry in an (extended) state table.
267#[derive(Clone, Debug)]
268pub struct StateEntry<T = NoPayload> {
269    /// Index of the next state.
270    pub new_state: u16,
271    /// Flag values are table specific.
272    pub flags: u16,
273    /// Payload is table specific.
274    pub payload: T,
275}
276
277impl<'a, T: bytemuck::AnyBitPattern + FixedSize> FontRead<'a> for StateEntry<T> {
278    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
279        let mut cursor = data.cursor();
280        let new_state = cursor.read()?;
281        let flags = cursor.read()?;
282        let remaining = cursor.remaining().ok_or(ReadError::OutOfBounds)?;
283        let payload = *remaining.read_ref_at(0)?;
284        Ok(Self {
285            new_state,
286            flags,
287            payload,
288        })
289    }
290}
291
292impl<T> FixedSize for StateEntry<T>
293where
294    T: FixedSize,
295{
296    // Two u16 fields + payload
297    const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + u16::RAW_BYTE_LEN + T::RAW_BYTE_LEN;
298}
299
300/// Table for driving a finite state machine for layout.
301///
302/// The input to the state machine consists of the current state
303/// and a glyph class. The output is an [entry](StateEntry) containing
304/// the next state and a payload that is dependent on the type of
305/// layout action being performed.
306///
307/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#StateHeader>
308/// for more detail.
309#[derive(Clone)]
310pub struct StateTable<'a> {
311    header: StateHeader<'a>,
312}
313
314impl StateTable<'_> {
315    /// Returns the class table entry for the given glyph identifier.
316    pub fn class(&self, glyph_id: GlyphId16) -> Result<u8, ReadError> {
317        let glyph_id = glyph_id.to_u16();
318        if glyph_id == 0xFFFF {
319            return Ok(class::DELETED_GLYPH);
320        }
321        let class_table = self.header.class_table()?;
322        glyph_id
323            .checked_sub(class_table.first_glyph())
324            .and_then(|ix| class_table.class_array().get(ix as usize).copied())
325            .ok_or(ReadError::OutOfBounds)
326    }
327
328    /// Returns the entry for the given state and class.
329    pub fn entry(&self, state: u16, class: u8) -> Result<StateEntry, ReadError> {
330        // Each state has a 1-byte entry per class so state_size == n_classes
331        let n_classes = self.header.state_size() as usize;
332        if n_classes == 0 {
333            // Avoid potential divide by zero below
334            return Err(ReadError::MalformedData("empty AAT state table"));
335        }
336        let mut class = class as usize;
337        if class >= n_classes {
338            class = class::OUT_OF_BOUNDS as usize;
339        }
340        let state_array = self.header.state_array()?.data();
341        let entry_ix = state_array
342            .get(
343                (state as usize)
344                    .checked_mul(n_classes)
345                    .ok_or(ReadError::OutOfBounds)?
346                    + class,
347            )
348            .copied()
349            .ok_or(ReadError::OutOfBounds)? as usize;
350        let entry_offset = entry_ix * 4;
351        let entry_data = self
352            .header
353            .entry_table()?
354            .data()
355            .get(entry_offset..)
356            .ok_or(ReadError::OutOfBounds)?;
357        let mut entry = StateEntry::read(FontData::new(entry_data))?;
358        // For legacy state tables, the newState is a byte offset into
359        // the state array. Convert this to an index for consistency.
360        let new_state = (entry.new_state as i32)
361            .checked_sub(self.header.state_array_offset().to_u32() as i32)
362            .ok_or(ReadError::OutOfBounds)?
363            / n_classes as i32;
364        entry.new_state = new_state.try_into().map_err(|_| ReadError::OutOfBounds)?;
365        Ok(entry)
366    }
367}
368
369impl<'a> FontRead<'a> for StateTable<'a> {
370    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
371        Ok(Self {
372            header: StateHeader::read(data)?,
373        })
374    }
375}
376
377#[cfg(feature = "experimental_traverse")]
378impl<'a> SomeTable<'a> for StateTable<'a> {
379    fn type_name(&self) -> &str {
380        "StateTable"
381    }
382
383    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
384        self.header.get_field(idx)
385    }
386}
387
388#[derive(Clone)]
389pub struct ExtendedStateTable<'a, T = NoPayload> {
390    n_classes: usize,
391    class_table: LookupU16<'a>,
392    state_array: &'a [BigEndian<u16>],
393    entry_table: &'a [u8],
394    _marker: std::marker::PhantomData<fn() -> T>,
395}
396
397impl<T> ExtendedStateTable<'_, T> {
398    pub const HEADER_LEN: usize = u32::RAW_BYTE_LEN * 4;
399}
400
401/// Table for driving a finite state machine for layout.
402///
403/// The input to the state machine consists of the current state
404/// and a glyph class. The output is an [entry](StateEntry) containing
405/// the next state and a payload that is dependent on the type of
406/// layout action being performed.
407///
408/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#StateHeader>
409/// for more detail.
410impl<T> ExtendedStateTable<'_, T>
411where
412    T: FixedSize + bytemuck::AnyBitPattern,
413{
414    /// Returns the class table entry for the given glyph identifier.
415    pub fn class(&self, glyph_id: GlyphId) -> Result<u16, ReadError> {
416        let glyph_id: u16 = glyph_id
417            .to_u32()
418            .try_into()
419            .map_err(|_| ReadError::OutOfBounds)?;
420        if glyph_id == 0xFFFF {
421            return Ok(class::DELETED_GLYPH as u16);
422        }
423        self.class_table.value(glyph_id)
424    }
425
426    /// Returns the entry for the given state and class.
427    pub fn entry(&self, state: u16, class: u16) -> Result<StateEntry<T>, ReadError> {
428        let mut class = class as usize;
429        if class >= self.n_classes {
430            class = class::OUT_OF_BOUNDS as usize;
431        }
432        let state_ix = state as usize * self.n_classes + class;
433        let entry_ix = self
434            .state_array
435            .get(state_ix)
436            .copied()
437            .ok_or(ReadError::OutOfBounds)?
438            .get() as usize;
439        let entry_offset = entry_ix * StateEntry::<T>::RAW_BYTE_LEN;
440        let entry_data = self
441            .entry_table
442            .get(entry_offset..)
443            .ok_or(ReadError::OutOfBounds)?;
444        StateEntry::read(FontData::new(entry_data))
445    }
446}
447
448impl<'a, T> FontRead<'a> for ExtendedStateTable<'a, T> {
449    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
450        let header = StxHeader::read(data)?;
451        let n_classes = header.n_classes() as usize;
452        let class_table = header.class_table()?;
453        let state_array = header.state_array()?.data();
454        let entry_table = header.entry_table()?.data();
455        Ok(Self {
456            n_classes,
457            class_table,
458            state_array,
459            entry_table,
460            _marker: std::marker::PhantomData,
461        })
462    }
463}
464
465#[cfg(feature = "experimental_traverse")]
466impl<'a, T> SomeTable<'a> for ExtendedStateTable<'a, T> {
467    fn type_name(&self) -> &str {
468        "ExtendedStateTable"
469    }
470
471    fn get_field(&self, _idx: usize) -> Option<Field<'a>> {
472        None
473    }
474}
475
476/// Reads an array of T from the given FontData, ensuring that the byte length
477/// is a multiple of the size of T.
478///
479/// Many of the `morx` subtables have arrays without associated lengths so we
480/// simply read to the end of the available data. The `FontData::read_array`
481/// method will fail if the byte range provided is not exact so this helper
482/// allows us to force the lengths to an acceptable value.
483pub(crate) fn safe_read_array_to_end<'a, T: bytemuck::AnyBitPattern + FixedSize>(
484    data: &FontData<'a>,
485    offset: usize,
486) -> Result<&'a [T], ReadError> {
487    let len = data
488        .len()
489        .checked_sub(offset)
490        .ok_or(ReadError::OutOfBounds)?;
491    let end = offset + len / T::RAW_BYTE_LEN * T::RAW_BYTE_LEN;
492    data.read_array(offset..end)
493}
494
495#[cfg(test)]
496mod tests {
497    use font_test_data::bebuffer::BeBuffer;
498
499    use super::*;
500
501    #[test]
502    fn lookup_format_0() {
503        #[rustfmt::skip]
504        let words = [
505            0_u16, // format
506            0, 2, 4, 6, 8, 10, 12, 14, 16, // maps all glyphs to gid * 2
507        ];
508        let mut buf = BeBuffer::new();
509        buf = buf.extend(words);
510        let lookup = LookupU16::read(buf.data().into()).unwrap();
511        for gid in 0..=8 {
512            assert_eq!(lookup.value(gid).unwrap(), gid * 2);
513        }
514        assert!(lookup.value(9).is_err());
515    }
516
517    // Taken from example 2 at https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
518    #[test]
519    fn lookup_format_2() {
520        #[rustfmt::skip]
521        let words = [
522            2_u16, // format
523            6,     // unit size (6 bytes)
524            3,     // number of units
525            12,    // search range
526            1,     // entry selector
527            6,     // range shift
528            22, 20, 4, // First segment, mapping glyphs 20 through 22 to class 4
529            24, 23, 5, // Second segment, mapping glyph 23 and 24 to class 5
530            28, 25, 6, // Third segment, mapping glyphs 25 through 28 to class 6
531        ];
532        let mut buf = BeBuffer::new();
533        buf = buf.extend(words);
534        let lookup = LookupU16::read(buf.data().into()).unwrap();
535        let expected = [(20..=22, 4), (23..=24, 5), (25..=28, 6)];
536        for (range, class) in expected {
537            for gid in range {
538                assert_eq!(lookup.value(gid).unwrap(), class);
539            }
540        }
541        for fail in [0, 10, 19, 29, 0xFFFF] {
542            assert!(lookup.value(fail).is_err());
543        }
544    }
545
546    #[test]
547    fn lookup_format_4() {
548        #[rustfmt::skip]
549        let words = [
550            4_u16, // format
551            6,     // unit size (6 bytes)
552            3,     // number of units
553            12,    // search range
554            1,     // entry selector
555            6,     // range shift
556            22, 20, 30, // First segment, mapping glyphs 20 through 22 to mapped data at offset 30
557            24, 23, 36, // Second segment, mapping glyph 23 and 24 to mapped data at offset 36
558            28, 25, 40, // Third segment, mapping glyphs 25 through 28 to mapped data at offset 40
559            // mapped data
560            3, 2, 1,
561            100, 150,
562            8, 6, 7, 9
563        ];
564        let mut buf = BeBuffer::new();
565        buf = buf.extend(words);
566        let lookup = LookupU16::read(buf.data().into()).unwrap();
567        let expected = [
568            (20, 3),
569            (21, 2),
570            (22, 1),
571            (23, 100),
572            (24, 150),
573            (25, 8),
574            (26, 6),
575            (27, 7),
576            (28, 9),
577        ];
578        for (in_glyph, out_glyph) in expected {
579            assert_eq!(lookup.value(in_glyph).unwrap(), out_glyph);
580        }
581        for fail in [0, 10, 19, 29, 0xFFFF] {
582            assert!(lookup.value(fail).is_err());
583        }
584    }
585
586    // Taken from example 1 at https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
587    #[test]
588    fn lookup_format_6() {
589        #[rustfmt::skip]
590        let words = [
591            6_u16, // format
592            4,     // unit size (4 bytes)
593            4,     // number of units
594            16,    // search range
595            2,     // entry selector
596            0,     // range shift
597            50, 600, // Input glyph 50 maps to glyph 600
598            51, 601, // Input glyph 51 maps to glyph 601
599            201, 602, // Input glyph 201 maps to glyph 602
600            202, 900, // Input glyph 202 maps to glyph 900
601        ];
602        let mut buf = BeBuffer::new();
603        buf = buf.extend(words);
604        let lookup = LookupU16::read(buf.data().into()).unwrap();
605        let expected = [(50, 600), (51, 601), (201, 602), (202, 900)];
606        for (in_glyph, out_glyph) in expected {
607            assert_eq!(lookup.value(in_glyph).unwrap(), out_glyph);
608        }
609        for fail in [0, 10, 49, 52, 203, 0xFFFF] {
610            assert!(lookup.value(fail).is_err());
611        }
612    }
613
614    #[test]
615    fn lookup_format_8() {
616        #[rustfmt::skip]
617        let words = [
618            8_u16, // format
619            201,   // first glyph
620            7,     // glyph count
621            3, 8, 2, 9, 1, 200, 60, // glyphs 201..209 mapped to these values
622        ];
623        let mut buf = BeBuffer::new();
624        buf = buf.extend(words);
625        let lookup = LookupU16::read(buf.data().into()).unwrap();
626        let expected = &words[3..];
627        for (gid, expected) in (201..209).zip(expected) {
628            assert_eq!(lookup.value(gid).unwrap(), *expected);
629        }
630        for fail in [0, 10, 200, 210, 0xFFFF] {
631            assert!(lookup.value(fail).is_err());
632        }
633    }
634
635    #[test]
636    fn lookup_format_10() {
637        #[rustfmt::skip]
638        let words = [
639            10_u16, // format
640            4,      // unit size, use 4 byte values
641            201,   // first glyph
642            7,     // glyph count
643        ];
644        // glyphs 201..209 mapped to these values
645        let mapped = [3_u32, 8, 2902384, 9, 1, u32::MAX, 60];
646        let mut buf = BeBuffer::new();
647        buf = buf.extend(words).extend(mapped);
648        let lookup = LookupU32::read(buf.data().into()).unwrap();
649        for (gid, expected) in (201..209).zip(mapped) {
650            assert_eq!(lookup.value(gid).unwrap(), expected);
651        }
652        for fail in [0, 10, 200, 210, 0xFFFF] {
653            assert!(lookup.value(fail).is_err());
654        }
655    }
656
657    #[test]
658    fn extended_state_table() {
659        #[rustfmt::skip]
660        let header = [
661            6_u32, // number of classes
662            20, // byte offset to class table
663            56, // byte offset to state array
664            92, // byte offset to entry array
665            0, // padding
666        ];
667        #[rustfmt::skip]
668        let class_table = [
669            6_u16, // format
670            4,     // unit size (4 bytes)
671            5,     // number of units
672            16,    // search range
673            2,     // entry selector
674            0,     // range shift
675            50, 4, // Input glyph 50 maps to class 4
676            51, 4, // Input glyph 51 maps to class 4
677            80, 5, // Input glyph 80 maps to class 5
678            201, 4, // Input glyph 201 maps to class 4
679            202, 4, // Input glyph 202 maps to class 4
680            !0, !0
681        ];
682        #[rustfmt::skip]
683        let state_array: [u16; 18] = [
684            0, 0, 0, 0, 0, 1,
685            0, 0, 0, 0, 0, 1,
686            0, 0, 0, 0, 2, 1,
687        ];
688        #[rustfmt::skip]
689        let entry_table: [u16; 12] = [
690            0, 0, u16::MAX, u16::MAX,
691            2, 0, u16::MAX, u16::MAX,
692            0, 0, u16::MAX, 0,
693        ];
694        let buf = BeBuffer::new()
695            .extend(header)
696            .extend(class_table)
697            .extend(state_array)
698            .extend(entry_table);
699        let table = ExtendedStateTable::<ContextualData>::read(buf.data().into()).unwrap();
700        // check class lookups
701        let [class_50, class_80, class_201] =
702            [50, 80, 201].map(|gid| table.class(GlyphId::new(gid)).unwrap());
703        assert_eq!(class_50, 4);
704        assert_eq!(class_80, 5);
705        assert_eq!(class_201, 4);
706        // initial state
707        let entry = table.entry(0, 4).unwrap();
708        assert_eq!(entry.new_state, 0);
709        assert_eq!(entry.payload.current_index, !0);
710        // entry (state 0, class 5) should transition to state 2
711        let entry = table.entry(0, 5).unwrap();
712        assert_eq!(entry.new_state, 2);
713        // from state 2, we transition back to state 0 when class is not 5
714        // this also enables an action (payload.current_index != -1)
715        let entry = table.entry(2, 4).unwrap();
716        assert_eq!(entry.new_state, 0);
717        assert_eq!(entry.payload.current_index, 0);
718    }
719
720    #[derive(Copy, Clone, Debug, bytemuck::AnyBitPattern)]
721    #[repr(C, packed)]
722    struct ContextualData {
723        _mark_index: BigEndian<u16>,
724        current_index: BigEndian<u16>,
725    }
726
727    impl FixedSize for ContextualData {
728        const RAW_BYTE_LEN: usize = 4;
729    }
730
731    // Take from example at <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html>
732    // with class table trimmed to 4 glyphs
733    #[test]
734    fn state_table() {
735        #[rustfmt::skip]
736        let header = [
737            7_u16, // number of classes
738            10, // byte offset to class table
739            18, // byte offset to state array
740            40, // byte offset to entry array
741            64, // byte offset to value array (unused here)
742        ];
743        #[rustfmt::skip]
744        let class_table = [
745            3_u16, // first glyph
746            4, // number of glyphs
747        ];
748        let classes = [1u8, 2, 3, 4];
749        #[rustfmt::skip]
750        let state_array: [u8; 22] = [
751            2, 0, 0, 2, 1, 0, 0,
752            2, 0, 0, 2, 1, 0, 0,
753            2, 3, 3, 2, 3, 4, 5,
754            0, // padding
755        ];
756        #[rustfmt::skip]
757        let entry_table: [u16; 10] = [
758            // The first column are offsets from the beginning of the state
759            // table to some position in the state array
760            18, 0x8112,
761            32, 0x8112,
762            18, 0x0000,
763            32, 0x8114,
764            18, 0x8116,
765        ];
766        let buf = BeBuffer::new()
767            .extend(header)
768            .extend(class_table)
769            .extend(classes)
770            .extend(state_array)
771            .extend(entry_table);
772        let table = StateTable::read(buf.data().into()).unwrap();
773        // check class lookups
774        for i in 0..4u8 {
775            assert_eq!(table.class(GlyphId16::from(i as u16 + 3)).unwrap(), i + 1);
776        }
777        // (state, class) -> (new_state, flags)
778        let cases = [
779            ((0, 4), (2, 0x8112)),
780            ((2, 1), (2, 0x8114)),
781            ((1, 3), (0, 0x0000)),
782            ((2, 5), (0, 0x8116)),
783        ];
784        for ((state, class), (new_state, flags)) in cases {
785            let entry = table.entry(state, class).unwrap();
786            assert_eq!(
787                entry.new_state, new_state,
788                "state {state}, class {class} should map to new state {new_state} (got {})",
789                entry.new_state
790            );
791            assert_eq!(
792                entry.flags, flags,
793                "state {state}, class {class} should map to flags 0x{flags:X} (got 0x{:X})",
794                entry.flags
795            );
796        }
797    }
798}