Skip to main content

harfrust/hb/ot/
mod.rs

1use super::buffer::GlyphPropsFlags;
2use super::ot_layout::TableIndex;
3use super::{common::TagExt, set_digest::hb_set_digest_t};
4use crate::hb::hb_tag_t;
5use crate::hb::ot_layout_gsubgpos::{BinaryCache, MappingCache};
6use crate::hb::tables::TableRanges;
7use alloc::vec::Vec;
8use lookup::{LookupCache, LookupInfo};
9use read_fonts::tables::layout::{ClassRangeRecord, RangeRecord};
10use read_fonts::types::GlyphId16;
11use read_fonts::{
12    tables::{
13        gdef::Gdef,
14        gpos::{AnchorTable, DeviceOrVariationIndex, Gpos},
15        gsub::{ClassDef, FeatureList, FeatureVariations, Gsub, ScriptList},
16        layout::{Feature, LangSys, Script},
17        varc::{Condition, CoverageTable},
18        variations::{DeltaSetIndex, ItemVariationStore},
19    },
20    types::{BigEndian, F2Dot14, GlyphId, Offset32},
21    FontData, FontRef, ReadError, ResolveOffset, TableProvider,
22};
23
24pub mod contextual;
25pub mod gpos;
26pub mod gsub;
27pub mod lookup;
28
29pub struct OtCache {
30    pub gsub: LookupCache,
31    pub gpos: LookupCache,
32    pub gdef_glyph_props_cache: MappingCache,
33    pub gdef_mark_set_digests: Vec<hb_set_digest_t>,
34}
35
36impl OtCache {
37    pub fn new<'a>(font: &impl TableProvider<'a>) -> Self {
38        let gsub = font
39            .gsub()
40            .map(|t| LookupCache::new(&t))
41            .unwrap_or_default();
42        let gpos = font
43            .gpos()
44            .map(|t| LookupCache::new(&t))
45            .unwrap_or_default();
46        let mut gdef_mark_set_digests = Vec::new();
47        if let Ok(gdef) = font.gdef() {
48            if let Some(Ok(mark_sets)) = gdef.mark_glyph_sets_def() {
49                gdef_mark_set_digests.extend(mark_sets.coverages().iter().map(|set| {
50                    set.ok()
51                        .map(|coverage| hb_set_digest_t::from_coverage(&coverage))
52                        .unwrap_or_default()
53                }));
54            }
55        }
56        Self {
57            gsub,
58            gpos,
59            gdef_glyph_props_cache: MappingCache::new(),
60            gdef_mark_set_digests,
61        }
62    }
63}
64
65#[derive(Clone)]
66pub struct GsubTable<'a> {
67    pub table: Gsub<'a>,
68    pub lookups: &'a LookupCache,
69}
70
71impl crate::hb::ot_layout::LayoutTable for GsubTable<'_> {
72    const INDEX: TableIndex = TableIndex::GSUB;
73    const IN_PLACE: bool = false;
74
75    fn get_lookup(&self, index: u16) -> Option<&LookupInfo> {
76        self.lookups.get(&self.table, index)
77    }
78}
79
80#[derive(Clone)]
81pub struct GposTable<'a> {
82    pub table: Gpos<'a>,
83    pub lookups: &'a LookupCache,
84}
85
86impl crate::hb::ot_layout::LayoutTable for GposTable<'_> {
87    const INDEX: TableIndex = TableIndex::GPOS;
88    const IN_PLACE: bool = true;
89
90    fn get_lookup(&self, index: u16) -> Option<&LookupInfo> {
91        self.lookups.get(&self.table, index)
92    }
93}
94
95#[derive(Clone, Default)]
96pub struct GdefTable<'a> {
97    pub(crate) table: Option<Gdef<'a>>,
98    classes: Option<ClassDef<'a>>,
99    mark_classes: Option<ClassDef<'a>>,
100    mark_sets: Option<(FontData<'a>, &'a [BigEndian<Offset32>])>,
101}
102
103impl<'a> GdefTable<'a> {
104    fn new(gdef: Gdef<'a>) -> Self {
105        let classes = gdef.glyph_class_def().transpose().ok().flatten();
106        let mark_classes = gdef.mark_attach_class_def().transpose().ok().flatten();
107        let mark_sets = gdef
108            .mark_glyph_sets_def()
109            .transpose()
110            .ok()
111            .flatten()
112            .map(|sets| (sets.offset_data(), sets.coverage_offsets()));
113        Self {
114            table: Some(gdef),
115            classes,
116            mark_classes,
117            mark_sets,
118        }
119    }
120}
121
122#[derive(Clone)]
123pub struct OtTables<'a> {
124    pub gsub: Option<GsubTable<'a>>,
125    pub gpos: Option<GposTable<'a>>,
126    pub gdef: GdefTable<'a>,
127    pub gdef_glyph_props_cache: &'a MappingCache,
128    pub gdef_mark_set_digests: &'a [hb_set_digest_t],
129    pub coords: &'a [F2Dot14],
130    pub var_store: Option<ItemVariationStore<'a>>,
131    pub feature_variations: [Option<u32>; 2],
132}
133
134impl<'a> OtTables<'a> {
135    pub fn new(
136        font: &FontRef<'a>,
137        cache: &'a OtCache,
138        table_offsets: &TableRanges,
139        coords: &'a [F2Dot14],
140        feature_variations: [Option<u32>; 2],
141    ) -> Self {
142        let gsub = table_offsets
143            .gsub
144            .resolve_table(font)
145            .map(|table| GsubTable {
146                table,
147                lookups: &cache.gsub,
148            });
149        let gpos = table_offsets
150            .gpos
151            .resolve_table(font)
152            .map(|table| GposTable {
153                table,
154                lookups: &cache.gpos,
155            });
156        let coords = if coords.iter().any(|coord| *coord != F2Dot14::ZERO) {
157            coords
158        } else {
159            &[]
160        };
161        let gdef = if is_gdef_blocklisted(
162            table_offsets.gdef.len(),
163            table_offsets.gsub.len(),
164            table_offsets.gpos.len(),
165        ) {
166            GdefTable::default()
167        } else {
168            table_offsets
169                .gdef
170                .resolve_table(font)
171                .map(GdefTable::new)
172                .unwrap_or_default()
173        };
174        let var_store = if !coords.is_empty() {
175            gdef.table
176                .as_ref()
177                .and_then(|gdef| gdef.item_var_store().transpose().ok().flatten())
178        } else {
179            None
180        };
181        Self {
182            gsub,
183            gpos,
184            gdef,
185            gdef_glyph_props_cache: &cache.gdef_glyph_props_cache,
186            gdef_mark_set_digests: &cache.gdef_mark_set_digests,
187            var_store,
188            coords,
189            feature_variations,
190        }
191    }
192
193    pub fn from_tables(
194        font: &impl TableProvider<'a>,
195        cache: &'a OtCache,
196        coords: &'a [F2Dot14],
197        feature_variations: [Option<u32>; 2],
198    ) -> Self {
199        let gsub = font.gsub().ok().map(|table| GsubTable {
200            table,
201            lookups: &cache.gsub,
202        });
203        let gpos = font.gpos().ok().map(|table| GposTable {
204            table,
205            lookups: &cache.gpos,
206        });
207        let coords = if coords.iter().any(|coord| *coord != F2Dot14::ZERO) {
208            coords
209        } else {
210            &[]
211        };
212        let gdef = if let Ok(gdef) = font.gdef() {
213            let gsub_len = gsub
214                .as_ref()
215                .map(|t| t.table.offset_data().len() as u32)
216                .unwrap_or_default();
217            let gpos_len = gpos
218                .as_ref()
219                .map(|t| t.table.offset_data().len() as u32)
220                .unwrap_or_default();
221            if is_gdef_blocklisted(gdef.offset_data().len() as u32, gsub_len, gpos_len) {
222                GdefTable::default()
223            } else {
224                GdefTable::new(gdef)
225            }
226        } else {
227            GdefTable::default()
228        };
229        let var_store = if !coords.is_empty() {
230            gdef.table
231                .as_ref()
232                .and_then(|gdef| gdef.item_var_store().transpose().ok().flatten())
233        } else {
234            None
235        };
236        Self {
237            gsub,
238            gpos,
239            gdef,
240            gdef_glyph_props_cache: &cache.gdef_glyph_props_cache,
241            gdef_mark_set_digests: &cache.gdef_mark_set_digests,
242            var_store,
243            coords,
244            feature_variations,
245        }
246    }
247
248    pub fn has_glyph_classes(&self) -> bool {
249        self.gdef.classes.is_some()
250    }
251
252    pub fn glyph_class(&self, glyph_id: u32) -> u16 {
253        self.gdef
254            .classes
255            .as_ref()
256            .map_or(0, |class_def| class_def.get(glyph_id))
257    }
258
259    pub fn glyph_mark_attachment_class(&self, glyph_id: u32) -> u16 {
260        self.gdef
261            .mark_classes
262            .as_ref()
263            .map_or(0, |class_def| class_def.get(glyph_id))
264    }
265
266    pub(crate) fn glyph_props(&self, glyph: GlyphId) -> u16 {
267        let glyph = glyph.to_u32();
268
269        if let Some(props) = self.gdef_glyph_props_cache.get(glyph) {
270            return props as u16;
271        }
272
273        let props = match self.glyph_class(glyph) {
274            1 => GlyphPropsFlags::BASE_GLYPH.bits(),
275            2 => GlyphPropsFlags::LIGATURE.bits(),
276            3 => {
277                let class = self.glyph_mark_attachment_class(glyph);
278                (class << 8) | GlyphPropsFlags::MARK.bits()
279            }
280            _ => 0,
281        };
282
283        self.gdef_glyph_props_cache.set(glyph, props as u32);
284
285        props
286    }
287
288    #[inline(never)]
289    pub fn is_mark_glyph_gdef(&self, glyph_id: u32, set_index: u16) -> bool {
290        self.gdef
291            .mark_sets
292            .as_ref()
293            .and_then(|(data, offsets)| Some((data, offsets.get(set_index as usize)?.get())))
294            .and_then(|(data, offset)| offset.resolve::<CoverageTable>(*data).ok())
295            .is_some_and(|coverage| coverage.get(glyph_id).is_some())
296    }
297
298    #[inline(always)]
299    pub fn is_mark_glyph(&self, glyph_id: u32, set_index: u16) -> bool {
300        if self
301            .gdef_mark_set_digests
302            .get(set_index as usize)
303            .is_some_and(|digest| digest.may_have(glyph_id))
304        {
305            self.is_mark_glyph_gdef(glyph_id, set_index)
306        } else {
307            false
308        }
309    }
310
311    pub fn table_data(&self, table_index: TableIndex) -> Option<&'a [u8]> {
312        if table_index == TableIndex::GSUB {
313            self.gsub.as_ref().map(|t| t.table.offset_data().as_bytes())
314        } else {
315            self.gpos.as_ref().map(|t| t.table.offset_data().as_bytes())
316        }
317    }
318
319    pub fn table_data_and_lookup(
320        &self,
321        table_index: TableIndex,
322        lookup_index: u16,
323    ) -> Option<(&'a [u8], &'a LookupInfo)> {
324        if table_index == TableIndex::GSUB {
325            let table = self.gsub.as_ref()?;
326            Some((
327                table.table.offset_data().as_bytes(),
328                table.lookups.get(&table.table, lookup_index)?,
329            ))
330        } else {
331            let table = self.gpos.as_ref()?;
332            Some((
333                table.table.offset_data().as_bytes(),
334                table.lookups.get(&table.table, lookup_index)?,
335            ))
336        }
337    }
338
339    pub(super) fn resolve_anchor(&self, anchor: &AnchorTable) -> (f32, f32) {
340        let mut x = anchor.x_coordinate() as f32;
341        let mut y = anchor.y_coordinate() as f32;
342        if let Some(vs) = self.var_store.as_ref() {
343            // Keep the delta fractional; the caller rounds once when scaling,
344            // matching HarfBuzz's Anchor::get_anchor.
345            let delta = |val: Option<Result<DeviceOrVariationIndex<'_>, ReadError>>| match val {
346                Some(Ok(DeviceOrVariationIndex::VariationIndex(varix))) => {
347                    vs.compute_float_delta(
348                        DeltaSetIndex {
349                            outer: varix.delta_set_outer_index(),
350                            inner: varix.delta_set_inner_index(),
351                        },
352                        self.coords,
353                    )
354                    .unwrap_or_default()
355                    .to_f64() as f32
356                }
357                _ => 0.0,
358            };
359            x += delta(anchor.x_device());
360            y += delta(anchor.y_device());
361        }
362        (x, y)
363    }
364}
365
366pub enum LayoutTable<'a> {
367    Gsub(Gsub<'a>),
368    Gpos(Gpos<'a>),
369}
370
371impl<'a> LayoutTable<'a> {
372    fn script_list(&self) -> Option<ScriptList<'a>> {
373        match self {
374            Self::Gsub(gsub) => gsub.script_list().ok(),
375            Self::Gpos(gpos) => gpos.script_list().ok(),
376        }
377    }
378
379    fn feature_list(&self) -> Option<FeatureList<'a>> {
380        match self {
381            Self::Gsub(gsub) => gsub.feature_list().ok(),
382            Self::Gpos(gpos) => gpos.feature_list().ok(),
383        }
384    }
385
386    fn feature_variations(&self) -> Option<FeatureVariations<'a>> {
387        match self {
388            Self::Gsub(gsub) => gsub.feature_variations(),
389            Self::Gpos(gpos) => gpos.feature_variations(),
390        }
391        .transpose()
392        .ok()
393        .flatten()
394    }
395
396    fn script(&self, index: u16) -> Option<Script<'a>> {
397        self.script_list()?
398            .get(index)
399            .ok()
400            .map(|script| script.element)
401    }
402
403    fn langsys_index(&self, script_index: u16, tag: hb_tag_t) -> Option<u16> {
404        let script = self.script(script_index)?;
405        script.lang_sys_index_for_tag(tag)
406    }
407
408    fn langsys(&self, script_index: u16, langsys_index: Option<u16>) -> Option<LangSys<'a>> {
409        let script = self.script(script_index)?;
410        if let Some(index) = langsys_index {
411            let record = script.lang_sys_records().get(index as usize)?;
412            record.lang_sys(script.offset_data()).ok()
413        } else {
414            script.default_lang_sys().transpose().ok().flatten()
415        }
416    }
417
418    pub(crate) fn feature(&self, index: u16) -> Option<Feature<'a>> {
419        self.feature_list()?
420            .get(index)
421            .ok()
422            .map(|feature| feature.element)
423    }
424
425    fn feature_tag(&self, index: u16) -> Option<hb_tag_t> {
426        self.feature_list()?
427            .get(index)
428            .ok()
429            .map(|feature| feature.tag)
430    }
431
432    pub(crate) fn feature_variation_index(&self, coords: &[F2Dot14]) -> Option<u32> {
433        let feature_variations = self.feature_variations()?;
434        for (index, rec) in feature_variations
435            .feature_variation_records()
436            .iter()
437            .enumerate()
438        {
439            // If the ConditionSet offset is 0, this is treated as the
440            // universal condition: all contexts are matched.
441            if rec.condition_set_offset().is_null() {
442                return Some(index as u32);
443            }
444            let Some(Ok(condition_set)) = rec.condition_set(feature_variations.offset_data())
445            else {
446                continue;
447            };
448            // Otherwise, all conditions must be satisfied.
449            if condition_set
450                .conditions()
451                .iter()
452                // .. except we ignore errors
453                .filter_map(Result::ok)
454                .all(|cond| match cond {
455                    Condition::Format1AxisRange(format1) => {
456                        let coord = coords
457                            .get(format1.axis_index() as usize)
458                            .copied()
459                            .unwrap_or_default();
460                        coord >= format1.filter_range_min_value()
461                            && coord <= format1.filter_range_max_value()
462                    }
463                    _ => false,
464                })
465            {
466                return Some(index as u32);
467            }
468        }
469        None
470    }
471
472    pub(crate) fn feature_substitution(
473        &self,
474        variation_index: u32,
475        feature_index: u16,
476    ) -> Option<Feature<'a>> {
477        let feature_variations = self.feature_variations()?;
478        let record = feature_variations
479            .feature_variation_records()
480            .get(variation_index as usize)?;
481        let subst_table = record
482            .feature_table_substitution(feature_variations.offset_data())?
483            .ok()?;
484        let subst_records = subst_table.substitutions();
485        match subst_records.binary_search_by_key(&feature_index, |subst| subst.feature_index()) {
486            Ok(ix) => Some(
487                subst_records
488                    .get(ix)?
489                    .alternate_feature(subst_table.offset_data())
490                    .ok()?,
491            ),
492            _ => None,
493        }
494    }
495
496    pub(crate) fn feature_index(&self, tag: hb_tag_t) -> Option<u16> {
497        let list = self.feature_list()?;
498        for (index, feature) in list.feature_records().iter().enumerate() {
499            if feature.feature_tag() == tag {
500                return Some(index as u16);
501            }
502        }
503        None
504    }
505
506    pub(crate) fn lookup_count(&self) -> u16 {
507        match self {
508            Self::Gsub(gsub) => gsub
509                .lookup_list()
510                .map(|list| list.lookup_count())
511                .unwrap_or_default(),
512            Self::Gpos(gpos) => gpos
513                .lookup_list()
514                .map(|list| list.lookup_count())
515                .unwrap_or_default(),
516        }
517    }
518
519    // hb_ot_layout_table_select_script
520    /// Returns true + index and tag of the first found script tag in the given GSUB or GPOS table
521    /// or false + index and tag if falling back to a default script.
522    pub(crate) fn select_script(&self, script_tags: &[hb_tag_t]) -> Option<(bool, u16, hb_tag_t)> {
523        let selected = self.script_list()?.select(script_tags)?;
524        Some((!selected.is_fallback, selected.index, selected.tag))
525    }
526
527    // hb_ot_layout_script_select_language
528    /// Returns the index of the first found language tag in the given GSUB or GPOS table,
529    /// underneath the specified script index.
530    pub(crate) fn select_script_language(
531        &self,
532        script_index: u16,
533        lang_tags: &[hb_tag_t],
534    ) -> Option<u16> {
535        for &tag in lang_tags {
536            if let Some(index) = self.langsys_index(script_index, tag) {
537                return Some(index);
538            }
539        }
540
541        // try finding 'dflt'
542        if let Some(index) = self.langsys_index(script_index, hb_tag_t::default_language()) {
543            return Some(index);
544        }
545
546        None
547    }
548
549    // hb_ot_layout_language_get_required_feature
550    /// Returns the index and tag of a required feature in the given GSUB or GPOS table,
551    /// underneath the specified script and language.
552    pub(crate) fn get_required_language_feature(
553        &self,
554        script_index: u16,
555        lang_index: Option<u16>,
556    ) -> Option<(u16, hb_tag_t)> {
557        let sys = self.langsys(script_index, lang_index)?;
558        let idx = sys.required_feature_index();
559        if idx == 0xFFFF {
560            return None;
561        }
562        let tag = self.feature_tag(idx)?;
563        Some((idx, tag))
564    }
565
566    // hb_ot_layout_language_find_feature
567    /// Returns the index of a given feature tag in the given GSUB or GPOS table,
568    /// underneath the specified script and language.
569    pub(crate) fn find_language_feature(
570        &self,
571        script_index: u16,
572        lang_index: Option<u16>,
573        feature_tag: hb_tag_t,
574    ) -> Option<u16> {
575        self.langsys(script_index, lang_index)?
576            .feature_index_for_tag(&self.feature_list()?, feature_tag)
577    }
578}
579
580fn coverage_index(coverage: Result<CoverageTable, ReadError>, gid: GlyphId) -> Option<u16> {
581    coverage.ok().and_then(|coverage| coverage.get(gid))
582}
583
584fn coverage_index_cached(
585    coverage: impl Fn(GlyphId) -> Option<u16>,
586    gid: GlyphId,
587    cache: &MappingCache,
588) -> Option<u16> {
589    if let Some(index) = cache.get(gid.into()) {
590        if index == MappingCache::MAX_VALUE {
591            None
592        } else {
593            Some(index as u16)
594        }
595    } else {
596        let index = coverage(gid);
597        if let Some(index) = index {
598            if (index as u32) < MappingCache::MAX_VALUE {
599                cache.set(gid.into(), index as u32);
600            }
601            Some(index)
602        } else {
603            cache.set(gid.into(), MappingCache::MAX_VALUE);
604            None
605        }
606    }
607}
608
609fn coverage_binary_cached(
610    coverage: impl Fn(GlyphId) -> Option<u16>,
611    gid: GlyphId,
612    cache: &BinaryCache,
613) -> Option<bool> {
614    if let Some(index) = cache.get(gid.into()) {
615        if index == BinaryCache::MAX_VALUE {
616            None
617        } else {
618            Some(true)
619        }
620    } else {
621        let index = coverage(gid);
622        if index.is_some() {
623            cache.set(gid.into(), 0);
624            Some(true)
625        } else {
626            cache.set(gid.into(), BinaryCache::MAX_VALUE);
627            None
628        }
629    }
630}
631
632fn covered(coverage: Result<CoverageTable, ReadError>, gid: GlyphId) -> bool {
633    coverage_index(coverage, gid).is_some()
634}
635
636fn glyph_class(class_def: Result<ClassDef, ReadError>, gid: GlyphId) -> u16 {
637    class_def
638        .map(|class_def| class_def.get(gid))
639        .unwrap_or_default()
640}
641
642fn glyph_class_cached(
643    class_def: impl Fn(GlyphId) -> u16,
644    gid: GlyphId,
645    cache: &MappingCache,
646) -> u16 {
647    if let Some(index) = cache.get(gid.into()) {
648        index as u16
649    } else {
650        let index = class_def(gid);
651        cache.set(gid.into(), index as u32);
652        index
653    }
654}
655
656#[derive(Copy, Clone, Default, Debug)]
657pub(crate) struct CoverageInfo {
658    pub offset: u16,
659    pub format: u16,
660    pub count: u16,
661}
662
663impl CoverageInfo {
664    pub fn new(parent_data: &FontData, offset: u16) -> Option<Self> {
665        if offset == 0 {
666            return None;
667        }
668        let format = parent_data.read_at::<u16>(offset as usize).ok()?;
669        if format != 1 && format != 2 {
670            return None;
671        }
672        let count = parent_data.read_at::<u16>(offset as usize + 2).ok()?;
673        Some(Self {
674            offset,
675            format,
676            count,
677        })
678    }
679
680    pub fn index(&self, parent_data: &FontData, gid: GlyphId) -> Option<u16> {
681        if self.offset == 0 {
682            return None;
683        }
684        let gid = gid.to_u32();
685        let data_offset = self.offset as usize + 4;
686        let len = self.count as usize;
687        if self.format == 1 {
688            let glyphs = parent_data
689                .read_array::<BigEndian<GlyphId16>>(data_offset..data_offset + len * 2)
690                .ok()?;
691            glyphs
692                .binary_search_by_key(&gid, |g| g.get().to_u32())
693                .ok()
694                .map(|idx| idx as _)
695        } else {
696            use core::cmp::Ordering;
697            let records = parent_data
698                .read_array::<RangeRecord>(
699                    data_offset..data_offset + len * size_of::<RangeRecord>(),
700                )
701                .ok()?;
702            records
703                .binary_search_by(|rec| {
704                    if rec.end_glyph_id().to_u32() < gid {
705                        Ordering::Less
706                    } else if rec.start_glyph_id().to_u32() > gid {
707                        Ordering::Greater
708                    } else {
709                        Ordering::Equal
710                    }
711                })
712                .ok()
713                .map(|idx| {
714                    let rec = &records[idx];
715                    (rec.start_coverage_index() as u32 + gid - rec.start_glyph_id().to_u32()) as u16
716                })
717        }
718    }
719}
720
721#[derive(Copy, Clone, Default, Debug)]
722pub(crate) struct ClassDefInfo {
723    pub offset: u16,
724    pub format: u16,
725    // For format 1 only
726    pub start_glyph_id: u16,
727    pub count: u16,
728}
729
730impl ClassDefInfo {
731    pub fn new(parent_data: &FontData, offset: u16) -> Option<Self> {
732        if offset == 0 {
733            return None;
734        }
735        let format = parent_data.read_at::<u16>(offset as usize).ok()?;
736        if format != 1 && format != 2 {
737            return None;
738        }
739        let (start_glyph_id, count) = if format == 1 {
740            let start_glyph_id = parent_data.read_at::<u16>(offset as usize + 2).ok()?;
741            let count = parent_data.read_at::<u16>(offset as usize + 4).ok()?;
742            (start_glyph_id, count)
743        } else if format == 2 {
744            let count = parent_data.read_at::<u16>(offset as usize + 2).ok()?;
745            (0, count)
746        } else {
747            return None;
748        };
749        Some(Self {
750            offset,
751            format,
752            start_glyph_id,
753            count,
754        })
755    }
756
757    pub fn class(&self, parent_data: &FontData, gid: GlyphId) -> u16 {
758        let offset = self.offset as usize;
759        if offset == 0 {
760            return 0;
761        }
762        let gid = gid.to_u32();
763        if self.format == 1 {
764            let Some(idx) = gid.checked_sub(self.start_glyph_id as u32) else {
765                return 0;
766            };
767            if idx >= self.count as u32 {
768                return 0;
769            }
770            parent_data
771                .read_at::<u16>(offset + 6 + idx as usize * 2)
772                .unwrap_or(0)
773        } else {
774            use core::cmp::Ordering;
775            let start = offset + 4;
776            let end = start + self.count as usize * size_of::<ClassRangeRecord>();
777            let Ok(records) = parent_data.read_array::<ClassRangeRecord>(start..end) else {
778                return 0;
779            };
780            records
781                .binary_search_by(|rec| {
782                    if rec.end_glyph_id().to_u32() < gid {
783                        Ordering::Less
784                    } else if rec.start_glyph_id().to_u32() > gid {
785                        Ordering::Greater
786                    } else {
787                        Ordering::Equal
788                    }
789                })
790                .ok()
791                .map_or(0, |idx| records[idx].class())
792        }
793    }
794}
795
796use super::algs::HB_CODEPOINT_ENCODE3 as encode3;
797
798/// Blocklist specific broken GDEF tables identified by the combination of
799/// GDEF, GSUB, and GPOS table lengths. Nuke the GDEF tables to avoid
800/// unwanted width-zeroing.
801///
802/// In certain versions of Times New Roman Italic and Bold Italic,
803/// ASCII double quotation mark U+0022 has wrong glyph class 3 (mark)
804/// in GDEF. Many versions of Tahoma have bad GDEF tables that
805/// incorrectly classify some spacing marks such as certain IPA
806/// symbols as glyph class 3. So do older versions of Microsoft
807/// Himalaya, and the version of Cantarell shipped by Ubuntu 16.04.
808///
809/// See https://lists.freedesktop.org/archives/harfbuzz/2016-February/005489.html
810///     https://bugzilla.mozilla.org/show_bug.cgi?id=1279925
811///     https://bugzilla.mozilla.org/show_bug.cgi?id=1279693
812///     https://bugzilla.mozilla.org/show_bug.cgi?id=1279875
813///
814/// Upstream: hb-ot-layout.cc OT::GDEF::is_blocklisted
815fn is_gdef_blocklisted(gdef_len: u32, gsub_len: u32, gpos_len: u32) -> bool {
816    const BLOCKLIST: &[u64] = &[
817        // Windows 7? timesi.ttf
818        encode3(442, 2874, 42038),
819        // Windows 7? timesbi.ttf
820        encode3(430, 2874, 40662),
821        // Windows 7 timesi.ttf
822        encode3(442, 2874, 39116),
823        // Windows 7 timesbi.ttf
824        encode3(430, 2874, 39374),
825        // OS X 10.11.3 Times New Roman Italic.ttf
826        encode3(490, 3046, 41638),
827        // OS X 10.11.3 Times New Roman Bold Italic.ttf
828        encode3(478, 3046, 41902),
829        // tahoma.ttf from Windows 8
830        encode3(898, 12554, 46470),
831        // tahomabd.ttf from Windows 8
832        encode3(910, 12566, 47732),
833        // tahoma.ttf from Windows 8.1
834        encode3(928, 23298, 59332),
835        // tahomabd.ttf from Windows 8.1
836        encode3(940, 23310, 60732),
837        // tahoma.ttf v6.04 from Windows 8.1 x64
838        encode3(964, 23836, 60072),
839        // tahomabd.ttf v6.04 from Windows 8.1 x64
840        encode3(976, 23832, 61456),
841        // tahoma.ttf from Windows 10
842        encode3(994, 24474, 60336),
843        // tahomabd.ttf from Windows 10
844        encode3(1006, 24470, 61740),
845        // tahoma.ttf v6.91 from Windows 10 x64
846        encode3(1006, 24576, 61346),
847        // tahomabd.ttf v6.91 from Windows 10 x64
848        encode3(1018, 24572, 62828),
849        // tahoma.ttf from Windows 10 AU
850        encode3(1006, 24576, 61352),
851        // tahomabd.ttf from Windows 10 AU
852        encode3(1018, 24572, 62834),
853        // Tahoma.ttf from Mac OS X 10.9
854        encode3(832, 7324, 47162),
855        // Tahoma Bold.ttf from Mac OS X 10.9
856        encode3(844, 7302, 45474),
857        // himalaya.ttf from Windows 7
858        encode3(180, 13054, 7254),
859        // himalaya.ttf from Windows 8
860        encode3(192, 12638, 7254),
861        // himalaya.ttf from Windows 8.1
862        encode3(192, 12690, 7254),
863        // cantarell-fonts-0.0.21 Cantarell-Regular.otf / Cantarell-Oblique.otf
864        encode3(188, 248, 3852),
865        // cantarell-fonts-0.0.21 Cantarell-Bold.otf / Cantarell-Bold-Oblique.otf
866        encode3(188, 264, 3426),
867        // padauk-2.80/Padauk.ttf RHEL 7.2
868        encode3(1058, 47032, 11818),
869        // padauk-2.80/Padauk-Bold.ttf RHEL 7.2
870        encode3(1046, 47030, 12600),
871        // padauk-2.80/Padauk.ttf Ubuntu 16.04
872        encode3(1058, 71796, 16770),
873        // padauk-2.80/Padauk-Bold.ttf Ubuntu 16.04
874        encode3(1046, 71790, 17862),
875        // padauk-2.80/Padauk-book.ttf
876        encode3(1046, 71788, 17112),
877        // padauk-2.80/Padauk-bookbold.ttf
878        encode3(1058, 71794, 17514),
879        // padauk-3.0/Padauk-book.ttf
880        encode3(1330, 109_904, 57938),
881        // padauk-3.0/Padauk-bookbold.ttf
882        encode3(1330, 109_904, 58972),
883        // Padauk.ttf "Version 2.5", https://crbug.com/681813
884        encode3(1004, 59092, 14836),
885        // Courier New.ttf from macOS 15
886        encode3(588, 5078, 14418),
887        // Courier New Bold.ttf from macOS 15
888        encode3(588, 5078, 14238),
889        // cour.ttf from Windows 10
890        encode3(894, 17162, 33960),
891        // courbd.ttf from Windows 10
892        encode3(894, 17154, 34472),
893        // cour.ttf from Windows 8.1
894        encode3(816, 7868, 17052),
895        // courbd.ttf from Windows 8.1
896        encode3(816, 7868, 17138),
897    ];
898    let key = encode3(gdef_len, gsub_len, gpos_len);
899    BLOCKLIST.contains(&key)
900}