Skip to main content

read_fonts/tables/
morx.rs

1//! The [morx (Extended Glyph Metamorphosis)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html) table.
2
3use super::aat::{safe_read_array_to_end, ExtendedStateTable, LookupU16, StateTableParts};
4
5include!("../../generated/generated_morx.rs");
6
7impl VarSize for Chain<'_> {
8    type Size = u32;
9
10    fn read_len_at(data: FontData, pos: usize) -> Option<usize> {
11        // Size in a chain is second field beyond 4 byte `defaultFlags`
12        data.read_at::<u32>(pos.checked_add(u32::RAW_BYTE_LEN)?)
13            .ok()
14            .map(|size| size as usize)
15    }
16}
17
18impl VarSize for Subtable<'_> {
19    type Size = u32;
20
21    fn read_len_at(data: FontData, pos: usize) -> Option<usize> {
22        // The default implementation assumes that the length field itself
23        // is not included in the total size which is not true of this
24        // table.
25        data.read_at::<u32>(pos).ok().map(|size| size as usize)
26    }
27}
28
29impl<'a> Subtable<'a> {
30    /// If true, this subtable will process glyphs in logical order (or reverse
31    /// logical order, depending on the value of bit 0x80000000).
32    #[inline]
33    pub fn is_logical(&self) -> bool {
34        self.coverage() & 0x10000000 != 0
35    }
36
37    /// If true, this subtable will be applied to both horizontal and vertical
38    /// text (i.e. the state of bit 0x80000000 is ignored).
39    #[inline]
40    pub fn is_all_directions(&self) -> bool {
41        self.coverage() & 0x20000000 != 0
42    }
43
44    /// If true, this subtable will process glyphs in descending order.
45    /// Otherwise, it will process the glyphs in ascending order.
46    #[inline]
47    pub fn is_backwards(&self) -> bool {
48        self.coverage() & 0x40000000 != 0
49    }
50
51    /// If true, this subtable will only be applied to vertical text.
52    /// Otherwise, this subtable will only be applied to horizontal
53    /// text.
54    #[inline]
55    pub fn is_vertical(&self) -> bool {
56        self.coverage() & 0x80000000 != 0
57    }
58
59    /// Returns an enum representing the actual subtable data.
60    pub fn kind(&self) -> Result<SubtableKind<'a>, ReadError> {
61        SubtableKind::read_with_args(FontData::new(self.data()), self.coverage())
62    }
63}
64
65/// The various `morx` subtable formats.
66#[derive(Clone)]
67pub enum SubtableKind<'a> {
68    Rearrangement(ExtendedStateTable<'a>),
69    Contextual(ContextualSubtable<'a>),
70    Ligature(LigatureSubtable<'a>),
71    NonContextual(LookupU16<'a>),
72    Insertion(InsertionSubtable<'a>),
73}
74
75impl ReadArgs for SubtableKind<'_> {
76    type Args = u32;
77}
78
79impl<'a> FontRead<'a> for SubtableKind<'a> {
80    fn read_with_args(data: FontData<'a>, args: Self::Args) -> Result<Self, ReadError> {
81        // Format is low byte of coverage
82        let format = args & 0xFF;
83        match format {
84            0 => Ok(Self::Rearrangement(ExtendedStateTable::read(data)?)),
85            1 => Ok(Self::Contextual(ContextualSubtable::read(data)?)),
86            2 => Ok(Self::Ligature(LigatureSubtable::read(data)?)),
87            // 3 is reserved
88            4 => Ok(Self::NonContextual(LookupU16::read(data)?)),
89            5 => Ok(Self::Insertion(InsertionSubtable::read(data)?)),
90            _ => Err(ReadError::InvalidFormat(format as _)),
91        }
92    }
93}
94
95/// Pre-resolved, lifetime-free description of a `morx` subtable's layout,
96/// captured once with [SubtableKind::parts] and replayed cheaply with
97/// [SubtableKind::from_parts] to avoid re-reading headers on every
98/// application.
99#[derive(Clone, Copy, Debug, Default)]
100pub struct SubtableParts {
101    /// Low byte of coverage: the subtable format (0/1/2/4/5).
102    pub format: u8,
103    pub state: StateTableParts,
104    /// Format-dependent extra offsets read after the state table header:
105    /// contextual: [lookups_offset, 0, 0]; ligature: [lig_action, component,
106    /// ligature]; insertion: [glyphs_offset, 0, 0]; others unused.
107    pub extra: [u32; 3],
108}
109
110impl<'a> SubtableKind<'a> {
111    /// Captures the offsets needed to rebuild this subtable kind from the
112    /// same data with [SubtableKind::from_parts].
113    pub fn parts(data: FontData<'a>, coverage: u32) -> Result<SubtableParts, ReadError> {
114        let format = (coverage & 0xFF) as u8;
115        let mut parts = SubtableParts {
116            format,
117            ..Default::default()
118        };
119        if format == 4 {
120            // Non-contextual: a bare lookup table, no state header.
121            return Ok(parts);
122        }
123        parts.state = StateTableParts::read(data)?;
124        let mut cursor = data.cursor();
125        cursor.advance_by(ExtendedStateTable::<()>::HEADER_LEN);
126        match format {
127            1 | 5 => {
128                parts.extra[0] = cursor.read::<u32>()?;
129            }
130            2 => {
131                parts.extra[0] = cursor.read::<u32>()?;
132                parts.extra[1] = cursor.read::<u32>()?;
133                parts.extra[2] = cursor.read::<u32>()?;
134            }
135            _ => {}
136        }
137        Ok(parts)
138    }
139
140    /// Rebuilds the subtable kind from `data` and offsets previously
141    /// captured with [SubtableKind::parts] on the same data.
142    #[inline]
143    pub fn from_parts(data: FontData<'a>, parts: &SubtableParts) -> Result<Self, ReadError> {
144        match parts.format {
145            0 => Ok(Self::Rearrangement(ExtendedStateTable::from_parts(
146                data,
147                &parts.state,
148            )?)),
149            1 => {
150                let state_table = ExtendedStateTable::from_parts(data, &parts.state)?;
151                let offset = parts.extra[0] as usize;
152                let end = data.len();
153                let offsets_data = FontData::new(data.read_array(offset..end)?);
154                let raw_offsets: &[BigEndian<Offset32>] = safe_read_array_to_end(&offsets_data, 0)?;
155                let lookups = ArrayOfOffsets::new(raw_offsets, offsets_data, ());
156                Ok(Self::Contextual(ContextualSubtable {
157                    state_table,
158                    lookups,
159                }))
160            }
161            2 => Ok(Self::Ligature(LigatureSubtable {
162                state_table: ExtendedStateTable::from_parts(data, &parts.state)?,
163                ligature_actions: safe_read_array_to_end(&data, parts.extra[0] as usize)?,
164                components: safe_read_array_to_end(&data, parts.extra[1] as usize)?,
165                ligatures: safe_read_array_to_end(&data, parts.extra[2] as usize)?,
166            })),
167            4 => Ok(Self::NonContextual(LookupU16::read(data)?)),
168            5 => Ok(Self::Insertion(InsertionSubtable {
169                state_table: ExtendedStateTable::from_parts(data, &parts.state)?,
170                glyphs: safe_read_array_to_end(&data, parts.extra[0] as usize)?,
171            })),
172            _ => Err(ReadError::InvalidFormat(parts.format as _)),
173        }
174    }
175}
176
177/// Contextual glyph substitution subtable.
178#[derive(Clone)]
179pub struct ContextualSubtable<'a> {
180    pub state_table: ExtendedStateTable<'a, ContextualEntryData>,
181    /// List of lookups specifying substitutions. The index into this array
182    /// is specified by the action in the state table.
183    pub lookups: ArrayOfOffsets<'a, LookupU16<'a>, Offset32>,
184}
185
186impl ReadArgs for ContextualSubtable<'_> {
187    type Args = ();
188}
189
190impl<'a> FontRead<'a> for ContextualSubtable<'a> {
191    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
192        let state_table = ExtendedStateTable::read(data)?;
193        let mut cursor = data.cursor();
194        cursor.advance_by(ExtendedStateTable::<()>::HEADER_LEN);
195        let offset = cursor.read::<u32>()? as usize;
196        let end = data.len();
197        let offsets_data = FontData::new(data.read_array(offset..end)?);
198        let raw_offsets: &[BigEndian<Offset32>] = safe_read_array_to_end(&offsets_data, 0)?;
199        let lookups = ArrayOfOffsets::new(raw_offsets, offsets_data, ());
200        Ok(Self {
201            state_table,
202            lookups,
203        })
204    }
205}
206
207/// Ligature glyph substitution subtable.
208#[derive(Clone)]
209pub struct LigatureSubtable<'a> {
210    pub state_table: ExtendedStateTable<'a, BigEndian<u16>>,
211    /// Contains the set of ligature stack actions, one for each state.
212    pub ligature_actions: &'a [BigEndian<u32>],
213    /// Array of component indices which are summed to determine the index
214    /// of the final ligature glyph.
215    pub components: &'a [BigEndian<u16>],
216    /// Output ligature glyphs.
217    pub ligatures: &'a [BigEndian<GlyphId16>],
218}
219
220impl ReadArgs for LigatureSubtable<'_> {
221    type Args = ();
222}
223
224impl<'a> FontRead<'a> for LigatureSubtable<'a> {
225    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
226        let state_table = ExtendedStateTable::read(data)?;
227        let mut cursor = data.cursor();
228        cursor.advance_by(ExtendedStateTable::<()>::HEADER_LEN);
229        // None of these arrays have associated sizes, so we just read until
230        // the end of the data.
231        let lig_action_offset = cursor.read::<u32>()? as usize;
232        let component_offset = cursor.read::<u32>()? as usize;
233        let ligature_offset = cursor.read::<u32>()? as usize;
234        let ligature_actions = safe_read_array_to_end(&data, lig_action_offset)?;
235        let components = safe_read_array_to_end(&data, component_offset)?;
236        let ligatures = safe_read_array_to_end(&data, ligature_offset)?;
237        Ok(Self {
238            state_table,
239            ligature_actions,
240            components,
241            ligatures,
242        })
243    }
244}
245
246/// Insertion glyph substitution subtable.
247#[derive(Clone)]
248pub struct InsertionSubtable<'a> {
249    pub state_table: ExtendedStateTable<'a, InsertionEntryData>,
250    /// Insertion glyph table. The index and count of glyphs to insert is
251    /// determined by the state machine.
252    pub glyphs: &'a [BigEndian<GlyphId16>],
253}
254
255impl ReadArgs for InsertionSubtable<'_> {
256    type Args = ();
257}
258
259impl<'a> FontRead<'a> for InsertionSubtable<'a> {
260    fn read_with_args(data: FontData<'a>, _: ()) -> Result<Self, ReadError> {
261        let state_table = ExtendedStateTable::read(data)?;
262        let mut cursor = data.cursor();
263        cursor.advance_by(ExtendedStateTable::<()>::HEADER_LEN);
264        let glyphs_offset = cursor.read::<u32>()? as usize;
265        let glyphs = safe_read_array_to_end(&data, glyphs_offset)?;
266        Ok(Self {
267            state_table,
268            glyphs,
269        })
270    }
271}
272
273#[cfg(feature = "experimental_traverse")]
274impl<'a> SomeRecord<'a> for Chain<'a> {
275    fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> {
276        RecordResolver {
277            name: "Chain",
278            get_field: Box::new(move |idx, _data| match idx {
279                0usize => Some(Field::new("default_flags", self.default_flags())),
280                _ => None,
281            }),
282            data,
283        }
284    }
285}
286
287#[cfg(feature = "experimental_traverse")]
288impl<'a> SomeRecord<'a> for Subtable<'a> {
289    fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> {
290        RecordResolver {
291            name: "Subtable",
292            get_field: Box::new(move |idx, _data| match idx {
293                0usize => Some(Field::new("coverage", self.coverage())),
294                1usize => Some(Field::new("sub_feature_flags", self.sub_feature_flags())),
295                _ => None,
296            }),
297            data,
298        }
299    }
300}
301
302#[cfg(test)]
303// Literal bytes are grouped according to layout in the spec
304// for readabiity
305#[allow(clippy::unusual_byte_groupings)]
306mod tests {
307    use super::*;
308    use crate::{FontRef, TableProvider};
309
310    #[test]
311    fn parse_chain_flags_features() {
312        let font = FontRef::new(font_test_data::morx::FOUR).unwrap();
313        let morx = font.morx().unwrap();
314        let chain = morx.chains().iter().next().unwrap().unwrap();
315        assert_eq!(chain.default_flags(), 1);
316        let feature = chain.features()[0];
317        assert_eq!(feature.feature_type(), 4);
318        assert_eq!(feature.feature_settings(), 0);
319        assert_eq!(feature.enable_flags(), 1);
320        assert_eq!(feature.disable_flags(), 0xFFFFFFFF);
321    }
322
323    #[test]
324    fn parse_rearrangement() {
325        let font = FontRef::new(font_test_data::morx::FOUR).unwrap();
326        let morx = font.morx().unwrap();
327        let chain = morx.chains().iter().next().unwrap().unwrap();
328        let subtable = chain.subtables().iter().next().unwrap().unwrap();
329        assert_eq!(subtable.coverage(), 0x20_0000_00);
330        // Rearrangement is just a state table
331        let SubtableKind::Rearrangement(_kind) = subtable.kind().unwrap() else {
332            panic!("expected rearrangement subtable!");
333        };
334    }
335
336    #[test]
337    fn parse_contextual() {
338        let font = FontRef::new(font_test_data::morx::EIGHTEEN).unwrap();
339        let morx = font.morx().unwrap();
340        let chain = morx.chains().iter().next().unwrap().unwrap();
341        let subtable = chain.subtables().iter().next().unwrap().unwrap();
342        assert_eq!(subtable.coverage(), 0x20_0000_01);
343        let SubtableKind::Contextual(kind) = subtable.kind().unwrap() else {
344            panic!("expected contextual subtable!");
345        };
346        let lookup = kind.lookups.get(0).unwrap();
347        let expected = [None, None, Some(7u16), Some(8), Some(9), Some(10), Some(11)];
348        let values = (0..7).map(|gid| lookup.value(gid).ok()).collect::<Vec<_>>();
349        assert_eq!(values, &expected);
350    }
351
352    #[test]
353    fn parse_ligature() {
354        let font = FontRef::new(font_test_data::morx::FORTY_ONE).unwrap();
355        let morx = font.morx().unwrap();
356        let chain = morx.chains().iter().next().unwrap().unwrap();
357        let subtable = chain.subtables().iter().next().unwrap().unwrap();
358        assert_eq!(subtable.coverage(), 0x20_0000_02);
359        let SubtableKind::Ligature(kind) = subtable.kind().unwrap() else {
360            panic!("expected ligature subtable!");
361        };
362        let expected_actions = [0x3FFFFFFE, 0xBFFFFFFE];
363        // Note, we limit the number of elements because the arrays do not
364        // have specified lengths in the table
365        let actions = kind
366            .ligature_actions
367            .iter()
368            .take(2)
369            .map(|action| action.get())
370            .collect::<Vec<_>>();
371        assert_eq!(actions, &expected_actions);
372        let expected_components = [0u16, 1, 0, 0];
373        // See above explanation for the limit
374        let components = kind
375            .components
376            .iter()
377            .take(4)
378            .map(|comp| comp.get())
379            .collect::<Vec<_>>();
380        assert_eq!(components, &expected_components);
381        let expected_ligatures = [GlyphId16::new(5), GlyphId16::new(6)];
382        let ligatures = kind
383            .ligatures
384            .iter()
385            .map(|gid| gid.get())
386            .collect::<Vec<_>>();
387        assert_eq!(ligatures, &expected_ligatures);
388    }
389
390    #[test]
391    fn parse_non_contextual() {
392        let font = FontRef::new(font_test_data::morx::ONE).unwrap();
393        let morx = font.morx().unwrap();
394        let chain = morx.chains().iter().next().unwrap().unwrap();
395        let subtable = chain.subtables().iter().next().unwrap().unwrap();
396        assert_eq!(subtable.coverage(), 0x20_0000_04);
397        let SubtableKind::NonContextual(kind) = subtable.kind().unwrap() else {
398            panic!("expected non-contextual subtable!");
399        };
400        let expected_values = [None, None, Some(5u16), None, Some(7)];
401        let values = (0..5).map(|gid| kind.value(gid).ok()).collect::<Vec<_>>();
402        assert_eq!(values, &expected_values);
403    }
404
405    #[test]
406    fn parse_insertion() {
407        let font = FontRef::new(font_test_data::morx::THIRTY_FOUR).unwrap();
408        let morx = font.morx().unwrap();
409        let chain = morx.chains().iter().next().unwrap().unwrap();
410        let subtable = chain.subtables().iter().next().unwrap().unwrap();
411        assert_eq!(subtable.coverage(), 0x20_0000_05);
412        let SubtableKind::Insertion(kind) = subtable.kind().unwrap() else {
413            panic!("expected insertion subtable!");
414        };
415        let mut expected_glyphs = vec![];
416        for _ in 0..9 {
417            for gid in [3, 2] {
418                expected_glyphs.push(GlyphId16::new(gid));
419            }
420        }
421        assert_eq!(kind.glyphs, &expected_glyphs);
422    }
423}