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}