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