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
13use super::layout::header::HeaderOffset;
14/// reexport stuff from layout that we use
15pub use super::layout::{
16    ClassDef, CoverageTable, Device, DeviceOrVariationIndex, FeatureList, FeatureVariations,
17    Lookup, ScriptList,
18};
19use super::layout::{ExtensionLookup, LookupFlag, SelectedLookupList, Subtables};
20pub use value_record::ValueRecord;
21
22#[cfg(test)]
23#[path = "../tests/test_gpos.rs"]
24mod spec_tests;
25
26use crate::types::Compatible;
27
28include!("../../generated/generated_gpos.rs");
29
30impl<'a> Gpos<'a> {
31    /// The offset to the script list, original or extended.
32    pub(crate) fn selected_script_list_offset(&self) -> Result<HeaderOffset, ReadError> {
33        self.select_offset(self.script_list_offset(), self.script_list2_offset())
34    }
35
36    /// The offset to the feature list, original or extended.
37    pub(crate) fn selected_feature_list_offset(&self) -> Result<HeaderOffset, ReadError> {
38        self.select_offset(self.feature_list_offset(), self.feature_list2_offset())
39    }
40
41    /// The offset to the lookup list, original or extended.
42    pub(crate) fn selected_lookup_list_offset(&self) -> Result<HeaderOffset, ReadError> {
43        self.select_offset(self.lookup_list_offset(), self.lookup_list2_offset())
44    }
45
46    fn select_offset(
47        &self,
48        original: Nullable<Offset16>,
49        extended: Option<Nullable<Offset32>>,
50    ) -> Result<HeaderOffset, ReadError> {
51        HeaderOffset::select(original, extended, self.version().compatible((1, 2)))
52    }
53
54    /// Resolves the table, preferring the 32-bit offset when it is nonzero.
55    pub fn script_list(&self) -> Result<ScriptList<'a>, ReadError> {
56        self.selected_script_list_offset()?
57            .offset()
58            .resolve(self.offset_data())
59    }
60
61    /// Resolves the original 16-bit offset without applying precedence.
62    pub fn original_script_list(&self) -> Option<Result<ScriptList<'a>, ReadError>> {
63        self.script_list_offset().resolve(self.offset_data())
64    }
65
66    /// Resolves the table, preferring the 32-bit offset when it is nonzero.
67    pub fn feature_list(&self) -> Result<FeatureList<'a>, ReadError> {
68        self.selected_feature_list_offset()?
69            .offset()
70            .resolve(self.offset_data())
71    }
72
73    /// Resolves the original 16-bit offset without applying precedence.
74    pub fn original_feature_list(&self) -> Option<Result<FeatureList<'a>, ReadError>> {
75        self.feature_list_offset().resolve(self.offset_data())
76    }
77
78    /// Resolves the lookup list, preferring LookupList2 when its offset is nonzero.
79    pub fn lookup_list(&self) -> Result<PositionLookupList<'a>, ReadError> {
80        let data = self.offset_data();
81        match self.selected_lookup_list_offset()? {
82            HeaderOffset::Original(offset) => {
83                offset.resolve(data).map(SelectedLookupList::Original)
84            }
85            HeaderOffset::Extended(offset) => {
86                offset.resolve(data).map(SelectedLookupList::Extended)
87            }
88        }
89    }
90
91    /// Resolves the original 16-bit lookup-list offset.
92    pub fn original_lookup_list(
93        &self,
94    ) -> Option<Result<OriginalPositionLookupList<'a>, ReadError>> {
95        self.lookup_list_offset().resolve(self.offset_data())
96    }
97}
98
99/// The GPOS lookup list selected by the header.
100pub type PositionLookupList<'a> = super::layout::SelectedLookupList<'a, PositionLookup<'a>>;
101
102/// A lookup list with the original 16-bit offsets.
103pub type OriginalPositionLookupList<'a> = super::layout::LookupList<'a, PositionLookup<'a>>;
104
105/// A lookup list with 32-bit offsets.
106pub type PositionLookupList2<'a> = super::layout::LookupList2<'a, PositionLookup<'a>>;
107
108/// A GPOS [SequenceContext](super::layout::SequenceContext)
109pub type PositionSequenceContext<'a> = super::layout::SequenceContext<'a>;
110
111/// A GPOS [ChainedSequenceContext](super::layout::ChainedSequenceContext)
112pub type PositionChainContext<'a> = super::layout::ChainedSequenceContext<'a>;
113
114impl<'a> AnchorTable<'a> {
115    /// Attempt to resolve the `Device` or `VariationIndex` table for the
116    /// x_coordinate, if present
117    pub fn x_device(&self) -> Option<Result<DeviceOrVariationIndex<'a>, ReadError>> {
118        match self {
119            AnchorTable::Format3(inner) => inner.x_device(),
120            _ => None,
121        }
122    }
123
124    /// Attempt to resolve the `Device` or `VariationIndex` table for the
125    /// y_coordinate, if present
126    pub fn y_device(&self) -> Option<Result<DeviceOrVariationIndex<'a>, ReadError>> {
127        match self {
128            AnchorTable::Format3(inner) => inner.y_device(),
129            _ => None,
130        }
131    }
132}
133
134impl<'a, T: FontRead<'a, Args = ()>> ExtensionLookup<'a, T> for ExtensionPosFormat1<'a, T> {
135    fn extension(&self) -> Result<T, ReadError> {
136        self.extension()
137    }
138}
139
140type PosSubtables<'a, T> = Subtables<'a, T, ExtensionPosFormat1<'a, T>>;
141
142/// The subtables from a GPOS lookup.
143///
144/// This type is a convenience that removes the need to dig into the
145/// [`PositionLookup`] enum in order to access subtables, and it also abstracts
146/// away the distinction between extension and non-extension lookups.
147pub enum PositionSubtables<'a> {
148    Single(PosSubtables<'a, SinglePos<'a>>),
149    Pair(PosSubtables<'a, PairPos<'a>>),
150    Cursive(PosSubtables<'a, CursivePos<'a>>),
151    MarkToBase(PosSubtables<'a, MarkBasePos<'a>>),
152    MarkToLig(PosSubtables<'a, MarkLigPos<'a>>),
153    MarkToMark(PosSubtables<'a, MarkMarkPos<'a>>),
154    Contextual(PosSubtables<'a, PositionSequenceContext<'a>>),
155    ChainContextual(PosSubtables<'a, PositionChainContext<'a>>),
156    /// An extension lookup did not have any subtables
157    EmptyExtension,
158}
159
160impl<'a> PositionLookup<'a> {
161    pub fn lookup_flag(&self) -> LookupFlag {
162        self.of_unit_type().lookup_flag()
163    }
164
165    /// Different enumerations for GSUB and GPOS
166    pub fn lookup_type(&self) -> u16 {
167        self.of_unit_type().lookup_type()
168    }
169
170    pub fn mark_filtering_set(&self) -> Option<u16> {
171        self.of_unit_type().mark_filtering_set()
172    }
173
174    /// Return the subtables for this lookup.
175    ///
176    /// This method handles both extension and non-extension lookups, and saves
177    /// the caller needing to dig into the `PositionLookup` enum itself.
178    pub fn subtables(&self) -> Result<PositionSubtables<'a>, ReadError> {
179        let raw_lookup = self.of_unit_type();
180        let offsets = raw_lookup.subtable_offsets();
181        let data = raw_lookup.offset_data();
182        match raw_lookup.lookup_type() {
183            1 => Ok(PositionSubtables::Single(Subtables::new(offsets, data))),
184            2 => Ok(PositionSubtables::Pair(Subtables::new(offsets, data))),
185            3 => Ok(PositionSubtables::Cursive(Subtables::new(offsets, data))),
186            4 => Ok(PositionSubtables::MarkToBase(Subtables::new(offsets, data))),
187            5 => Ok(PositionSubtables::MarkToLig(Subtables::new(offsets, data))),
188            6 => Ok(PositionSubtables::MarkToMark(Subtables::new(offsets, data))),
189            7 => Ok(PositionSubtables::Contextual(Subtables::new(offsets, data))),
190            8 => Ok(PositionSubtables::ChainContextual(Subtables::new(
191                offsets, data,
192            ))),
193            9 => {
194                // look through subtable offsets to try and find a lookup type.
195                // this is robust in the case where the first subtable offset is
196                // malformed, but a later one is okay.
197                let Some(lookup_type) = offsets.iter().find_map(|off| {
198                    off.get()
199                        .resolve::<ExtensionPosFormat1<()>>(data)
200                        .ok()
201                        .map(|ext| ext.extension_lookup_type())
202                }) else {
203                    return Ok(PositionSubtables::EmptyExtension);
204                };
205
206                match lookup_type {
207                    1 => Ok(PositionSubtables::Single(Subtables::new_ext(offsets, data))),
208                    2 => Ok(PositionSubtables::Pair(Subtables::new_ext(offsets, data))),
209                    3 => Ok(PositionSubtables::Cursive(Subtables::new_ext(
210                        offsets, data,
211                    ))),
212                    4 => Ok(PositionSubtables::MarkToBase(Subtables::new_ext(
213                        offsets, data,
214                    ))),
215                    5 => Ok(PositionSubtables::MarkToLig(Subtables::new_ext(
216                        offsets, data,
217                    ))),
218                    6 => Ok(PositionSubtables::MarkToMark(Subtables::new_ext(
219                        offsets, data,
220                    ))),
221                    7 => Ok(PositionSubtables::Contextual(Subtables::new_ext(
222                        offsets, data,
223                    ))),
224                    8 => Ok(PositionSubtables::ChainContextual(Subtables::new_ext(
225                        offsets, data,
226                    ))),
227                    other => Err(ReadError::InvalidFormat(other as _)),
228                }
229            }
230            other => Err(ReadError::InvalidFormat(other as _)),
231        }
232    }
233}
234
235/// Extends the generated pair value records with a binary search.
236impl<'a> PairSet<'a> {
237    /// Returns the pair value record for `second_glyph`, using a binary search.
238    ///
239    /// Pair value records are ordered by second glyph, and are located rather
240    /// than read, so this reads only the glyph id of each probed record.
241    pub fn find_pair_value_record(&self, second_glyph: GlyphId16) -> Option<PairValueRecord<'a>> {
242        let records = self.pair_value_records();
243        let (mut lo, mut hi) = (0usize, records.len());
244        while lo < hi {
245            // deliberately not usize::midpoint, which widens to u128; these
246            // values are bounded by a u16 count
247            #[allow(clippy::manual_midpoint)]
248            let mid = (lo + hi) / 2;
249            let record = records.get(mid).ok()?;
250            match record.second_glyph().cmp(&second_glyph) {
251                core::cmp::Ordering::Less => lo = mid + 1,
252                core::cmp::Ordering::Greater => hi = mid,
253                core::cmp::Ordering::Equal => return Some(record),
254            }
255        }
256        None
257    }
258}
259
260/// The two-dimensional array of value records in a [`PairPosFormat2`], located
261/// but not read.
262///
263/// The class counts, value formats and record stride are read once when this is
264/// built, so addressing a record afterwards is pure arithmetic. Callers walking
265/// many class pairs should build this once and reuse it rather than going
266/// through [`PairPosFormat2::value_record_refs`], which rebuilds it every call.
267#[derive(Copy, Clone)]
268pub struct ClassValueRecords<'a> {
269    data: FontData<'a>,
270    start: u32,
271    record_size: u32,
272    format1_len: u32,
273    class1_count: u16,
274    class2_count: u16,
275    format1: ValueFormat,
276    format2: ValueFormat,
277}
278
279impl<'a> ClassValueRecords<'a> {
280    /// Number of classes in classDef1, including class 0.
281    #[inline]
282    pub fn class1_count(&self) -> u16 {
283        self.class1_count
284    }
285
286    /// Number of classes in classDef2, including class 0.
287    #[inline]
288    pub fn class2_count(&self) -> u16 {
289        self.class2_count
290    }
291
292    /// Returns the pair of value records for the given classes.
293    ///
294    /// Returns `None` if either class is out of range.
295    #[inline]
296    pub fn get(&self, class1: u16, class2: u16) -> Option<[ValueRecord<'a>; 2]> {
297        if class1 >= self.class1_count || class2 >= self.class2_count {
298            return None;
299        }
300        // Compute an offset into the 2D array of positioning records. Every
301        // term is bounded by a u16 count times a u16-derived stride, so this
302        // cannot overflow usize on any supported target.
303        let record_offset = self.start as usize
304            + class1 as usize * self.record_size as usize * self.class2_count as usize
305            + class2 as usize * self.record_size as usize;
306        Some([
307            ValueRecord::new(self.data, record_offset, self.format1),
308            ValueRecord::new(
309                self.data,
310                record_offset + self.format1_len as usize,
311                self.format2,
312            ),
313        ])
314    }
315}
316
317impl<'a> PairPosFormat2<'a> {
318    /// Returns the two-dimensional array of class value records, located but
319    /// not read.
320    pub fn class_value_records(&self) -> ClassValueRecords<'a> {
321        let format1 = self.value_format1();
322        let format2 = self.value_format2();
323        let format1_len = format1.record_byte_len();
324        ClassValueRecords {
325            data: self.offset_data(),
326            start: self.class1_records_byte_range().start as u32,
327            record_size: (format1_len + format2.record_byte_len()) as u32,
328            format1_len: format1_len as u32,
329            class1_count: self.class1_count(),
330            class2_count: self.class2_count(),
331            format1,
332            format2,
333        }
334    }
335
336    /// Returns the pair of value records for the given classes, located but
337    /// not read.
338    ///
339    /// Returns `None` if either class is out of range. When reading more than
340    /// one class pair, prefer [`class_value_records`][Self::class_value_records],
341    /// which reads the table's counts and formats only once.
342    #[inline]
343    pub fn value_record_refs(&self, class1: u16, class2: u16) -> Option<[ValueRecord<'a>; 2]> {
344        self.class_value_records().get(class1, class2)
345    }
346}
347
348impl<'a> PairPosFormat4<'a> {
349    /// Returns the two-dimensional array of class value records, located but
350    /// not read.
351    pub fn class_value_records(&self) -> ClassValueRecords<'a> {
352        let format1 = self.value_format1();
353        let format2 = self.value_format2();
354        let format1_len = format1.record_byte_len();
355        ClassValueRecords {
356            data: self.offset_data(),
357            start: self.class1_records_byte_range().start as u32,
358            record_size: (format1_len + format2.record_byte_len()) as u32,
359            format1_len: format1_len as u32,
360            class1_count: self.class1_count(),
361            class2_count: self.class2_count(),
362            format1,
363            format2,
364        }
365    }
366
367    /// Returns the pair of value records for the given classes, located but
368    /// not read.
369    ///
370    /// Returns `None` if either class is out of range. When reading more than
371    /// one class pair, prefer [`class_value_records`][Self::class_value_records],
372    /// which reads the table's counts and formats only once.
373    #[inline]
374    pub fn value_record_refs(&self, class1: u16, class2: u16) -> Option<[ValueRecord<'a>; 2]> {
375        self.class_value_records().get(class1, class2)
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    /// A value record is located, not read, so two ways of arriving at the
384    /// same record must land on the same bytes.
385    fn assert_same_record(a: ValueRecord, b: ValueRecord) {
386        assert_eq!(a.format(), b.format());
387        assert_eq!(a.bytes(), b.bytes());
388        assert_eq!(a, b);
389    }
390
391    /// `class_value_records` reads the counts and formats once and addresses
392    /// the 2D array arithmetically; walking `class1_records` reads them per
393    /// row. Both must reach the same records.
394    #[test]
395    fn pair_pos2_hoisted_matches_nested() {
396        let data = FontData::new(font_test_data::gpos::PAIRPOSFORMAT2);
397        let table = PairPosFormat2::read(data).unwrap();
398        let class1_count = table.class1_count();
399        let class2_count = table.class2_count();
400        assert!(class1_count > 0 && class2_count > 0);
401
402        let hoisted = table.class_value_records();
403        let rows = table.class1_records();
404        for class1 in 0..class1_count {
405            let row = rows.get(class1 as usize).unwrap();
406            let cells = row.class2_records();
407            for class2 in 0..class2_count {
408                let cell = cells.get(class2 as usize).unwrap();
409                let [a, b] = hoisted.get(class1, class2).unwrap();
410                assert_same_record(a, cell.value_record1);
411                assert_same_record(b, cell.value_record2);
412            }
413        }
414        assert!(hoisted.get(class1_count, 0).is_none());
415        assert!(hoisted.get(0, class2_count).is_none());
416    }
417
418    /// The binary search must find exactly what a linear scan finds.
419    #[test]
420    fn pair_set_find_matches_scan() {
421        let data = FontData::new(font_test_data::gpos::PAIRPOSFORMAT1);
422        let table = PairPosFormat1::read(data).unwrap();
423        let mut seen = 0;
424        for set_offset in table.pair_set_offsets() {
425            let pair_set = set_offset
426                .get()
427                .resolve_with_args::<PairSet>(data, (table.value_format1(), table.value_format2()))
428                .unwrap();
429            let records = pair_set.pair_value_records();
430            assert!(!records.is_empty());
431            for i in 0..records.len() {
432                let want = records.get(i).unwrap();
433                let found = pair_set
434                    .find_pair_value_record(want.second_glyph())
435                    .unwrap();
436                assert_eq!(found.second_glyph(), want.second_glyph());
437                assert_same_record(found.value_record1, want.value_record1);
438                assert_same_record(found.value_record2, want.value_record2);
439                seen += 1;
440            }
441            assert!(pair_set
442                .find_pair_value_record(GlyphId16::new(0xFFFF))
443                .is_none());
444        }
445        assert!(seen > 0);
446    }
447
448    /// Device offsets in a value record resolve against the table containing
449    /// the record, so a located record must carry that table's data.
450    #[test]
451    fn value_record_resolves_devices_against_its_table() {
452        let data = FontData::new(font_test_data::gpos::VALUEFORMATTABLE);
453        let table = SinglePosFormat1::read(data).unwrap();
454        let record = table.value_record();
455        assert!(record
456            .format()
457            .intersects(ValueFormat::ANY_DEVICE_OR_VARIDX));
458        // the offsets are measured from the start of the table, not the record,
459        // which begins six bytes in
460        assert!(record.offset() > 0);
461        assert!(record.x_placement_device().unwrap().is_ok());
462        assert!(record.y_advance_device().unwrap().is_ok());
463    }
464
465    #[test]
466    fn single_pos2_records_are_evenly_spaced() {
467        let data = FontData::new(font_test_data::gpos::SINGLEPOSFORMAT2);
468        let table = SinglePosFormat2::read(data).unwrap();
469        let records = table.value_records();
470        let stride = table.value_format().record_byte_len();
471        assert!(records.len() > 1);
472        let first = records.get(0).unwrap();
473        for i in 0..records.len() {
474            let rec = records.get(i).unwrap();
475            assert_eq!(rec.offset(), first.offset() + i * stride);
476            assert_eq!(rec.format(), table.value_format());
477        }
478        assert!(records.get(records.len()).is_err());
479    }
480
481    #[test]
482    fn default_for_generics() {
483        let ExtensionSubtable::Single(inner) = ExtensionSubtable::default() else {
484            panic!("this is quite bad");
485        };
486
487        // this is invalid, but we the default impl for the extension offset
488        // will be the first variant of the enum anyway
489        assert_eq!(inner.extension_lookup_type(), 0);
490
491        let SinglePos::Format1(_hmm) = inner.extension().unwrap_or_default() else {
492            panic!("unexpected");
493        };
494    }
495}