Skip to main content

oxideav_ttf/
lib.rs

1//! Pure-Rust TrueType / OpenType font parser.
2//!
3//! Round-1 scope:
4//! - sfnt + table directory walker (`parser`).
5//! - Core OpenType tables: `head`, `hhea`, `maxp`, `cmap` (base formats
6//!   0/4/6/12 + format 14 Unicode Variation Sequences as a sidecar),
7//!   `name`, `OS/2`, `hmtx`, `loca`, `glyf` (simple + composite), `post`.
8//! - Legacy `kern` table (format 0 subtable).
9//! - `GSUB` LookupType 1 (single substitution: positional forms,
10//!   small-caps, vertical alternates), LookupType 2 (multiple
11//!   substitution — split one input glyph into N), LookupType 3
12//!   (alternate substitution — `aalt` / `salt` per-coverage
13//!   alternates), LookupType 4 (ligature substitution — both walker
14//!   and lookup-index-specific entry points), LookupType 5
15//!   (contextual substitution — formats 1 / 2 / 3), LookupType 6
16//!   (chained contexts substitution — formats 1 / 2 / 3, with
17//!   recursive sub-lookup dispatch), and LookupType 8 (reverse
18//!   chained context single substitution), discoverable via the
19//!   ScriptList / FeatureList / LookupList common-table walk.
20//! - `GPOS` LookupType 1 (single adjustment), LookupType 2
21//!   (pair-adjustment / kerning), LookupType 3 (cursive attachment),
22//!   LookupType 4 (mark-to-base attachment for diacritics), LookupType 5
23//!   (mark-to-ligature attachment), LookupType 6 (mark-to-mark
24//!   attachment for stacked diacritics), LookupType 7 (contextual
25//!   positioning — `SequenceContext` formats 1/2/3 with recursive
26//!   nested-lookup dispatch), and LookupType 8 (chained contexts
27//!   positioning).
28//! - `GDEF` (glyph class definitions).
29//! - Adobe Glyph List (AGL) glyph-name → Unicode resolution:
30//!   [`glyph_name_to_codepoints`] / [`glyph_name_to_char`] (direct
31//!   table lookup against the staged AGL data).
32//! - `gasp` (grid-fitting and scan-conversion procedure table, ISO/IEC
33//!   14496-22:2019 §5.3.7) — both version 0 and 1, per-record flag
34//!   accessors, behaviour-for-ppem lookup.
35//!
36//! The crate is read-only (parsing-only) and dependency-light: only
37//! `oxideav-core` for shared types. CFF/Type 2 charstrings live in the
38//! sibling `oxideav-otf` crate. TrueType hinting, bidi, and complex
39//! shaping are deferred to later rounds.
40//!
41//! Variable fonts (`fvar`/`avar`/`gvar`) are supported as of round
42//! 4: see [`Font::variation_axes`], [`Font::named_instances`],
43//! [`Font::set_variation_coords`], and [`Font::glyph_outline`] (which
44//! applies gvar deltas via the current axis-coord vector when set).
45//!
46//! See `README.md` for the public API tour.
47
48#![deny(missing_debug_implementations)]
49#![warn(rust_2018_idioms)]
50
51pub mod agl;
52pub mod collection;
53pub mod outline;
54// internal — exposed for tests/fuzz; not part of the stable API
55#[doc(hidden)]
56pub mod parser;
57pub mod shape;
58pub mod tables;
59
60pub use agl::{glyph_name_to_char, glyph_name_to_codepoints};
61pub use collection::{is_collection, CollectionHeader, TTC_MAGIC};
62pub use shape::ShapedGlyph;
63
64use crate::parser::TableDirectory;
65use crate::tables::{
66    avar::AvarTable,
67    base::BaseTable,
68    cbdt::CbdtTable,
69    cblc::CblcTable,
70    cff::CffTable,
71    cff2::Cff2Table,
72    cmap::CmapTable,
73    colr::ColrTable,
74    cpal::CpalTable,
75    cvar::CvarTable,
76    dsig::DsigTable,
77    ebdt::EbdtTable,
78    ebsc::EbscTable,
79    fvar::FvarTable,
80    gasp::GaspTable,
81    gdef::GdefTable,
82    glyf::GlyfTable,
83    gpos::GposTable,
84    gsub::GsubTable,
85    gvar::GvarTable,
86    hdmx::HdmxTable,
87    head::HeadTable,
88    hhea::HheaTable,
89    hmtx::HmtxTable,
90    hvar::HvarTable,
91    jstf::JstfTable,
92    kern::KernTable,
93    loca::LocaTable,
94    ltsh::LtshTable,
95    math::{GrowDirection, MathKernCorner, MathTable},
96    maxp::MaxpTable,
97    merg::MergTable,
98    meta::MetaTable,
99    mvar::MvarTable,
100    name::NameTable,
101    os2::Os2Table,
102    pclt::PcltTable,
103    post::PostTable,
104    sbix::SbixTable,
105    stat::StatTable,
106    svg::SvgTable,
107    vdmx::VdmxTable,
108    vhea::VheaTable,
109    vmtx::VmtxTable,
110    vorg::VorgTable,
111    vvar::VvarTable,
112};
113
114pub use outline::{BBox, Contour, Point, TtOutline};
115// internal — exposed for tests/fuzz; not part of the stable API (the
116// stable BASE surface is the `Font::base_*` accessor family)
117#[doc(hidden)]
118pub use tables::base::{
119    AxisTable as BaseAxisTable, BaseCoord, BaseLangSysRecord, BaseScriptRecord, BaseScriptTable,
120    BaseValuesTable, FeatMinMaxRecord, MinMaxTable as BaseMinMaxTable, BASE_MAJOR_VERSION,
121    BASE_MINOR_VERSION_0, BASE_MINOR_VERSION_1,
122};
123pub use tables::cbdt::ColorBitmap;
124pub use tables::cblc::{BigGlyphMetrics, SmallGlyphMetrics};
125pub use tables::colr::{
126    Affine2x3, ClipBox, ColorLayer, ColorLine, ColorStop, CompositeMode, Extend, Paint, PaintRef,
127};
128// internal — exposed for tests/fuzz; not part of the stable API
129#[doc(hidden)]
130pub use tables::device::DeviceOrVariationIndex;
131pub use tables::dsig::{Signature as DsigSignature, DSIG_BLOCK_FORMAT_PKCS7, DSIG_VERSION};
132pub use tables::ebdt::{CompositeBitmap, EbdtComponent, GrayBitmap};
133pub use tables::ebsc::{BitmapScale, SbitLineMetrics, EBSC_MAJOR_VERSION, EBSC_MINOR_VERSION};
134pub use tables::fvar::{NamedInstance, VariationAxis};
135pub use tables::gasp::{
136    GaspRange, GASP_DOGRAY, GASP_GRIDFIT, GASP_PPEM_SENTINEL, GASP_RESERVED_MASK,
137    GASP_SYMMETRIC_GRIDFIT, GASP_SYMMETRIC_SMOOTHING, GASP_TABLE_TAG, GASP_VERSION_0,
138    GASP_VERSION_1,
139};
140pub use tables::gpos::{CursiveAttachment, GposFeature, PosRecord, PosValue};
141pub use tables::gsub::GsubFeature;
142pub use tables::hdmx::{HdmxRecord, HDMX_TABLE_TAG, HDMX_VERSION_0};
143// internal — exposed for tests/fuzz; not part of the stable API
144#[doc(hidden)]
145pub use tables::hdmx::{HDMX_HEADER_LEN, HDMX_RECORD_HEADER_LEN};
146pub use tables::head::{
147    HEAD_FLAG_BASELINE_AT_Y0, HEAD_FLAG_CLEARTYPE_OPTIMIZED, HEAD_FLAG_CONVERTED,
148    HEAD_FLAG_INSTRUCTIONS_ALTER_ADVANCE, HEAD_FLAG_LAST_RESORT, HEAD_FLAG_LOSSLESS,
149    MAC_STYLE_BOLD, MAC_STYLE_CONDENSED, MAC_STYLE_EXTENDED, MAC_STYLE_ITALIC,
150};
151// internal — exposed for tests/fuzz; not part of the stable API
152#[doc(hidden)]
153pub use tables::hvar::DeltaSetIndexMap;
154pub use tables::kern::HeaderVariant as KernHeaderVariant;
155pub use tables::ltsh::{LTSH_ALWAYS_LINEAR, LTSH_TABLE_TAG, LTSH_VERSION_0};
156pub use tables::merg::{
157    MergeEntry, GROUP_LTR, GROUP_RTL, MERGE_LTR, MERGE_RTL, SECOND_IS_SUBORDINATE_LTR,
158    SECOND_IS_SUBORDINATE_RTL,
159};
160pub use tables::meta::{
161    is_valid_meta_tag, script_lang_tags, MetaRecord, ScriptLangTag, META_TABLE_TAG, META_TAG_APPL,
162    META_TAG_BILD, META_TAG_DLNG, META_TAG_SLNG, META_VERSION_1,
163};
164// internal — exposed for tests/fuzz; not part of the stable API
165#[doc(hidden)]
166pub use tables::meta::{META_DATA_MAP_LEN, META_HEADER_LEN};
167// internal — exposed for tests/fuzz; not part of the stable API
168#[doc(hidden)]
169pub use tables::mvar::ItemVariationStore;
170pub use tables::name::{name_id, platform, NameRecord};
171pub use tables::os2::{
172    FSSELECTION_BOLD, FSSELECTION_ITALIC, FSSELECTION_OBLIQUE, FSSELECTION_REGULAR,
173    FSSELECTION_USE_TYPO_METRICS, FSTYPE_BITMAP_ONLY, FSTYPE_EDITABLE, FSTYPE_NO_SUBSETTING,
174    FSTYPE_PREVIEW_PRINT, FSTYPE_RESTRICTED_LICENSE,
175};
176pub use tables::pclt::{
177    PCLT_MAJOR_VERSION, PCLT_STROKE_WEIGHT_RANGE, PCLT_TABLE_TAG, PCLT_WIDTH_TYPE_RANGE,
178};
179// internal — exposed for tests/fuzz; not part of the stable API
180#[doc(hidden)]
181pub use tables::pclt::PCLT_TABLE_LEN;
182pub use tables::post::{
183    standard_mac_glyph_name, GlyphNameRef, PostFormat, PostV20, PostV25, POST_TABLE_TAG,
184    POST_VERSION_10, POST_VERSION_20, POST_VERSION_25, POST_VERSION_30,
185    RECOMMENDED_GLYPH_NAME_MAX_LEN, STANDARD_MAC_GLYPH_COUNT, STANDARD_MAC_GLYPH_NAMES,
186};
187// internal — exposed for tests/fuzz; not part of the stable API
188#[doc(hidden)]
189pub use tables::post::POST_HEADER_LEN;
190pub use tables::sbix::{SbixGlyph, MAX_DUPE_DEPTH as SBIX_MAX_DUPE_DEPTH};
191pub use tables::stat::{
192    AxisRecord as StatAxisRecord, AxisValue as StatAxisValue,
193    FLAG_ELIDABLE_AXIS_VALUE_NAME as STAT_FLAG_ELIDABLE_AXIS_VALUE_NAME,
194    FLAG_OLDER_SIBLING_FONT_ATTRIBUTE as STAT_FLAG_OLDER_SIBLING_FONT_ATTRIBUTE,
195    RANGE_MAX_POS_INFINITY as STAT_RANGE_MAX_POS_INFINITY,
196    RANGE_MIN_NEG_INFINITY as STAT_RANGE_MIN_NEG_INFINITY,
197};
198pub use tables::svg::{SvgDocument, SVG_GZIP_MAGIC, SVG_TABLE_TAG, SVG_VERSION_0};
199// internal — exposed for tests/fuzz; not part of the stable API
200#[doc(hidden)]
201pub use tables::svg::{SVG_DOCUMENT_RECORD_LEN, SVG_HEADER_LEN};
202pub use tables::vdmx::{
203    RatioRange as VdmxRatioRange, VdmxGroup, VdmxVTableRecord, VDMX_TABLE_TAG, VDMX_VERSION_0,
204    VDMX_VERSION_1,
205};
206// internal — exposed for tests/fuzz; not part of the stable API
207#[doc(hidden)]
208pub use tables::vdmx::{
209    VDMX_GROUP_HEADER_LEN, VDMX_HEADER_LEN, VDMX_OFFSET_LEN, VDMX_RATIO_RECORD_LEN,
210    VDMX_VTABLE_RECORD_LEN,
211};
212pub use tables::vhea::{VHEA_VERSION_1_0, VHEA_VERSION_1_1};
213pub use tables::vorg::{VertOriginEntry, VORG_MAJOR_VERSION, VORG_MINOR_VERSION};
214
215/// Errors emitted during font parsing or glyph lookup.
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub enum Error {
218    /// The input slice is too short for the requested header / structure.
219    UnexpectedEof,
220    /// The sfnt magic version did not match `0x00010000`, `OTTO`, or `true`.
221    BadMagic,
222    /// The table count in the sfnt header is implausibly large.
223    BadHeader,
224    /// A required table was missing from the table directory.
225    MissingTable(&'static str),
226    /// A length / offset field pointed outside the file.
227    BadOffset,
228    /// A glyph index was out of range vs. `maxp.numGlyphs`.
229    GlyphOutOfRange(u16),
230    /// A cmap subtable used a format we do not implement in round 1.
231    UnsupportedCmapFormat(u16),
232    /// A composite-glyph chain exceeded the max recursion depth (16).
233    CompositeTooDeep,
234    /// A loca offset pointed past the end of `glyf`.
235    BadLocaOffset,
236    /// A varying-length structure was malformed.
237    BadStructure(&'static str),
238    /// A `from_collection_bytes` call asked for a subfont index that
239    /// the TTC header does not contain. Carries the requested index.
240    SubfontOutOfRange(u32),
241}
242
243impl core::fmt::Display for Error {
244    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
245        match self {
246            Self::UnexpectedEof => f.write_str("unexpected end of font data"),
247            Self::BadMagic => f.write_str("not a TrueType / OpenType font (bad magic)"),
248            Self::BadHeader => f.write_str("malformed sfnt header"),
249            Self::MissingTable(t) => write!(f, "required table missing: {t}"),
250            Self::BadOffset => f.write_str("table offset out of range"),
251            Self::GlyphOutOfRange(g) => write!(f, "glyph index {g} out of range"),
252            Self::UnsupportedCmapFormat(fmt) => {
253                write!(f, "cmap format {fmt} not implemented in round 1")
254            }
255            Self::CompositeTooDeep => f.write_str("composite glyph recursion too deep"),
256            Self::BadLocaOffset => f.write_str("loca offset past end of glyf"),
257            Self::BadStructure(s) => write!(f, "malformed structure: {s}"),
258            Self::SubfontOutOfRange(i) => write!(f, "subfont index {i} not in collection"),
259        }
260    }
261}
262
263impl std::error::Error for Error {}
264
265/// A parsed TrueType / OpenType font, lifetime-bound to the input bytes.
266///
267/// `Font::from_bytes` walks the sfnt header + table directory once; the
268/// individual `*Table` parsers are run on first use and cached as
269/// already-validated slices on the struct. Lookup methods (`glyph_index`,
270/// `glyph_outline`, etc.) are O(log n) or O(n) over the raw table bytes —
271/// no glyphs are pre-decoded or cached.
272#[derive(Debug)]
273pub struct Font<'a> {
274    bytes: &'a [u8],
275    head: HeadTable,
276    hhea: HheaTable,
277    maxp: MaxpTable,
278    cmap: CmapTable<'a>,
279    name: NameTable<'a>,
280    os2: Option<Os2Table>,
281    hmtx: HmtxTable<'a>,
282    /// Vertical header table (`vhea`, ISO/IEC 14496-22:2019 §5.7.9).
283    /// Optional — only fonts intended for vertical layout ship one;
284    /// in particular, CJK fonts and the rare Mongolian / Manchu font.
285    /// When present, the companion `vmtx` table is also required per
286    /// §5.7.10 ("OFFvertical fonts require both a vertical header
287    /// table ('vhea') and the vertical metrics table").
288    vhea: Option<VheaTable>,
289    /// Vertical metrics table (`vmtx`, ISO/IEC 14496-22:2019 §5.7.10).
290    /// Always paired with `vhea`; only present when the font supplies
291    /// vertical layout data.
292    vmtx: Option<VmtxTable<'a>>,
293    /// Vertical origin table (`VORG`, ISO/IEC 14496-22:2019 §5.4.4).
294    /// Optional table that records, per glyph, the Y coordinate of the
295    /// glyph's vertical origin in font design units. Per §5.4.4 the
296    /// table is restricted to CFF-flavoured sfnts ("If present in
297    /// TrueType OFF fonts it must be ignored by font clients"); when a
298    /// TrueType-flavoured sfnt nonetheless ships one we still parse it
299    /// here so the bytes are available, but the
300    /// [`Font::vert_origin_y_from_vorg`] accessor respects the
301    /// ignore-on-TrueType policy and returns `None` once `glyf` is
302    /// present.
303    vorg: Option<VorgTable>,
304    /// Glyph-location offsets into `glyf`. Optional because CBDT/CBLC-only
305    /// colour-emoji fonts (e.g. NotoColorEmoji.ttf) ship without `loca`
306    /// and `glyf` — every glyph is a colour bitmap and there are no
307    /// outlines to address.
308    loca: Option<LocaTable<'a>>,
309    glyf: Option<GlyfTable<'a>>,
310    /// `CFF ` outlines (PostScript / Type 2 charstrings). Present in
311    /// OTTO-flavoured fonts; mutually exclusive with `glyf` in practice.
312    cff: Option<CffTable<'a>>,
313    /// `CFF2` outlines (variable PostScript charstrings). Present in
314    /// CFF2-flavoured variable fonts; we render the default instance.
315    cff2: Option<Cff2Table<'a>>,
316    post: Option<PostTable>,
317    kern: Option<KernTable<'a>>,
318    gsub: Option<GsubTable<'a>>,
319    gpos: Option<GposTable<'a>>,
320    gdef: Option<GdefTable<'a>>,
321    cblc: Option<CblcTable<'a>>,
322    cbdt: Option<CbdtTable<'a>>,
323    /// Embedded bitmap *location* table (`EBLC`, ISO/IEC 14496-22:2019
324    /// §5.6.3). The monochrome / grayscale analog of `CBLC`; identical
325    /// on-wire layout (the shared [`CblcTable`] walker accepts both),
326    /// paired with [`Font::ebdt`](Self::ebdt) rather than `CBDT`.
327    eblc: Option<CblcTable<'a>>,
328    /// Embedded monochrome / grayscale bitmap data (`EBDT`, ISO/IEC
329    /// 14496-22:2019 §5.6.2). Located through the shared `EBLC`/`CBLC`
330    /// walker (the same `CblcTable` used for colour bitmaps); an `EBLC`
331    /// (major == 2) strike resolves the same way a `CBLC` colour strike
332    /// does. Present on legacy
333    /// pixel / CJK bitmap faces.
334    ebdt: Option<EbdtTable<'a>>,
335    /// Embedded bitmap *scaling* table (`EBSC`, ISO/IEC 14496-22:2019
336    /// §5.6.4). Declares synthesised strikes built by scaling an existing
337    /// `EBLC`/`EBDT` strike up or down (small Kanji sizes are the spec's
338    /// motivating case). Owns no glyph imagery; it redirects a requested
339    /// ppem to a real `substitutePpem` strike. Carries no lifetime — every
340    /// field copies out of the slice at parse time.
341    ebsc: Option<EbscTable>,
342    colr: Option<ColrTable<'a>>,
343    cpal: Option<CpalTable<'a>>,
344    sbix: Option<SbixTable<'a>>,
345    /// Variable-font axes header (`fvar`). Absent for static fonts.
346    fvar: Option<FvarTable>,
347    /// Per-axis non-linear remap (`avar`). Absent unless the font
348    /// publishes one (most variable fonts do, identity for axes that
349    /// don't need bending).
350    avar: Option<AvarTable>,
351    /// Per-glyph TupleVariationStore (`gvar`). Required when `fvar`
352    /// is present and the outline kind is TrueType; not populated for
353    /// CFF2 (which interleaves its deltas inside the `CFF2` table).
354    gvar: Option<GvarTable<'a>>,
355    /// CVT-variations table (`cvar`, ISO/IEC 14496-22:2019 §7.3.2).
356    /// Present in TrueType-hinted variable fonts; supplies per-instance
357    /// deltas for the `cvt ` Control Value Table entries.
358    cvar: Option<CvarTable<'a>>,
359    /// Raw `cvt ` Control Value Table bytes (an array of big-endian
360    /// `int16` FWORDs). Held so [`Font::cvt_value`] / [`Font::cvt_count`]
361    /// can resolve entries, optionally with `cvar` deltas applied.
362    cvt_bytes: Option<&'a [u8]>,
363    /// Raw `fpgm` font-program bytes (TrueType bytecode, run once when the
364    /// font is first used — ISO/IEC 14496-22:2019 §5.3.3). This crate does
365    /// not execute the program; the bytes are surfaced through
366    /// [`Font::fpgm_program`] for tooling that introspects or round-trips
367    /// the hinting program.
368    fpgm_bytes: Option<&'a [u8]>,
369    /// Raw `prep` control-value-program bytes (TrueType bytecode, run
370    /// whenever size / transform changes — ISO/IEC 14496-22:2019 §5.3.x).
371    /// Surfaced raw through [`Font::prep_program`]; not executed.
372    prep_bytes: Option<&'a [u8]>,
373    /// Font-wide metrics-variation table (`MVAR`). Present in many
374    /// variable fonts; carries per-instance adjustments for `OS/2`,
375    /// `hhea`, `vhea`, `post`, `gasp` metric fields keyed by the
376    /// §7.3.6.3 value-tag registry.
377    mvar: Option<MvarTable>,
378    /// Per-glyph horizontal-metrics variation table (`HVAR`,
379    /// ISO/IEC 14496-22:2019 §7.3.5). Variable fonts with TrueType
380    /// outlines are encouraged to ship one; CFF2 variable fonts are
381    /// required to. Provides interpolated adjustments for `hmtx`
382    /// advance widths plus optional left- and right-side bearings.
383    hvar: Option<HvarTable>,
384    /// Per-glyph vertical-metrics variation table (`VVAR`,
385    /// ISO/IEC 14496-22:2019 §7.3.8). Optional in TrueType variable
386    /// fonts (where `gvar` phantom points carry the same data); for
387    /// CFF2 variable fonts that support vertical layout it is required
388    /// (§7.3.8.1). Provides interpolated adjustments for `vmtx`
389    /// advance heights plus optional top-/bottom-side bearings and —
390    /// for CFF2 fonts that publish a `VORG` table — vertical-origin
391    /// Y coordinates.
392    vvar: Option<VvarTable>,
393    /// Style attributes table (`STAT`, ISO/IEC 14496-22:2019 §7.3.7).
394    /// Required in all variable fonts; optional otherwise. Carries
395    /// design-axis records and per-axis-value name mappings used by
396    /// font pickers to compose family / subfamily strings under the
397    /// R/B/I/BI, WWS, and unrestricted naming models.
398    stat: Option<StatTable>,
399    /// Baseline table (`BASE`, ISO/IEC 14496-22:2019 §6.3.1). Optional
400    /// table that supplies per-script baseline coordinates and
401    /// per-script / per-language-system / per-feature minimum and
402    /// maximum glyph extents. Carries one Axis sub-table per text
403    /// direction (HorizAxis for Y baselines / horizontal text;
404    /// VertAxis for X baselines / vertical text).
405    base: Option<BaseTable>,
406    /// Grid-fitting and scan-conversion procedure table (`gasp`,
407    /// ISO/IEC 14496-22:2019 §5.3.7). Optional; carries the
408    /// per-ppem-range rasterisation hints (grid-fit / grayscale /
409    /// ClearType-symmetric flags) sorted by `rangeMaxPPEM`. Used by
410    /// callers that drive a font rasteriser and want to pick the
411    /// font-author-recommended hinting policy at a given pixel size.
412    gasp: Option<GaspTable>,
413    /// Linear threshold table (`LTSH`, ISO/IEC 14496-22:2019 §5.7.4).
414    /// Optional; carries one byte per glyph recording the lowest ppem
415    /// at which the grid-fitted advance width has converged on the
416    /// rounded linear advance, so a rasteriser at or above that ppem
417    /// can round the linear advance arithmetically without scan-
418    /// converting the glyph. The §5.7.4 sentinel `1` means "always
419    /// scales linearly" (the glyph carries no instructions on its
420    /// sidebearings).
421    ltsh: Option<LtshTable>,
422    /// Horizontal device metrics table (`hdmx`, ISO/IEC 14496-22:2019
423    /// §5.7.2). Optional; carries one device record per selected ppem,
424    /// each holding the per-glyph grid-fitted advance width in integer
425    /// pixels. The precomputed-advance counterpart to `LTSH`: instead
426    /// of recording when the grid-fit advance converges to the linear
427    /// advance, `hdmx` records the exact grid-fit advance for a fixed
428    /// set of ppem sizes. §7.3.5 forbids `hdmx` in variable fonts;
429    /// callers that want to honour that rule can cross-check
430    /// `is_variable()` before consulting these accessors.
431    hdmx: Option<HdmxTable>,
432    /// Vertical device metrics table (`VDMX`, ISO/IEC 14496-22:2019
433    /// §5.7.8). Optional; carries one or more groups of vTable
434    /// records (`yPelHeight` → `(yMax, yMin)` pel envelope) indexed
435    /// via a per-aspect-ratio RatioRange array. The precomputed-extent
436    /// counterpart to `hdmx`'s per-glyph advance widths: instead of
437    /// publishing each glyph's grid-fitted advance, `VDMX` publishes
438    /// the font-wide vertical extent at a curated ppem set so a
439    /// rasteriser can pick a render bitmap height without
440    /// grid-fitting every glyph in the font. §7.3.5 forbids `VDMX`
441    /// in variable fonts; callers can cross-check `is_variable()`
442    /// before consulting these accessors.
443    vdmx: Option<VdmxTable>,
444    /// Metadata table (`meta`, ISO/IEC 14496-22:2019 §5.7.6). Optional;
445    /// carries a tagged DataMap array whose payloads describe font-wide
446    /// metadata in either UTF-8 text (`'dlng'`, `'slng'`) or vendor-
447    /// defined binary form. Records borrow from the on-wire `meta`
448    /// byte slice — the table itself does not copy the payload data.
449    meta: Option<MetaTable<'a>>,
450    /// PCL 5 table (`PCLT`, ISO/IEC 14496-22:2019 §5.7.7). Optional
451    /// (and "strongly discouraged for OFF fonts with TrueType
452    /// outlines" per the spec); carries the PCL 5 font-selection
453    /// attributes — HP font number, pitch / x-height / cap-height,
454    /// packed style / type-family / symbol-set words, the 16-byte
455    /// typeface string, the 8-byte character-complement bitfield,
456    /// the 6-byte PCL file name, and the stroke-weight / width-type
457    /// / serif-style classification bytes.
458    pclt: Option<PcltTable>,
459    /// SVG table (`SVG `, ISO/IEC 14496-22:2019/Amd.1:2020 §5.5.1).
460    /// Optional; carries per-glyph-range SVG 1.1 vector colour-glyph
461    /// documents (plain UTF-8 or gzip-encoded). Records borrow from the
462    /// on-wire `SVG ` byte slice — the table does not copy the markup.
463    svg: Option<SvgTable<'a>>,
464    /// Math typesetting table (`MATH`, ISO/IEC 14496-22:2019 §6.3.6).
465    /// Present in fonts designed for mathematical layout; carries the
466    /// MathConstants / MathGlyphInfo / MathVariants sub-tables that a
467    /// math-layout engine consumes. Borrows from the on-wire slice.
468    math: Option<MathTable<'a>>,
469    /// Justification table (`JSTF`, ISO/IEC 14496-22:2019 §6.3.5).
470    /// Optional; carries per-script/language justification suggestions
471    /// (GSUB/GPOS lookup enable/disable lists + extender glyphs).
472    jstf: Option<JstfTable<'a>>,
473    /// Digital signature table (`DSIG`, ISO/IEC 14496-22:2019 §8.x).
474    /// Optional; carries the font's digital signature as one or more
475    /// `SignatureRecord`s pointing at PKCS#7 signature blocks. Block
476    /// payloads borrow from the on-wire `DSIG` slice — this crate decodes
477    /// the table structure but does not verify the signature.
478    dsig: Option<DsigTable<'a>>,
479    /// Merge table (`MERG`, ISO/IEC 14496-22:2019 §5.7.5). Optional;
480    /// declares which glyph-class pairs a renderer should merge or group
481    /// before antialias filtering. Copies its ClassDef + merge-entry bytes
482    /// out at parse time, so it carries no lifetime.
483    merg: Option<MergTable>,
484    /// Current user-space coordinate vector, one per axis (defaults
485    /// to each axis's `default` value when `fvar` is present, empty
486    /// vec otherwise). `set_variation_coords` updates this; the
487    /// outline accessor consults [`Self::normalised_coords`] to
488    /// derive the per-axis weight applied to gvar deltas.
489    var_coords: Vec<f32>,
490}
491
492impl<'a> Font<'a> {
493    /// Parse the `index`-th subfont out of a TrueType Collection (`.ttc` /
494    /// `'ttcf'`) byte slice.
495    ///
496    /// TTC files start with a `'ttcf'` magic followed by a list of byte
497    /// offsets pointing at per-subfont sfnt headers. This entry point
498    /// reads the TTC header, then runs the regular sfnt parse path
499    /// against the slice rooted at the chosen subfont. The returned
500    /// `Font<'a>` borrows from the original `bytes` (sub-slicing is
501    /// done internally; the lifetime stays tied to the input).
502    ///
503    /// Returns:
504    /// - `Error::BadMagic` if `bytes` is not a TTC.
505    /// - `Error::SubfontOutOfRange(index)` if the chosen index exceeds
506    ///   `numFonts`.
507    /// - Whatever the underlying sfnt path emits otherwise (typically
508    ///   `MissingTable` / `BadOffset` for a malformed subfont).
509    ///
510    /// Spec: Microsoft OpenType §"Font Collections", Apple TrueType
511    /// Reference / "TrueType Collections".
512    pub fn from_collection_bytes(bytes: &'a [u8], index: u32) -> Result<Self, Error> {
513        let header = CollectionHeader::parse(bytes)?;
514        let offset = header
515            .font_offset(index)
516            .ok_or(Error::SubfontOutOfRange(index))? as usize;
517        // The TTC spec requires the subfont's table directory offsets to
518        // be FILE-relative (not subfont-relative), so we hand
519        // `from_bytes_at` the full file slice and the subfont header
520        // offset rather than slicing the file from `offset` onwards.
521        Self::from_bytes_at(bytes, offset)
522    }
523
524    /// Parse a font from a borrowed byte slice.
525    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
526        Self::from_bytes_at(bytes, 0)
527    }
528
529    /// Parse a font whose sfnt header sits at `header_offset` inside
530    /// `bytes`. Used by `from_collection_bytes` for TTC subfonts (whose
531    /// table records carry file-relative offsets, not subfont-relative
532    /// ones); equivalent to `from_bytes` when `header_offset == 0`.
533    fn from_bytes_at(bytes: &'a [u8], header_offset: usize) -> Result<Self, Error> {
534        let dir = TableDirectory::parse(bytes, header_offset)?;
535
536        let head = HeadTable::parse(dir.required(b"head", bytes)?)?;
537        let hhea = HheaTable::parse(dir.required(b"hhea", bytes)?)?;
538        let maxp = MaxpTable::parse(dir.required(b"maxp", bytes)?)?;
539        let cmap = CmapTable::parse(dir.required(b"cmap", bytes)?)?;
540        let name = NameTable::parse(dir.required(b"name", bytes)?)?;
541        let hmtx = HmtxTable::parse(
542            dir.required(b"hmtx", bytes)?,
543            hhea.num_long_hor_metrics,
544            maxp.num_glyphs,
545        )?;
546        // `vhea` + `vmtx` are jointly optional: a font that lacks
547        // either is treated as horizontal-only. §5.7.10 mandates that
548        // a font shipping one ship both ("OFFvertical fonts require
549        // both"), so a half-pair is rejected as a malformed file
550        // rather than silently degraded.
551        let vhea = dir.find(b"vhea", bytes).map(VheaTable::parse).transpose()?;
552        let vmtx_slice = dir.find(b"vmtx", bytes);
553        let vmtx = match (vhea.as_ref(), vmtx_slice) {
554            (Some(vh), Some(slice)) => Some(VmtxTable::parse(
555                slice,
556                vh.num_long_ver_metrics,
557                maxp.num_glyphs,
558            )?),
559            (None, None) => None,
560            (Some(_), None) => {
561                return Err(Error::BadStructure(
562                    "vhea present but vmtx missing (§5.7.10 requires both)",
563                ));
564            }
565            (None, Some(_)) => {
566                return Err(Error::BadStructure(
567                    "vmtx present but vhea missing (§5.7.10 requires both)",
568                ));
569            }
570        };
571        // `loca` + `glyf` are jointly optional: CBDT/CBLC-only colour-
572        // emoji fonts (e.g. NotoColorEmoji.ttf) ship without either.
573        // When loca is present we still require glyf (and vice versa)
574        // because a half-pair would be malformed.
575        let loca = match (dir.find(b"loca", bytes), dir.find(b"glyf", bytes)) {
576            (Some(l), Some(_g)) => Some(LocaTable::parse(
577                l,
578                maxp.num_glyphs,
579                head.index_to_loc_format,
580            )?),
581            (None, None) => None,
582            _ => {
583                return Err(Error::BadStructure(
584                    "loca/glyf must both be present or both absent",
585                ))
586            }
587        };
588        let glyf = dir.find(b"glyf", bytes).map(GlyfTable::new);
589        // `CFF ` carries PostScript outlines (OTTO fonts). The tag has a
590        // trailing space.
591        let cff = dir.find(b"CFF ", bytes).map(CffTable::parse).transpose()?;
592        // `CFF2` carries variable PostScript outlines; we render the
593        // default instance.
594        let cff2 = dir.find(b"CFF2", bytes).map(Cff2Table::parse).transpose()?;
595
596        let os2 = dir.find(b"OS/2", bytes).map(Os2Table::parse).transpose()?;
597        let post = dir.find(b"post", bytes).map(PostTable::parse).transpose()?;
598        let kern = dir.find(b"kern", bytes).map(KernTable::parse).transpose()?;
599        let gsub = dir.find(b"GSUB", bytes).map(GsubTable::parse).transpose()?;
600        let gpos = dir.find(b"GPOS", bytes).map(GposTable::parse).transpose()?;
601        let gdef = dir.find(b"GDEF", bytes).map(GdefTable::parse).transpose()?;
602        let cblc = dir.find(b"CBLC", bytes).map(CblcTable::parse).transpose()?;
603        let cbdt = dir.find(b"CBDT", bytes).map(CbdtTable::parse).transpose()?;
604        let eblc = dir.find(b"EBLC", bytes).map(CblcTable::parse).transpose()?;
605        let ebdt = dir.find(b"EBDT", bytes).map(EbdtTable::parse).transpose()?;
606        let ebsc = dir.find(b"EBSC", bytes).map(EbscTable::parse).transpose()?;
607        let colr = dir.find(b"COLR", bytes).map(ColrTable::parse).transpose()?;
608        let cpal = dir.find(b"CPAL", bytes).map(CpalTable::parse).transpose()?;
609        let sbix = dir
610            .find(b"sbix", bytes)
611            .map(|s| SbixTable::parse(s, maxp.num_glyphs))
612            .transpose()?;
613
614        // Variable-font tables. `fvar` is the gate: if it's absent the
615        // font is static and we skip the rest. If it's present we still
616        // try to load `gvar` (TrueType deltas) and `avar` (axis remap)
617        // but a missing `gvar` is acceptable for non-outline (CBDT-only)
618        // variable fonts.
619        let fvar = dir.find(b"fvar", bytes).map(FvarTable::parse).transpose()?;
620        let avar = dir.find(b"avar", bytes).map(AvarTable::parse).transpose()?;
621        let gvar = dir.find(b"gvar", bytes).map(GvarTable::parse).transpose()?;
622        let cvar = dir.find(b"cvar", bytes).map(CvarTable::parse).transpose()?;
623        // `cvt ` is a plain `int16[]` Control Value Table; held raw.
624        let cvt_bytes = dir.find(b"cvt ", bytes);
625        // `fpgm` / `prep` are raw TrueType bytecode programs (§5.3.3 /
626        // §5.3.x). Not executed by this crate — held raw for tooling.
627        let fpgm_bytes = dir.find(b"fpgm", bytes);
628        let prep_bytes = dir.find(b"prep", bytes);
629        let mvar = dir.find(b"MVAR", bytes).map(MvarTable::parse).transpose()?;
630        let hvar = dir.find(b"HVAR", bytes).map(HvarTable::parse).transpose()?;
631        let vvar = dir.find(b"VVAR", bytes).map(VvarTable::parse).transpose()?;
632        let stat = dir.find(b"STAT", bytes).map(StatTable::parse).transpose()?;
633        let base = dir.find(b"BASE", bytes).map(BaseTable::parse).transpose()?;
634        let gasp = dir.find(b"gasp", bytes).map(GaspTable::parse).transpose()?;
635        let vorg = dir.find(b"VORG", bytes).map(VorgTable::parse).transpose()?;
636        // §5.7.4 says `LTSH.numGlyphs` "should be the same as the
637        // numGlyphs field in the 'maxp' table". A mismatch would either
638        // truncate or over-read the per-glyph lookups, so cross-check
639        // at parse time and reject as `BadStructure`.
640        let ltsh = dir
641            .find(b"LTSH", bytes)
642            .map(|s| LtshTable::parse_with_glyph_count(s, maxp.num_glyphs))
643            .transpose()?;
644        // §5.7.2 fixes the per-record `widths[]` length at
645        // `maxp.numGlyphs`. Cross-checking against `maxp.num_glyphs`
646        // at parse time rejects under-sized records (`UnexpectedEof`)
647        // and protects per-ppem lookups from over-reading the slice.
648        let hdmx = dir
649            .find(b"hdmx", bytes)
650            .map(|s| HdmxTable::parse(s, maxp.num_glyphs))
651            .transpose()?;
652        // §5.7.8 describes a fixed-shape table: 6-byte header, then a
653        // RatioRange + Offset16 pair of arrays followed by VDMX groups
654        // referenced from those offsets. No per-glyph cross-check
655        // against `maxp` is needed — the table publishes font-wide
656        // extents indexed by ppem only, not per-glyph data. `parse`
657        // enforces the §5.7.8 sort + sentinel invariants.
658        let vdmx = dir.find(b"VDMX", bytes).map(VdmxTable::parse).transpose()?;
659        // §5.7.6 metadata table — header + DataMap array indexed by
660        // four-character ASCII tags. The data payloads sit later in
661        // the same byte slice and `MetaRecord::payload` borrows from
662        // there; the `'a` lifetime of `Font<'a>` therefore covers
663        // every payload exposed through `meta_*` accessors.
664        let meta = dir.find(b"meta", bytes).map(MetaTable::parse).transpose()?;
665        // §5.7.7 PCL 5 table — fixed 54-byte struct of PCL font-
666        // selection attributes. All fields copy out of the slice at
667        // parse time so the parsed table carries no lifetime.
668        let pclt = dir.find(b"PCLT", bytes).map(PcltTable::parse).transpose()?;
669        // §5.5.1 (Amd.1:2020) SVG table — per-glyph-range SVG 1.1 vector
670        // colour-glyph documents. The tag carries a trailing space
671        // (`'SVG '`). Document payloads borrow from this byte slice so
672        // the `'a` lifetime of `Font<'a>` covers every document exposed
673        // through the `svg_*` accessors.
674        let svg = dir
675            .find(&SVG_TABLE_TAG, bytes)
676            .map(SvgTable::parse)
677            .transpose()?;
678        // §6.3.6 MATH table — math-layout parameters. Borrows from the
679        // on-wire slice.
680        let math = dir.find(b"MATH", bytes).map(MathTable::parse).transpose()?;
681        // §6.3.5 JSTF table — justification suggestions. Borrows from the
682        // on-wire slice.
683        let jstf = dir.find(b"JSTF", bytes).map(JstfTable::parse).transpose()?;
684        // §8.x DSIG table — digital signature. Structural decode only; the
685        // PKCS#7 block payloads borrow from this slice.
686        let dsig = dir.find(b"DSIG", bytes).map(DsigTable::parse).transpose()?;
687        // §5.7.5 MERG table — glyph-merge declarations for antialias
688        // filtering. Copies its bytes out at parse time.
689        let merg = dir.find(b"MERG", bytes).map(MergTable::parse).transpose()?;
690        let var_coords = match fvar.as_ref() {
691            Some(f) => f.axes().iter().map(|a| a.default).collect(),
692            None => Vec::new(),
693        };
694
695        Ok(Self {
696            bytes,
697            head,
698            hhea,
699            maxp,
700            cmap,
701            name,
702            os2,
703            hmtx,
704            vhea,
705            vmtx,
706            vorg,
707            loca,
708            glyf,
709            cff,
710            cff2,
711            post,
712            kern,
713            gsub,
714            gpos,
715            gdef,
716            cblc,
717            cbdt,
718            eblc,
719            ebdt,
720            ebsc,
721            colr,
722            cpal,
723            sbix,
724            fvar,
725            avar,
726            gvar,
727            cvar,
728            cvt_bytes,
729            fpgm_bytes,
730            prep_bytes,
731            mvar,
732            hvar,
733            vvar,
734            stat,
735            base,
736            gasp,
737            ltsh,
738            hdmx,
739            vdmx,
740            meta,
741            pclt,
742            svg,
743            math,
744            jstf,
745            dsig,
746            merg,
747            var_coords,
748        })
749    }
750
751    /// Raw bytes used to build this `Font`. Mostly useful for debugging.
752    pub fn bytes(&self) -> &'a [u8] {
753        self.bytes
754    }
755
756    // ---- metadata ----------------------------------------------------------
757
758    /// Family name from the `name` table (Windows English first, falls back
759    /// to Mac Roman if that's all the font has).
760    pub fn family_name(&self) -> Option<&str> {
761        // 1 = Family name
762        self.name.find(1)
763    }
764
765    /// Full name (typically family + style) from the `name` table.
766    pub fn full_name(&self) -> Option<&str> {
767        // 4 = Full name
768        self.name.find(4)
769    }
770
771    /// Subfamily (style) name from the `name` table — e.g. "Bold",
772    /// "Italic", "Regular". `nameID` 2 (Adobe TN5149 §1.4).
773    pub fn subfamily_name(&self) -> Option<&str> {
774        self.name.find(name_id::SUBFAMILY)
775    }
776
777    /// Typographic (preferred) family name — `nameID` 16 — falling back to
778    /// the standard family name (`nameID` 1) when the font omits it.
779    /// Adobe TN5149 §1.4: when `nameID` 16 equals `nameID` 1 it may be
780    /// omitted, so the fallback reconstructs the intended value.
781    pub fn typographic_family_name(&self) -> Option<&str> {
782        self.name
783            .find(name_id::TYPOGRAPHIC_FAMILY)
784            .or_else(|| self.name.find(name_id::FAMILY))
785    }
786
787    /// Typographic (preferred) subfamily name — `nameID` 17 — falling back
788    /// to the standard subfamily name (`nameID` 2). Same omission rule as
789    /// [`Self::typographic_family_name`] (TN5149 §1.4).
790    pub fn typographic_subfamily_name(&self) -> Option<&str> {
791        self.name
792            .find(name_id::TYPOGRAPHIC_SUBFAMILY)
793            .or_else(|| self.name.find(name_id::SUBFAMILY))
794    }
795
796    /// PostScript name — `nameID` 6 (TN5149 §1.5). The unique name a
797    /// PostScript interpreter uses to select the font.
798    pub fn postscript_name(&self) -> Option<&str> {
799        self.name.find(name_id::POSTSCRIPT)
800    }
801
802    /// Version string — `nameID` 5 (TN5149 §1.9), e.g. "Version 1.000".
803    pub fn version_string(&self) -> Option<&str> {
804        self.name.find(name_id::VERSION)
805    }
806
807    /// Copyright notice — `nameID` 0 (TN5149 §1.3).
808    pub fn copyright(&self) -> Option<&str> {
809        self.name.find(name_id::COPYRIGHT)
810    }
811
812    /// Trademark — `nameID` 7 (TN5149 §1.10).
813    pub fn trademark(&self) -> Option<&str> {
814        self.name.find(name_id::TRADEMARK)
815    }
816
817    /// Manufacturer name — `nameID` 8 (TN5149 §1.10).
818    pub fn manufacturer(&self) -> Option<&str> {
819        self.name.find(name_id::MANUFACTURER)
820    }
821
822    /// Designer name — `nameID` 9 (TN5149 §1.10).
823    pub fn designer(&self) -> Option<&str> {
824        self.name.find(name_id::DESIGNER)
825    }
826
827    /// Description — `nameID` 10 (TN5149 §1.10).
828    pub fn description(&self) -> Option<&str> {
829        self.name.find(name_id::DESCRIPTION)
830    }
831
832    /// Font vendor URL — `nameID` 11 (TN5149 §1.10).
833    pub fn vendor_url(&self) -> Option<&str> {
834        self.name.find(name_id::VENDOR_URL)
835    }
836
837    /// Font designer URL — `nameID` 12 (TN5149 §1.10).
838    pub fn designer_url(&self) -> Option<&str> {
839        self.name.find(name_id::DESIGNER_URL)
840    }
841
842    /// Licence description — `nameID` 13 (TN5149 §1.10).
843    pub fn license_description(&self) -> Option<&str> {
844        self.name.find(name_id::LICENSE)
845    }
846
847    /// Licence URL — `nameID` 14 (TN5149 §1.10).
848    pub fn license_url(&self) -> Option<&str> {
849        self.name.find(name_id::LICENSE_URL)
850    }
851
852    /// Arbitrary `name`-table string by `nameID`, picking the best-ranked
853    /// locale (Windows English first). The well-known IDs are exported as
854    /// [`name_id`] constants. Use [`Self::name_string_for`] to target a
855    /// specific platform + language.
856    pub fn name_string(&self, name_id: u16) -> Option<&str> {
857        self.name.find(name_id)
858    }
859
860    /// A specific `(nameID, platformID, languageID)` string — no ranking,
861    /// the exact locale you name (e.g. `(name_id::FAMILY,
862    /// platform::WINDOWS, 0x0411)` for the Japanese family name). Returns
863    /// an owned `String` because non-ASCII records are decoded into a new
864    /// buffer. `None` when no record matches or its encoding is one we
865    /// cannot decode without an unstaged legacy codepage table (Macintosh
866    /// non-Roman scripts — TN5149 §1.2).
867    pub fn name_string_for(
868        &self,
869        name_id: u16,
870        platform_id: u16,
871        language_id: u16,
872    ) -> Option<String> {
873        self.name.find_for(name_id, platform_id, language_id)
874    }
875
876    /// Every `name`-table record, decoded where possible (see
877    /// [`NameRecord`]). The locator tuple `(platformID, encodingID,
878    /// languageID, nameID)` is always present; `string` is `None` for
879    /// encodings we cannot decode in-crate.
880    pub fn name_records(&self) -> Vec<NameRecord> {
881        self.name.records()
882    }
883
884    /// `head.unitsPerEm`. Almost always 1024 or 2048; never zero in valid
885    /// fonts.
886    pub fn units_per_em(&self) -> u16 {
887        self.head.units_per_em
888    }
889
890    /// Borrow the parsed `head` table (ISO/IEC 14496-22:2019 §5.2.1),
891    /// exposing `fontRevision`, the `flags` / `macStyle` words (with
892    /// decoded predicates), the created / modified timestamps,
893    /// `lowestRecPPEM`, `fontDirectionHint`, and `glyphDataFormat`.
894    pub fn head_table(&self) -> &HeadTable {
895        &self.head
896    }
897
898    /// `head.fontRevision` — the font designer's revision number as a
899    /// 16.16 fixed value (e.g. `2.37`).
900    pub fn font_revision(&self) -> f32 {
901        self.head.font_revision
902    }
903
904    /// `head.lowestRecPPEM` — the smallest size, in pixels, at which the
905    /// font is intended to remain legible.
906    pub fn lowest_rec_ppem(&self) -> u16 {
907        self.head.lowest_rec_ppem
908    }
909
910    /// Typographic ascent. We prefer `OS/2.sTypoAscender` if present
911    /// (Windows-clean), falling back to `hhea.ascent`.
912    pub fn ascent(&self) -> i16 {
913        self.os2
914            .as_ref()
915            .and_then(|o| o.s_typo_ascender)
916            .unwrap_or(self.hhea.ascent)
917    }
918
919    /// Typographic descent (typically negative).
920    pub fn descent(&self) -> i16 {
921        self.os2
922            .as_ref()
923            .and_then(|o| o.s_typo_descender)
924            .unwrap_or(self.hhea.descent)
925    }
926
927    /// Suggested gap between lines.
928    pub fn line_gap(&self) -> i16 {
929        self.os2
930            .as_ref()
931            .and_then(|o| o.s_typo_line_gap)
932            .unwrap_or(self.hhea.line_gap)
933    }
934
935    /// `maxp.numGlyphs`.
936    pub fn glyph_count(&self) -> u16 {
937        self.maxp.num_glyphs
938    }
939
940    /// Borrow the parsed `hhea` table (ISO/IEC 14496-22:2019 §5.2.4),
941    /// exposing the horizontal header in full: ascent / descent / line gap,
942    /// `advanceWidthMax`, the min side-bearing extremes, `xMaxExtent`, the
943    /// caret-slope rise / run / offset, and `numberOfHMetrics`.
944    pub fn hhea_table(&self) -> &HheaTable {
945        &self.hhea
946    }
947
948    /// Borrow the parsed `maxp` table (ISO/IEC 14496-22:2019 §5.2.5). For a
949    /// v1.0 (TrueType) table the `v1` field carries the rasteriser-sizing
950    /// maxima (`maxPoints`, composite limits, bytecode resource caps,
951    /// `maxComponentDepth`); `v1` is `None` for a v0.5 (CFF) table.
952    pub fn maxp_table(&self) -> &MaxpTable {
953        &self.maxp
954    }
955
956    /// `OS/2.usWeightClass` (100..1000), or 400 (Regular) if `OS/2` absent.
957    pub fn weight_class(&self) -> u16 {
958        self.os2.as_ref().map(|o| o.us_weight_class).unwrap_or(400)
959    }
960
961    /// `OS/2.usWidthClass` (1..9, where 5 = Medium/Normal), or 5 if `OS/2`
962    /// is absent (ISO/IEC 14496-22:2019 §5.2.3).
963    pub fn width_class(&self) -> u16 {
964        self.os2.as_ref().map(|o| o.us_width_class).unwrap_or(5)
965    }
966
967    /// Borrow the parsed `OS/2` table (ISO/IEC 14496-22:2019 §5.2.3), when
968    /// the font publishes one. Exposes the full field set: classification
969    /// (weight / width / PANOSE / family class), `fsType` embedding
970    /// permissions, `fsSelection` style bits, the sub/superscript and
971    /// strikeout metrics, Unicode / code-page coverage ranges, vendor id,
972    /// the typographic / Windows vertical metrics, and (versioned) x-height
973    /// / cap-height / optical-size range.
974    pub fn os2_table(&self) -> Option<&Os2Table> {
975        self.os2.as_ref()
976    }
977
978    /// The `OS/2.fsType` embedding-permission state, distilled to the
979    /// single most-restrictive applicable flag, or `None` when `OS/2` is
980    /// absent. `installable` (no restriction bit) is the permissive
981    /// default. See [`Os2Table`]'s `embedding_*` predicates for the raw
982    /// bits.
983    pub fn embedding_installable(&self) -> Option<bool> {
984        self.os2.as_ref().map(|o| o.embedding_installable())
985    }
986
987    /// `post.italicAngle` in degrees (negative for forward-slanted).
988    pub fn italic_angle(&self) -> f32 {
989        self.post.as_ref().map(|p| p.italic_angle).unwrap_or(0.0)
990    }
991
992    /// `true` when the font ships a `post` table (any version).
993    pub fn has_post(&self) -> bool {
994        self.post.is_some()
995    }
996
997    /// `true` when the font carries PostScript (`CFF `) outlines rather
998    /// than (or in addition to) TrueType `glyf` outlines.
999    pub fn has_cff_outlines(&self) -> bool {
1000        self.cff.is_some()
1001    }
1002
1003    /// Borrow the parsed `CFF ` table, when the font ships one.
1004    pub fn cff_table(&self) -> Option<&CffTable<'a>> {
1005        self.cff.as_ref()
1006    }
1007
1008    /// `true` when the font carries variable PostScript (`CFF2`) outlines.
1009    pub fn has_cff2_outlines(&self) -> bool {
1010        self.cff2.is_some()
1011    }
1012
1013    /// Borrow the parsed `CFF2` table, when the font ships one.
1014    pub fn cff2_table(&self) -> Option<&Cff2Table<'a>> {
1015        self.cff2.as_ref()
1016    }
1017
1018    /// `true` when the `CFF ` table is CID-keyed (Adobe TN #5176 §18).
1019    pub fn is_cid_keyed(&self) -> bool {
1020        self.cff.as_ref().is_some_and(|c| c.is_cid())
1021    }
1022
1023    /// `true` when the font ships a `MATH` table (math typesetting data).
1024    pub fn has_math(&self) -> bool {
1025        self.math.is_some()
1026    }
1027
1028    /// Borrow the parsed `MATH` table, when the font publishes one
1029    /// (ISO/IEC 14496-22:2019 §6.3.6).
1030    pub fn math_table(&self) -> Option<&MathTable<'a>> {
1031        self.math.as_ref()
1032    }
1033
1034    /// A `MathConstants` value (one of the `tables::math::constant::*`
1035    /// indices) resolved at the font's current variation instance.
1036    ///
1037    /// Folds in the record's VariationIndex correction (§6.3.6.2.1)
1038    /// against the GDEF `ItemVariationStore` at the instance set via
1039    /// [`Self::set_variation_coords`]. Returns `None` when the font has no
1040    /// MATH table or no MathConstants sub-table; the value is in font
1041    /// design units (fractional after variation). For a non-variable font
1042    /// the result equals the plain `MathConstants` design-unit value.
1043    pub fn math_constant_var(&self, index: usize) -> Option<f32> {
1044        let c = self.math.as_ref()?.constants()?;
1045        let ivs = self.gdef_item_variation_store();
1046        let coords = self.normalised_coords();
1047        Some(c.value_resolved(index, ivs.as_ref(), &coords))
1048    }
1049
1050    /// Per-glyph MATH italics correction for `gid` resolved at the current
1051    /// variation instance (§6.3.6.2.5 + §6.3.6.2.1). `None` when there is
1052    /// no MATH table, no MathGlyphInfo, or `gid` is uncovered.
1053    pub fn math_italics_correction_var(&self, gid: u16) -> Option<f32> {
1054        let gi = self.math.as_ref()?.glyph_info()?;
1055        let ivs = self.gdef_item_variation_store();
1056        let coords = self.normalised_coords();
1057        gi.italics_correction_resolved(gid, ivs.as_ref(), &coords)
1058    }
1059
1060    /// Per-glyph MATH top-accent attachment point for `gid` resolved at the
1061    /// current variation instance (§6.3.6.2.6 + §6.3.6.2.1). `None` when
1062    /// uncovered (the layout engine then uses the glyph's geometric centre).
1063    pub fn math_top_accent_attachment_var(&self, gid: u16) -> Option<f32> {
1064        let gi = self.math.as_ref()?.glyph_info()?;
1065        let ivs = self.gdef_item_variation_store();
1066        let coords = self.normalised_coords();
1067        gi.top_accent_attachment_resolved(gid, ivs.as_ref(), &coords)
1068    }
1069
1070    /// MATH per-corner kern value for `gid` at correction `height`,
1071    /// resolved at the current variation instance (§6.3.6.2.8/.9 +
1072    /// §6.3.6.2.1). `None` when `gid` has no kern table for `corner`.
1073    pub fn math_kern_var(&self, gid: u16, corner: MathKernCorner, height: i16) -> Option<f32> {
1074        let gi = self.math.as_ref()?.glyph_info()?;
1075        let ivs = self.gdef_item_variation_store();
1076        let coords = self.normalised_coords();
1077        gi.math_kern_resolved(gid, corner, height, ivs.as_ref(), &coords)
1078    }
1079
1080    /// MATH glyph-assembly italics correction for `gid` growing in `dir`,
1081    /// resolved at the current variation instance (§6.3.6.2.12 +
1082    /// §6.3.6.2.1). `None` when `gid` has no assembly in `dir`.
1083    pub fn math_assembly_italics_correction_var(
1084        &self,
1085        gid: u16,
1086        dir: GrowDirection,
1087    ) -> Option<f32> {
1088        let v = self.math.as_ref()?.variants()?;
1089        let ivs = self.gdef_item_variation_store();
1090        let coords = self.normalised_coords();
1091        v.assembly_italics_correction_resolved(gid, dir, ivs.as_ref(), &coords)
1092    }
1093
1094    /// `true` when the font ships a `JSTF` table (justification data).
1095    pub fn has_jstf(&self) -> bool {
1096        self.jstf.is_some()
1097    }
1098
1099    /// Borrow the parsed `JSTF` table, when the font publishes one
1100    /// (ISO/IEC 14496-22:2019 §6.3.5).
1101    pub fn jstf_table(&self) -> Option<&JstfTable<'a>> {
1102        self.jstf.as_ref()
1103    }
1104
1105    /// `true` when the font ships a `DSIG` table (a digital signature).
1106    pub fn has_dsig(&self) -> bool {
1107        self.dsig.is_some()
1108    }
1109
1110    /// Borrow the parsed `DSIG` table (ISO/IEC 14496-22:2019 §8.x), when
1111    /// the font publishes one. The table carries one or more PKCS#7
1112    /// signature blocks surfaced as raw bytes; this crate decodes the table
1113    /// structure but does not verify the signature cryptographically.
1114    pub fn dsig_table(&self) -> Option<&DsigTable<'a>> {
1115        self.dsig.as_ref()
1116    }
1117
1118    /// `true` when the font ships a `MERG` table (glyph-merge declarations
1119    /// for antialias filtering, ISO/IEC 14496-22:2019 §5.7.5).
1120    pub fn has_merg(&self) -> bool {
1121        self.merg.is_some()
1122    }
1123
1124    /// Borrow the parsed `MERG` table, when the font publishes one. The
1125    /// table maps glyphs to merge classes and gives a per-class-pair
1126    /// merge-entry byte; the run-processing algorithm that consumes those
1127    /// entries is a renderer concern.
1128    pub fn merg_table(&self) -> Option<&MergTable> {
1129        self.merg.as_ref()
1130    }
1131
1132    /// Borrow the parsed `post` table. `None` when the font does not
1133    /// publish one.
1134    pub fn post_table(&self) -> Option<&PostTable> {
1135        self.post.as_ref()
1136    }
1137
1138    /// Resolve glyph `gid`'s `post`-table name reference, when the
1139    /// table publishes one.
1140    ///
1141    /// Returns:
1142    ///
1143    /// - `Some(GlyphNameRef::Custom(name))` — the font supplied the
1144    ///   glyph's name as a v2.0 Pascal string. The string is already
1145    ///   trimmed of its length byte.
1146    /// - `Some(GlyphNameRef::StandardMac { index })` — the glyph
1147    ///   resolves to entry `index` of the 258-name standard Macintosh
1148    ///   glyph table (referenced through v1.0, v2.0, or v2.5). The
1149    ///   258-name array is staged in `docs/text/opentype/` and exposed
1150    ///   as [`STANDARD_MAC_GLYPH_NAMES`]; [`Font::glyph_name`] resolves
1151    ///   the index into the canonical name. This lower-level accessor
1152    ///   surfaces the raw index so tooling can introspect the reference
1153    ///   without name resolution.
1154    /// - `None` — the font has no `post` table, the table is v3.0
1155    ///   (no glyph names at all), `gid` falls outside the v2.0 /
1156    ///   v2.5 index array, or the index references a Pascal string
1157    ///   the pool cannot satisfy.
1158    pub fn glyph_name_ref(&self, gid: u16) -> Option<GlyphNameRef<'_>> {
1159        self.post.as_ref()?.glyph_name_ref(gid)
1160    }
1161
1162    /// Convenience accessor: return the glyph's PostScript name,
1163    /// resolving **both** `post`-name branches.
1164    ///
1165    /// A font-supplied v2.0 Pascal string is returned directly; a
1166    /// `StandardMac { index }` reference (from v1.0, v2.0 with
1167    /// `glyphNameIndex < 258`, or v2.5) is resolved through the
1168    /// [`STANDARD_MAC_GLYPH_NAMES`] table into its canonical standard
1169    /// Macintosh name.
1170    ///
1171    /// Returns `None` when no name is available — the font has no
1172    /// `post` table, the table is v3.0 (no names at all), or `gid`
1173    /// falls outside the table's index space. Use
1174    /// [`Font::glyph_name_ref`] to distinguish the custom and
1175    /// standard-Mac branches when that matters.
1176    pub fn glyph_name(&self, gid: u16) -> Option<&str> {
1177        match self.glyph_name_ref(gid) {
1178            Some(GlyphNameRef::Custom(s)) => Some(s),
1179            Some(GlyphNameRef::StandardMac { index }) => {
1180                crate::tables::post::standard_mac_glyph_name(index)
1181            }
1182            None => {
1183                // OTTO/CFF fonts commonly ship a `post` v3.0 (no names);
1184                // the `CFF ` charset is then the only name source.
1185                self.cff.as_ref().and_then(|c| c.glyph_name(gid))
1186            }
1187        }
1188    }
1189
1190    /// Reverse lookup: the glyph id named `name` by the `post` table,
1191    /// inverting [`Font::glyph_name`].
1192    ///
1193    /// Resolves over every named glyph the table publishes — v2.0
1194    /// custom Pascal strings and standard-Macintosh names alike (from
1195    /// v1.0, v2.0 with `glyphNameIndex < 258`, or v2.5). The comparison
1196    /// is exact byte equality (PostScript glyph names are ASCII).
1197    ///
1198    /// Returns the **lowest** glyph id carrying that name, or `None`
1199    /// when the font has no `post` table, the table is v3.0, or no glyph
1200    /// is named `name`.
1201    pub fn gid_for_glyph_name(&self, name: &str) -> Option<u16> {
1202        if let Some(gid) = self.post.as_ref().and_then(|p| p.gid_for_name(name)) {
1203            return Some(gid);
1204        }
1205        // OTTO/CFF fonts with a `post` v3.0 (no names) resolve through the
1206        // CFF charset instead.
1207        self.cff.as_ref().and_then(|c| c.gid_for_name(name))
1208    }
1209
1210    /// Iterate every `(glyph_id, post-table name)` pair the font
1211    /// publishes, in ascending glyph-id order.
1212    ///
1213    /// Standard-Macintosh references are resolved to their canonical
1214    /// names; v2.0 custom strings are returned directly. Glyph ids the
1215    /// `post` table names with an unsatisfiable reference are skipped.
1216    /// The iterator is empty when the font has no `post` table or the
1217    /// table is v3.0 (no names at all).
1218    pub fn iter_glyph_names(&self) -> Box<dyn Iterator<Item = (u16, &str)> + '_> {
1219        // Prefer `post`-table names; fall back to the CFF charset for OTTO
1220        // fonts whose `post` is v3.0 (no names).
1221        let has_post_names = self
1222            .post
1223            .as_ref()
1224            .is_some_and(|p| p.iter_glyph_names().next().is_some());
1225        if has_post_names {
1226            Box::new(self.post.iter().flat_map(|p| p.iter_glyph_names()))
1227        } else if let Some(cff) = self.cff.as_ref() {
1228            Box::new(cff.iter_glyph_names())
1229        } else {
1230            Box::new(std::iter::empty())
1231        }
1232    }
1233
1234    // ---- glyph lookup ------------------------------------------------------
1235
1236    /// Map a Unicode codepoint to its glyph id.
1237    pub fn glyph_index(&self, codepoint: char) -> Option<u16> {
1238        self.cmap.lookup(codepoint as u32)
1239    }
1240
1241    /// Look up the variant glyph for a `(codepoint, variation_selector)`
1242    /// pair from the cmap format-14 (Unicode Variation Sequences)
1243    /// subtable.
1244    ///
1245    /// Returns:
1246    ///
1247    /// - `Some(glyph)` from the **non-default** UVS table when the
1248    ///   variation selector overrides the base glyph (e.g. emoji
1249    ///   presentation `<emoji, U+FE0F>`, text presentation
1250    ///   `<emoji, U+FE0E>`, or registered Ideographic Variation
1251    ///   Sequence `<CJK, U+E0100..U+E01EF>`).
1252    /// - `Some(base)` when the pair is in the **default** UVS table —
1253    ///   semantically "render the base codepoint's default glyph; the
1254    ///   variation selector is just a hint". Equivalent to
1255    ///   [`Self::glyph_index`] for the base codepoint, returned for
1256    ///   API symmetry so callers don't have to special-case the
1257    ///   default-presentation branch.
1258    /// - `None` when the font has no format-14 subtable, the variation
1259    ///   selector isn't enumerated, or neither UVS table covers the
1260    ///   base codepoint.
1261    pub fn lookup_variation(&self, codepoint: char, variation_selector: char) -> Option<u16> {
1262        self.cmap
1263            .lookup_variation(codepoint as u32, variation_selector as u32)
1264    }
1265
1266    /// Decode the TrueType outline for `glyph_id`. Empty / blank glyphs
1267    /// (e.g. the space glyph) return an outline with zero contours.
1268    ///
1269    /// Returns an empty outline when the font has no `glyf`/`loca`
1270    /// (CBDT/CBLC-only colour-emoji fonts). Callers that care should
1271    /// check [`Font::has_color_bitmaps`] first.
1272    ///
1273    /// **Variable fonts:** if the font ships `fvar`/`gvar` and the
1274    /// caller has set non-default coordinates via
1275    /// [`Font::set_variation_coords`], the static outline returned
1276    /// here has gvar deltas applied (with avar remap on the input
1277    /// coords first).
1278    ///
1279    /// Both simple **and** composite glyphs are retargeted. For a
1280    /// composite glyph the gvar packed point numbers address the
1281    /// *components* (plus four phantom points), not flattened outline
1282    /// points, per ISO/IEC 14496-22:2019 §7.3.4.3 — the per-component
1283    /// `(dx, dy)` placement deltas are folded into each component's
1284    /// X/Y offset (and scaled with the offset where
1285    /// `SCALED_COMPONENT_OFFSET` is set) before the children are
1286    /// flattened. Point-matched components take no delta, and nested
1287    /// components inherit their own glyph's variation when decoded as
1288    /// top-level glyphs, matching the spec's "most deeply-nested
1289    /// first" processing order.
1290    pub fn glyph_outline(&self, glyph_id: u16) -> Result<TtOutline, Error> {
1291        if glyph_id >= self.maxp.num_glyphs {
1292            return Err(Error::GlyphOutOfRange(glyph_id));
1293        }
1294        // OTTO (PostScript-outline) fonts carry no `glyf`; reconstruct the
1295        // outline from the `CFF ` Type 2 charstring instead. CFF outlines
1296        // are not gvar-variable in this crate (CFF2 is a separate table),
1297        // so the variation path below never applies to them.
1298        if self.glyf.is_none() {
1299            if let Some(cff) = self.cff.as_ref() {
1300                return Ok(cff.glyph_outline(glyph_id).unwrap_or_default());
1301            }
1302            if let Some(cff2) = self.cff2.as_ref() {
1303                // CFF2 outline at the current variation instance. When the
1304                // caller has set non-default axis coordinates, the
1305                // avar-bent normalised vector drives the `blend` operator;
1306                // otherwise the default instance is rendered.
1307                let cff2_variable = self.fvar.is_some()
1308                    && !self.var_coords.is_empty()
1309                    && self.coords_differ_from_default();
1310                let normalised = if cff2_variable {
1311                    self.normalised_coords()
1312                } else {
1313                    Vec::new()
1314                };
1315                return Ok(cff2
1316                    .glyph_outline_at(glyph_id, &normalised)
1317                    .unwrap_or_default());
1318            }
1319        }
1320        let variable =
1321            self.gvar.is_some() && !self.var_coords.is_empty() && self.coords_differ_from_default();
1322        // Compute the avar-bent normalised coordinate vector once and
1323        // share it across the whole (possibly recursive) composite walk.
1324        let normalised = if variable {
1325            self.normalised_coords()
1326        } else {
1327            Vec::new()
1328        };
1329        self.glyph_outline_at_depth(glyph_id, 0, variable, &normalised)
1330    }
1331
1332    /// Recursive outline resolver. `depth` guards composite recursion;
1333    /// `variable` + `normalised` carry the variation context down through
1334    /// the §7.3.4.3 component walk so each component glyph is resolved
1335    /// with its own gvar deltas applied before placement.
1336    fn glyph_outline_at_depth(
1337        &self,
1338        glyph_id: u16,
1339        depth: u8,
1340        variable: bool,
1341        normalised: &[f32],
1342    ) -> Result<TtOutline, Error> {
1343        if glyph_id >= self.maxp.num_glyphs {
1344            return Err(Error::GlyphOutOfRange(glyph_id));
1345        }
1346        let (loca, glyf) = match (self.loca.as_ref(), self.glyf.as_ref()) {
1347            (Some(l), Some(g)) => (l, g),
1348            _ => return Ok(TtOutline::default()),
1349        };
1350        let range = loca.glyph_range(glyph_id)?;
1351        if range.is_empty() {
1352            return Ok(TtOutline::default());
1353        }
1354
1355        // Composite-glyph variation path (§7.3.4.3): apply per-component
1356        // placement deltas inside the composite decode rather than to
1357        // flattened outline points, and resolve each component glyph's
1358        // own variation via a recursive child resolver.
1359        if variable {
1360            if let Ok(n_comp) = glyf.composite_component_count(range.clone()) {
1361                if n_comp > 0 {
1362                    let gvar = self.gvar.as_ref().unwrap();
1363                    if let Ok(deltas) = gvar.glyph_component_deltas(glyph_id, n_comp, normalised) {
1364                        let resolve = |child_gid: u16, child_depth: u8| {
1365                            self.glyph_outline_at_depth(
1366                                child_gid,
1367                                child_depth,
1368                                variable,
1369                                normalised,
1370                            )
1371                        };
1372                        return glyf.glyph_outline_var(range, loca, depth, &deltas, &resolve);
1373                    }
1374                }
1375            }
1376        }
1377
1378        let mut out = glyf.glyph_outline(range, loca, depth)?;
1379        if variable {
1380            let gvar = self.gvar.as_ref().unwrap();
1381            let n_pts: usize = out.contours.iter().map(|c| c.points.len()).sum();
1382            if n_pts > 0 && n_pts <= u16::MAX as usize {
1383                // Build the static contour structure + default grid
1384                // coordinates so the gvar layer can infer deltas for
1385                // points a tuple omits (IUP, ISO/IEC 14496-22:2019
1386                // §7.3.4.4). The default coordinates must be the
1387                // pre-delta outline points, in gvar point-number order
1388                // (= contour-concatenated order), which is exactly the
1389                // order `out.contours` flattens to here.
1390                let contours: Vec<Vec<(i32, i32)>> = out
1391                    .contours
1392                    .iter()
1393                    .map(|c| c.points.iter().map(|p| (p.x as i32, p.y as i32)).collect())
1394                    .collect();
1395                let info = tables::gvar::SimpleOutlineInfo::from_contours(&contours);
1396                if let Ok(deltas) = gvar.glyph_deltas_iup(glyph_id, &info, normalised) {
1397                    let mut idx = 0usize;
1398                    for c in out.contours.iter_mut() {
1399                        for p in c.points.iter_mut() {
1400                            let (dx, dy) = deltas[idx];
1401                            let nx = p.x as i32 + dx;
1402                            let ny = p.y as i32 + dy;
1403                            p.x = clamp_i16_for_outline(nx);
1404                            p.y = clamp_i16_for_outline(ny);
1405                            idx += 1;
1406                        }
1407                    }
1408                    // Re-derive bounds after delta application.
1409                    out.bounds = outline::derive_bbox(&out.contours);
1410                }
1411            }
1412        }
1413        Ok(out)
1414    }
1415
1416    /// Per-glyph advance width in font units.
1417    ///
1418    /// For a composite glyph whose components include one carrying the
1419    /// `USE_MY_METRICS` flag (§5.3.4), the advance is taken from that
1420    /// component's `hmtx` entry rather than the composite's own — the spec
1421    /// uses this to force a composite (e.g. `i`-circumflex) to inherit a
1422    /// component's (e.g. dotless-`i`) metrics. The last flagged component
1423    /// wins; the chase is depth-bounded.
1424    pub fn glyph_advance(&self, glyph_id: u16) -> i16 {
1425        let effective = self.metrics_source_glyph(glyph_id);
1426        self.hmtx.advance(effective) as i16
1427    }
1428
1429    /// Per-glyph left-side bearing in font units. Honours `USE_MY_METRICS`
1430    /// the same way as [`Font::glyph_advance`] (the spec forces both `aw`
1431    /// and `lsb` to the flagged component's values).
1432    pub fn glyph_lsb(&self, glyph_id: u16) -> i16 {
1433        let effective = self.metrics_source_glyph(glyph_id);
1434        self.hmtx.lsb(effective)
1435    }
1436
1437    /// Resolve the glyph whose `hmtx` metrics a composite should adopt,
1438    /// following the `USE_MY_METRICS` component chain (§5.3.4). Returns
1439    /// `glyph_id` itself for simple glyphs, fonts without `glyf`/`loca`, or
1440    /// composites where no component sets the flag. The chase is bounded by
1441    /// the composite-depth limit and guards against a self-reference.
1442    fn metrics_source_glyph(&self, glyph_id: u16) -> u16 {
1443        let (loca, glyf) = match (self.loca.as_ref(), self.glyf.as_ref()) {
1444            (Some(l), Some(g)) => (l, g),
1445            _ => return glyph_id,
1446        };
1447        let mut current = glyph_id;
1448        // Bound the chase: a USE_MY_METRICS component can itself be a
1449        // composite that sets the flag, so follow the chain but never more
1450        // than a few hops (matching the outline composite-depth guard).
1451        for _ in 0..8u8 {
1452            let range = match loca.glyph_range(current) {
1453                Ok(r) => r,
1454                Err(_) => return current,
1455            };
1456            match glyf.use_my_metrics_glyph(range) {
1457                Ok(Some(next)) if next != current => current = next,
1458                _ => return current,
1459            }
1460        }
1461        current
1462    }
1463
1464    /// `true` when the font ships both a `vhea` and `vmtx` table —
1465    /// i.e. it supplies vertical-layout metrics for CJK / Mongolian
1466    /// or other top-to-bottom-written scripts.
1467    pub fn has_vertical_metrics(&self) -> bool {
1468        self.vhea.is_some() && self.vmtx.is_some()
1469    }
1470
1471    /// Borrow the parsed `vhea` table, when present.
1472    /// (ISO/IEC 14496-22:2019 §5.7.9.)
1473    pub fn vhea_table(&self) -> Option<&VheaTable> {
1474        self.vhea.as_ref()
1475    }
1476
1477    /// Vertical typographic ascender from `vhea`. For v1.1 this is
1478    /// `vertTypoAscender` (distance in font design units from the
1479    /// ideographic em-box centre baseline to the right side of the
1480    /// em-box, per §5.7.9 v1.1 row 2); for v1.0 the same bytes are
1481    /// the centre-line-relative `ascent` field. Returns `None` if the
1482    /// font lacks a `vhea` table.
1483    pub fn vertical_ascent(&self) -> Option<i16> {
1484        self.vhea.map(|v| v.vert_typo_ascender)
1485    }
1486
1487    /// Vertical typographic descender from `vhea` (v1.1
1488    /// `vertTypoDescender`; v1.0 `descent`).
1489    pub fn vertical_descent(&self) -> Option<i16> {
1490        self.vhea.map(|v| v.vert_typo_descender)
1491    }
1492
1493    /// Vertical typographic line gap from `vhea` (v1.1
1494    /// `vertTypoLineGap`; v1.0 row "Reserved; set to 0", so static
1495    /// v1.0 fonts will return `Some(0)` here).
1496    pub fn vertical_line_gap(&self) -> Option<i16> {
1497        self.vhea.map(|v| v.vert_typo_line_gap)
1498    }
1499
1500    /// `vhea.advanceHeightMax` — the maximum advance height in the
1501    /// font, in design units. Per §5.7.9 the field is `int16`.
1502    pub fn advance_height_max(&self) -> Option<i16> {
1503        self.vhea.map(|v| v.advance_height_max)
1504    }
1505
1506    /// Borrow the parsed `vmtx` table, when present.
1507    /// (ISO/IEC 14496-22:2019 §5.7.10.)
1508    pub fn vmtx_table(&self) -> Option<&VmtxTable<'a>> {
1509        self.vmtx.as_ref()
1510    }
1511
1512    /// Per-glyph advance height in font design units. Returns `None`
1513    /// when the font lacks `vhea`/`vmtx`; otherwise returns the
1514    /// `vMetrics` advance for `glyph_id`, with the §5.7.10 "monospaced
1515    /// tail" rule (glyphs beyond `numOfLongVerMetrics` inherit the
1516    /// last pair's advance height) applied transparently.
1517    pub fn glyph_advance_height(&self, glyph_id: u16) -> Option<u16> {
1518        Some(self.vmtx.as_ref()?.advance_height(glyph_id))
1519    }
1520
1521    /// Per-glyph top side bearing in font design units. Returns
1522    /// `None` when the font lacks `vmtx`.
1523    pub fn glyph_top_side_bearing(&self, glyph_id: u16) -> Option<i16> {
1524        Some(self.vmtx.as_ref()?.top_side_bearing(glyph_id))
1525    }
1526
1527    /// Per-glyph vertical origin Y coordinate in font design units.
1528    /// Per §5.7.10 ("Vertical Origin and Advance Height"), this is
1529    /// `topSideBearing + glyph_bounding_box.y_max`. Returns `None`
1530    /// when the font lacks `vmtx` or when the glyph has no outline
1531    /// bounding box (empty glyph, blank glyph, or a CBDT-only colour-
1532    /// emoji font with no `glyf`/`loca`). For CFF fonts the spec
1533    /// recommends the optional `VORG` table instead; that path is not
1534    /// implemented here (TrueType outlines only).
1535    pub fn glyph_vertical_origin_y(&self, glyph_id: u16) -> Option<i16> {
1536        let tsb = self.vmtx.as_ref()?.top_side_bearing(glyph_id);
1537        let bbox = self.glyph_bounding_box(glyph_id)?;
1538        // Saturating add keeps a pathological bbox from panicking;
1539        // real-world fonts are nowhere near i16::MAX in this dim.
1540        Some(tsb.saturating_add(bbox.y_max))
1541    }
1542
1543    /// `true` when the font ships a `VORG` table per §5.4.4. The table
1544    /// is optional and, per spec, restricted to CFF-flavoured sfnts;
1545    /// it appears occasionally in TrueType sfnts as well, in which case
1546    /// the parser surfaces the bytes but [`Self::vert_origin_y_from_vorg`]
1547    /// declines to consult it (the spec mandates "If present in
1548    /// TrueType OFF fonts it must be ignored by font clients").
1549    pub fn has_vorg(&self) -> bool {
1550        self.vorg.is_some()
1551    }
1552
1553    /// Borrow the parsed `VORG` table, when present. Surfaced verbatim
1554    /// so callers that want to introspect the metrics array directly
1555    /// (e.g. font tooling) can do so without re-parsing the bytes.
1556    pub fn vorg_table(&self) -> Option<&VorgTable> {
1557        self.vorg.as_ref()
1558    }
1559
1560    /// Default vertical-origin Y per §5.4.4, in font design units.
1561    /// Returns `None` when no `VORG` table is present.
1562    pub fn vorg_default_vert_origin_y(&self) -> Option<i16> {
1563        self.vorg.as_ref().map(|v| v.default_vert_origin_y)
1564    }
1565
1566    /// Y coordinate of the vertical origin for `glyph_id` per `VORG`
1567    /// §5.4.4, in font design units.
1568    ///
1569    /// Returns:
1570    ///  - `None` when the font has no `VORG`.
1571    ///  - `None` when the font is TrueType-flavoured (a `glyf` table is
1572    ///    present). §5.4.4 mandates "If present in TrueType OFF fonts
1573    ///    it must be ignored by font clients, just as any other
1574    ///    unrecognized table would be"; we honour that rule here.
1575    ///    Callers that want the TrueType-derived origin should use
1576    ///    [`Self::glyph_vertical_origin_y`] (which derives the value
1577    ///    from `vmtx.topSideBearing` + `glyf` bbox per §5.7.10).
1578    ///  - `Some(default_vert_origin_y)` when the glyph has no per-glyph
1579    ///    override entry — §5.4.4 size-optimised form ("glyphs whose
1580    ///    vertical origin's y coordinate equals defaultVertOriginY will
1581    ///    not have an entry").
1582    ///  - `Some(vert_origin_y)` from the metrics-array override when
1583    ///    one is present.
1584    pub fn vert_origin_y_from_vorg(&self, glyph_id: u16) -> Option<i16> {
1585        let vorg = self.vorg.as_ref()?;
1586        // §5.4.4: TrueType clients must ignore the table. The presence
1587        // of `glyf` is the canonical sfnt signal that the outlines are
1588        // TrueType (a CFF font carries `CFF ` or `CFF2` instead and has
1589        // no `glyf`/`loca`).
1590        if self.glyf.is_some() {
1591            return None;
1592        }
1593        Some(vorg.vert_origin_y(glyph_id))
1594    }
1595
1596    /// `true` when the font ships a `BASE` table (ISO/IEC 14496-22:2019
1597    /// §6.3.1). The table is optional for both TrueType and CFF sfnts
1598    /// and is consulted by text-layout clients when aligning glyphs
1599    /// from different scripts on a common baseline.
1600    pub fn has_base(&self) -> bool {
1601        self.base.is_some()
1602    }
1603
1604    /// Borrow the parsed `BASE` table when present. Exposes the
1605    /// HorizAxis / VertAxis trees plus (in v1.1 tables) the
1606    /// ItemVariationStore offset for variable-font baseline deltas.
1607    pub fn base_table(&self) -> Option<&BaseTable> {
1608        self.base.as_ref()
1609    }
1610
1611    /// Per-script default Y baseline (HorizAxis, §6.3.1.3) for the
1612    /// given script tag and baseline tag. Returns the design-unit
1613    /// coordinate from the BaseValues entry whose index matches
1614    /// `baseline_tag` inside the Axis's BaseTagList.
1615    ///
1616    /// Returns `None` when:
1617    ///  - the font has no `BASE` table;
1618    ///  - the HorizAxis is missing (typical for CJK vertical-only
1619    ///    fonts);
1620    ///  - the script tag is not listed in the Axis's BaseScriptList
1621    ///    (§6.3.1.3 "If a script is not listed here, then the
1622    ///    text-processing client will render the script using the
1623    ///    layout information specified for the entire font");
1624    ///  - the BaseTagList is NULL or `baseline_tag` is not in it;
1625    ///  - the BaseValues array is shorter than the BaseTagList index.
1626    pub fn base_horiz_y_for_script_baseline(
1627        &self,
1628        script_tag: [u8; 4],
1629        baseline_tag: [u8; 4],
1630    ) -> Option<i16> {
1631        let base = self.base.as_ref()?;
1632        let h = base.horiz_axis.as_ref()?;
1633        let idx = h.baseline_index_for_tag(baseline_tag)?;
1634        let bs = h.base_script_for_tag(script_tag)?;
1635        let bv = bs.base_values.as_ref()?;
1636        bv.base_coords.get(idx).map(|c| c.coordinate())
1637    }
1638
1639    /// Per-script default X baseline (VertAxis, §6.3.1.3) for the given
1640    /// script tag and baseline tag. Mirror of
1641    /// [`Self::base_horiz_y_for_script_baseline`] for vertical layout.
1642    pub fn base_vert_x_for_script_baseline(
1643        &self,
1644        script_tag: [u8; 4],
1645        baseline_tag: [u8; 4],
1646    ) -> Option<i16> {
1647        let base = self.base.as_ref()?;
1648        let v = base.vert_axis.as_ref()?;
1649        let idx = v.baseline_index_for_tag(baseline_tag)?;
1650        let bs = v.base_script_for_tag(script_tag)?;
1651        let bv = bs.base_values.as_ref()?;
1652        bv.base_coords.get(idx).map(|c| c.coordinate())
1653    }
1654
1655    /// Variation-aware sibling of
1656    /// [`Self::base_horiz_y_for_script_baseline`]: a `BaseCoordFormat3`
1657    /// VariationIndex device offset is resolved against the BASE
1658    /// `ItemVariationStore` at the font's current instance, so the
1659    /// baseline Y tracks the design axes.
1660    pub fn base_horiz_y_for_script_baseline_var(
1661        &self,
1662        script_tag: [u8; 4],
1663        baseline_tag: [u8; 4],
1664    ) -> Option<i16> {
1665        let coords = self.normalised_coords();
1666        self.base
1667            .as_ref()?
1668            .horiz_baseline_y_resolved(script_tag, baseline_tag, &coords)
1669    }
1670
1671    /// Variation-aware sibling of
1672    /// [`Self::base_vert_x_for_script_baseline`].
1673    pub fn base_vert_x_for_script_baseline_var(
1674        &self,
1675        script_tag: [u8; 4],
1676        baseline_tag: [u8; 4],
1677    ) -> Option<i16> {
1678        let coords = self.normalised_coords();
1679        self.base
1680            .as_ref()?
1681            .vert_baseline_x_resolved(script_tag, baseline_tag, &coords)
1682    }
1683
1684    /// `true` when the font carries a `gasp` table
1685    /// (ISO/IEC 14496-22:2019 §5.3.7). Absent in many fonts; the
1686    /// rasteriser applies its default policy when missing.
1687    pub fn has_gasp(&self) -> bool {
1688        self.gasp.is_some()
1689    }
1690
1691    /// Borrow the parsed `gasp` table when present. Carries the
1692    /// per-ppem rasterisation hints (`GASP_GRIDFIT`, `GASP_DOGRAY`,
1693    /// `GASP_SYMMETRIC_GRIDFIT`, `GASP_SYMMETRIC_SMOOTHING`) sorted
1694    /// by `rangeMaxPPEM`.
1695    pub fn gasp_table(&self) -> Option<&GaspTable> {
1696        self.gasp.as_ref()
1697    }
1698
1699    /// Pick the `gasp` record that governs rasterisation at the given
1700    /// pixel-per-em size — the first record whose `rangeMaxPPEM` is at
1701    /// least `ppem` (§5.3.7). Returns `None` when the font ships no
1702    /// `gasp` table or every record's upper limit is below `ppem`; in
1703    /// either case the caller should fall back to the rasteriser's
1704    /// default policy.
1705    pub fn gasp_behavior_for_ppem(&self, ppem: u16) -> Option<&GaspRange> {
1706        self.gasp.as_ref()?.behavior_for_ppem(ppem)
1707    }
1708
1709    /// `true` when the font ships an `LTSH` table (ISO/IEC 14496-22:2019
1710    /// §5.7.4). Absent in most fonts; rasterisers without one always
1711    /// grid-fit (or consult `hdmx` / `vdmx` if those are present
1712    /// instead) to find each glyph's true advance width.
1713    pub fn has_ltsh(&self) -> bool {
1714        self.ltsh.is_some()
1715    }
1716
1717    /// Borrow the parsed `LTSH` table when present. Carries the
1718    /// per-glyph `yPels` array recording each glyph's linear-threshold
1719    /// ppem per §5.7.4.
1720    pub fn ltsh_table(&self) -> Option<&LtshTable> {
1721        self.ltsh.as_ref()
1722    }
1723
1724    /// Lowest ppem at which the grid-fitted advance for `glyph_id` has
1725    /// converged on the rounded linear advance per §5.7.4 — i.e. the
1726    /// rasteriser may round the design-unit advance to integer pixels
1727    /// at every ppem at least the returned value. Returns `None` when
1728    /// the font ships no `LTSH` table or `glyph_id` is out of range.
1729    pub fn ltsh_threshold(&self, glyph_id: u16) -> Option<u8> {
1730        self.ltsh.as_ref()?.linear_threshold(glyph_id)
1731    }
1732
1733    /// `true` when `glyph_id` is safe to advance-scale linearly at
1734    /// `ppem` per §5.7.4 — i.e. `ppem >= LTSH.yPels[glyph_id]`. When
1735    /// the font ships no `LTSH` table, returns `false` so the caller
1736    /// falls back to grid-fitting (which is what §5.7.4 also prescribes
1737    /// for fonts without an `LTSH`). Returns `false` for out-of-range
1738    /// `glyph_id`.
1739    pub fn ltsh_linearly_scales_at_ppem(&self, glyph_id: u16, ppem: u16) -> bool {
1740        match self.ltsh.as_ref() {
1741            Some(t) => t.linearly_scales_at_ppem(glyph_id, ppem),
1742            None => false,
1743        }
1744    }
1745
1746    /// `true` when the font ships an `hdmx` table (ISO/IEC 14496-22:2019
1747    /// §5.7.2). Optional table; absent in most fonts. §7.3.5 forbids
1748    /// `hdmx` in variable fonts — a caller that wants to validate the
1749    /// font shape may pair this with [`Self::is_variable`].
1750    pub fn has_hdmx(&self) -> bool {
1751        self.hdmx.is_some()
1752    }
1753
1754    /// Borrow the parsed `hdmx` table when present. Carries the
1755    /// per-ppem device records mapping each glyph to its grid-fitted
1756    /// integer-pixel advance width at that ppem.
1757    pub fn hdmx_table(&self) -> Option<&HdmxTable> {
1758        self.hdmx.as_ref()
1759    }
1760
1761    /// Grid-fitted advance width of `glyph_id` at the requested
1762    /// `ppem`, in integer pixels, per §5.7.2. Returns `None` when the
1763    /// font ships no `hdmx`, when the requested `ppem` is not in the
1764    /// table's record array (§5.7.2 has no "round down" rule — the
1765    /// caller falls back to scan-converting), or when `glyph_id`
1766    /// exceeds the recorded per-glyph array. `ppem` is `u8` because
1767    /// the on-wire field that drives the lookup is `uint8`; values
1768    /// above 255 ppem are not representable in the table.
1769    pub fn hdmx_advance_pixels(&self, glyph_id: u16, ppem: u8) -> Option<u8> {
1770        self.hdmx.as_ref()?.advance_pixels(glyph_id, ppem)
1771    }
1772
1773    /// The set of ppem sizes the font's `hdmx` table covers, in
1774    /// ascending order. Returns an empty `Vec` when no `hdmx` is
1775    /// present.
1776    pub fn hdmx_recorded_ppem_sizes(&self) -> Vec<u8> {
1777        match self.hdmx.as_ref() {
1778            Some(t) => t.recorded_ppem_sizes(),
1779            None => Vec::new(),
1780        }
1781    }
1782
1783    /// `true` when the font ships a `VDMX` table (ISO/IEC 14496-22:2019
1784    /// §5.7.8). Optional table; absent in most fonts. §7.3.5 forbids
1785    /// `VDMX` in variable fonts — pair with [`Self::is_variable`] when
1786    /// validating a font's shape.
1787    pub fn has_vdmx(&self) -> bool {
1788        self.vdmx.is_some()
1789    }
1790
1791    /// Borrow the parsed `VDMX` table when present. Carries one or
1792    /// more VDMX groups indexed via a per-aspect-ratio RatioRange
1793    /// array; each group publishes per-ppem `(yMax, yMin)` envelopes
1794    /// for the font as a whole.
1795    pub fn vdmx_table(&self) -> Option<&VdmxTable> {
1796        self.vdmx.as_ref()
1797    }
1798
1799    /// `(yMax, yMin)` pel envelope for `(ppem, deviceXRatio,
1800    /// deviceYRatio)`, per §5.7.8's first-match RatioRange search.
1801    /// Returns `None` when the font ships no `VDMX`, when no
1802    /// RatioRange matches the device pair (and there is no `(0,0,0)`
1803    /// sentinel), or when the matched group does not record the
1804    /// exact `ppem` requested (§5.7.8 "need not be continuous" — no
1805    /// fallback to neighbouring records).
1806    ///
1807    /// For square-pixel screens the canonical call is
1808    /// `vdmx_y_extent_for_device(ppem, 1, 1)`.
1809    pub fn vdmx_y_extent_for_device(
1810        &self,
1811        ppem: u16,
1812        device_x_ratio: u8,
1813        device_y_ratio: u8,
1814    ) -> Option<(i16, i16)> {
1815        self.vdmx
1816            .as_ref()?
1817            .y_extent_for_device(ppem, device_x_ratio, device_y_ratio)
1818    }
1819
1820    /// Convenience for the common square-pixel case: equivalent to
1821    /// `vdmx_y_extent_for_device(ppem, 1, 1)`. Returns the `(yMax,
1822    /// yMin)` pel envelope at `ppem` under the 1:1 RatioRange
1823    /// (matching either the explicit `(xRatio=1, yStartRatio=1,
1824    /// yEndRatio=1)` entry, or the `(0,0,0)` catch-all sentinel
1825    /// when present), or `None` otherwise.
1826    pub fn vdmx_y_extent_square(&self, ppem: u16) -> Option<(i16, i16)> {
1827        self.vdmx_y_extent_for_device(ppem, 1, 1)
1828    }
1829
1830    /// `true` when the font ships a `meta` (Metadata) table per
1831    /// ISO/IEC 14496-22:2019 §5.7.6.
1832    pub fn has_meta(&self) -> bool {
1833        self.meta.is_some()
1834    }
1835
1836    /// Borrow the parsed `meta` table when present.
1837    ///
1838    /// The returned [`MetaTable`] carries the §5.7.6 DataMap array;
1839    /// per-record payloads borrow from the on-wire `meta` byte slice
1840    /// for the lifetime of the [`Font`].
1841    pub fn meta_table(&self) -> Option<&MetaTable<'a>> {
1842        self.meta.as_ref()
1843    }
1844
1845    /// First `meta` DataMap record whose tag equals `tag`, or
1846    /// `None`. §5.7.6.1's closing paragraph permits multiple records
1847    /// for the same tag but specifies that "any instances after the
1848    /// first may be ignored" for single-record tags; this accessor
1849    /// honours that rule by returning the first match. Callers that
1850    /// want every record for a duplicated tag should iterate
1851    /// [`MetaTable::records`] directly.
1852    pub fn meta_record(&self, tag: &[u8; 4]) -> Option<MetaRecord<'_>> {
1853        self.meta.as_ref()?.record(tag)
1854    }
1855
1856    /// Design-language declaration from the `meta` table's `'dlng'`
1857    /// record (ISO/IEC 14496-22:2019 §5.7.6.2), if present and
1858    /// well-formed UTF-8. The value is a comma-separated list of
1859    /// ScriptLangTags identifying the languages or scripts the font
1860    /// was primarily designed for.
1861    pub fn meta_design_languages(&self) -> Option<&'a str> {
1862        self.meta.as_ref()?.design_languages()
1863    }
1864
1865    /// Supported-language declaration from the `meta` table's
1866    /// `'slng'` record (ISO/IEC 14496-22:2019 §5.7.6.2), if present
1867    /// and well-formed UTF-8. Used to declare languages or scripts
1868    /// the font is capable of supporting (a superset of
1869    /// [`Self::meta_design_languages`] in typical use).
1870    pub fn meta_supported_languages(&self) -> Option<&'a str> {
1871        self.meta.as_ref()?.supported_languages()
1872    }
1873
1874    /// `true` when the font ships a `PCLT` (PCL 5) table per ISO/IEC
1875    /// 14496-22:2019 §5.7.7. The spec deems the table "strongly
1876    /// discouraged for OFF fonts with TrueType outlines", so a `true`
1877    /// here typically marks a legacy font.
1878    pub fn has_pclt(&self) -> bool {
1879        self.pclt.is_some()
1880    }
1881
1882    /// Borrow the parsed `PCLT` table when present.
1883    ///
1884    /// The returned [`PcltTable`] carries the §5.7.7 PCL 5
1885    /// font-selection attributes: HP font number, pitch / x-height /
1886    /// cap-height design-unit metrics, the packed style / type-family
1887    /// / symbol-set words, the typeface "font print" string, the
1888    /// character-complement bitfield, the PCL file name, and the
1889    /// stroke-weight / width-type / serif-style classification bytes.
1890    pub fn pclt_table(&self) -> Option<&PcltTable> {
1891        self.pclt.as_ref()
1892    }
1893
1894    /// `true` when the font ships an `SVG ` table per ISO/IEC
1895    /// 14496-22:2019/Amd.1:2020 §5.5.1 — vector colour-glyph
1896    /// descriptions as SVG 1.1 documents. This is one of the four
1897    /// colour-glyph mechanisms (`COLR`/`CPAL`, `CBDT`/`CBLC`, `sbix`,
1898    /// `SVG `); a font may ship more than one.
1899    pub fn has_svg(&self) -> bool {
1900        self.svg.is_some()
1901    }
1902
1903    /// Borrow the parsed `SVG ` table when present.
1904    ///
1905    /// The returned [`SvgTable`] carries the §5.5.1 document records,
1906    /// each covering a contiguous glyph-ID range. Document payloads
1907    /// borrow from the on-wire `SVG ` byte slice and are surfaced raw
1908    /// (plain UTF-8 markup or gzip-encoded — test with
1909    /// [`SvgDocument::is_gzip_encoded`]).
1910    pub fn svg_table(&self) -> Option<&SvgTable<'a>> {
1911        self.svg.as_ref()
1912    }
1913
1914    /// Resolve the raw SVG document covering `glyph_id`, or `None` when
1915    /// the font has no `SVG ` table or no document range covers the
1916    /// glyph. The returned [`SvgDocument`] borrows the on-wire document
1917    /// bytes (plain UTF-8 SVG 1.1 markup or a gzip-encoded stream per
1918    /// §5.5.2); inflation + XML parsing are the consumer renderer's
1919    /// responsibility, matching the raw-payload policy used for `sbix`
1920    /// and `CBDT` image strikes.
1921    pub fn svg_document(&self, glyph_id: u16) -> Option<&SvgDocument<'a>> {
1922        self.svg.as_ref()?.document_for_glyph(glyph_id)
1923    }
1924
1925    /// Glyph bounding box from the `glyf` header (xMin/yMin/xMax/yMax).
1926    /// Returns `None` for empty / blank glyphs and for fonts that lack
1927    /// a `glyf`/`loca` pair (CBDT-only colour-emoji fonts).
1928    pub fn glyph_bounding_box(&self, glyph_id: u16) -> Option<BBox> {
1929        if glyph_id >= self.maxp.num_glyphs {
1930            return None;
1931        }
1932        let (loca, glyf) = (self.loca.as_ref()?, self.glyf.as_ref()?);
1933        let range = loca.glyph_range(glyph_id).ok()?;
1934        if range.is_empty() {
1935            return None;
1936        }
1937        glyf.bbox(range)
1938    }
1939
1940    // ---- shaping support ---------------------------------------------------
1941
1942    /// Look up a ligature substitution for the input glyph run.
1943    ///
1944    /// Returns `Some((replacement, consumed))` if a GSUB LookupType 4 rule
1945    /// matches a prefix of `glyphs` of length `consumed >= 2`. Returns
1946    /// `None` otherwise (no ligature, or no GSUB table).
1947    pub fn lookup_ligature(&self, glyphs: &[u16]) -> Option<(u16, usize)> {
1948        self.gsub.as_ref().and_then(|g| g.lookup_ligature(glyphs))
1949    }
1950
1951    /// Resolve every GSUB feature active for `script_tag` under
1952    /// `lang_tag` to a list of `GsubFeature { tag, lookup_indices }`.
1953    ///
1954    /// `lang_tag = None` selects the script's `DefaultLangSys`. If
1955    /// `lang_tag` is supplied but isn't enumerated for the script, the
1956    /// lookup falls back to `DefaultLangSys` (matching the spec's
1957    /// "language system not present in script → use default" rule).
1958    ///
1959    /// The resulting `Vec` is empty when the font has no GSUB table or
1960    /// the script tag isn't in the ScriptList. Order matches the
1961    /// LangSys's `featureIndices` field, so a shaper can apply features
1962    /// in declaration order. The required feature (when present) is
1963    /// emitted first.
1964    ///
1965    /// Used by the consumer crate's Arabic shaper to discover which
1966    /// lookup indices implement `init` / `medi` / `fina` / `isol` for
1967    /// the current script — modern Arabic fonts (Noto Sans Arabic UI,
1968    /// most Indic fonts) ship positional forms via GSUB rather than
1969    /// the legacy Presentation Forms-B Unicode block.
1970    pub fn gsub_features_for_script(
1971        &self,
1972        script_tag: [u8; 4],
1973        lang_tag: Option<[u8; 4]>,
1974    ) -> Vec<GsubFeature> {
1975        match self.gsub.as_ref() {
1976            Some(g) => g.features_for_script(script_tag, lang_tag),
1977            None => Vec::new(),
1978        }
1979    }
1980
1981    /// Like [`Self::gsub_features_for_script`], but honours the GSUB
1982    /// **FeatureVariations** table (ISO/IEC 14496-22:2019 §6.2.9) at the
1983    /// font's current variation instance.
1984    ///
1985    /// A variable font may publish a version-1.1 GSUB header that swaps
1986    /// the lookups behind a feature for an alternate set when the
1987    /// current instance falls inside a normalised range on one or more
1988    /// `fvar` axes (the canonical use is optical-size- or
1989    /// weight-conditional substitution). This accessor evaluates the
1990    /// active condition set against [`Self::normalised_coords`] and, for
1991    /// every feature whose index is overridden by the matching
1992    /// FeatureTableSubstitution, returns the alternate lookup-index list
1993    /// while keeping the feature tag unchanged.
1994    ///
1995    /// For static fonts, v1.0 GSUB headers, or instances that match no
1996    /// condition set, the result is identical to
1997    /// [`Self::gsub_features_for_script`]. Set the instance with
1998    /// [`Self::set_variation_coords`] first.
1999    pub fn gsub_features_for_script_at_instance(
2000        &self,
2001        script_tag: [u8; 4],
2002        lang_tag: Option<[u8; 4]>,
2003    ) -> Vec<GsubFeature> {
2004        match self.gsub.as_ref() {
2005            Some(g) => {
2006                let coords = self.normalised_coords();
2007                g.features_for_script_at_coords(script_tag, lang_tag, &coords)
2008            }
2009            None => Vec::new(),
2010        }
2011    }
2012
2013    /// `true` when the GSUB table carries a §6.2.9 FeatureVariations
2014    /// table (a version-1.1 header with a non-zero offset). When this is
2015    /// `false`, [`Self::gsub_features_for_script_at_instance`] is
2016    /// identical to [`Self::gsub_features_for_script`].
2017    pub fn gsub_has_feature_variations(&self) -> bool {
2018        self.gsub
2019            .as_ref()
2020            .map(|g| g.has_feature_variations())
2021            .unwrap_or(false)
2022    }
2023
2024    /// Return all GPOS features active for `script_tag` under `lang_tag`,
2025    /// each resolved to the list of lookup indices that implement it.
2026    ///
2027    /// The GPOS sibling of [`Self::gsub_features_for_script`]: it walks
2028    /// the same OpenType Layout ScriptList / FeatureList / LangSys
2029    /// substructure but over the positioning table, so a shaper can
2030    /// discover which lookup indices implement `kern` / `mark` / `mkmk`
2031    /// / `curs` / `cpsp` for the current script and feed them to the
2032    /// matching `gpos_apply_lookup_type_*` path.
2033    ///
2034    /// `lang_tag = None` selects the script's `DefaultLangSys`; an
2035    /// unrecognised `lang_tag` falls back to it too. The required
2036    /// feature (when present) is emitted first, then the LangSys's
2037    /// declared features in order. Returns an empty `Vec` when the font
2038    /// has no GPOS table or the script is absent.
2039    pub fn gpos_features_for_script(
2040        &self,
2041        script_tag: [u8; 4],
2042        lang_tag: Option<[u8; 4]>,
2043    ) -> Vec<GposFeature> {
2044        match self.gpos.as_ref() {
2045            Some(g) => g.features_for_script(script_tag, lang_tag),
2046            None => Vec::new(),
2047        }
2048    }
2049
2050    /// Like [`Self::gpos_features_for_script`], but honours the GPOS
2051    /// **FeatureVariations** table (the shared ISO/IEC 14496-22:2019
2052    /// §6.2.9 substructure, reachable through a version-1.1 GPOS header)
2053    /// at the font's current variation instance.
2054    ///
2055    /// A variable font may publish a version-1.1 GPOS header that swaps
2056    /// the lookups behind a positioning feature for an alternate set
2057    /// when the current instance falls inside a normalised range on one
2058    /// or more `fvar` axes (e.g. weight-conditional kerning). This
2059    /// accessor evaluates the active condition set against
2060    /// [`Self::normalised_coords`] and, for every feature whose index is
2061    /// overridden by the matching FeatureTableSubstitution, returns the
2062    /// alternate lookup-index list while keeping the feature tag
2063    /// unchanged.
2064    ///
2065    /// For static fonts, v1.0 GPOS headers, or instances that match no
2066    /// condition set, the result is identical to
2067    /// [`Self::gpos_features_for_script`]. Set the instance with
2068    /// [`Self::set_variation_coords`] first.
2069    pub fn gpos_features_for_script_at_instance(
2070        &self,
2071        script_tag: [u8; 4],
2072        lang_tag: Option<[u8; 4]>,
2073    ) -> Vec<GposFeature> {
2074        match self.gpos.as_ref() {
2075            Some(g) => {
2076                let coords = self.normalised_coords();
2077                g.features_for_script_at_coords(script_tag, lang_tag, &coords)
2078            }
2079            None => Vec::new(),
2080        }
2081    }
2082
2083    /// `true` when the GPOS table carries a §6.2.9 FeatureVariations
2084    /// table (a version-1.1 header with a non-zero offset). When this is
2085    /// `false`, [`Self::gpos_features_for_script_at_instance`] is
2086    /// identical to [`Self::gpos_features_for_script`].
2087    pub fn gpos_has_feature_variations(&self) -> bool {
2088        self.gpos
2089            .as_ref()
2090            .map(|g| g.has_feature_variations())
2091            .unwrap_or(false)
2092    }
2093
2094    /// Apply GSUB LookupType 1 (Single Substitution) lookup
2095    /// `lookup_index` to a single input glyph `gid`.
2096    ///
2097    /// Returns `Some(replacement_gid)` when the lookup's coverage
2098    /// covers `gid`, or `None` when no substitution applies (caller
2099    /// keeps the input glyph unchanged). `None` is also returned when
2100    /// the font has no GSUB, the lookup index is out of range, or the
2101    /// referenced lookup isn't a single-substitution lookup (e.g. a
2102    /// ligature lookup is silently skipped here — call
2103    /// [`Self::lookup_ligature`] for those).
2104    ///
2105    /// Format 1 (delta) and Format 2 (substitute-array) sub-tables are
2106    /// both supported; ExtensionSubst (LookupType 7) wrappers are
2107    /// unwrapped transparently.
2108    pub fn gsub_apply_lookup_type_1(&self, lookup_index: u16, gid: u16) -> Option<u16> {
2109        self.gsub.as_ref()?.apply_lookup_type_1(lookup_index, gid)
2110    }
2111
2112    /// Apply GSUB LookupType 4 (Ligature Substitution) lookup
2113    /// `lookup_index` to a prefix of `gids`.
2114    ///
2115    /// Returns `Some((replacement_gid, consumed))` when a sub-table in
2116    /// the named lookup matches a prefix of `gids` of length `consumed`
2117    /// (typically `>= 2` for real ligatures). Returns `None` when no
2118    /// rule applies, the lookup index is out of range, the referenced
2119    /// lookup is not a ligature lookup, or the font has no GSUB table.
2120    /// ExtensionSubst (LookupType 7) wrappers are unwrapped
2121    /// transparently.
2122    ///
2123    /// This is the lookup-index-specific counterpart of
2124    /// [`Self::lookup_ligature`] (which walks every lookup) and is the
2125    /// API a feature-driven shaper uses after resolving the `liga` /
2126    /// `rlig` / `dlig` feature for the active script via
2127    /// [`Self::gsub_features_for_script`].
2128    pub fn gsub_apply_lookup_type_4(
2129        &self,
2130        lookup_index: u16,
2131        gids: &[u16],
2132    ) -> Option<(u16, usize)> {
2133        self.gsub.as_ref()?.apply_lookup_type_4(lookup_index, gids)
2134    }
2135
2136    /// Apply GSUB LookupType 6 (Chained Contexts Substitution) lookup
2137    /// `lookup_index` to the glyph run starting at `pos`.
2138    ///
2139    /// Returns `Some(rewritten_run)` — a fresh `Vec<u16>` of the full
2140    /// run with any sub-lookups dispatched at the matched
2141    /// `(backtrack, input, lookahead)` window — when one of the
2142    /// lookup's sub-tables (Format 1 / 2 / 3) matches around `pos`.
2143    /// Returns `None` when no chained-context rule applies, the lookup
2144    /// index is out of range, the referenced lookup is not a
2145    /// chain-context lookup, or the font has no GSUB table.
2146    ///
2147    /// Each `SubstLookupRecord { sequenceIndex, lookupListIndex }`
2148    /// inside the matched rule is recursively dispatched: LookupType 1
2149    /// substitutes the single glyph at the relative `sequenceIndex`,
2150    /// LookupType 4 substitutes `componentCount` glyphs starting there.
2151    /// Nested LookupType 6 references are also handled (bounded depth).
2152    /// ExtensionSubst (LookupType 7) is unwrapped transparently.
2153    ///
2154    /// This is the biggest GSUB unlock for complex scripts: Arabic
2155    /// shaping cascades, Indic reordering, and most ligature-with-
2156    /// context rules (e.g. Latin `ct` only between word boundaries)
2157    /// all run through chained-context lookups.
2158    pub fn gsub_apply_lookup_type_6(
2159        &self,
2160        lookup_index: u16,
2161        gids: &[u16],
2162        pos: usize,
2163    ) -> Option<Vec<u16>> {
2164        self.gsub
2165            .as_ref()?
2166            .apply_lookup_type_6(lookup_index, gids, pos)
2167    }
2168
2169    /// Apply GSUB LookupType 2 (Multiple Substitution) lookup
2170    /// `lookup_index` to a single input glyph `gid`.
2171    ///
2172    /// Returns `Some(substitute_sequence)` — a `Vec<u16>` of the
2173    /// expanded glyph sequence — when the lookup's coverage covers
2174    /// `gid`. Returns `None` when no rule applies, the lookup index is
2175    /// out of range, the referenced lookup is not a multiple
2176    /// substitution, or the font has no GSUB table. ExtensionSubst
2177    /// (LookupType 7) wrappers are unwrapped transparently. The spec
2178    /// permits `glyphCount = 0` (deletion); such hits surface as
2179    /// `Some(Vec::new())`.
2180    pub fn gsub_apply_lookup_type_2(&self, lookup_index: u16, gid: u16) -> Option<Vec<u16>> {
2181        self.gsub.as_ref()?.apply_lookup_type_2(lookup_index, gid)
2182    }
2183
2184    /// Apply GSUB LookupType 3 (Alternate Substitution) lookup
2185    /// `lookup_index` to `gid`, picking `alternate_index` from the
2186    /// resolved `AlternateSet`.
2187    ///
2188    /// Returns `Some(replacement_gid)` when the lookup covers `gid`
2189    /// AND `alternate_index` is in range for that coverage's
2190    /// `AlternateSet`. Returns `None` on coverage miss, out-of-range
2191    /// alternate index, non-alternate-substitution referenced lookup,
2192    /// or a font without GSUB. Default callers should pass
2193    /// `alternate_index = 0` — the spec doesn't register a
2194    /// per-feature variant index. ExtensionSubst (LookupType 7) is
2195    /// unwrapped transparently.
2196    pub fn gsub_apply_lookup_type_3(
2197        &self,
2198        lookup_index: u16,
2199        gid: u16,
2200        alternate_index: u16,
2201    ) -> Option<u16> {
2202        self.gsub
2203            .as_ref()?
2204            .apply_lookup_type_3(lookup_index, gid, alternate_index)
2205    }
2206
2207    /// Apply GSUB LookupType 5 (Contextual Substitution) lookup
2208    /// `lookup_index` to the glyph run starting at `pos`.
2209    ///
2210    /// LookupType 5 mirrors LookupType 6 minus backtrack and
2211    /// lookahead — the input window is the only context. Returns
2212    /// `Some(rewritten_run)` — a fresh `Vec<u16>` with any sub-lookups
2213    /// dispatched at the matched input window — when one of the
2214    /// lookup's sub-tables (Format 1 / 2 / 3) matches around `pos`.
2215    /// Returns `None` when no contextual rule applies, the lookup
2216    /// index is out of range, the referenced lookup is not a
2217    /// contextual lookup, or the font has no GSUB.
2218    /// ExtensionSubst (LookupType 7) is unwrapped transparently.
2219    /// Recursive sub-lookup expansion is bounded.
2220    pub fn gsub_apply_lookup_type_5(
2221        &self,
2222        lookup_index: u16,
2223        gids: &[u16],
2224        pos: usize,
2225    ) -> Option<Vec<u16>> {
2226        self.gsub
2227            .as_ref()?
2228            .apply_lookup_type_5(lookup_index, gids, pos)
2229    }
2230
2231    /// Apply GSUB LookupType 8 (Reverse Chained Context Substitution)
2232    /// lookup `lookup_index` to the glyph at `gids[pos]`.
2233    ///
2234    /// Returns `Some(replacement_gid)` when the input coverage covers
2235    /// `gids[pos]` AND every backtrack / lookahead coverage matches
2236    /// the surrounding glyphs. Returns `None` otherwise (no rule, out
2237    /// of range, wrong lookup type, no GSUB). ExtensionSubst
2238    /// (LookupType 7) is unwrapped transparently.
2239    ///
2240    /// The spec mandates reverse-text processing of the input run
2241    /// (essential for Arabic isolated forms in some fonts) — a higher-
2242    /// level shaper is what walks `pos` from right to left; this
2243    /// per-position entry point answers "does the rule fire here?".
2244    pub fn gsub_apply_lookup_type_8(
2245        &self,
2246        lookup_index: u16,
2247        gids: &[u16],
2248        pos: usize,
2249    ) -> Option<u16> {
2250        self.gsub
2251            .as_ref()?
2252            .apply_lookup_type_8(lookup_index, gids, pos)
2253    }
2254
2255    /// On-disk header variant of the legacy `kern` table, if present.
2256    ///
2257    /// Two header layouts coexist: Microsoft-format `kern` (every
2258    /// Windows-authored / most Adobe / Google TTF — `u16 version,
2259    /// u16 nTables`) and Apple-format `kern` (macOS-bundled TTFs —
2260    /// `u32 version = 0x00010000, u32 nTables`, with different
2261    /// per-subtable header bytes). This crate decodes Microsoft-format
2262    /// Format-0 horizontal kerning subtables; Apple-format tables
2263    /// parse cleanly but their subtable bodies surface as zero pairs
2264    /// (see [`KernHeaderVariant::Apple`]).
2265    ///
2266    /// Returns `None` for fonts that don't ship a `kern` table at all
2267    /// (modern OpenType fonts use GPOS LookupType 2 instead).
2268    pub fn kern_header_variant(&self) -> Option<KernHeaderVariant> {
2269        self.kern.as_ref().map(|k| k.header_variant())
2270    }
2271
2272    /// Look up the kerning between an ordered glyph pair, in font units.
2273    ///
2274    /// Tries GPOS LookupType 2 first; falls back to the legacy `kern`
2275    /// table (format 0). Returns 0 if neither is present or the pair has
2276    /// no defined kerning.
2277    pub fn lookup_kerning(&self, left: u16, right: u16) -> i16 {
2278        if let Some(gpos) = &self.gpos {
2279            let v = gpos.lookup_kerning(left, right, self.gdef.as_ref());
2280            if v != 0 {
2281                return v;
2282            }
2283        }
2284        if let Some(kern) = &self.kern {
2285            return kern.lookup(left, right);
2286        }
2287        0
2288    }
2289
2290    /// Look up a mark-to-base attachment offset for a `(base, mark)`
2291    /// glyph pair. Returns `(dx, dy)` in font units (TT Y-up convention)
2292    /// to add to the mark's pen origin so its anchor lands on the
2293    /// base's anchor for the mark's class.
2294    ///
2295    /// Walks GPOS LookupType 4 sub-tables; returns `None` if no
2296    /// matching MarkBasePos rule covers both glyphs (or if the font has
2297    /// no GPOS table). Used by the consumer crate's shaper to position
2298    /// diacritics above / below their base glyph (essential for
2299    /// European Latin extended, Vietnamese, polytonic Greek).
2300    ///
2301    /// Whether `mark` is actually a mark glyph (per `GDEF`) is the
2302    /// caller's responsibility — typically the shaper checks
2303    /// [`Font::is_mark_glyph`] before calling this. The lookup itself
2304    /// works for any pair the font's MarkBasePos coverage tables
2305    /// list, regardless of GDEF.
2306    pub fn lookup_mark_to_base(&self, base: u16, mark: u16) -> Option<(i16, i16)> {
2307        self.gpos.as_ref()?.lookup_mark_to_base(base, mark)
2308    }
2309
2310    /// Look up a mark-to-mark attachment offset for a `(mark1, mark2)`
2311    /// glyph pair, where `mark1` is the previously-positioned mark
2312    /// (already attached to a base via a prior mark-to-base lookup) and
2313    /// `mark2` is the mark we want to stack on top of (or below) it.
2314    /// Returns `(dx, dy)` in font units (TT Y-up convention) to add to
2315    /// `mark2`'s pen origin so its anchor lands on `mark1`'s anchor for
2316    /// `mark2`'s class.
2317    ///
2318    /// Walks GPOS LookupType 6 sub-tables; returns `None` if no
2319    /// matching MarkMarkPos rule covers both glyphs (or if the font
2320    /// has no GPOS table). Used by the consumer crate's shaper to
2321    /// build multi-mark stacks (e.g. polytonic Greek `α + tonos +
2322    /// dialytika`, Vietnamese `a + circumflex + acute`).
2323    pub fn lookup_mark_to_mark(&self, mark1: u16, mark2: u16) -> Option<(i16, i16)> {
2324        self.gpos.as_ref()?.lookup_mark_to_mark(mark1, mark2)
2325    }
2326
2327    /// Decode the GDEF `ItemVariationStore` (v1.3+), if present. The
2328    /// store feeds every variable-font GPOS / GDEF VariationIndex
2329    /// resolution. Returns `None` for fonts without a GDEF IVS or when
2330    /// the embedded store is malformed.
2331    fn gdef_item_variation_store(&self) -> Option<ItemVariationStore> {
2332        let bytes = self.gdef.as_ref()?.item_var_store_bytes()?;
2333        ItemVariationStore::parse(bytes).ok()
2334    }
2335
2336    /// Variation-aware sibling of [`Self::lookup_kerning`].
2337    ///
2338    /// Resolves a GPOS pair's `xAdvance` VariationIndex against the GDEF
2339    /// `ItemVariationStore` at the font's current variation instance
2340    /// (set via [`Self::set_variation_coords`]), so variable kerning
2341    /// tracks the design axes. Falls back to the legacy `kern` table
2342    /// exactly like the static accessor. For a non-variable font, or
2343    /// one at its default instance, the result equals
2344    /// [`Self::lookup_kerning`].
2345    pub fn lookup_kerning_var(&self, left: u16, right: u16) -> i16 {
2346        if let Some(gpos) = &self.gpos {
2347            let ivs = self.gdef_item_variation_store();
2348            let coords = self.normalised_coords();
2349            let v = gpos.lookup_kerning_var(left, right, self.gdef.as_ref(), ivs.as_ref(), &coords);
2350            if v != 0 {
2351                return v;
2352            }
2353        }
2354        if let Some(kern) = &self.kern {
2355            return kern.lookup(left, right);
2356        }
2357        0
2358    }
2359
2360    /// Variation-aware sibling of [`Self::lookup_mark_to_base`]:
2361    /// resolves AnchorFormat3 VariationIndex offsets against the GDEF
2362    /// `ItemVariationStore` at the current instance so the diacritic
2363    /// attachment point tracks the design axes.
2364    pub fn lookup_mark_to_base_var(&self, base: u16, mark: u16) -> Option<(i16, i16)> {
2365        let gpos = self.gpos.as_ref()?;
2366        let ivs = self.gdef_item_variation_store();
2367        let coords = self.normalised_coords();
2368        gpos.lookup_mark_to_base_var(base, mark, ivs.as_ref(), &coords)
2369    }
2370
2371    /// Variation-aware sibling of [`Self::lookup_mark_to_mark`]:
2372    /// resolves AnchorFormat3 VariationIndex offsets against the GDEF
2373    /// `ItemVariationStore` at the current instance so the mark-on-mark
2374    /// stacking offset tracks the design axes.
2375    pub fn lookup_mark_to_mark_var(&self, mark1: u16, mark2: u16) -> Option<(i16, i16)> {
2376        let gpos = self.gpos.as_ref()?;
2377        let ivs = self.gdef_item_variation_store();
2378        let coords = self.normalised_coords();
2379        gpos.lookup_mark_to_mark_var(mark1, mark2, ivs.as_ref(), &coords)
2380    }
2381
2382    /// Variation-aware sibling of [`Self::lookup_cursive_attachment`]:
2383    /// resolves AnchorFormat3 VariationIndex offsets on the entry / exit
2384    /// anchors against the GDEF `ItemVariationStore` at the current
2385    /// instance.
2386    pub fn lookup_cursive_attachment_var(&self, gid: u16) -> Option<CursiveAttachment> {
2387        let gpos = self.gpos.as_ref()?;
2388        let ivs = self.gdef_item_variation_store();
2389        let coords = self.normalised_coords();
2390        gpos.lookup_cursive_attachment_var(gid, ivs.as_ref(), &coords)
2391    }
2392
2393    /// Variation-aware sibling of [`Self::gpos_apply_lookup_type_1`]:
2394    /// resolves the matched ValueRecord's VariationIndex device offsets
2395    /// against the GDEF `ItemVariationStore` at the current instance.
2396    pub fn gpos_apply_lookup_type_1_var(&self, lookup_index: u16, gid: u16) -> Option<PosValue> {
2397        let gpos = self.gpos.as_ref()?;
2398        let ivs = self.gdef_item_variation_store();
2399        let coords = self.normalised_coords();
2400        gpos.apply_lookup_type_1_var(lookup_index, gid, ivs.as_ref(), &coords)
2401    }
2402
2403    /// Resolve a ligature glyph's GDEF carets to concrete font-unit
2404    /// coordinates at the current variation instance (CaretValueFormat3
2405    /// VariationIndex deltas applied from the GDEF `ItemVariationStore`;
2406    /// Format2 contour-point carets surface as `None`). Returns `None`
2407    /// when the font has no GDEF ligature-caret list covering `gid`.
2408    /// See [`GdefTable::ligature_carets_resolved`].
2409    pub fn ligature_carets_resolved(&self, gid: u16) -> Option<Vec<Option<i16>>> {
2410        let gdef = self.gdef.as_ref()?;
2411        let ivs = self.gdef_item_variation_store();
2412        let coords = self.normalised_coords();
2413        gdef.ligature_carets_resolved(gid, ivs.as_ref(), &coords)
2414    }
2415
2416    /// Is this glyph classified as a mark by the font's `GDEF` table?
2417    /// Returns `false` if the font has no GDEF or the glyph isn't
2418    /// enumerated. Used by the consumer crate's shaper to decide
2419    /// whether to attempt mark-to-base attachment for an adjacent
2420    /// glyph pair.
2421    pub fn is_mark_glyph(&self, glyph_id: u16) -> bool {
2422        self.gdef
2423            .as_ref()
2424            .map(|g| g.is_mark(glyph_id))
2425            .unwrap_or(false)
2426    }
2427
2428    /// Apply GPOS LookupType 1 (Single Adjustment Positioning) to
2429    /// `gid` via the lookup at `lookup_index`.
2430    ///
2431    /// Returns `Some(PosValue)` with the four geometric adjustments
2432    /// (`xPlacement`, `yPlacement`, `xAdvance`, `yAdvance`) when the
2433    /// lookup's coverage covers `gid`, or `None` when no rule applies
2434    /// (or the font has no GPOS). Both SinglePosFormat 1 (one shared
2435    /// ValueRecord) and Format 2 (per-glyph ValueRecord) are
2436    /// supported; ExtensionPos (LookupType 9) wrappers are unwrapped
2437    /// transparently.
2438    ///
2439    /// Use this for features that don't need pair context — e.g. the
2440    /// `cpsp` (capital spacing) feature applies a SinglePos to every
2441    /// uppercase glyph to add side bearing.
2442    pub fn gpos_apply_lookup_type_1(&self, lookup_index: u16, gid: u16) -> Option<PosValue> {
2443        self.gpos.as_ref()?.apply_lookup_type_1(lookup_index, gid)
2444    }
2445
2446    /// Apply GPOS LookupType 3 (Cursive Attachment) to `gid` via the
2447    /// lookup at `lookup_index`.
2448    ///
2449    /// Returns `Some(CursiveAttachment { entry, exit })` when the
2450    /// lookup's coverage covers `gid`. Either anchor may be `None`
2451    /// (the spec allows one-sided cursive glyphs at cluster
2452    /// boundaries). Returns `None` when no rule applies, the lookup
2453    /// index is out of range, the referenced lookup is not a cursive
2454    /// lookup, or the font has no GPOS. ExtensionPos (LookupType 9)
2455    /// wrappers are unwrapped transparently.
2456    ///
2457    /// Cursive attachment chains glyph N+1 onto glyph N: the shaper
2458    /// translates glyph N+1's pen origin so its `entry` anchor lands
2459    /// on glyph N's `exit` anchor — i.e. the per-glyph delta is
2460    /// `prev.exit - this.entry` in (x, y) font units.
2461    pub fn gpos_apply_lookup_type_3(
2462        &self,
2463        lookup_index: u16,
2464        gid: u16,
2465    ) -> Option<CursiveAttachment> {
2466        self.gpos.as_ref()?.apply_lookup_type_3(lookup_index, gid)
2467    }
2468
2469    /// Walk every GPOS LookupType-3 (Cursive Attachment) lookup
2470    /// looking for `gid`'s entry/exit anchor pair. Convenience wrapper
2471    /// around [`Self::gpos_apply_lookup_type_3`] for fonts that ship a
2472    /// single `curs` lookup (the common Arabic Nastaliq case). Returns
2473    /// the first hit in lookup order.
2474    pub fn lookup_cursive_attachment(&self, gid: u16) -> Option<CursiveAttachment> {
2475        self.gpos.as_ref()?.lookup_cursive_attachment(gid)
2476    }
2477
2478    /// Apply GPOS LookupType 5 (Mark-to-Ligature Attachment) to the
2479    /// `(ligature, ligature_component, mark)` triple via the lookup
2480    /// at `lookup_index`.
2481    ///
2482    /// Returns `Some((dx, dy))` (font units, TT Y-up) — the offset to
2483    /// add to the mark's pen origin so its class anchor lands on the
2484    /// selected component's anchor. `ligature_component` is 0-indexed
2485    /// (component 0 = first component, e.g. `f` in `fi`). Returns
2486    /// `None` when no rule covers both glyphs, when the component
2487    /// index is out of range, or when no anchor exists for the mark's
2488    /// class on the requested component. ExtensionPos (LookupType 9)
2489    /// wrappers are unwrapped transparently.
2490    ///
2491    /// Closes the "fi + dot-above" gap: a mark following the second
2492    /// codepoint of a 2-component ligature attaches to component 1.
2493    pub fn gpos_apply_lookup_type_5(
2494        &self,
2495        lookup_index: u16,
2496        ligature: u16,
2497        ligature_component: u16,
2498        mark: u16,
2499    ) -> Option<(i16, i16)> {
2500        self.gpos
2501            .as_ref()?
2502            .apply_lookup_type_5(lookup_index, ligature, ligature_component, mark)
2503    }
2504
2505    /// Walk every GPOS LookupType-5 (Mark-to-Ligature) lookup looking
2506    /// for the `(ligature, ligature_component, mark)` triple.
2507    /// Convenience wrapper around [`Self::gpos_apply_lookup_type_5`]
2508    /// that scans the LookupList rather than a specific index.
2509    pub fn lookup_mark_to_ligature(
2510        &self,
2511        ligature: u16,
2512        ligature_component: u16,
2513        mark: u16,
2514    ) -> Option<(i16, i16)> {
2515        self.gpos
2516            .as_ref()?
2517            .lookup_mark_to_ligature(ligature, ligature_component, mark)
2518    }
2519
2520    /// Variation-aware sibling of [`Self::lookup_mark_to_ligature`]:
2521    /// resolves AnchorFormat3 VariationIndex offsets against the GDEF
2522    /// `ItemVariationStore` at the font's current instance.
2523    pub fn lookup_mark_to_ligature_var(
2524        &self,
2525        ligature: u16,
2526        ligature_component: u16,
2527        mark: u16,
2528    ) -> Option<(i16, i16)> {
2529        let gpos = self.gpos.as_ref()?;
2530        let ivs = self.gdef_item_variation_store();
2531        let coords = self.normalised_coords();
2532        gpos.lookup_mark_to_ligature_var(ligature, ligature_component, mark, ivs.as_ref(), &coords)
2533    }
2534
2535    /// Apply GPOS LookupType 7 (Contextual Positioning) to the glyph
2536    /// run starting at `pos` via the lookup at `lookup_index`.
2537    ///
2538    /// LookupType 7 is the non-chained sibling of LookupType 8: it
2539    /// matches an input glyph sequence (no backtrack / lookahead) and,
2540    /// on a hit, dispatches the rule's `SequenceLookupRecord[]` into
2541    /// nested per-glyph positioning lookups. Returns `Some(records)` —
2542    /// a `Vec<PosRecord>` of the per-glyph adjustments emitted — when a
2543    /// sub-table matches the input window at `pos`. Each
2544    /// `PosRecord.glyph_index` is an absolute offset into `gids`.
2545    ///
2546    /// All three sub-table formats (1 glyph-sequence, 2 class-based,
2547    /// 3 coverage-based) are supported. ExtensionPos (LookupType 9)
2548    /// wrappers are unwrapped transparently; nested records into
2549    /// LookupType 1 / 2 / 3 / 4 / 6 / 7 / 8 dispatch through the same
2550    /// bounded-recursion machinery as the chained path.
2551    pub fn gpos_apply_lookup_type_7(
2552        &self,
2553        lookup_index: u16,
2554        gids: &[u16],
2555        pos: usize,
2556    ) -> Option<Vec<PosRecord>> {
2557        self.gpos
2558            .as_ref()?
2559            .apply_lookup_type_7(lookup_index, gids, pos)
2560    }
2561
2562    /// Apply GPOS LookupType 8 (Chained Contexts Positioning) to the
2563    /// glyph run starting at `pos` via the lookup at `lookup_index`.
2564    ///
2565    /// Returns `Some(records)` — a `Vec<PosRecord>` listing every
2566    /// per-glyph adjustment the matched chain rule emits — when one
2567    /// of the lookup's sub-tables matches the
2568    /// `(backtrack, input, lookahead)` window around `pos`. Each
2569    /// `PosRecord.glyph_index` is an absolute offset into `gids`.
2570    ///
2571    /// All three sub-table formats (1 glyph-sequence, 2 class-based,
2572    /// 3 coverage-based) are supported. ExtensionPos (LookupType 9)
2573    /// wrappers are unwrapped transparently. Nested
2574    /// `PosLookupRecord` references into LookupType 1 / 2 / 4 / 6 / 8
2575    /// dispatch through the same machinery; recursion is bounded.
2576    pub fn gpos_apply_lookup_type_8(
2577        &self,
2578        lookup_index: u16,
2579        gids: &[u16],
2580        pos: usize,
2581    ) -> Option<Vec<PosRecord>> {
2582        self.gpos
2583            .as_ref()?
2584            .apply_lookup_type_8(lookup_index, gids, pos)
2585    }
2586
2587    /// Enumerate every GPOS lookup as `(lookup_index, lookup_type,
2588    /// subtable_count)`.
2589    ///
2590    /// The reported `lookup_type` is the **effective** type after
2591    /// unwrapping any LookupType-9 ExtensionPos wrapper. Returns an
2592    /// empty iterator when the font has no GPOS table.
2593    ///
2594    /// Use this to find every chained-context positioning lookup, or
2595    /// every mark-to-ligature lookup, etc., without probing each
2596    /// index in turn — for example,
2597    /// `font.gpos_lookup_list().filter(|(_, t, _)| *t == 8)` enumerates
2598    /// the chained-context-positioning lookups.
2599    pub fn gpos_lookup_list(&self) -> Vec<(u16, u16, u16)> {
2600        match self.gpos.as_ref() {
2601            Some(g) => g.lookup_list().collect(),
2602            None => Vec::new(),
2603        }
2604    }
2605
2606    /// Enumerate every GSUB lookup as `(lookup_index, lookup_type,
2607    /// subtable_count)`. Same shape as [`Self::gpos_lookup_list`] —
2608    /// the reported `lookup_type` is post-unwrap of any
2609    /// LookupType-7 ExtensionSubst wrapper.
2610    pub fn gsub_lookup_list(&self) -> Vec<(u16, u16, u16)> {
2611        match self.gsub.as_ref() {
2612            Some(g) => g.lookup_list().collect(),
2613            None => Vec::new(),
2614        }
2615    }
2616
2617    /// The `lookupFlag` of GSUB lookup `lookup_index` (`0` when there's
2618    /// no GSUB or the index is out of range). The low-byte skip bits —
2619    /// RIGHT_TO_LEFT `0x0001`, IGNORE_BASE_GLYPHS `0x0002`,
2620    /// IGNORE_LIGATURES `0x0004`, IGNORE_MARKS `0x0008`,
2621    /// USE_MARK_FILTERING_SET `0x0010` — control which glyphs a shaper
2622    /// skips when matching the lookup's input; the high byte is the
2623    /// `markAttachmentType` class. [`Self::shape`] honours these.
2624    pub fn gsub_lookup_flags(&self, lookup_index: u16) -> u16 {
2625        self.gsub
2626            .as_ref()
2627            .map(|g| g.lookup_flags(lookup_index))
2628            .unwrap_or(0)
2629    }
2630
2631    /// The `lookupFlag` of GPOS lookup `lookup_index` (`0` when there's
2632    /// no GPOS or the index is out of range). Same bit layout as
2633    /// [`Self::gsub_lookup_flags`].
2634    pub fn gpos_lookup_flags(&self, lookup_index: u16) -> u16 {
2635        self.gpos
2636            .as_ref()
2637            .map(|g| g.lookup_flags(lookup_index))
2638            .unwrap_or(0)
2639    }
2640
2641    /// The `markFilteringSet` index of GSUB lookup `lookup_index`, or
2642    /// `None` when the lookup does not carry `USE_MARK_FILTERING_SET`
2643    /// (`0x0010`). When present, the value indexes the GDEF
2644    /// `MarkGlyphSets` structure and the layout engine skips every mark
2645    /// glyph *not* in that set ([`Self::shape`] honours this through the
2646    /// shared skip predicate).
2647    pub fn gsub_lookup_mark_filtering_set(&self, lookup_index: u16) -> Option<u16> {
2648        self.gsub
2649            .as_ref()
2650            .and_then(|g| g.mark_filtering_set(lookup_index))
2651    }
2652
2653    /// The `markFilteringSet` index of GPOS lookup `lookup_index`, or
2654    /// `None` when the lookup does not carry `USE_MARK_FILTERING_SET`.
2655    /// See [`Self::gsub_lookup_mark_filtering_set`].
2656    pub fn gpos_lookup_mark_filtering_set(&self, lookup_index: u16) -> Option<u16> {
2657        self.gpos
2658            .as_ref()
2659            .and_then(|g| g.mark_filtering_set(lookup_index))
2660    }
2661
2662    /// The shared §2 ("Common Table Formats") lookup skip predicate:
2663    /// returns `true` when a lookup with `flags` (its `lookupFlag`) and
2664    /// the optional `mark_filtering_set` index must *skip* `glyph_id`
2665    /// while matching its input / backtrack / lookahead sequences.
2666    ///
2667    /// The rule, per the LookupFlag bit enumeration:
2668    ///
2669    /// * `IGNORE_BASE_GLYPHS` (`0x0002`) — skip glyphs whose GDEF
2670    ///   GlyphClassDef class is *base* (1).
2671    /// * `IGNORE_LIGATURES` (`0x0004`) — skip glyphs whose class is
2672    ///   *ligature* (2).
2673    /// * `IGNORE_MARKS` (`0x0008`) — skip every mark glyph (class 3).
2674    /// * `MARK_ATTACHMENT_CLASS_FILTER` (high byte `0xFF00`, non-zero) —
2675    ///   skip every *mark* glyph whose GDEF MarkAttachClassDef class is
2676    ///   not the specified class. Non-mark glyphs are unaffected.
2677    /// * `USE_MARK_FILTERING_SET` (`0x0010`) — skip every *mark* glyph
2678    ///   that is not a member of the GDEF mark glyph set named by
2679    ///   `mark_filtering_set`.
2680    ///
2681    /// `IGNORE_MARKS` subsumes both mark-specific filters (a lookup that
2682    /// already skips all marks ignores the mark-class / filtering-set
2683    /// qualifiers). With no GDEF table the predicate degenerates to
2684    /// "never skip", matching the §2 requirement that a GlyphClassDef
2685    /// table be present whenever a skip bit is set.
2686    pub fn lookup_skips_glyph(
2687        &self,
2688        flags: u16,
2689        mark_filtering_set: Option<u16>,
2690        glyph_id: u16,
2691    ) -> bool {
2692        let gdef = match self.gdef.as_ref() {
2693            Some(g) => g,
2694            None => return false,
2695        };
2696        let class = gdef.glyph_class(glyph_id);
2697        if flags & 0x0002 != 0 && class == tables::gdef::CLASS_BASE {
2698            return true;
2699        }
2700        if flags & 0x0004 != 0 && class == tables::gdef::CLASS_LIGATURE {
2701            return true;
2702        }
2703        let is_mark = class == tables::gdef::CLASS_MARK;
2704        if flags & 0x0008 != 0 && is_mark {
2705            return true;
2706        }
2707        // The two mark-class qualifiers only filter mark glyphs, and only
2708        // matter when IGNORE_MARKS has not already removed every mark.
2709        if is_mark {
2710            let attach_class = (flags >> 8) & 0x00FF;
2711            if attach_class != 0 && gdef.mark_attach_class(glyph_id) != attach_class {
2712                return true;
2713            }
2714            if let Some(set) = mark_filtering_set {
2715                if !gdef.mark_glyph_set_contains(set, glyph_id) {
2716                    return true;
2717                }
2718            }
2719        }
2720        false
2721    }
2722
2723    // ---- color bitmap glyphs (CBDT/CBLC) ---------------------------------
2724
2725    /// `true` if this font ships a CBDT/CBLC pair — i.e. carries
2726    /// embedded colour bitmap glyphs (Noto Color Emoji, Apple Color
2727    /// Emoji's Google-format counterparts, and most Android emoji
2728    /// fonts). Returns `false` for plain outline-only fonts.
2729    pub fn has_color_bitmaps(&self) -> bool {
2730        self.cblc.is_some() && self.cbdt.is_some()
2731    }
2732
2733    /// All `(ppem_x, ppem_y)` strikes the colour-bitmap tables ship.
2734    /// Returns an empty iterator when the font lacks CBDT/CBLC.
2735    /// Useful for picking a strike before calling
2736    /// [`Font::glyph_color_bitmap`].
2737    pub fn color_strike_sizes(&self) -> Vec<(u8, u8)> {
2738        self.cblc
2739            .as_ref()
2740            .map(|c| c.ppem_sizes().collect())
2741            .unwrap_or_default()
2742    }
2743
2744    /// Resolve `glyph_id`'s colour bitmap at the strike whose `ppem_y`
2745    /// is closest to `target_ppem`. Returns `None` if the font has no
2746    /// CBDT/CBLC tables OR no strike contains `glyph_id` OR the strike's
2747    /// per-glyph entry is in a CBDT format we don't decode (anything
2748    /// other than 17/18/19 — the three PNG-payload formats).
2749    ///
2750    /// On success returns a [`ColorBitmap`] with raw `png_bytes` ready
2751    /// to feed into `oxideav-png` in the consumer crate. We deliberately
2752    /// don't decode the PNG here so this crate stays dependency-light.
2753    pub fn glyph_color_bitmap(&self, glyph_id: u16, target_ppem: u8) -> Option<ColorBitmap<'a>> {
2754        let cblc = self.cblc.as_ref()?;
2755        let cbdt = self.cbdt.as_ref()?;
2756        let entry = cblc.lookup_glyph(glyph_id, target_ppem)?;
2757        cbdt.lookup(&entry).ok().flatten()
2758    }
2759
2760    // ---- monochrome / grayscale bitmap glyphs (EBDT/EBLC) ----------------
2761
2762    /// `true` if this font ships an EBDT/EBLC pair — i.e. carries
2763    /// embedded monochrome or grayscale bitmap glyphs (legacy pixel /
2764    /// CJK bitmap faces, hand-hinted small-size strikes). Returns `false`
2765    /// for outline-only and colour-bitmap-only fonts.
2766    pub fn has_gray_bitmaps(&self) -> bool {
2767        self.eblc.is_some() && self.ebdt.is_some()
2768    }
2769
2770    /// All `(ppem_x, ppem_y)` strikes the monochrome / grayscale bitmap
2771    /// tables ship, in declaration order. Empty when the font lacks
2772    /// EBDT/EBLC. Useful for picking a strike before calling
2773    /// [`Font::glyph_gray_bitmap`].
2774    pub fn gray_strike_sizes(&self) -> Vec<(u8, u8)> {
2775        self.eblc
2776            .as_ref()
2777            .map(|c| c.ppem_sizes().collect())
2778            .unwrap_or_default()
2779    }
2780
2781    /// Resolve `glyph_id`'s monochrome / grayscale bitmap at the strike
2782    /// whose `ppem_y` is closest to `target_ppem`. Returns `None` if the
2783    /// font has no EBDT/EBLC tables OR no strike contains `glyph_id` OR
2784    /// the strike's per-glyph entry is in an EBDT format we don't decode
2785    /// (format 4 compressed).
2786    ///
2787    /// Composite formats 8 / 9 (§5.6.2.2.8 / §5.6.2.2.9) **are** decoded:
2788    /// the composite's component glyphs are resolved out of the same strike
2789    /// and blitted onto the composite's canvas at their per-component
2790    /// `(xOffset, yOffset)` offsets (nested composites are followed up to a
2791    /// bounded depth). The returned `GrayBitmap` is the assembled image.
2792    ///
2793    /// On success returns a [`GrayBitmap`] whose `pixels` field is an
2794    /// unpacked `width * height` row-major grid of alpha coverage
2795    /// (`0x00` = transparent, `0xFF` = opaque), ready to blit as a glyph
2796    /// mask at `(bearing_x, bearing_y)`. Bit depths 1 / 2 / 4 / 8 are all
2797    /// expanded to the full 0..=255 range (§5.6.2.2 / §5.6.3.1).
2798    pub fn glyph_gray_bitmap(&self, glyph_id: u16, target_ppem: u8) -> Option<GrayBitmap> {
2799        let eblc = self.eblc.as_ref()?;
2800        let ebdt = self.ebdt.as_ref()?;
2801        let entry = eblc.lookup_glyph(glyph_id, target_ppem)?;
2802        // Pixel formats (1/2/5/6/7) decode directly. Composite formats
2803        // (8/9) assemble component glyphs from the *same* strike — resolve
2804        // them recursively at that strike's exact ppemY so every component
2805        // lands in the same pixel grid.
2806        if matches!(entry.image_format, 8 | 9) {
2807            return self.composite_gray_bitmap(glyph_id, entry.ppem_y, 0);
2808        }
2809        ebdt.lookup(&entry).ok().flatten()
2810    }
2811
2812    /// Maximum `EBDT` composite (format 8 / 9) nesting depth. §5.6.2.2 says
2813    /// "the number of nesting levels is determined by implementation stack
2814    /// space"; we bound it to keep a malformed self-referential composite
2815    /// from recursing without limit.
2816    const EBDT_COMPOSITE_MAX_DEPTH: u8 = 8;
2817
2818    /// Resolve `glyph_id` to a `GrayBitmap` at the *exact* strike ppemY,
2819    /// assembling composite (format 8 / 9) glyphs by recursively resolving
2820    /// and blitting their components. `depth` guards against runaway
2821    /// recursion in a malformed font.
2822    fn composite_gray_bitmap(&self, glyph_id: u16, ppem_y: u8, depth: u8) -> Option<GrayBitmap> {
2823        if depth > Self::EBDT_COMPOSITE_MAX_DEPTH {
2824            return None;
2825        }
2826        let eblc = self.eblc.as_ref()?;
2827        let ebdt = self.ebdt.as_ref()?;
2828        let entry = eblc.lookup_glyph(glyph_id, ppem_y)?;
2829        // A non-composite component decodes directly as pixels.
2830        if !matches!(entry.image_format, 8 | 9) {
2831            return ebdt.lookup(&entry).ok().flatten();
2832        }
2833        let comp = ebdt.lookup_composite(&entry).ok().flatten()?;
2834        // Allocate the composite's own canvas (white / transparent).
2835        let cw = comp.width as usize;
2836        let ch = comp.height as usize;
2837        let mut canvas = vec![0u8; cw.checked_mul(ch)?];
2838        // §5.6.2.2: each component's (xOffset, yOffset) places the top-left
2839        // corner of the component bitmap within the composite. Components
2840        // are blitted back-to-front in array order; a non-zero alpha
2841        // pixel overwrites what is underneath (the bitmaps are alpha masks,
2842        // so "max coverage wins" preserves overlap without a true blend —
2843        // the spec leaves the composite raster model to the rasteriser).
2844        for component in &comp.components {
2845            // Guard against a component pointing back at itself.
2846            if component.glyph_id == glyph_id {
2847                continue;
2848            }
2849            let part = self.composite_gray_bitmap(component.glyph_id, ppem_y, depth + 1)?;
2850            let pw = part.width as usize;
2851            let ph = part.height as usize;
2852            for py in 0..ph {
2853                let cy = component.y_offset as isize + py as isize;
2854                if cy < 0 || cy as usize >= ch {
2855                    continue;
2856                }
2857                for px in 0..pw {
2858                    let cx = component.x_offset as isize + px as isize;
2859                    if cx < 0 || cx as usize >= cw {
2860                        continue;
2861                    }
2862                    let src = part.pixels.get(py * pw + px).copied().unwrap_or(0);
2863                    let dst = &mut canvas[cy as usize * cw + cx as usize];
2864                    *dst = (*dst).max(src);
2865                }
2866            }
2867        }
2868        Some(GrayBitmap {
2869            width: comp.width,
2870            height: comp.height,
2871            bearing_x: comp.bearing_x,
2872            bearing_y: comp.bearing_y,
2873            advance: comp.advance,
2874            ppem: comp.ppem,
2875            bit_depth: comp.bit_depth,
2876            pixels: canvas,
2877        })
2878    }
2879
2880    // ---- scaled embedded bitmaps (EBSC) ----------------------------------
2881
2882    /// `true` if this font ships an `EBSC` table (ISO/IEC 14496-22:2019
2883    /// §5.6.4) — i.e. declares one or more synthesised bitmap strikes
2884    /// built by scaling a real `EBLC`/`EBDT` strike. Returns `false` for
2885    /// fonts without `EBSC`, including the common case of a font that has
2886    /// real embedded bitmaps but never scales them.
2887    pub fn has_ebsc(&self) -> bool {
2888        self.ebsc.is_some()
2889    }
2890
2891    /// The parsed `EBSC` table, for tooling that wants to introspect the
2892    /// `BitmapScale` records directly (target / substitute ppem pairs and
2893    /// the per-strike line metrics).
2894    pub fn ebsc_table(&self) -> Option<&EbscTable> {
2895        self.ebsc.as_ref()
2896    }
2897
2898    /// All target `(ppemX, ppemY)` sizes the `EBSC` table can synthesise
2899    /// by scaling, in declaration order. These are sizes at which a
2900    /// rasteriser can obtain a bitmap *without* a real strike existing at
2901    /// that ppem — [`Font::glyph_gray_bitmap_scaled`] resolves them.
2902    /// Empty when the font has no `EBSC`.
2903    pub fn ebsc_target_sizes(&self) -> Vec<(u8, u8)> {
2904        self.ebsc
2905            .as_ref()
2906            .map(|t| t.target_ppem_sizes().collect())
2907            .unwrap_or_default()
2908    }
2909
2910    /// Resolve `glyph_id` at an `EBSC`-synthesised strike whose target
2911    /// `ppemY` equals `target_ppem`, returning a [`GrayBitmap`] whose
2912    /// pixel grid is the **real** substitute strike's imagery with the
2913    /// per-glyph metrics (width, height, bearings, advance) scaled by the
2914    /// `target / substitute` ppem ratio and rounded to the nearest integer
2915    /// pixel per §5.6.4. The `ppem` field of the returned bitmap is set to
2916    /// the synthesised target so the caller knows the intended display
2917    /// size.
2918    ///
2919    /// The pixel buffer itself is *not* resampled here — §5.6.4 leaves the
2920    /// actual scaling to the rasteriser ("a font to define a bitmap strike
2921    /// as a scaled version of another strike"); this method performs the
2922    /// table-level redirection and the metric scaling the spec mandates,
2923    /// and hands the source pixels through so the consumer crate can
2924    /// resample at its chosen filter quality. The reported `width` /
2925    /// `height` are the scaled dimensions the resampled grid should target.
2926    ///
2927    /// Returns `None` when the font has no `EBSC`, no `BitmapScale` record
2928    /// targets `target_ppem`, no real strike exists at the record's
2929    /// `substitutePpemY`, the substitute strike lacks `glyph_id`, or the
2930    /// substitute entry is in an undecoded `EBDT` format.
2931    pub fn glyph_gray_bitmap_scaled(&self, glyph_id: u16, target_ppem: u8) -> Option<GrayBitmap> {
2932        use crate::tables::ebsc::scale_metric;
2933        let ebsc = self.ebsc.as_ref()?;
2934        let eblc = self.eblc.as_ref()?;
2935        let ebdt = self.ebdt.as_ref()?;
2936        let scale = ebsc.scale_for_target_ppem(target_ppem)?;
2937        // Pull the real (substitute) strike's bitmap. We ask for the exact
2938        // substitute ppemY; `lookup_glyph` picks the strike whose ppemY is
2939        // closest, which lands on an exact match when the substitute strike
2940        // is present.
2941        let entry = eblc.lookup_glyph(glyph_id, scale.substitute_ppem_y)?;
2942        let src = ebdt.lookup(&entry).ok().flatten()?;
2943        // Scale metrics independently in X and Y per §5.6.4 ("scaling in
2944        // the x direction is independent of scaling in the y direction").
2945        let sx = scale.substitute_ppem_x;
2946        let sy = scale.substitute_ppem_y;
2947        let scaled_width = scale_metric(src.width as i32, scale.ppem_x, sx).clamp(0, 255) as u8;
2948        let scaled_height = scale_metric(src.height as i32, scale.ppem_y, sy).clamp(0, 255) as u8;
2949        let scaled_bearing_x =
2950            scale_metric(src.bearing_x as i32, scale.ppem_x, sx).clamp(-128, 127) as i8;
2951        let scaled_bearing_y =
2952            scale_metric(src.bearing_y as i32, scale.ppem_y, sy).clamp(-128, 127) as i8;
2953        let scaled_advance = scale_metric(src.advance as i32, scale.ppem_x, sx).clamp(0, 255) as u8;
2954        Some(GrayBitmap {
2955            width: scaled_width,
2956            height: scaled_height,
2957            bearing_x: scaled_bearing_x,
2958            bearing_y: scaled_bearing_y,
2959            advance: scaled_advance,
2960            ppem: scale.ppem_y,
2961            bit_depth: src.bit_depth,
2962            pixels: src.pixels,
2963        })
2964    }
2965
2966    // ---- color layer glyphs (COLR / CPAL) --------------------------------
2967
2968    /// `true` if this font ships a `COLR` + `CPAL` pair — i.e. carries
2969    /// vector colour-emoji glyphs as a per-glyph layer stack
2970    /// (Microsoft's Segoe UI Emoji, Twemoji's Mozilla cut, FiraCode's
2971    /// "color" variant, and so on). Returns `false` for plain
2972    /// outline-only fonts and for CBDT-only colour-emoji fonts.
2973    ///
2974    /// Both COLR versions are decoded: the v0 flat layer stack through
2975    /// [`Font::color_layers`], and the v1 paint graph through
2976    /// [`Font::color_paint_root`] / [`Font::color_paint`]. The spec
2977    /// prefers a v1 paint graph over a v0 layer stack for the same
2978    /// base glyph, so check [`Font::color_paint_root`] first when
2979    /// [`Font::has_colr_v1`] is set.
2980    pub fn has_color_layers(&self) -> bool {
2981        self.colr.is_some() && self.cpal.is_some()
2982    }
2983
2984    /// All colour layers for `glyph_id`, in back-to-front paint order.
2985    /// Each layer carries an outline-glyph id (whose outline you fetch
2986    /// via [`Font::glyph_outline`]) and a CPAL palette-entry index.
2987    /// The reserved palette index `0xFFFF` means "use the renderer's
2988    /// foreground colour" — substitute your own.
2989    ///
2990    /// Returns an empty `Vec` when the font has no `COLR` table or
2991    /// `glyph_id` isn't a base glyph (i.e. it's a single-colour
2992    /// outline glyph or a layer-only glyph used by other bases).
2993    pub fn color_layers(&self, glyph_id: u16) -> Vec<ColorLayer> {
2994        match self.colr.as_ref() {
2995            Some(colr) => colr.layers(glyph_id),
2996            None => Vec::new(),
2997        }
2998    }
2999
3000    // ---- COLR v1 paint graph ---------------------------------------------
3001
3002    /// `true` if this font's `COLR` table carries a version-1
3003    /// BaseGlyphList — i.e. at least one glyph is defined as a paint
3004    /// graph (gradients / transforms / composites) rather than, or in
3005    /// addition to, a v0 flat layer stack.
3006    pub fn has_colr_v1(&self) -> bool {
3007        self.colr
3008            .as_ref()
3009            .map(|c| c.has_paint_graph())
3010            .unwrap_or(false)
3011    }
3012
3013    /// Resolve `glyph_id` to the root [`PaintRef`] of its COLR v1
3014    /// colour-glyph graph (a binary search over the BaseGlyphList).
3015    /// `None` when the font has no v1 COLR data or the glyph has no
3016    /// paint record — fall back to [`Font::color_layers`] then, per
3017    /// the spec's v1-over-v0 preference order.
3018    pub fn color_paint_root(&self, glyph_id: u16) -> Option<PaintRef> {
3019        self.colr.as_ref()?.base_glyph_paint(glyph_id)
3020    }
3021
3022    /// Decode one Paint node of a COLR v1 graph **at the current
3023    /// variation instance** (set with [`Font::set_variation_coords`] /
3024    /// [`Font::set_axis_value`]; static fonts and the default instance
3025    /// resolve identically). Every `PaintVar*` wire form folds its
3026    /// deltas into the same resolved [`Paint`] variant as its static
3027    /// twin.
3028    ///
3029    /// Child paints are surfaced as further [`PaintRef`]s: the caller
3030    /// owns traversal and **must bound depth / track visited refs** —
3031    /// the spec requires the graph to be acyclic, but a hostile font
3032    /// can tie a loop (e.g. through `PaintColrGlyph`).
3033    ///
3034    /// Returns `None` for an unrecognised paint format (the spec's
3035    /// forward-compatibility rule is to ignore it) or a malformed
3036    /// node.
3037    pub fn color_paint(&self, paint: PaintRef) -> Option<Paint> {
3038        let coords = self.normalised_coords();
3039        self.colr.as_ref()?.paint(paint, &coords)
3040    }
3041
3042    /// The raw wire `format` byte of the Paint table at `paint` —
3043    /// distinguishes e.g. the four scale wire forms that
3044    /// [`Font::color_paint`] folds into [`Paint::Scale`], and a
3045    /// `PaintVar*` from its static twin.
3046    pub fn color_paint_format(&self, paint: PaintRef) -> Option<u8> {
3047        self.colr.as_ref()?.paint_format(paint)
3048    }
3049
3050    /// The precomputed COLR v1 clip box covering `glyph_id`, resolved
3051    /// at the current variation instance. Variable clip boxes
3052    /// (ClipBoxFormat 2) round *outward* per the spec so the box only
3053    /// ever expands. `None` when the font has no ClipList or no clip
3054    /// record covers the glyph — compute the bound from the graph
3055    /// then.
3056    pub fn color_clip_box(&self, glyph_id: u16) -> Option<ClipBox> {
3057        let coords = self.normalised_coords();
3058        self.colr.as_ref()?.clip_box(glyph_id, &coords)
3059    }
3060
3061    /// `true` when the COLR table ships a varIndexMap that does not
3062    /// decode — an unrecognised future format byte, reserved
3063    /// entryFormat bits, or a truncated map. Both defined
3064    /// `DeltaSetIndexMap` formats (0 and 1, per the staged OFF
3065    /// common-formats chapter) decode, so this only fires on
3066    /// malformed or future-format maps: the paint graph still decodes
3067    /// but every variation delta resolves to 0 (default-instance
3068    /// values).
3069    pub fn colr_var_index_map_unsupported(&self) -> bool {
3070        self.colr
3071            .as_ref()
3072            .map(|c| c.var_index_map_unsupported())
3073            .unwrap_or(false)
3074    }
3075
3076    /// Whether the COLR v1 colour glyph rooted at `glyph_id` is
3077    /// *bounded* — a well-formedness requirement (staged reference §9:
3078    /// "A version-1 color glyph definition must be bounded";
3079    /// `PaintGlyph` is inherently bounded and `PaintComposite` follows
3080    /// the per-mode §6 table). `Some(false)` = the graph decodes but
3081    /// paints an unbounded region; `None` = not well-formed (no paint
3082    /// record, an undecodable node, a cycle, or an adversarial graph
3083    /// that exhausts the bounded analysis budget). Renderers should
3084    /// refuse `Some(false)` / `None` glyphs or clip them to
3085    /// [`Font::color_clip_box`].
3086    pub fn color_glyph_is_bounded(&self, glyph_id: u16) -> Option<bool> {
3087        self.colr.as_ref()?.color_glyph_is_bounded(glyph_id)
3088    }
3089
3090    /// Resolve a COLR colour reference — a `(palette entry, alpha)`
3091    /// pair from a [`Paint::Solid`] or a
3092    /// [`tables::colr::ColorStop`] — against CPAL palette
3093    /// `palette_index`, applying the spec's alpha-multiplication rule:
3094    /// the COLR alpha (clamped to `[0, 1]`) scales the CPAL entry's
3095    /// own alpha channel. RGB channels pass through untouched.
3096    ///
3097    /// Returns `None` for the `0xFFFF` "text foreground" sentinel (the
3098    /// caller substitutes its own foreground colour and applies
3099    /// `alpha` to it), for a missing `CPAL` table, or for an
3100    /// out-of-range index.
3101    pub fn colr_effective_color(
3102        &self,
3103        palette_index: u16,
3104        entry_index: u16,
3105        alpha: f32,
3106    ) -> Option<[u8; 4]> {
3107        if entry_index == 0xFFFF {
3108            return None;
3109        }
3110        let [r, g, b, a] = self.cpal_color(palette_index, entry_index)?;
3111        let scaled = (a as f32 * alpha.clamp(0.0, 1.0)).round().clamp(0.0, 255.0) as u8;
3112        Some([r, g, b, scaled])
3113    }
3114
3115    /// Resolve a single CPAL colour by `(palette_index, color_index)`.
3116    /// Returns `[r, g, b, a]` (the byte order swizzled out of CPAL's
3117    /// on-disk BGRA) or `None` when either index is out of range or the
3118    /// font has no `CPAL` table.
3119    ///
3120    /// Palette 0 is the spec's "default" palette. CPAL v1's palette
3121    /// flags (`USABLE_WITH_LIGHT_BACKGROUND`,
3122    /// `USABLE_WITH_DARK_BACKGROUND`) are exposed via
3123    /// [`Font::cpal_palette_type`] for renderers that want to pick a
3124    /// theme-appropriate palette.
3125    pub fn cpal_color(&self, palette_index: u16, color_index: u16) -> Option<[u8; 4]> {
3126        self.cpal.as_ref()?.color(palette_index, color_index)
3127    }
3128
3129    /// All colours for palette `palette_index` as an `Vec<[u8; 4]>`
3130    /// (RGBA byte order). `None` if the font has no CPAL table or
3131    /// `palette_index` is out of range.
3132    pub fn cpal_palette(&self, palette_index: u16) -> Option<Vec<[u8; 4]>> {
3133        self.cpal.as_ref()?.palette(palette_index)
3134    }
3135
3136    /// Number of CPAL palettes the font ships, or `0` if there's no
3137    /// `CPAL` table. Mostly useful for renderers that pick a palette
3138    /// based on `cpal_palette_type` flags.
3139    pub fn cpal_num_palettes(&self) -> u16 {
3140        self.cpal.as_ref().map(|c| c.num_palettes()).unwrap_or(0)
3141    }
3142
3143    /// CPAL v1 palette-type flags for `palette_index`. Returns 0 when
3144    /// the font has no CPAL table, the table is v0, or the palette
3145    /// index is out of range.
3146    ///
3147    /// Bit 0 (`0x0001`) = USABLE_WITH_LIGHT_BACKGROUND
3148    /// Bit 1 (`0x0002`) = USABLE_WITH_DARK_BACKGROUND
3149    pub fn cpal_palette_type(&self, palette_index: u16) -> u32 {
3150        self.cpal
3151            .as_ref()
3152            .map(|c| c.palette_type(palette_index))
3153            .unwrap_or(0)
3154    }
3155
3156    /// CPAL v1 palette **label**: the `name` table ID of a UI string
3157    /// naming palette `palette_index` (e.g. "Regular", "High Contrast").
3158    /// Returns `None` when the font has no CPAL table, the table is v0,
3159    /// the `paletteLabelArray` is absent, the palette index is out of
3160    /// range, or the slot holds the `0xFFFF` "no label" sentinel. Pass
3161    /// the returned ID to a `name`-table lookup to fetch the localized
3162    /// string.
3163    pub fn cpal_palette_label(&self, palette_index: u16) -> Option<u16> {
3164        self.cpal.as_ref()?.palette_label(palette_index)
3165    }
3166
3167    /// CPAL v1 palette-**entry** label: the `name` table ID of a UI
3168    /// string naming palette entry `entry_index` (e.g. "Outline",
3169    /// "Fill"). The label applies uniformly across every palette in the
3170    /// font. Returns `None` when the font has no CPAL table, the table
3171    /// is v0, the `paletteEntryLabelArray` is absent, the entry index is
3172    /// out of range, or the slot holds the `0xFFFF` "no label" sentinel.
3173    pub fn cpal_palette_entry_label(&self, entry_index: u16) -> Option<u16> {
3174        self.cpal.as_ref()?.palette_entry_label(entry_index)
3175    }
3176
3177    // ---- sbix bitmap glyphs (Apple Color Emoji format) -------------------
3178
3179    /// `true` if this font ships an `sbix` table — Apple's PNG/JPEG/
3180    /// TIFF bitmap-strike container, used by Apple Color Emoji and
3181    /// every macOS/iOS-native colour-emoji font. Returns `false` for
3182    /// outline-only fonts and for CBDT/CBLC- or COLR/CPAL-flavoured
3183    /// colour fonts.
3184    pub fn has_sbix(&self) -> bool {
3185        self.sbix.is_some()
3186    }
3187
3188    /// All strike ppem sizes the `sbix` table ships, sorted ascending
3189    /// and de-duplicated. Apple Color Emoji typically lists eight
3190    /// strikes in the 20-160 ppem range. Returns an empty `Vec` when
3191    /// the font has no `sbix` table.
3192    pub fn sbix_strikes(&self) -> Vec<u16> {
3193        self.sbix
3194            .as_ref()
3195            .map(|s| s.all_ppems_unique_sorted())
3196            .unwrap_or_default()
3197    }
3198
3199    /// Resolve `glyph_id`'s sbix bitmap from the strike whose `ppem`
3200    /// is closest to the requested `ppem` (ties favour the larger
3201    /// strike, per the spec recommendation). Returns `None` if the
3202    /// font has no `sbix` table OR no strike contains a bitmap for
3203    /// `glyph_id`.
3204    ///
3205    /// `SbixGlyph::graphic_type` is one of `*b"png "`, `*b"jpg "`,
3206    /// `*b"tiff"`, or `*b"dupe"` — the consumer crate is expected to
3207    /// route the payload to the right decoder. The special `'dupe'`
3208    /// value indicates a 2-byte big-endian glyph id whose bitmap
3209    /// should be substituted; this method surfaces the indirection
3210    /// sentinel as-is for byte-level introspection. Use
3211    /// [`Self::sbix_glyph_resolved`] when the caller wants the
3212    /// indirection chased for them.
3213    pub fn sbix_glyph(&self, glyph_id: u16, ppem: u16) -> Option<SbixGlyph<'a>> {
3214        self.sbix.as_ref()?.lookup_best_fit(glyph_id, ppem)
3215    }
3216
3217    /// Like [`Self::sbix_glyph`], but chases `'dupe'` indirections
3218    /// within the chosen strike — up to [`SBIX_MAX_DUPE_DEPTH`] hops
3219    /// — with explicit cycle detection. Returns the first reachable
3220    /// non-`'dupe'` entry, or `None` if the chain cycles, exceeds the
3221    /// hop cap, or hits a malformed / out-of-range target. Callers
3222    /// that need to introspect the raw `'dupe'` sentinel keep using
3223    /// [`Self::sbix_glyph`].
3224    pub fn sbix_glyph_resolved(&self, glyph_id: u16, ppem: u16) -> Option<SbixGlyph<'a>> {
3225        self.sbix.as_ref()?.lookup_best_fit_resolved(glyph_id, ppem)
3226    }
3227
3228    // ---- variable fonts (fvar / avar / gvar) -----------------------------
3229
3230    /// `true` if the font ships an `fvar` table — i.e. it exposes one
3231    /// or more variation axes. Returns `false` for static fonts.
3232    pub fn is_variable(&self) -> bool {
3233        self.fvar.is_some()
3234    }
3235
3236    /// All variation axes the font publishes (`fvar`), in declaration
3237    /// order. Returns an empty slice for static fonts.
3238    pub fn variation_axes(&self) -> &[VariationAxis] {
3239        self.fvar.as_ref().map(|f| f.axes()).unwrap_or(&[])
3240    }
3241
3242    /// All named instances the font ships (`fvar`), in declaration
3243    /// order. Each carries a coordinate vector matching
3244    /// [`Self::variation_axes`] (one f32 per axis) plus a `name`
3245    /// table id for the human-readable subfamily label.
3246    pub fn named_instances(&self) -> &[NamedInstance] {
3247        self.fvar.as_ref().map(|f| f.instances()).unwrap_or(&[])
3248    }
3249
3250    /// Current user-space variation coordinates (one entry per axis,
3251    /// in `fvar` declaration order). Empty slice for static fonts.
3252    /// Defaults to each axis's `default` value at parse time;
3253    /// updated by [`Self::set_variation_coords`].
3254    pub fn variation_coords(&self) -> &[f32] {
3255        &self.var_coords
3256    }
3257
3258    /// Replace the current variation coordinates. Each entry is in
3259    /// **user-space** units (e.g. `wght` is 100..900). The vector
3260    /// must be the same length as [`Self::variation_axes`]; shorter
3261    /// vectors leave the trailing axes at their previous value, longer
3262    /// vectors are truncated. Out-of-range values are clamped to each
3263    /// axis's `[min, max]`.
3264    ///
3265    /// No-op when the font is static (`is_variable() == false`).
3266    pub fn set_variation_coords(&mut self, coords: &[f32]) {
3267        let axes = match self.fvar.as_ref() {
3268            Some(f) => f.axes(),
3269            None => return,
3270        };
3271        for (i, &v) in coords.iter().enumerate() {
3272            if i >= self.var_coords.len() {
3273                break;
3274            }
3275            let a = &axes[i];
3276            self.var_coords[i] = v.clamp(a.min, a.max);
3277        }
3278    }
3279
3280    /// Index of the axis carrying the four-byte `tag` (e.g. `*b"wght"`),
3281    /// or `None` when the font has no such axis (or is static).
3282    pub fn axis_index(&self, tag: &[u8; 4]) -> Option<usize> {
3283        self.variation_axes().iter().position(|a| &a.tag == tag)
3284    }
3285
3286    /// Current user-space value of the axis with the four-byte `tag`,
3287    /// or `None` when the font has no such axis.
3288    pub fn axis_value(&self, tag: &[u8; 4]) -> Option<f32> {
3289        let i = self.axis_index(tag)?;
3290        self.var_coords.get(i).copied()
3291    }
3292
3293    /// Set a single variation axis (identified by its four-byte `tag`,
3294    /// e.g. `*b"wght"` / `*b"wdth"` / `*b"slnt"` / `*b"opsz"` / `*b"ital"`)
3295    /// to a user-space `value`, leaving every other axis at its current
3296    /// value. The value is clamped to the axis's `[min, max]` range, like
3297    /// [`Self::set_variation_coords`].
3298    ///
3299    /// Returns `true` when the axis was found and updated, `false` for a
3300    /// static font or an unknown tag (in which case nothing changes).
3301    pub fn set_axis_value(&mut self, tag: &[u8; 4], value: f32) -> bool {
3302        let axes = match self.fvar.as_ref() {
3303            Some(f) => f.axes(),
3304            None => return false,
3305        };
3306        let i = match axes.iter().position(|a| &a.tag == tag) {
3307            Some(i) => i,
3308            None => return false,
3309        };
3310        if i >= self.var_coords.len() {
3311            return false;
3312        }
3313        let a = &axes[i];
3314        self.var_coords[i] = value.clamp(a.min, a.max);
3315        true
3316    }
3317
3318    /// Set the variation coordinates to the named instance at `index`
3319    /// (its position in [`Self::named_instances`]). After this call the
3320    /// font renders/shapes as that designer-chosen design variant (e.g.
3321    /// "Bold", "Condensed Light").
3322    ///
3323    /// Each named-instance coordinate is clamped to its axis range, like
3324    /// [`Self::set_variation_coords`]. Instances whose stored coordinate
3325    /// vector is shorter than the axis count leave the trailing axes at
3326    /// their current value; longer vectors are truncated.
3327    ///
3328    /// Returns `true` when the instance existed and was applied, `false`
3329    /// for a static font or an out-of-range `index`.
3330    pub fn apply_named_instance(&mut self, index: usize) -> bool {
3331        let coords = match self.fvar.as_ref() {
3332            Some(f) => match f.instances().get(index) {
3333                Some(inst) => inst.coords.clone(),
3334                None => return false,
3335            },
3336            None => return false,
3337        };
3338        self.set_variation_coords(&coords);
3339        true
3340    }
3341
3342    /// Compute the normalised coordinate vector (each entry in
3343    /// `[-1, +1]`) by mapping each user-space value through the
3344    /// `fvar` axis triple, then through the `avar` per-axis remap.
3345    /// Returns an empty vec for static fonts.
3346    pub fn normalised_coords(&self) -> Vec<f32> {
3347        let axes = match self.fvar.as_ref() {
3348            Some(f) => f.axes(),
3349            None => return Vec::new(),
3350        };
3351        let mut initial = Vec::with_capacity(axes.len());
3352        for (i, axis) in axes.iter().enumerate() {
3353            let v = self.var_coords.get(i).copied().unwrap_or(axis.default);
3354            let n = if (v - axis.default).abs() < f32::EPSILON {
3355                0.0
3356            } else if v < axis.default {
3357                if (axis.default - axis.min).abs() < f32::EPSILON {
3358                    0.0
3359                } else {
3360                    ((v - axis.default) / (axis.default - axis.min)).clamp(-1.0, 0.0)
3361                }
3362            } else if (axis.max - axis.default).abs() < f32::EPSILON {
3363                0.0
3364            } else {
3365                ((v - axis.default) / (axis.max - axis.default)).clamp(0.0, 1.0)
3366            };
3367            initial.push(n);
3368        }
3369        // avar stages 2 + 3: per-axis segment-map bending, then — for
3370        // an avar version-2 table — the cross-axis delta application
3371        // against the intermediate vector (staged v2 reference §4).
3372        match self.avar.as_ref() {
3373            Some(a) => a.remap_vector(&initial),
3374            None => initial,
3375        }
3376    }
3377
3378    /// `true` when the font's `avar` table is version 2 and ships an
3379    /// `axisIndexMap` that does not decode — an unrecognised future
3380    /// format byte, reserved entryFormat bits, or a truncated map.
3381    /// Both defined `DeltaSetIndexMap` formats (0 and 1, per the
3382    /// staged OFF common-formats chapter) decode, so this only fires
3383    /// on malformed or future-format maps: the cross-axis delta stage
3384    /// is skipped for the whole table (the v1 segment maps still
3385    /// apply).
3386    pub fn avar_axis_index_map_unsupported(&self) -> bool {
3387        self.avar
3388            .as_ref()
3389            .map(|a| a.axis_index_map_unsupported())
3390            .unwrap_or(false)
3391    }
3392
3393    // ---- Control Value Table (cvt) + CVT variations (cvar) ---------------
3394
3395    /// Number of entries in the `cvt ` Control Value Table, or `0` when
3396    /// the font has no `cvt ` table. Each entry is an `int16` FWORD
3397    /// (ISO/IEC 14496-22:2019 §5.3.2); the count is the table length
3398    /// divided by two (a trailing odd byte, if any, is ignored).
3399    pub fn cvt_count(&self) -> u16 {
3400        match self.cvt_bytes {
3401            Some(b) => (b.len() / 2).min(u16::MAX as usize) as u16,
3402            None => 0,
3403        }
3404    }
3405
3406    /// The static (un-varied) value of `cvt ` entry `index`, or `None`
3407    /// when the font has no `cvt ` table or `index` is out of range.
3408    /// This is the raw FWORD as authored, before any `cvar` instance
3409    /// delta is applied — see [`Self::cvt_value_varied`].
3410    pub fn cvt_value(&self, index: u16) -> Option<i16> {
3411        let b = self.cvt_bytes?;
3412        let off = index as usize * 2;
3413        crate::parser::read_i16(b, off).ok()
3414    }
3415
3416    /// `true` if the font ships a `cvar` CVT-variations table.
3417    pub fn has_cvar(&self) -> bool {
3418        self.cvar.is_some()
3419    }
3420
3421    /// Borrow the parsed `cvar` table, when present.
3422    pub fn cvar_table(&self) -> Option<&CvarTable<'a>> {
3423        self.cvar.as_ref()
3424    }
3425
3426    /// Per-`cvt`-entry deltas for the current variation instance,
3427    /// computed against the `avar`-bent normalised coordinate vector
3428    /// (ISO/IEC 14496-22:2019 §7.3.2). Returns a `Vec<i32>` of length
3429    /// [`Self::cvt_count`]; every entry is `0` for a static font, a
3430    /// font without `cvar`, or the default instance. Index `i` is the
3431    /// delta to add to `cvt ` entry `i`.
3432    pub fn cvt_deltas(&self) -> Vec<i32> {
3433        let n = self.cvt_count();
3434        let cvar = match self.cvar.as_ref() {
3435            Some(c) => c,
3436            None => return vec![0; n as usize],
3437        };
3438        let axis_count = self.fvar.as_ref().map(|f| f.axes().len()).unwrap_or(0) as u16;
3439        let coords = self.normalised_coords();
3440        cvar.cvt_deltas(axis_count, n, &coords)
3441            .unwrap_or_else(|_| vec![0; n as usize])
3442    }
3443
3444    /// The `cvt ` entry `index` with the current instance's `cvar`
3445    /// delta applied (saturating to the `i16` FWORD range), or `None`
3446    /// when the font has no `cvt ` table or `index` is out of range.
3447    /// For a static font or the default instance this equals
3448    /// [`Self::cvt_value`].
3449    pub fn cvt_value_varied(&self, index: u16) -> Option<i16> {
3450        let base = self.cvt_value(index)? as i32;
3451        let delta = match self.cvar.as_ref() {
3452            Some(cvar) => {
3453                let axis_count = self.fvar.as_ref().map(|f| f.axes().len()).unwrap_or(0) as u16;
3454                let coords = self.normalised_coords();
3455                cvar.cvt_deltas(axis_count, self.cvt_count(), &coords)
3456                    .ok()
3457                    .and_then(|d| d.get(index as usize).copied())
3458                    .unwrap_or(0)
3459            }
3460            None => 0,
3461        };
3462        Some((base + delta).clamp(i16::MIN as i32, i16::MAX as i32) as i16)
3463    }
3464
3465    // ---- TrueType hinting programs (fpgm / prep) -------------------------
3466
3467    /// The raw `fpgm` font-program bytes (TrueType bytecode run once when
3468    /// the font is first used, ISO/IEC 14496-22:2019 §5.3.3), or `None`
3469    /// when the font ships no `fpgm` table.
3470    ///
3471    /// This crate does **not** execute the bytecode (TrueType hinting is
3472    /// out of scope — modern anti-aliasing at typical sizes does not need
3473    /// it). The bytes are surfaced for tooling that introspects, edits, or
3474    /// round-trips the hinting program, and for a downstream interpreter.
3475    pub fn fpgm_program(&self) -> Option<&'a [u8]> {
3476        self.fpgm_bytes
3477    }
3478
3479    /// The raw `prep` control-value-program bytes (TrueType bytecode run
3480    /// whenever the point size / font / transform changes, ISO/IEC
3481    /// 14496-22:2019 §5.3.x), or `None` when absent. Like `fpgm`, surfaced
3482    /// raw and not executed.
3483    pub fn prep_program(&self) -> Option<&'a [u8]> {
3484        self.prep_bytes
3485    }
3486
3487    /// `true` if the font carries any TrueType hinting program (`fpgm`,
3488    /// `prep`, or a non-empty `cvt `). A purely outline-driven font with no
3489    /// hinting returns `false`. Note the bytecode is surfaced raw, never
3490    /// executed.
3491    pub fn has_hinting_program(&self) -> bool {
3492        self.fpgm_bytes.is_some_and(|b| !b.is_empty())
3493            || self.prep_bytes.is_some_and(|b| !b.is_empty())
3494            || self.cvt_count() != 0
3495    }
3496
3497    /// Borrow the parsed `MVAR` table, when present. Static fonts and
3498    /// variable fonts that omit MVAR return `None`.
3499    pub fn mvar_table(&self) -> Option<&MvarTable> {
3500        self.mvar.as_ref()
3501    }
3502
3503    /// Interpolated `MVAR` adjustment for a four-byte metric tag (e.g.
3504    /// `*b"xhgt"`, `*b"cpht"`, `*b"hasc"`) at the current variation
3505    /// coordinates.
3506    ///
3507    /// Per ISO/IEC 14496-22:2019 §7.3.6.2, the adjustment is computed
3508    /// against the current **normalised** coordinate vector (i.e.
3509    /// after the `avar` remap, see [`Self::normalised_coords`]). The
3510    /// returned value is a delta to be **added** to the corresponding
3511    /// field in `OS/2` / `hhea` / `vhea` / `post` / `gasp`.
3512    ///
3513    /// Returns `None` when:
3514    /// * the font lacks an `MVAR` table, or
3515    /// * the requested `tag` is not present in MVAR's value-record
3516    ///   array (the spec's "if the tag does not occur, the item is
3517    ///   constant across the variation space" rule).
3518    ///
3519    /// Returns `Some(0.0)` when the variation evaluates to zero at the
3520    /// current instance (e.g. at the axis defaults).
3521    pub fn metric_variation_delta(&self, tag: &[u8; 4]) -> Option<f32> {
3522        let m = self.mvar.as_ref()?;
3523        let coords = self.normalised_coords();
3524        m.delta_for_tag(tag, &coords)
3525    }
3526
3527    /// Borrow the parsed `HVAR` table, when present.
3528    pub fn hvar_table(&self) -> Option<&HvarTable> {
3529        self.hvar.as_ref()
3530    }
3531
3532    /// Interpolated `HVAR` adjustment to the advance width of
3533    /// `glyph_id` at the current variation coordinates.
3534    ///
3535    /// Per ISO/IEC 14496-22:2019 §7.3.5.3, the application reads the
3536    /// default advance width from `hmtx` and adds this delta to derive
3537    /// the per-instance advance. When an `advanceWidthMapping` table
3538    /// is published, that map provides the `(outer, inner)` index
3539    /// pair; otherwise the glyph ID itself acts as the inner index
3540    /// and the outer index is zero (the implicit form).
3541    ///
3542    /// Returns `None` when the font lacks `HVAR` or when the resolved
3543    /// index pair is out of range for the embedded item variation
3544    /// store. Returns `Some(0.0)` when the variation evaluates to
3545    /// zero at the current instance (e.g. at the axis defaults).
3546    pub fn advance_width_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3547        let h = self.hvar.as_ref()?;
3548        let coords = self.normalised_coords();
3549        h.advance_width_delta(glyph_id, &coords)
3550    }
3551
3552    /// Interpolated `HVAR` adjustment to the left side bearing of
3553    /// `glyph_id`. Requires that the font ship a left-side-bearing
3554    /// mapping table (§7.3.5.2 says LSB / RSB lookups always need
3555    /// one); returns `None` otherwise.
3556    pub fn lsb_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3557        let h = self.hvar.as_ref()?;
3558        let coords = self.normalised_coords();
3559        h.lsb_delta(glyph_id, &coords)
3560    }
3561
3562    /// Interpolated `HVAR` adjustment to the right side bearing of
3563    /// `glyph_id`. Requires a right-side-bearing mapping table per
3564    /// §7.3.5.2; returns `None` otherwise.
3565    pub fn rsb_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3566        let h = self.hvar.as_ref()?;
3567        let coords = self.normalised_coords();
3568        h.rsb_delta(glyph_id, &coords)
3569    }
3570
3571    /// Per-glyph advance width **at the current variation instance**:
3572    /// the static `hmtx` advance (see [`Self::glyph_advance`]) plus the
3573    /// `HVAR` delta (§7.3.5.3), rounded to the nearest font unit. For a
3574    /// static font, a font without `HVAR`, or the default instance this
3575    /// equals [`Self::glyph_advance`]. The result is clamped to the
3576    /// `i32` range only in pathological inputs; advances are unsigned in
3577    /// `hmtx` but the fused value is returned signed for symmetry with
3578    /// [`Self::glyph_advance`].
3579    pub fn glyph_advance_varied(&self, glyph_id: u16) -> i16 {
3580        let base = self.hmtx.advance(glyph_id) as f32;
3581        let delta = self.advance_width_variation_delta(glyph_id).unwrap_or(0.0);
3582        (base + delta)
3583            .round()
3584            .clamp(i16::MIN as f32, i16::MAX as f32) as i16
3585    }
3586
3587    /// Per-glyph left-side bearing **at the current variation
3588    /// instance**: the static `hmtx` LSB (see [`Self::glyph_lsb`]) plus
3589    /// the `HVAR` LSB delta (§7.3.5.2), rounded to the nearest font
3590    /// unit. Equals [`Self::glyph_lsb`] for a static font, a font
3591    /// without an `HVAR` LSB mapping, or the default instance.
3592    pub fn glyph_lsb_varied(&self, glyph_id: u16) -> i16 {
3593        let base = self.hmtx.lsb(glyph_id) as f32;
3594        let delta = self.lsb_variation_delta(glyph_id).unwrap_or(0.0);
3595        (base + delta)
3596            .round()
3597            .clamp(i16::MIN as f32, i16::MAX as f32) as i16
3598    }
3599
3600    /// Borrow the parsed `VVAR` table, when present.
3601    pub fn vvar_table(&self) -> Option<&VvarTable> {
3602        self.vvar.as_ref()
3603    }
3604
3605    /// Interpolated `VVAR` adjustment to the advance height of
3606    /// `glyph_id` at the current variation coordinates.
3607    ///
3608    /// Per ISO/IEC 14496-22:2019 §7.3.8.2 (cross-referenced back to
3609    /// §7.3.5.3), the application reads the default advance height
3610    /// from `vmtx` and adds this delta to derive the per-instance
3611    /// advance. When an `advanceHeightMapping` table is published,
3612    /// that map provides the `(outer, inner)` index pair; otherwise
3613    /// the glyph ID itself acts as the inner index and the outer index
3614    /// is zero (the implicit form).
3615    ///
3616    /// Returns `None` when the font lacks `VVAR` or when the resolved
3617    /// index pair is out of range for the embedded item variation
3618    /// store. Returns `Some(0.0)` when the variation evaluates to zero
3619    /// at the current instance (e.g. at the axis defaults).
3620    pub fn advance_height_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3621        let v = self.vvar.as_ref()?;
3622        let coords = self.normalised_coords();
3623        v.advance_height_delta(glyph_id, &coords)
3624    }
3625
3626    /// Interpolated `VVAR` adjustment to the top side bearing of
3627    /// `glyph_id`. Requires that the font ship a top-side-bearing
3628    /// mapping table (§7.3.8.2 inherits the §7.3.5.2 rule that side-
3629    /// bearing lookups always need a map); returns `None` otherwise.
3630    pub fn tsb_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3631        let v = self.vvar.as_ref()?;
3632        let coords = self.normalised_coords();
3633        v.tsb_delta(glyph_id, &coords)
3634    }
3635
3636    /// Interpolated `VVAR` adjustment to the bottom side bearing of
3637    /// `glyph_id`. Requires a bottom-side-bearing mapping table per
3638    /// §7.3.8.2; returns `None` otherwise.
3639    pub fn bsb_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3640        let v = self.vvar.as_ref()?;
3641        let coords = self.normalised_coords();
3642        v.bsb_delta(glyph_id, &coords)
3643    }
3644
3645    /// Per-glyph advance height **at the current variation instance**:
3646    /// the static `vmtx` advance height (see
3647    /// [`Self::glyph_advance_height`]) plus the `VVAR` advance-height
3648    /// delta (§7.3.8.2), rounded to the nearest font unit. Returns
3649    /// `None` when the font lacks `vhea`/`vmtx`. Equals
3650    /// [`Self::glyph_advance_height`] for a font without `VVAR` or at
3651    /// the default instance.
3652    pub fn glyph_advance_height_varied(&self, glyph_id: u16) -> Option<u16> {
3653        let base = self.vmtx.as_ref()?.advance_height(glyph_id) as f32;
3654        let delta = self.advance_height_variation_delta(glyph_id).unwrap_or(0.0);
3655        Some((base + delta).round().clamp(0.0, u16::MAX as f32) as u16)
3656    }
3657
3658    /// Interpolated `VVAR` adjustment to the vertical-origin Y of
3659    /// `glyph_id`. §7.3.8.2 final paragraph: a mapping table is
3660    /// required for vertical-origin variation data, and the data is
3661    /// "not used in fonts with TrueType outlines" — populated only by
3662    /// CFF2 variable fonts that publish a `VORG` table. Returns
3663    /// `None` otherwise.
3664    pub fn vorg_variation_delta(&self, glyph_id: u16) -> Option<f32> {
3665        let v = self.vvar.as_ref()?;
3666        let coords = self.normalised_coords();
3667        v.vorg_delta(glyph_id, &coords)
3668    }
3669
3670    /// Borrow the parsed `STAT` table, when present. Static fonts may
3671    /// omit it; variable fonts are required by ISO/IEC 14496-22:2019
3672    /// §7.3.7 to ship one.
3673    pub fn stat_table(&self) -> Option<&StatTable> {
3674        self.stat.as_ref()
3675    }
3676
3677    /// `STAT.designAxes` — one record per design axis. For a variable
3678    /// font, every `fvar` axis must appear here; the order is arbitrary
3679    /// (sort by `axis_ordering` if a stable UI order is needed).
3680    /// Returns an empty slice when no STAT table is present.
3681    pub fn stat_axes(&self) -> &[StatAxisRecord] {
3682        match self.stat.as_ref() {
3683            Some(s) => s.axes(),
3684            None => &[],
3685        }
3686    }
3687
3688    /// `STAT.axisValueTables` — every axis value record in document
3689    /// order. Filter by axis tag with [`Self::stat_axis_values_for_tag`]
3690    /// or walk by format to compose subfamily strings under the
3691    /// R/B/I/BI, WWS, or unrestricted naming models (§7.3.7.3).
3692    /// Returns an empty slice when no STAT table is present.
3693    pub fn stat_axis_values(&self) -> &[StatAxisValue] {
3694        match self.stat.as_ref() {
3695            Some(s) => s.axis_values(),
3696            None => &[],
3697        }
3698    }
3699
3700    /// `STAT.elidedFallbackNameID` — the `name` table nameID applied
3701    /// when every component of a composed subfamily string would be
3702    /// elided (§7.3.7.1). Returns `None` when the font ships no STAT
3703    /// table; returns name ID 2 ("Regular") for the deprecated v1.0
3704    /// header that lacked the field.
3705    pub fn stat_elided_fallback_name_id(&self) -> Option<u16> {
3706        Some(self.stat.as_ref()?.elided_fallback_name_id())
3707    }
3708
3709    /// Every STAT axis-value record whose axis is `axis_tag` (e.g.
3710    /// `*b"wght"`, `*b"wdth"`). Format-4 records are matched when one
3711    /// of their contributing axes references this tag. Returns an
3712    /// empty iterator when the font has no STAT table or the tag is
3713    /// not in the design-axes array.
3714    pub fn stat_axis_values_for_tag(
3715        &self,
3716        axis_tag: [u8; 4],
3717    ) -> Box<dyn Iterator<Item = &StatAxisValue> + '_> {
3718        match self.stat.as_ref() {
3719            Some(s) => Box::new(s.axis_values_for_tag(axis_tag)),
3720            None => Box::new(core::iter::empty()),
3721        }
3722    }
3723
3724    /// `true` if any current coordinate diverges from its axis default.
3725    fn coords_differ_from_default(&self) -> bool {
3726        let axes = match self.fvar.as_ref() {
3727            Some(f) => f.axes(),
3728            None => return false,
3729        };
3730        for (i, axis) in axes.iter().enumerate() {
3731            if let Some(v) = self.var_coords.get(i) {
3732                if (v - axis.default).abs() > f32::EPSILON {
3733                    return true;
3734                }
3735            }
3736        }
3737        false
3738    }
3739}
3740
3741#[inline]
3742fn clamp_i16_for_outline(v: i32) -> i16 {
3743    if v < i16::MIN as i32 {
3744        i16::MIN
3745    } else if v > i16::MAX as i32 {
3746        i16::MAX
3747    } else {
3748        v as i16
3749    }
3750}