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