Skip to main content

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