Skip to main content

read_fonts/tables/
gpos.rs

1//! the [GPOS] table
2//!
3//! [GPOS]: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
4
5#[path = "./value_record.rs"]
6mod value_record;
7
8#[cfg(feature = "std")]
9mod closure;
10
11use crate::array::ComputedArray;
12
13/// reexport stuff from layout that we use
14pub use super::layout::{
15    ClassDef, CoverageTable, Device, DeviceOrVariationIndex, FeatureList, FeatureVariations,
16    Lookup, ScriptList,
17};
18use super::layout::{ExtensionLookup, LookupFlag, Subtables};
19pub use value_record::{Value, ValueContext, ValueRecord};
20
21#[cfg(test)]
22#[path = "../tests/test_gpos.rs"]
23mod spec_tests;
24
25include!("../../generated/generated_gpos.rs");
26
27/// A typed GPOS [LookupList](super::layout::LookupList) table
28pub type PositionLookupList<'a> = super::layout::LookupList<'a, PositionLookup<'a>>;
29
30/// A GPOS [SequenceContext](super::layout::SequenceContext)
31pub type PositionSequenceContext<'a> = super::layout::SequenceContext<'a>;
32
33/// A GPOS [ChainedSequenceContext](super::layout::ChainedSequenceContext)
34pub type PositionChainContext<'a> = super::layout::ChainedSequenceContext<'a>;
35
36impl<'a> AnchorTable<'a> {
37    /// Attempt to resolve the `Device` or `VariationIndex` table for the
38    /// x_coordinate, if present
39    pub fn x_device(&self) -> Option<Result<DeviceOrVariationIndex<'a>, ReadError>> {
40        match self {
41            AnchorTable::Format3(inner) => inner.x_device(),
42            _ => None,
43        }
44    }
45
46    /// Attempt to resolve the `Device` or `VariationIndex` table for the
47    /// y_coordinate, if present
48    pub fn y_device(&self) -> Option<Result<DeviceOrVariationIndex<'a>, ReadError>> {
49        match self {
50            AnchorTable::Format3(inner) => inner.y_device(),
51            _ => None,
52        }
53    }
54}
55
56impl<'a, T: FontRead<'a>> ExtensionLookup<'a, T> for ExtensionPosFormat1<'a, T> {
57    fn extension(&self) -> Result<T, ReadError> {
58        self.extension()
59    }
60}
61
62type PosSubtables<'a, T> = Subtables<'a, T, ExtensionPosFormat1<'a, T>>;
63
64/// The subtables from a GPOS lookup.
65///
66/// This type is a convenience that removes the need to dig into the
67/// [`PositionLookup`] enum in order to access subtables, and it also abstracts
68/// away the distinction between extension and non-extension lookups.
69pub enum PositionSubtables<'a> {
70    Single(PosSubtables<'a, SinglePos<'a>>),
71    Pair(PosSubtables<'a, PairPos<'a>>),
72    Cursive(PosSubtables<'a, CursivePosFormat1<'a>>),
73    MarkToBase(PosSubtables<'a, MarkBasePosFormat1<'a>>),
74    MarkToLig(PosSubtables<'a, MarkLigPosFormat1<'a>>),
75    MarkToMark(PosSubtables<'a, MarkMarkPosFormat1<'a>>),
76    Contextual(PosSubtables<'a, PositionSequenceContext<'a>>),
77    ChainContextual(PosSubtables<'a, PositionChainContext<'a>>),
78    /// An extension lookup did not have any subtables
79    EmptyExtension,
80}
81
82impl<'a> PositionLookup<'a> {
83    pub fn lookup_flag(&self) -> LookupFlag {
84        self.of_unit_type().lookup_flag()
85    }
86
87    /// Different enumerations for GSUB and GPOS
88    pub fn lookup_type(&self) -> u16 {
89        self.of_unit_type().lookup_type()
90    }
91
92    pub fn mark_filtering_set(&self) -> Option<u16> {
93        self.of_unit_type().mark_filtering_set()
94    }
95
96    /// Return the subtables for this lookup.
97    ///
98    /// This method handles both extension and non-extension lookups, and saves
99    /// the caller needing to dig into the `PositionLookup` enum itself.
100    pub fn subtables(&self) -> Result<PositionSubtables<'a>, ReadError> {
101        let raw_lookup = self.of_unit_type();
102        let offsets = raw_lookup.subtable_offsets();
103        let data = raw_lookup.offset_data();
104        match raw_lookup.lookup_type() {
105            1 => Ok(PositionSubtables::Single(Subtables::new(offsets, data))),
106            2 => Ok(PositionSubtables::Pair(Subtables::new(offsets, data))),
107            3 => Ok(PositionSubtables::Cursive(Subtables::new(offsets, data))),
108            4 => Ok(PositionSubtables::MarkToBase(Subtables::new(offsets, data))),
109            5 => Ok(PositionSubtables::MarkToLig(Subtables::new(offsets, data))),
110            6 => Ok(PositionSubtables::MarkToMark(Subtables::new(offsets, data))),
111            7 => Ok(PositionSubtables::Contextual(Subtables::new(offsets, data))),
112            8 => Ok(PositionSubtables::ChainContextual(Subtables::new(
113                offsets, data,
114            ))),
115            9 => {
116                // look through subtable offsets to try and find a lookup type.
117                // this is robust in the case where the first subtable offset is
118                // malformed, but a later one is okay.
119                let Some(lookup_type) = offsets.iter().find_map(|off| {
120                    off.get()
121                        .resolve::<ExtensionPosFormat1<()>>(data)
122                        .ok()
123                        .map(|ext| ext.extension_lookup_type())
124                }) else {
125                    return Ok(PositionSubtables::EmptyExtension);
126                };
127
128                match lookup_type {
129                    1 => Ok(PositionSubtables::Single(Subtables::new_ext(offsets, data))),
130                    2 => Ok(PositionSubtables::Pair(Subtables::new_ext(offsets, data))),
131                    3 => Ok(PositionSubtables::Cursive(Subtables::new_ext(
132                        offsets, data,
133                    ))),
134                    4 => Ok(PositionSubtables::MarkToBase(Subtables::new_ext(
135                        offsets, data,
136                    ))),
137                    5 => Ok(PositionSubtables::MarkToLig(Subtables::new_ext(
138                        offsets, data,
139                    ))),
140                    6 => Ok(PositionSubtables::MarkToMark(Subtables::new_ext(
141                        offsets, data,
142                    ))),
143                    7 => Ok(PositionSubtables::Contextual(Subtables::new_ext(
144                        offsets, data,
145                    ))),
146                    8 => Ok(PositionSubtables::ChainContextual(Subtables::new_ext(
147                        offsets, data,
148                    ))),
149                    other => Err(ReadError::InvalidFormat(other as _)),
150                }
151            }
152            other => Err(ReadError::InvalidFormat(other as _)),
153        }
154    }
155}
156
157impl PairPosFormat2<'_> {
158    /// Returns the pair of values for the given classes, optionally accounting
159    /// for variations.
160    ///
161    /// The `class1` and `class2` parameters can be computed by passing the
162    /// first and second glyphs of the pair to the [`ClassDef`]s returned by
163    /// [`Self::class_def1`] and [`Self::class_def2`] respectively.
164    #[inline]
165    pub fn values(
166        &self,
167        class1: u16,
168        class2: u16,
169        context: &ValueContext,
170    ) -> Result<[Value; 2], ReadError> {
171        let format1 = self.value_format1();
172        let format1_len = format1.record_byte_len();
173        let format2 = self.value_format2();
174        let record_size = format1_len + format2.record_byte_len();
175        let data = self.offset_data();
176        // Compute an offset into the 2D array of positioning records
177        let record_offset = (class1 as usize * record_size * self.class2_count() as usize)
178            + (class2 as usize * record_size)
179            + self.class1_records_byte_range().start;
180        Ok([
181            Value::read(data, record_offset, format1, context)?,
182            Value::read(data, record_offset + format1_len, format2, context)?,
183        ])
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn pair_pos2_values_match_value_records() {
193        let data = FontData::new(font_test_data::gpos::PAIRPOSFORMAT2);
194        let table = PairPosFormat2::read(data).unwrap();
195        let class1_count = table.class1_count();
196        let class2_count = table.class2_count();
197        let records = table.class1_records();
198        let context = ValueContext::default();
199        for class1 in 0..class1_count {
200            let class1_record = records.get(class1 as usize).unwrap();
201            let class2_records = class1_record.class2_records();
202            for class2 in 0..class2_count {
203                let record = class2_records.get(class2 as usize).unwrap();
204                let value_records = [record.value_record1, record.value_record2]
205                    .map(|rec| rec.value(data, &context).unwrap());
206                let values = table.values(class1, class2, &context).unwrap();
207                assert_eq!(value_records, values);
208            }
209        }
210    }
211
212    #[test]
213    fn default_for_generics() {
214        let ExtensionSubtable::Single(inner) = ExtensionSubtable::default() else {
215            panic!("this is quite bad");
216        };
217
218        // this is invalid, but we the default impl for the extension offset
219        // will be the first variant of the enum anyway
220        assert_eq!(inner.extension_lookup_type(), 0);
221
222        let SinglePos::Format1(_hmm) = inner.extension().unwrap_or_default() else {
223            panic!("unexpected");
224        };
225    }
226}