Skip to main content

cranpose_render_common/
gpos_kerning.rs

1//! Pair kerning read out of a font's `GPOS` table.
2//!
3//! `ab_glyph`'s `kern()` reads the TrueType `kern` table and nothing else. Neither
4//! font this framework actually draws with has one: Android's
5//! `/system/fonts/Roboto-Regular.ttf` carries its pair kerning in `GPOS` only,
6//! and so does the embedded `NotoSansMerged` fallback. Every string Cranpose
7//! measured or drew was therefore unkerned while the platform's own text stack
8//! kerned it — `LY` by −239 font units, `To` by −99, `AV` by −87 on Roboto.
9//!
10//! This reads the `kern` feature's pair-adjustment lookups directly, because
11//! that is the whole of the defect. It is deliberately *not* a shaper: no glyph
12//! substitution, no mark attachment, no cursive joining, no script-aware
13//! reordering. Those are `GSUB`, `mark`/`mkmk` and `curs`, and adopting them
14//! means adopting a shaping engine. What is implemented here is the one thing
15//! the measurement named, and the boundary is stated in
16//! [`GposKerning::parse`].
17//!
18//! Two behaviours are copied from HarfBuzz rather than from the OpenType prose,
19//! because HarfBuzz is what Android runs and therefore what fonts are compiled
20//! against:
21//!
22//! * a `PairPosFormat1` value record's device offset is measured from its
23//!   `PairSet`, not from the subtable. The specification says subtable; every
24//!   `VariationIndex` in Roboto only resolves with `PairSet`, so the ambiguity
25//!   is settled by the font.
26//! * within one lookup the first subtable that covers a pair wins and the rest
27//!   are skipped — including when the value it found is zero. Separate lookups
28//!   accumulate. Dropping zero-valued pairs would let a later subtable answer
29//!   for a pair an earlier one deliberately pinned at zero.
30
31use std::sync::Arc;
32
33use ab_glyph::{CodepointIdIter, Font, FontArc, GlyphId, GlyphSvg, Outline, v2};
34use ttf_parser::{Face, NormalizedCoordinate, Tag};
35
36use crate::{
37    ascii_glyphs::AsciiGlyphs,
38    font_features::{FeatureVariants, GlyphSubstitutions},
39};
40
41const VALUE_FIELDS: [u16; 8] = [
42    0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080,
43];
44const X_ADVANCE: u16 = 0x0004;
45const X_ADVANCE_DEVICE: u16 = 0x0040;
46
47const VARIATION_INDEX: u16 = 0x8000;
48
49#[derive(Debug, Clone)]
50struct PairLookup {
51    subtables: Vec<PairSubtable>,
52}
53
54#[derive(Debug, Clone)]
55enum PairSubtable {
56    Specific {
57        pairs: Vec<(u32, f32)>,
58    },
59    Class {
60        coverage: Vec<u16>,
61        first: ClassDef,
62        second: ClassDef,
63        second_count: u16,
64        matrix: Vec<f32>,
65    },
66}
67
68#[derive(Debug, Clone, Default)]
69struct ClassDef {
70    ranges: Vec<(u16, u16, u16)>,
71}
72
73impl ClassDef {
74    fn class_of(&self, glyph: u16) -> u16 {
75        match self.ranges.binary_search_by(|(start, end, _)| {
76            if glyph < *start {
77                std::cmp::Ordering::Greater
78            } else if glyph > *end {
79                std::cmp::Ordering::Less
80            } else {
81                std::cmp::Ordering::Equal
82            }
83        }) {
84            Ok(index) => self.ranges[index].2,
85            Err(_) => 0,
86        }
87    }
88
89    fn from_pairs(mut pairs: Vec<(u16, u16)>) -> Self {
90        pairs.sort_unstable();
91        let mut ranges: Vec<(u16, u16, u16)> = Vec::new();
92        for (glyph, class) in pairs {
93            if class == 0 {
94                continue;
95            }
96            match ranges.last_mut() {
97                Some((_, end, last_class))
98                    if *last_class == class && glyph == end.saturating_add(1) =>
99                {
100                    *end = glyph;
101                }
102                _ => ranges.push((glyph, glyph, class)),
103            }
104        }
105        Self { ranges }
106    }
107}
108
109/// Every `kern`-feature pair adjustment in a face, resolved for one instance.
110///
111/// "One instance" is load-bearing: on a variable face the values carry
112/// `VariationIndex` deltas, and Roboto's are worth up to 136 font units at the
113/// weights a Wear app asks for. They are resolved once here against the face's
114/// normalized coordinates, so a query is a lookup rather than a delta
115/// evaluation — and two instances of one file get two tables, which is already
116/// how the rest of the font cache treats them.
117#[derive(Debug, Clone, Default)]
118pub struct GposKerning {
119    lookups: Vec<PairLookup>,
120}
121
122impl GposKerning {
123    /// Reads the `kern` feature's pair adjustments out of `face`.
124    ///
125    /// Returns `None` when the face has no `GPOS`, no `kern` feature, or no
126    /// pair-adjustment lookup under it — all of which mean "nothing to add",
127    /// not "something went wrong".
128    ///
129    /// **Feature selection is the union over every script's default language
130    /// system**, rather than the script of the run being measured. HarfBuzz
131    /// picks one script per run, which needs the run's script, which needs
132    /// itemization — the first piece of a shaper. The union is safe in
133    /// practice because a lookup can only fire on a pair it covers, and it is
134    /// exact for the fonts in play: Roboto lists the identical feature set
135    /// under `DFLT`, `latn`, `cyrl` and `grek`. A face that kerned one script
136    /// differently from another through *different* `kern` lookups would need
137    /// the real thing; nothing here does.
138    ///
139    /// Language-specific systems are ignored for the same reason, and so is
140    /// `lookupFlag`: its glyph filters (`IgnoreMarks`, mark attachment classes)
141    /// only bite on runs with marks in them, which is again shaping.
142    pub fn parse(face: &Face<'_>) -> Option<Self> {
143        let gpos = face.raw_face().table(Tag::from_bytes(b"GPOS"))?;
144        let coordinates = face.variation_coordinates();
145        let gdef = face.tables().gdef;
146
147        let script_list = u16_at(gpos, 4)? as usize;
148        let feature_list = u16_at(gpos, 6)? as usize;
149        let lookup_list = u16_at(gpos, 8)? as usize;
150
151        let feature_indices = kern_feature_indices(gpos, script_list, feature_list)?;
152        let mut lookup_indices = Vec::new();
153        for index in feature_indices {
154            for lookup in feature_lookups(gpos, feature_list, index)? {
155                if !lookup_indices.contains(&lookup) {
156                    lookup_indices.push(lookup);
157                }
158            }
159        }
160        lookup_indices.sort_unstable();
161
162        let mut lookups = Vec::new();
163        for index in lookup_indices {
164            let Some(subtables) = pair_lookup(gpos, lookup_list, index, gdef, coordinates) else {
165                continue;
166            };
167            if !subtables.is_empty() {
168                lookups.push(PairLookup { subtables });
169            }
170        }
171
172        (!lookups.is_empty()).then_some(Self { lookups })
173    }
174
175    /// The pair adjustment for `first` followed by `second`, in font units.
176    pub fn kern_unscaled(&self, first: u16, second: u16) -> f32 {
177        let mut total = 0.0;
178        for lookup in &self.lookups {
179            for subtable in &lookup.subtables {
180                if let Some(value) = subtable.lookup(first, second) {
181                    total += value;
182                    break;
183                }
184            }
185        }
186        total
187    }
188
189    pub fn is_empty(&self) -> bool {
190        self.lookups.is_empty()
191    }
192}
193
194impl PairSubtable {
195    fn lookup(&self, first: u16, second: u16) -> Option<f32> {
196        match self {
197            Self::Specific { pairs } => {
198                let key = (u32::from(first) << 16) | u32::from(second);
199                pairs
200                    .binary_search_by_key(&key, |(pair, _)| *pair)
201                    .ok()
202                    .map(|index| pairs[index].1)
203            }
204            Self::Class {
205                coverage,
206                first: first_classes,
207                second: second_classes,
208                second_count,
209                matrix,
210            } => {
211                coverage.binary_search(&first).ok()?;
212                let row = first_classes.class_of(first);
213                let column = second_classes.class_of(second);
214                if column >= *second_count {
215                    return None;
216                }
217                let index = usize::from(row) * usize::from(*second_count) + usize::from(column);
218                matrix.get(index).copied()
219            }
220        }
221    }
222}
223
224fn u16_at(data: &[u8], offset: usize) -> Option<u16> {
225    let bytes = data.get(offset..offset + 2)?;
226    Some(u16::from_be_bytes([bytes[0], bytes[1]]))
227}
228
229fn i16_at(data: &[u8], offset: usize) -> Option<i16> {
230    u16_at(data, offset).map(|value| value as i16)
231}
232
233fn u32_at(data: &[u8], offset: usize) -> Option<u32> {
234    let bytes = data.get(offset..offset + 4)?;
235    Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
236}
237
238fn kern_feature_indices(gpos: &[u8], script_list: usize, feature_list: usize) -> Option<Vec<u16>> {
239    let script_count = u16_at(gpos, script_list)?;
240    let mut reachable = Vec::new();
241    for index in 0..script_count {
242        let record = script_list + 2 + usize::from(index) * 6;
243        let script = script_list + usize::from(u16_at(gpos, record + 4)?);
244        let default_lang_sys = u16_at(gpos, script)?;
245        if default_lang_sys == 0 {
246            continue;
247        }
248        let lang_sys = script + usize::from(default_lang_sys);
249        let count = u16_at(gpos, lang_sys + 4)?;
250        for slot in 0..count {
251            let feature = u16_at(gpos, lang_sys + 6 + usize::from(slot) * 2)?;
252            if !reachable.contains(&feature) {
253                reachable.push(feature);
254            }
255        }
256    }
257
258    let feature_count = u16_at(gpos, feature_list)?;
259    let mut kern = Vec::new();
260    for index in reachable {
261        if index >= feature_count {
262            continue;
263        }
264        let record = feature_list + 2 + usize::from(index) * 6;
265        let tag = gpos.get(record..record + 4)?;
266        if tag == b"kern" {
267            kern.push(index);
268        }
269    }
270    Some(kern)
271}
272
273fn feature_lookups(gpos: &[u8], feature_list: usize, index: u16) -> Option<Vec<u16>> {
274    let record = feature_list + 2 + usize::from(index) * 6;
275    let feature = feature_list + usize::from(u16_at(gpos, record + 4)?);
276    let count = u16_at(gpos, feature + 2)?;
277    let mut lookups = Vec::with_capacity(usize::from(count));
278    for slot in 0..count {
279        lookups.push(u16_at(gpos, feature + 4 + usize::from(slot) * 2)?);
280    }
281    Some(lookups)
282}
283
284fn pair_lookup(
285    gpos: &[u8],
286    lookup_list: usize,
287    index: u16,
288    gdef: Option<ttf_parser::gdef::Table<'_>>,
289    coordinates: &[NormalizedCoordinate],
290) -> Option<Vec<PairSubtable>> {
291    let count = u16_at(gpos, lookup_list)?;
292    if index >= count {
293        return None;
294    }
295    let lookup = lookup_list + usize::from(u16_at(gpos, lookup_list + 2 + usize::from(index) * 2)?);
296    let kind = u16_at(gpos, lookup)?;
297    let subtable_count = u16_at(gpos, lookup + 4)?;
298
299    let mut subtables = Vec::new();
300    for slot in 0..subtable_count {
301        let mut offset = lookup + usize::from(u16_at(gpos, lookup + 6 + usize::from(slot) * 2)?);
302        let mut resolved = kind;
303        if resolved == 9 {
304            resolved = u16_at(gpos, offset + 2)?;
305            offset += u32_at(gpos, offset + 4)? as usize;
306        }
307        if resolved != 2 {
308            continue;
309        }
310        if let Some(subtable) = pair_subtable(gpos, offset, gdef, coordinates) {
311            subtables.push(subtable);
312        }
313    }
314    Some(subtables)
315}
316
317fn pair_subtable(
318    gpos: &[u8],
319    offset: usize,
320    gdef: Option<ttf_parser::gdef::Table<'_>>,
321    coordinates: &[NormalizedCoordinate],
322) -> Option<PairSubtable> {
323    let format = u16_at(gpos, offset)?;
324    let coverage_offset = offset + usize::from(u16_at(gpos, offset + 2)?);
325    let value_format_1 = u16_at(gpos, offset + 4)?;
326    let value_format_2 = u16_at(gpos, offset + 6)?;
327    let coverage = coverage_glyphs(gpos, coverage_offset)?;
328
329    match format {
330        1 => {
331            let set_count = u16_at(gpos, offset + 8)?;
332            let record_len = 2 + value_size(value_format_1) + value_size(value_format_2);
333            let mut pairs = Vec::new();
334            for slot in 0..set_count {
335                let Some(&first) = coverage.get(usize::from(slot)) else {
336                    continue;
337                };
338                let set = offset + usize::from(u16_at(gpos, offset + 10 + usize::from(slot) * 2)?);
339                let count = u16_at(gpos, set)?;
340                for record in 0..count {
341                    let at = set + 2 + usize::from(record) * record_len;
342                    let Some(second) = u16_at(gpos, at) else {
343                        continue;
344                    };
345                    let value = x_advance(gpos, at + 2, value_format_1, set, gdef, coordinates);
346                    pairs.push(((u32::from(first) << 16) | u32::from(second), value));
347                }
348            }
349            pairs.sort_by_key(|(key, _)| *key);
350            pairs.dedup_by_key(|(key, _)| *key);
351            (!pairs.is_empty()).then_some(PairSubtable::Specific { pairs })
352        }
353        2 => {
354            let first = class_def(gpos, offset + usize::from(u16_at(gpos, offset + 8)?))?;
355            let second = class_def(gpos, offset + usize::from(u16_at(gpos, offset + 10)?))?;
356            let first_count = u16_at(gpos, offset + 12)?;
357            let second_count = u16_at(gpos, offset + 14)?;
358            let record_len = value_size(value_format_1) + value_size(value_format_2);
359            let cells = usize::from(first_count) * usize::from(second_count);
360            let mut matrix = Vec::with_capacity(cells);
361            for cell in 0..cells {
362                let at = offset + 16 + cell * record_len;
363                matrix.push(x_advance(
364                    gpos,
365                    at,
366                    value_format_1,
367                    offset,
368                    gdef,
369                    coordinates,
370                ));
371            }
372            Some(PairSubtable::Class {
373                coverage,
374                first,
375                second,
376                second_count,
377                matrix,
378            })
379        }
380        _ => None,
381    }
382}
383
384fn value_size(format: u16) -> usize {
385    usize::try_from((format & 0xFF).count_ones()).unwrap_or(0) * 2
386}
387
388fn x_advance(
389    gpos: &[u8],
390    at: usize,
391    format: u16,
392    device_base: usize,
393    gdef: Option<ttf_parser::gdef::Table<'_>>,
394    coordinates: &[NormalizedCoordinate],
395) -> f32 {
396    let mut cursor = at;
397    let mut advance = 0.0f32;
398    for field in VALUE_FIELDS {
399        if format & field == 0 {
400            continue;
401        }
402        if field == X_ADVANCE {
403            advance += f32::from(i16_at(gpos, cursor).unwrap_or(0));
404        }
405        if field == X_ADVANCE_DEVICE {
406            let device = u16_at(gpos, cursor).unwrap_or(0);
407            if device != 0 && !coordinates.is_empty() {
408                let table = device_base + usize::from(device);
409                if u16_at(gpos, table + 4) == Some(VARIATION_INDEX) {
410                    let outer = u16_at(gpos, table).unwrap_or(0);
411                    let inner = u16_at(gpos, table + 2).unwrap_or(0);
412                    advance += gdef
413                        .and_then(|gdef| gdef.glyph_variation_delta(outer, inner, coordinates))
414                        .unwrap_or(0.0);
415                }
416            }
417        }
418        cursor += 2;
419    }
420    advance
421}
422
423fn coverage_glyphs(gpos: &[u8], offset: usize) -> Option<Vec<u16>> {
424    match u16_at(gpos, offset)? {
425        1 => {
426            let count = u16_at(gpos, offset + 2)?;
427            let mut glyphs = Vec::with_capacity(usize::from(count));
428            for index in 0..count {
429                glyphs.push(u16_at(gpos, offset + 4 + usize::from(index) * 2)?);
430            }
431            Some(glyphs)
432        }
433        2 => {
434            let count = u16_at(gpos, offset + 2)?;
435            let mut glyphs = Vec::new();
436            for index in 0..count {
437                let record = offset + 4 + usize::from(index) * 6;
438                let start = u16_at(gpos, record)?;
439                let end = u16_at(gpos, record + 2)?;
440                for glyph in start..=end {
441                    glyphs.push(glyph);
442                }
443            }
444            Some(glyphs)
445        }
446        _ => None,
447    }
448}
449
450fn class_def(gpos: &[u8], offset: usize) -> Option<ClassDef> {
451    let mut pairs: Vec<(u16, u16)> = Vec::new();
452    match u16_at(gpos, offset)? {
453        1 => {
454            let start = u16_at(gpos, offset + 2)?;
455            let count = u16_at(gpos, offset + 4)?;
456            for index in 0..count {
457                let class = u16_at(gpos, offset + 6 + usize::from(index) * 2)?;
458                pairs.push((start.saturating_add(index), class));
459            }
460        }
461        2 => {
462            let count = u16_at(gpos, offset + 2)?;
463            for index in 0..count {
464                let record = offset + 4 + usize::from(index) * 6;
465                let start = u16_at(gpos, record)?;
466                let end = u16_at(gpos, record + 2)?;
467                let class = u16_at(gpos, record + 4)?;
468                for glyph in start..=end {
469                    pairs.push((glyph, class));
470                }
471            }
472        }
473        _ => return Some(ClassDef::default()),
474    }
475    Some(ClassDef::from_pairs(pairs))
476}
477
478/// A face paired with the pair kerning `ab_glyph` cannot read for itself,
479/// and with the single glyph substitutions of the OpenType features a text
480/// style enables.
481///
482/// This is a `Font` rather than a lookup the call sites reach for, because
483/// there are nine of them across measuring, line breaking, alignment and
484/// drawing, several behind `impl Font` generics that never see a
485/// `SoftwareTextFont`. Overriding `kern_unscaled` puts the rule under every
486/// existing `scaled_font.kern(..)` at once — including any this change did not
487/// go looking for — and keeps a caller from having to remember which width is
488/// the kerned one. Overriding `glyph_id` does the same for substitution: every
489/// glyph a run measures, kerns and draws is the substituted one.
490///
491/// Substitution covers `GSUB` single substitutions only (lookup type 1, and
492/// type 7 extensions of it), which is what `tnum`, `pnum`, `onum`, `lnum`,
493/// `zero`, `smcp`, `c2sc`, `case`, `sups`, `subs`, `salt` and `ss01`–`ss20`
494/// use. A feature with any other lookup type (ligature, multiple, alternate,
495/// contextual) is not applied at all, so none is ever half applied, and no
496/// feature is on unless the style's `font_feature_settings` turns it on.
497#[derive(Clone)]
498pub struct KernedFont {
499    font: FontArc,
500    kerning: Option<Arc<GposKerning>>,
501    ascii: Arc<AsciiGlyphs>,
502    substitutions: Option<Arc<GlyphSubstitutions>>,
503    variants: Arc<FeatureVariants>,
504}
505
506impl KernedFont {
507    pub fn new(font: FontArc, kerning: Option<Arc<GposKerning>>) -> Self {
508        let ascii = Arc::new(AsciiGlyphs::new());
509        Self {
510            font,
511            kerning,
512            variants: Arc::new(FeatureVariants::new(Arc::clone(&ascii))),
513            ascii,
514            substitutions: None,
515        }
516    }
517
518    pub(crate) fn with_feature_settings(&self, settings: Option<&str>) -> Option<Self> {
519        let wanted =
520            settings.and_then(|settings| self.variants.get(settings, self.font.font_data()));
521        let (substitutions, ascii) = match wanted {
522            Some(wanted)
523                if self
524                    .substitutions
525                    .as_ref()
526                    .is_some_and(|current| current.tags() == wanted.substitutions.tags()) =>
527            {
528                return None;
529            }
530            Some(wanted) => (Some(wanted.substitutions), wanted.ascii),
531            None if self.substitutions.is_none() => return None,
532            None => (None, Arc::clone(self.variants.base_ascii())),
533        };
534        Some(Self {
535            font: self.font.clone(),
536            kerning: self.kerning.clone(),
537            ascii,
538            substitutions,
539            variants: Arc::clone(&self.variants),
540        })
541    }
542
543    pub(crate) fn feature_key(&self) -> u64 {
544        self.substitutions
545            .as_ref()
546            .map_or(0, |substitutions| substitutions.key())
547    }
548
549    /// The glyph of `ch` and its advance in font units.
550    pub(crate) fn glyph_advance(&self, ch: char) -> (GlyphId, f32) {
551        self.ascii_glyph(ch).unwrap_or_else(|| {
552            let id = self.glyph_id(ch);
553            (id, self.h_advance_unscaled(id))
554        })
555    }
556
557    /// The glyph of a printable ASCII `ch` and its advance in font units;
558    /// `None` for any other character, which the caller's own cache answers.
559    pub(crate) fn ascii_glyph(&self, ch: char) -> Option<(GlyphId, f32)> {
560        self.ascii.glyph(self, ch)
561    }
562
563    /// The kerning in font units between the glyphs of two consecutive
564    /// printable ASCII characters; `None` for any other pair, which the
565    /// caller's own cache answers.
566    pub(crate) fn ascii_kern(
567        &self,
568        previous: (char, GlyphId),
569        next: (char, GlyphId),
570    ) -> Option<f32> {
571        self.ascii.kern(self, previous, next)
572    }
573
574    /// Reads the GPOS pair kerning for a face instanced at `variations`.
575    ///
576    /// `variations` are the `(tag, user value)` pairs actually applied to the
577    /// face, in the same units the caller handed `set_variation`; normalizing
578    /// them is `ttf_parser`'s job and doing it a second way here would be a
579    /// second answer.
580    pub fn read_kerning(bytes: &[u8], variations: &[([u8; 4], f32)]) -> Option<Arc<GposKerning>> {
581        let mut face = Face::parse(bytes, 0).ok()?;
582        for (tag, value) in variations {
583            face.set_variation(Tag::from_bytes(tag), *value);
584        }
585        GposKerning::parse(&face).map(Arc::new)
586    }
587}
588
589impl Font for KernedFont {
590    fn units_per_em(&self) -> Option<f32> {
591        self.font.units_per_em()
592    }
593
594    fn ascent_unscaled(&self) -> f32 {
595        self.font.ascent_unscaled()
596    }
597
598    fn descent_unscaled(&self) -> f32 {
599        self.font.descent_unscaled()
600    }
601
602    fn line_gap_unscaled(&self) -> f32 {
603        self.font.line_gap_unscaled()
604    }
605
606    fn italic_angle(&self) -> f32 {
607        self.font.italic_angle()
608    }
609
610    fn glyph_id(&self, c: char) -> GlyphId {
611        let id = self.font.glyph_id(c);
612        match &self.substitutions {
613            Some(substitutions) => substitutions.apply(id),
614            None => id,
615        }
616    }
617
618    fn h_advance_unscaled(&self, id: GlyphId) -> f32 {
619        self.font.h_advance_unscaled(id)
620    }
621
622    fn h_side_bearing_unscaled(&self, id: GlyphId) -> f32 {
623        self.font.h_side_bearing_unscaled(id)
624    }
625
626    fn v_advance_unscaled(&self, id: GlyphId) -> f32 {
627        self.font.v_advance_unscaled(id)
628    }
629
630    fn v_side_bearing_unscaled(&self, id: GlyphId) -> f32 {
631        self.font.v_side_bearing_unscaled(id)
632    }
633
634    fn kern_unscaled(&self, first: GlyphId, second: GlyphId) -> f32 {
635        match &self.kerning {
636            Some(kerning) => kerning.kern_unscaled(first.0, second.0),
637            None => self.font.kern_unscaled(first, second),
638        }
639    }
640
641    fn outline(&self, id: GlyphId) -> Option<Outline> {
642        self.font.outline(id)
643    }
644
645    fn glyph_count(&self) -> usize {
646        self.font.glyph_count()
647    }
648
649    fn codepoint_ids(&self) -> CodepointIdIter<'_> {
650        self.font.codepoint_ids()
651    }
652
653    fn glyph_raster_image2(&self, id: GlyphId, pixel_size: u16) -> Option<v2::GlyphImage<'_>> {
654        self.font.glyph_raster_image2(id, pixel_size)
655    }
656
657    fn glyph_svg_image(&self, id: GlyphId) -> Option<GlyphSvg<'_>> {
658        self.font.glyph_svg_image(id)
659    }
660
661    fn font_data(&self) -> &[u8] {
662        self.font.font_data()
663    }
664}
665
666#[cfg(test)]
667#[path = "tests/gpos_kerning_tests.rs"]
668mod tests;