Skip to main content

oxideav_otf/
lib.rs

1//! Pure-Rust OpenType / CFF font parser.
2//!
3//! Scope:
4//! - sfnt header + table directory walker (`parser`).
5//! - CFF (Adobe TN5176) Top DICT / Name / String INDEX / Charset /
6//!   Encoding / Private DICT / Local + Global Subrs, plus CID-keyed
7//!   fonts (ROS + FDArray Font DICTs + FDSelect GID→FD routing,
8//!   TN5176 §§18, 19).
9//! - CFF2 (OpenType 1.9.1 §6–§8): header, Top DICT, GlobalSubrINDEX,
10//!   CharStringINDEX, and FontDICTINDEX walks (the `cff2` module
11//!   defers variation-aware charstring decoding to a later round).
12//! - Type 2 charstring interpreter (Adobe TN5177): every common path
13//!   construction operator, the four flex variants, the deprecated
14//!   four-operand `seac` `endchar`, hint-recording stubs (no
15//!   enforcement; we anti-alias at >= 16 px), and subroutine
16//!   resolution with the well-known 107 / 1131 / 32768 bias formula.
17//! - Selected sfnt tables for metadata (`head`, `hhea`, `maxp`,
18//!   `hmtx`, `cmap` formats 0/4/6/12, `name`, `post`, `OS/2`,
19//!   `GDEF`, and the `GSUB` / `GPOS` headers with their
20//!   `ScriptList` / `FeatureList` / `LookupList` walks).
21//!
22//! The crate is read-only (parsing-only) and dependency-light: only
23//! `oxideav-core` for shared types. CFF2 charstring decoding (with
24//! blend / vsindex resolution against the VariationStore), per-glyph
25//! hinting interpretation, advanced GSUB/GPOS, and Bidi are deferred.
26//!
27//! See `README.md` for a tour of the public API.
28
29#![deny(missing_debug_implementations)]
30#![warn(rust_2018_idioms)]
31
32pub mod agl;
33pub mod cff;
34pub mod cff2;
35pub mod outline;
36pub mod parser;
37pub mod tables;
38
39pub use cff::{PrivateHints, RegistryOrdering, TopMetadata};
40pub use cff2::{
41    Cff2, Cff2Header, Cff2Op, Cff2TopDict, ItemVariationData, ItemVariationStore,
42    RegionAxisCoordinates, VariationRegion, DEFAULT_FONT_MATRIX,
43};
44pub use outline::{BBox, CubicContour, CubicOutline, CubicSegment, Point};
45
46use crate::cff::Cff;
47use crate::parser::TableDirectory;
48use crate::tables::{
49    cmap::CmapTable, gdef::GdefTable, gpos::GposTable, gsub::GsubTable, head::HeadTable,
50    hhea::HheaTable, hmtx::HmtxTable, maxp::MaxpTable, name::NameTable, os2::Os2Table,
51    post::PostTable,
52};
53
54pub use crate::tables::gdef::{
55    AttachList, AttachPoint, CaretValue, ClassDef, Coverage, CoverageIter, GlyphClass,
56    LigCaretList, LigGlyph, MarkGlyphSets,
57};
58pub use crate::tables::gpos::GposTable as GposView;
59pub use crate::tables::gpos::{
60    ExtensionPos, PairPos, PairPosIter, PairValue, SinglePos, SinglePosIter, ValueFormat,
61    ValueRecord, GPOS_LOOKUP_TYPE_CHAINED_CONTEXT, GPOS_LOOKUP_TYPE_CONTEXT,
62    GPOS_LOOKUP_TYPE_CURSIVE, GPOS_LOOKUP_TYPE_EXTENSION, GPOS_LOOKUP_TYPE_MARK_TO_BASE,
63    GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE, GPOS_LOOKUP_TYPE_MARK_TO_MARK, GPOS_LOOKUP_TYPE_PAIR,
64    GPOS_LOOKUP_TYPE_SINGLE,
65};
66pub use crate::tables::gsub::GsubTable as GsubView;
67pub use crate::tables::gsub::{
68    AlternateGlyphIter, AlternateSet, AlternateSubst, AlternateSubstIter, ExtensionSubst, Ligature,
69    LigatureComponentIter, LigatureSet, LigatureSubst, LigatureSubstIter, MultipleSubst,
70    MultipleSubstIter, Sequence, SequenceGlyphIter, SingleSubst, SingleSubstIter,
71    GSUB_LOOKUP_TYPE_ALTERNATE, GSUB_LOOKUP_TYPE_CHAINED_CONTEXT, GSUB_LOOKUP_TYPE_CONTEXT,
72    GSUB_LOOKUP_TYPE_EXTENSION, GSUB_LOOKUP_TYPE_LIGATURE, GSUB_LOOKUP_TYPE_MULTIPLE,
73    GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE, GSUB_LOOKUP_TYPE_SINGLE,
74};
75pub use crate::tables::layout::{
76    Feature, FeatureList, FeatureListIter, LangSys, Lookup, LookupFlag, LookupList, LookupListIter,
77    Script, ScriptList, ScriptListIter, NO_REQUIRED_FEATURE,
78};
79pub use crate::tables::name::{NameId, NameRecord};
80pub use crate::tables::os2::{
81    EmbeddingPermission, FS_SELECTION_BOLD, FS_SELECTION_ITALIC, FS_SELECTION_NEGATIVE,
82    FS_SELECTION_OBLIQUE, FS_SELECTION_OUTLINED, FS_SELECTION_REGULAR, FS_SELECTION_STRIKEOUT,
83    FS_SELECTION_UNDERSCORE, FS_SELECTION_USE_TYPO_METRICS, FS_SELECTION_WWS,
84    FS_TYPE_BITMAP_EMBEDDING_ONLY, FS_TYPE_EDITABLE, FS_TYPE_NO_SUBSETTING,
85    FS_TYPE_PREVIEW_AND_PRINT, FS_TYPE_RESTRICTED_LICENSE, FS_TYPE_USAGE_MASK,
86};
87pub use crate::tables::post::PostFormat;
88
89/// Errors emitted during font parsing or glyph lookup.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum Error {
92    /// The input slice was too short for the requested header / structure.
93    UnexpectedEof,
94    /// The sfnt magic version did not match `OTTO`, `0x00010000`, or `true`.
95    BadMagic,
96    /// The table count in the sfnt header is implausibly large.
97    BadHeader,
98    /// An offset / length field pointed outside the file.
99    BadOffset,
100    /// A required table was missing from the table directory.
101    MissingTable(&'static str),
102    /// The font has no `CFF ` or `CFF2` table.
103    MissingCff,
104    /// CFF2-flavoured fonts are parsed for metadata (header, Top
105    /// DICT, structural INDEXes — see the `cff2` module), but Type 2
106    /// charstring decoding for CFF2 (with `blend` + `vsindex`
107    /// resolution against the ItemVariationStore) is not yet
108    /// implemented; [`Font::glyph_outline`] returns this error on a
109    /// CFF2 font.
110    Cff2NotImplemented,
111    /// A glyph index was out of range vs. `maxp.numGlyphs` /
112    /// `CharStrings INDEX count`.
113    GlyphOutOfRange(u16),
114    /// A cmap subtable used a format we do not implement in round 1.
115    UnsupportedCmapFormat(u16),
116    /// CFF-specific failure with a brief reason.
117    Cff(&'static str),
118    /// A varying-length structure was malformed in a non-CFF table
119    /// (head, hhea, maxp, hmtx, name, cmap).
120    BadStructure(&'static str),
121
122    // --- Charstring interpreter errors ----------------------------------
123    /// Operand stack overflowed (>= 192 entries).
124    CharstringStackOverflow,
125    /// Operator consumed more operands than the stack held.
126    CharstringStackUnderflow,
127    /// Operator referenced a subroutine number outside the INDEX range.
128    CharstringBadSubrIndex(i32),
129    /// `callsubr` was used in a font that has no Local Subrs INDEX.
130    CharstringNoLocalSubrs,
131    /// Subroutine recursion exceeded the spec cap (TN5177 §4.5: 10).
132    CharstringTooDeep,
133    /// Charstring processed too many bytes (DoS bound).
134    CharstringTooLong,
135    /// Charstring used an operator we don't yet implement.
136    CharstringUnsupportedOp(u16),
137    /// Internal sentinel used by the interpreter to signal `endchar`;
138    /// never escapes the public API.
139    #[doc(hidden)]
140    CharstringEnd,
141    /// `endchar` was used in its deprecated four-operand `seac` form
142    /// (TN5177 Appendix C / Type 1 `seac`) but a referenced
143    /// component glyph could not be resolved through the Standard
144    /// Encoding table + the font's charset. The contained byte is
145    /// the unresolved Standard-Encoding code (bchar or achar).
146    CharstringSeacBadComponent(u8),
147    /// Nested `seac` was attempted. The spec forbids it (TN5177
148    /// Appendix C: "This construct may not be nested.").
149    CharstringSeacNested,
150    /// A `put` / `get` storage operator (TN5177 §4.5) referenced a
151    /// transient-array index outside `0..32` (Appendix B fixes the
152    /// array at 32 elements). The contained value is the offending
153    /// index.
154    CharstringTransientIndex(i32),
155}
156
157impl core::fmt::Display for Error {
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        match self {
160            Self::UnexpectedEof => f.write_str("unexpected end of font data"),
161            Self::BadMagic => f.write_str("not a TrueType / OpenType font (bad magic)"),
162            Self::BadHeader => f.write_str("malformed sfnt header"),
163            Self::BadOffset => f.write_str("table offset out of range"),
164            Self::MissingTable(t) => write!(f, "required table missing: {t}"),
165            Self::MissingCff => f.write_str("font has no CFF/CFF2 table"),
166            Self::Cff2NotImplemented => {
167                f.write_str("CFF2 charstring decode not implemented (metadata is parsed)")
168            }
169            Self::GlyphOutOfRange(g) => write!(f, "glyph index {g} out of range"),
170            Self::UnsupportedCmapFormat(fmt) => {
171                write!(f, "cmap format {fmt} not implemented in round 1")
172            }
173            Self::Cff(s) => write!(f, "CFF: {s}"),
174            Self::BadStructure(s) => write!(f, "malformed structure: {s}"),
175            Self::CharstringStackOverflow => {
176                f.write_str("Type 2 charstring: operand stack overflow")
177            }
178            Self::CharstringStackUnderflow => {
179                f.write_str("Type 2 charstring: operand stack underflow")
180            }
181            Self::CharstringBadSubrIndex(i) => {
182                write!(f, "Type 2 charstring: subr index {i} out of range")
183            }
184            Self::CharstringNoLocalSubrs => {
185                f.write_str("Type 2 charstring: callsubr but no local subrs INDEX")
186            }
187            Self::CharstringTooDeep => {
188                f.write_str("Type 2 charstring: subroutine recursion too deep")
189            }
190            Self::CharstringTooLong => f.write_str("Type 2 charstring: too many bytes processed"),
191            Self::CharstringUnsupportedOp(op) => {
192                write!(f, "Type 2 charstring: unsupported operator {op:#06x}")
193            }
194            Self::CharstringEnd => f.write_str("Type 2 charstring: end (internal)"),
195            Self::CharstringSeacBadComponent(code) => write!(
196                f,
197                "Type 2 charstring: seac component (Standard Encoding code {code}) \
198                 has no matching glyph in this font's charset"
199            ),
200            Self::CharstringSeacNested => {
201                f.write_str("Type 2 charstring: nested seac is forbidden (TN5177 Appendix C)")
202            }
203            Self::CharstringTransientIndex(i) => write!(
204                f,
205                "Type 2 charstring: transient-array index {i} out of range (0..32)"
206            ),
207        }
208    }
209}
210
211impl std::error::Error for Error {}
212
213/// A parsed OpenType / CFF font, lifetime-bound to the input bytes.
214///
215/// `Font::from_bytes` walks the sfnt header + table directory plus the
216/// CFF top-level structures once; per-glyph charstrings are decoded on
217/// demand by [`Font::glyph_outline`]. Lookup methods are O(log n) /
218/// O(n) over the raw table bytes — no glyphs are pre-decoded.
219#[derive(Debug)]
220pub struct Font<'a> {
221    bytes: &'a [u8],
222    dir: TableDirectory,
223    head: HeadTable,
224    hhea: HheaTable,
225    maxp: MaxpTable,
226    cmap: CmapTable<'a>,
227    name: NameTable<'a>,
228    hmtx: HmtxTable<'a>,
229    /// Optional per OpenType spec: every well-formed OpenType font
230    /// carries `post`, but some real-world stripped-down fonts omit
231    /// it. We tolerate absence rather than reject the whole font.
232    post: Option<PostTable<'a>>,
233    /// `OS/2 and Windows Metrics`. Required for OpenType but
234    /// occasionally missing on stripped-down or legacy TrueType-only
235    /// fonts; absence surfaces as `None` rather than rejecting the
236    /// whole font.
237    os2: Option<Os2Table>,
238    /// `GDEF` — Glyph Definition Table. Optional per the OpenType
239    /// spec: a font without GSUB / GPOS lookups doesn't need it. When
240    /// present, surfaces per-glyph class data + ligature carets + the
241    /// MarkAttachClassDef and MarkGlyphSets sub-tables that GSUB and
242    /// GPOS shaping consult.
243    gdef: Option<GdefTable<'a>>,
244    /// `GSUB` — Glyph Substitution Table header view. Optional
245    /// (a font that performs no glyph substitution omits the table).
246    /// When present, surfaces the ScriptList / FeatureList /
247    /// LookupList shape; per-lookup subtable decoding is deferred to a
248    /// future round.
249    gsub: Option<GsubTable<'a>>,
250    /// `GPOS` — Glyph Positioning Table header view. Optional. Same
251    /// header shape as `GSUB`; per-lookup positioning-subtable
252    /// decoding is deferred.
253    gpos: Option<GposTable<'a>>,
254    /// The font's CFF outline data, either CFF1 (Adobe TN5176) or CFF2
255    /// (OpenType 1.9.1). CFF1 carries full charstring decoding +
256    /// metadata; CFF2 carries structural metadata (header + Top DICT +
257    /// CharStringINDEX count) but defers Type 2 + blend charstring
258    /// decoding to a future round.
259    cff: CffFlavour<'a>,
260}
261
262/// Internal discriminant for the font's CFF table flavour. The two
263/// variants are boxed to keep the `Font` struct size and `CffFlavour`
264/// discriminant cheap to move; the CFF1 variant in particular carries
265/// a TopMetadata struct + 4 INDEX views + a Strings table and is ~500
266/// bytes on its own.
267#[derive(Debug)]
268enum CffFlavour<'a> {
269    Cff1(Box<Cff<'a>>),
270    Cff2(Box<Cff2<'a>>),
271}
272
273/// Process-wide spec-default [`PrivateHints`] (TN5176 §15 defaults).
274/// Returned by the `Font::private_hints` family for CFF2 fonts, whose
275/// Private DICT decoding is deferred to a future round. Lazily
276/// initialised so the cost is paid only when first queried.
277fn default_private_hints() -> &'static PrivateHints {
278    use std::sync::OnceLock;
279    static DEFAULTS: OnceLock<PrivateHints> = OnceLock::new();
280    DEFAULTS.get_or_init(PrivateHints::default)
281}
282
283impl<'a> Font<'a> {
284    /// Parse a font from a borrowed byte slice.
285    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
286        let dir = TableDirectory::parse(bytes)?;
287        let cff_tag = dir.cff_tag.ok_or(Error::MissingCff)?;
288
289        let head = HeadTable::parse(dir.required(b"head", bytes)?)?;
290        let hhea = HheaTable::parse(dir.required(b"hhea", bytes)?)?;
291        let maxp = MaxpTable::parse(dir.required(b"maxp", bytes)?)?;
292        let cmap = CmapTable::parse(dir.required(b"cmap", bytes)?)?;
293        let name = NameTable::parse(dir.required(b"name", bytes)?)?;
294        let hmtx = HmtxTable::parse(
295            dir.required(b"hmtx", bytes)?,
296            hhea.num_long_hor_metrics,
297            maxp.num_glyphs,
298        )?;
299
300        // `post` is one of the OpenType-spec required tables (per
301        // `otspec-otff.html` "Required Tables"); for OpenType-CFF1 the
302        // spec mandates version 3.0. Some real-world stripped-down
303        // fonts omit it, so we tolerate absence and surface a `None`.
304        let post = match dir.find(b"post", bytes) {
305            Some(slice) => Some(PostTable::parse(slice)?),
306            None => None,
307        };
308
309        // `OS/2` is one of the OpenType-spec required tables (per
310        // `otspec-otff.html` "Required Tables") — same tolerance
311        // policy as `post`: parse if present, surface `None`
312        // otherwise so a stripped-down TrueType-only `.otf` that
313        // omitted the table doesn't fail open.
314        let os2 = match dir.find(b"OS/2", bytes) {
315            Some(slice) => Some(Os2Table::parse(slice)?),
316            None => None,
317        };
318
319        // `GDEF` is optional — a font without GSUB/GPOS lookups can
320        // legitimately omit it. Parse if present.
321        let gdef = match dir.find(b"GDEF", bytes) {
322            Some(slice) => Some(GdefTable::parse(slice)?),
323            None => None,
324        };
325
326        // `GSUB` and `GPOS` are both optional in OpenType: a
327        // glyph-only font with neither substitution nor positioning
328        // rules omits both.
329        let gsub = match dir.find(b"GSUB", bytes) {
330            Some(slice) => Some(GsubTable::parse(slice)?),
331            None => None,
332        };
333        let gpos = match dir.find(b"GPOS", bytes) {
334            Some(slice) => Some(GposTable::parse(slice)?),
335            None => None,
336        };
337
338        let cff = if cff_tag == *b"CFF2" {
339            let cff2_bytes = dir.required(b"CFF2", bytes)?;
340            CffFlavour::Cff2(Box::new(Cff2::parse(cff2_bytes)?))
341        } else {
342            let cff_bytes = dir.required(b"CFF ", bytes)?;
343            CffFlavour::Cff1(Box::new(Cff::parse(cff_bytes)?))
344        };
345
346        Ok(Self {
347            bytes,
348            dir,
349            head,
350            hhea,
351            maxp,
352            cmap,
353            name,
354            hmtx,
355            post,
356            os2,
357            gdef,
358            gsub,
359            gpos,
360            cff,
361        })
362    }
363
364    /// Raw bytes used to build this `Font`. Mostly useful for debugging.
365    pub fn bytes(&self) -> &'a [u8] {
366        self.bytes
367    }
368
369    // ---- metadata ----------------------------------------------------------
370
371    /// Family name from the `name` table.
372    pub fn family_name(&self) -> Option<&str> {
373        self.name.find(1)
374    }
375
376    /// Full name (typically family + style) from the `name` table.
377    pub fn full_name(&self) -> Option<&str> {
378        self.name.find(4)
379    }
380
381    /// `head.unitsPerEm`. Almost always 1000 (CFF default) or 2048;
382    /// never zero in valid fonts.
383    pub fn units_per_em(&self) -> u16 {
384        self.head.units_per_em
385    }
386
387    /// Number of glyphs (`maxp.numGlyphs`).
388    pub fn glyph_count(&self) -> u16 {
389        self.maxp.num_glyphs
390    }
391
392    /// Typographic ascent from `hhea`.
393    pub fn ascent(&self) -> i16 {
394        self.hhea.ascent
395    }
396
397    /// Typographic descent from `hhea` (typically negative).
398    pub fn descent(&self) -> i16 {
399        self.hhea.descent
400    }
401
402    /// Suggested gap between lines from `hhea`.
403    pub fn line_gap(&self) -> i16 {
404        self.hhea.line_gap
405    }
406
407    /// PostScript font name from the CFF Name INDEX.
408    ///
409    /// CFF2 has no Name INDEX (the PostScript name lives in the sfnt
410    /// `name` table at name ID 6 instead); this accessor returns
411    /// `None` for CFF2 fonts. Callers wanting a PostScript name that
412    /// works for both flavours should use
413    /// `Font::name_string(NameId::PostScript)`.
414    pub fn ps_name(&self) -> Option<&str> {
415        std::str::from_utf8(self.cff1()?.ps_name()).ok()
416    }
417
418    /// Borrow the parsed CFF1 view, or `None` if this font uses CFF2
419    /// (TN5176 vs. OpenType 1.9.1 CFF2 are different table flavours
420    /// and only one is present in a given font).
421    fn cff1(&self) -> Option<&Cff<'a>> {
422        match &self.cff {
423            CffFlavour::Cff1(c) => Some(c),
424            CffFlavour::Cff2(_) => None,
425        }
426    }
427
428    /// Borrow the parsed CFF2 view, or `None` if this font uses CFF1.
429    fn cff2_view(&self) -> Option<&Cff2<'a>> {
430        match &self.cff {
431            CffFlavour::Cff1(_) => None,
432            CffFlavour::Cff2(c) => Some(c),
433        }
434    }
435
436    /// `true` if the font carries a `CFF2` table (OpenType 1.9.1
437    /// variation-aware CFF flavour) rather than the original `CFF `
438    /// table (Adobe TN5176 CFF version 1).
439    pub fn is_cff2(&self) -> bool {
440        matches!(self.cff, CffFlavour::Cff2(_))
441    }
442
443    /// Borrow the parsed CFF2 table, or `None` for CFF1 fonts. The
444    /// returned view exposes the CFF2 header, Top DICT, CharString
445    /// count, FontDICT INDEX, per-glyph CharString bytes, and (for
446    /// variable fonts) the parsed `ItemVariationStore`
447    /// ([`Font::variation_store`]); per-glyph outline decoding (the
448    /// `blend`/`vsindex` charstring math against the store) is deferred
449    /// to a future round and currently surfaces as
450    /// [`Error::Cff2NotImplemented`] from [`Font::glyph_outline`].
451    pub fn cff2(&self) -> Option<&Cff2<'a>> {
452        self.cff2_view()
453    }
454
455    /// Borrow the parsed CFF2 header (`major`, `minor`, `headerSize`,
456    /// `topDICTSize`), or `None` for CFF1 fonts.
457    pub fn cff2_header(&self) -> Option<&Cff2Header> {
458        self.cff2_view().map(Cff2::header)
459    }
460
461    /// Borrow the parsed CFF2 Top DICT, or `None` for CFF1 fonts. The
462    /// returned struct surfaces all five spec-permitted operators
463    /// (`CharStringINDEXOffset`, `VariationStoreOffset`,
464    /// `FontDICTINDEXOffset`, `FontDICTSelectOffset`, `FontMatrix`).
465    pub fn cff2_top_dict(&self) -> Option<&Cff2TopDict> {
466        self.cff2_view().map(Cff2::top_dict)
467    }
468
469    /// `true` if this is a CFF2 variable font — that is, the Top DICT
470    /// carries a `VariationStoreOffset` operator (per spec §7
471    /// "VariationStoreOffset" Occurrence: required in fonts with
472    /// variations, forbidden otherwise). Always `false` for CFF1
473    /// fonts (CFF1 has no variation mechanism).
474    pub fn is_variable(&self) -> bool {
475        self.cff2_view().is_some_and(Cff2::is_variable)
476    }
477
478    /// Borrow the CFF2 `ItemVariationStore` (§12) for a variable CFF2
479    /// font, or `None` for non-variable CFF2 fonts and all CFF1 fonts.
480    /// The store exposes the `VariationRegionList` (each region's
481    /// per-axis `start`/`peak`/`end` F2DOT14 intervals) and the
482    /// `ItemVariationData` subtables (`regionIndexes` selecting the
483    /// active regions for `vsindex`); these are the inputs a future
484    /// `blend` charstring pass needs.
485    pub fn variation_store(&self) -> Option<&ItemVariationStore> {
486        self.cff2_view().and_then(Cff2::variation_store)
487    }
488
489    // ---- glyph lookup ------------------------------------------------------
490
491    /// Map a Unicode codepoint to its glyph id.
492    pub fn glyph_index(&self, codepoint: char) -> Option<u16> {
493        self.cmap.lookup(codepoint as u32)
494    }
495
496    /// Decode the cubic-Bezier outline for `glyph_id`.
497    ///
498    /// CFF2 outlines (with `blend` + `vsindex` resolution against the
499    /// font's `VariationStore`) are not decoded this round; callers on
500    /// a CFF2 font receive [`Error::Cff2NotImplemented`] regardless of
501    /// `glyph_id`. The CFF2 charstring bytes are still reachable via
502    /// `Font::cff2().unwrap().charstring(gid)` for inspection.
503    pub fn glyph_outline(&self, glyph_id: u16) -> Result<CubicOutline, Error> {
504        if glyph_id >= self.maxp.num_glyphs {
505            return Err(Error::GlyphOutOfRange(glyph_id));
506        }
507        match &self.cff {
508            CffFlavour::Cff1(c) => c.glyph_outline(glyph_id),
509            CffFlavour::Cff2(_) => Err(Error::Cff2NotImplemented),
510        }
511    }
512
513    /// Per-glyph advance width in font units.
514    pub fn glyph_advance(&self, glyph_id: u16) -> i16 {
515        self.hmtx.advance(glyph_id) as i16
516    }
517
518    /// Per-glyph left-side bearing in font units.
519    pub fn glyph_lsb(&self, glyph_id: u16) -> i16 {
520        self.hmtx.lsb(glyph_id)
521    }
522
523    /// Glyph name (from CFF charset / strings) — useful for diagnostics
524    /// and for round-2 PostScript-style lookups. Returns `None` if the
525    /// charset doesn't have a SID for this gid.
526    ///
527    /// CFF2 fonts have no Charset or String INDEX (the per-glyph name
528    /// list lives in the sfnt `post` table or the AGL fallback); this
529    /// accessor returns `None` for CFF2 fonts.
530    pub fn glyph_name(&self, glyph_id: u16) -> Option<&str> {
531        let cff = self.cff1()?;
532        let sid = cff.charset().sid_of(glyph_id)?;
533        cff.strings().get(sid)
534    }
535
536    /// Borrow the CFF1 table view, or `None` for CFF2 fonts. Mostly for
537    /// tests and advanced callers; the higher-level accessors on
538    /// `Font` route through this internally.
539    pub fn cff(&self) -> Option<&Cff<'a>> {
540        self.cff1()
541    }
542
543    // ---- CID-keyed font metadata ------------------------------------------
544
545    /// `true` if the embedded CFF is a CID-keyed font (carries the
546    /// `ROS` operator + an FDArray / FDSelect, Adobe TN5176 §18).
547    /// CID-keyed fonts route each glyph to one of several Font DICTs;
548    /// the public glyph-outline / metrics API is identical either way.
549    /// Always `false` for CFF2 fonts (CFF2 has no `ROS` operator —
550    /// every glyph routes through FontDICTSelect to one of the
551    /// FontDICTs by spec §7.2 regardless of CID-ness).
552    pub fn is_cid(&self) -> bool {
553        self.cff1().is_some_and(Cff::is_cid)
554    }
555
556    /// Registry string of a CID-keyed font's `ROS` operator (e.g.
557    /// `"Adobe"`), resolved through the CFF Strings table. `None` for
558    /// non-CID fonts and for CFF2 fonts.
559    pub fn cid_registry(&self) -> Option<&str> {
560        let cff = self.cff1()?;
561        let ros = cff.registry_ordering()?;
562        cff.resolve_sid(ros.registry_sid)
563    }
564
565    /// Ordering string of a CID-keyed font's `ROS` operator (e.g.
566    /// `"Japan1"`, `"GB1"`, `"Identity"`). `None` for non-CID fonts
567    /// and for CFF2 fonts.
568    pub fn cid_ordering(&self) -> Option<&str> {
569        let cff = self.cff1()?;
570        let ros = cff.registry_ordering()?;
571        cff.resolve_sid(ros.ordering_sid)
572    }
573
574    /// Supplement number of a CID-keyed font's `ROS` operator (the
575    /// character-collection revision). `None` for non-CID fonts and
576    /// for CFF2 fonts.
577    pub fn cid_supplement(&self) -> Option<i32> {
578        Some(self.cff1()?.registry_ordering()?.supplement)
579    }
580
581    /// Number of Font DICTs in a CID-keyed font's FDArray (TN5176
582    /// §18) for CFF1, or in a CFF2 font's FontDICTINDEX (spec §7.2)
583    /// for CFF2. `0` for non-CID CFF1 fonts.
584    pub fn cff_fd_count(&self) -> usize {
585        match &self.cff {
586            CffFlavour::Cff1(c) => c.fd_count(),
587            CffFlavour::Cff2(c) => c.font_dict_count() as usize,
588        }
589    }
590
591    // ---- CFF Top DICT metadata --------------------------------------------
592    //
593    // Every accessor in this section returns a CFF1 Top DICT value
594    // when the font is CFF1, and a sensible default when the font is
595    // CFF2 (CFF2 deliberately omits these operators because the
596    // equivalent information lives in sfnt-level tables — see CFF2
597    // §1.2 "Comparison of 'glyf', 'CFF ' and CFF2 tables"). The one
598    // exception is `font_matrix`, which IS defined in CFF2 §7 with
599    // the spec's restricted `[s 0 0 s 0 0]` shape.
600
601    /// CFF1 Top DICT metadata, or `None` for CFF2 fonts. CFF2 callers
602    /// should use [`Font::cff2_top_dict`] instead — the two structs
603    /// are not interchangeable because CFF2's Top DICT carries only
604    /// five operators (per spec §7) and none of them are CFF1's
605    /// FontBBox / italic / underline / weight / notice family.
606    fn top_metadata_view(&self) -> Option<&TopMetadata> {
607        self.cff1().map(Cff::top_metadata)
608    }
609
610    /// Font-wide bounding box from CFF Top DICT `FontBBox` (TN5176
611    /// §9 op 5), in font-unit coordinates `[xMin, yMin, xMax, yMax]`.
612    /// CFF1's default is `[0, 0, 0, 0]` (a sentinel telling the
613    /// consumer to compute the bbox per-glyph by walking the
614    /// charstrings — use [`Font::glyph_bbox`] for the per-glyph
615    /// alternative). CFF2 has no `FontBBox` operator (spec §7) and
616    /// this accessor returns `[0, 0, 0, 0]`.
617    pub fn font_bbox(&self) -> [f32; 4] {
618        self.top_metadata_view()
619            .map(|m| m.font_bbox)
620            .unwrap_or([0.0; 4])
621    }
622
623    /// Italic angle in degrees, counterclockwise from vertical
624    /// (CFF Top DICT `ItalicAngle`, TN5176 §9 op 12 02). `0.0` for
625    /// upright fonts and for CFF2 fonts (CFF2 has no `ItalicAngle`
626    /// operator; the equivalent lives in `post.italicAngle`).
627    pub fn italic_angle(&self) -> f64 {
628        self.top_metadata_view()
629            .map(|m| m.italic_angle)
630            .unwrap_or(0.0)
631    }
632
633    /// Underline position in font units (CFF Top DICT
634    /// `UnderlinePosition`, TN5176 §9 op 12 03). Negative values
635    /// (the typographic convention) place the underline below the
636    /// baseline. Default per spec: -100. Returns `-100.0` for CFF2
637    /// fonts (`post.underlinePosition` is the CFF2-era source).
638    pub fn underline_position(&self) -> f64 {
639        self.top_metadata_view()
640            .map(|m| m.underline_position)
641            .unwrap_or(-100.0)
642    }
643
644    /// Underline stroke thickness in font units (CFF Top DICT
645    /// `UnderlineThickness`, TN5176 §9 op 12 04). Default: 50. Returns
646    /// `50.0` for CFF2 fonts.
647    pub fn underline_thickness(&self) -> f64 {
648        self.top_metadata_view()
649            .map(|m| m.underline_thickness)
650            .unwrap_or(50.0)
651    }
652
653    /// Whether the font is monospaced (CFF Top DICT `isFixedPitch`,
654    /// TN5176 §9 op 12 01). Default: false. Returns `false` for CFF2
655    /// fonts (`post.isFixedPitch` is the CFF2-era source).
656    pub fn is_fixed_pitch(&self) -> bool {
657        self.top_metadata_view().is_some_and(|m| m.is_fixed_pitch)
658    }
659
660    /// 2x3 affine glyph → PostScript-user-space matrix from the CFF
661    /// Top DICT `FontMatrix` operator, returned in spec order
662    /// `[a, b, c, d, tx, ty]`. Apply as
663    /// `x_user = a*x + c*y + tx`, `y_user = b*x + d*y + ty`.
664    ///
665    /// - CFF1 (TN5176 §9 op 12 07): unconstrained 2×3 affine; default
666    ///   `[0.001, 0, 0, 0.001, 0, 0]` (the 1000-unit-em convention).
667    /// - CFF2 (OpenType 1.9.1 §7): restricted to `[s 0 0 s 0 0]` with
668    ///   `s == 1 / unitsPerEm`; the operator is typically omitted
669    ///   when `unitsPerEm == 1000` and the spec default
670    ///   `[0.001, 0, 0, 0.001, 0, 0]` applies. We surface either the
671    ///   on-disk matrix or the default per [`DEFAULT_FONT_MATRIX`].
672    pub fn font_matrix(&self) -> [f64; 6] {
673        match &self.cff {
674            CffFlavour::Cff1(c) => c.top_metadata().font_matrix,
675            CffFlavour::Cff2(c) => c.top_dict().font_matrix,
676        }
677    }
678
679    /// Paint type from CFF Top DICT `PaintType` (TN5176 §9 op 12 05).
680    /// `0` = filled outline (the OpenType-CFF normal case), `2` =
681    /// stroked outline whose pen width is [`Font::stroke_width`].
682    /// Default: 0. CFF2 has no `PaintType` operator (every CFF2 glyph
683    /// is filled), so this returns `0` for CFF2 fonts.
684    pub fn paint_type(&self) -> i32 {
685        self.top_metadata_view().map(|m| m.paint_type).unwrap_or(0)
686    }
687
688    /// Charstring format from CFF Top DICT `CharstringType` (TN5176
689    /// §9 op 12 06). `2` is the only value embedded in an OpenType
690    /// CFF table; other values correspond to legacy PostScript
691    /// packaging. Default: 2. CFF2 uses a different charstring
692    /// dialect (§9 of the CFF2 spec, including `blend` and
693    /// `vsindex`); we still report `2` for CFF2 to match the on-disk
694    /// "CharString Type 2" lineage.
695    pub fn charstring_type(&self) -> i32 {
696        self.top_metadata_view()
697            .map(|m| m.charstring_type)
698            .unwrap_or(2)
699    }
700
701    /// Stroke width applied when [`Font::paint_type`] is `2`, in font
702    /// units (CFF Top DICT `StrokeWidth`, TN5176 §9 op 12 08).
703    /// Ignored for filled outlines (`paint_type == 0`). Default: 0.
704    /// Returns `0.0` for CFF2 fonts (no `StrokeWidth` operator).
705    pub fn stroke_width(&self) -> f64 {
706        self.top_metadata_view()
707            .map(|m| m.stroke_width)
708            .unwrap_or(0.0)
709    }
710
711    /// Weight name from CFF Top DICT (op 4), e.g. `"Regular"`,
712    /// `"Bold"`, `"Light"`. SID-resolved through the CFF Strings
713    /// table; for SIDs in the standard-strings range these are
714    /// PostScript-style ASCII names from TN5176 Appendix A. `None`
715    /// for CFF2 fonts (no Strings table; use [`Font::name_string`]
716    /// with `NameId::FontSubfamily`).
717    pub fn weight_name(&self) -> Option<&str> {
718        let cff = self.cff1()?;
719        cff.top_metadata()
720            .weight_sid
721            .and_then(|sid| cff.resolve_sid(sid))
722    }
723
724    /// Copyright / trademark notice from CFF Top DICT (op 1). `None`
725    /// for CFF2 fonts (use `Font::name_string(NameId::Copyright)`).
726    pub fn notice(&self) -> Option<&str> {
727        let cff = self.cff1()?;
728        cff.top_metadata()
729            .notice_sid
730            .and_then(|sid| cff.resolve_sid(sid))
731    }
732
733    /// Extended copyright field from CFF Top DICT (op 12 00). `None`
734    /// for CFF2 fonts.
735    pub fn copyright(&self) -> Option<&str> {
736        let cff = self.cff1()?;
737        cff.top_metadata()
738            .copyright_sid
739            .and_then(|sid| cff.resolve_sid(sid))
740    }
741
742    /// Version string from CFF Top DICT (op 0), typically dotted-decimal.
743    /// `None` for CFF2 fonts (use
744    /// `Font::name_string(NameId::Version)`).
745    pub fn version_string(&self) -> Option<&str> {
746        let cff = self.cff1()?;
747        cff.top_metadata()
748            .version_sid
749            .and_then(|sid| cff.resolve_sid(sid))
750    }
751
752    /// Embedded PostScript language code from CFF Top DICT
753    /// `PostScript` (TN5176 §9 op 12 21). Almost always `None` on
754    /// shipping OpenType-CFF fonts; non-`None` means the font contains
755    /// an arbitrary block of PostScript that the spec says is "added to
756    /// the font dictionary." Resolved through the CFF Strings table.
757    /// `None` for CFF2 fonts.
758    pub fn postscript(&self) -> Option<&str> {
759        let cff = self.cff1()?;
760        cff.top_metadata()
761            .postscript_sid
762            .and_then(|sid| cff.resolve_sid(sid))
763    }
764
765    /// `BaseFontName` from CFF Top DICT (TN5176 §9 op 12 22). For
766    /// synthetic fonts derived from a multiple-master master, this is
767    /// the FontName of the underlying master font. Resolved through
768    /// the CFF Strings table. `None` for CFF2 fonts.
769    pub fn base_font_name(&self) -> Option<&str> {
770        let cff = self.cff1()?;
771        cff.top_metadata()
772            .base_font_name_sid
773            .and_then(|sid| cff.resolve_sid(sid))
774    }
775
776    /// Legacy PostScript `UniqueID` (CFF Top DICT op 13, TN5176 §9
777    /// Table 9). Adobe-assigned 32-bit identifier; modern fonts prefer
778    /// [`Font::xuid`]. `None` if the operator is absent from the font
779    /// and `None` for CFF2 fonts.
780    pub fn unique_id(&self) -> Option<i32> {
781        self.top_metadata_view().and_then(|m| m.unique_id)
782    }
783
784    /// Extended unique identifier from CFF Top DICT `XUID` (op 14,
785    /// TN5176 §9 Table 9). Array of 32-bit numbers (the spec leaves
786    /// the length unconstrained beyond "array"). Deprecated in
787    /// OpenType-CFF per TN5176 4 Dec 03 Appendix H but still emitted
788    /// by older Type 1 / OpenType-CFF tooling. Empty slice if the
789    /// operator is absent or the font is CFF2.
790    pub fn xuid(&self) -> &[i32] {
791        self.top_metadata_view()
792            .map_or(&[][..], |m| m.xuid.as_slice())
793    }
794
795    /// Synthetic-font base index from CFF Top DICT `SyntheticBase`
796    /// (TN5176 §9 op 12 20). When present, the value is the index
797    /// into the Name INDEX of the base font that this synthetic font
798    /// derives its glyph shapes from. `None` for non-synthetic fonts
799    /// (the overwhelming common case) and for CFF2 fonts.
800    pub fn synthetic_base(&self) -> Option<i32> {
801        self.top_metadata_view().and_then(|m| m.synthetic_base)
802    }
803
804    /// Multiple-master `BaseFontBlend` user-design vector from CFF
805    /// Top DICT (TN5176 §9 op 12 23). The values are undeltified to
806    /// absolute floats per TN5176 §4 Table 4 "delta" semantics —
807    /// successive entries are running sums of the raw operands.
808    /// Empty slice if the operator is absent and for CFF2 fonts.
809    pub fn base_font_blend(&self) -> &[f64] {
810        self.top_metadata_view()
811            .map_or(&[][..], |m| m.base_font_blend.as_slice())
812    }
813
814    // ---- CFF Private DICT hint zones --------------------------------------
815
816    /// PostScript-style alignment / stem hinting parameters for the
817    /// Private DICT this font carries (CFF TN5176 §15 Table 23). For
818    /// non-CID fonts this is the single top-level Private DICT (every
819    /// glyph shares it); for CID-keyed fonts it is the FDArray entry at
820    /// index 0. The returned struct exposes the full TN5176 §15 hint
821    /// vocabulary: BlueValues / OtherBlues / FamilyBlues /
822    /// FamilyOtherBlues (undeltified into absolute y-coordinate pairs),
823    /// StdHW / StdVW (dominant stem widths), StemSnapH / StemSnapV
824    /// (supplementary stem widths, undeltified), BlueScale / BlueShift
825    /// / BlueFuzz (overshoot suppression tunables), ForceBold,
826    /// LanguageGroup, ExpansionFactor, and initialRandomSeed. The
827    /// round-1 outline decoder still does not enforce hints (we
828    /// anti-alias at >= 16 px); this surface is for callers inspecting
829    /// font metadata or implementing their own hinting.
830    ///
831    /// Callers wanting the per-FD hints of a CID-keyed font should use
832    /// [`Font::cff`].`private_hints_fd(fd_index)` directly. The
833    /// "hints that apply to a specific glyph" routing is
834    /// [`Font::glyph_private_hints`].
835    ///
836    /// For CFF2 fonts, the Private DICT vocabulary is parsed by the
837    /// CFF2 spec §10 with the same operators but is not yet exposed
838    /// through this accessor (a future round will lift it onto a
839    /// `cff2::PrivateDict` view); for now a spec-default
840    /// [`PrivateHints`] is returned.
841    pub fn private_hints(&self) -> &PrivateHints {
842        match &self.cff {
843            CffFlavour::Cff1(c) => c.private_hints(),
844            CffFlavour::Cff2(_) => default_private_hints(),
845        }
846    }
847
848    /// The CFF Private DICT hint zones that apply to `glyph_id`. For
849    /// non-CID fonts this returns the same value as
850    /// [`Font::private_hints`]; for CID-keyed fonts (TN5176 §18) the
851    /// glyph is routed through `FDSelect` to one of the FDArray Font
852    /// DICTs, and the hint zones returned are that FD's. Returns
853    /// `None` when `glyph_id` is past `glyph_count()` (since FDSelect
854    /// has no entry for it). For CFF2 fonts the returned hints are
855    /// the spec-default values (see [`Font::private_hints`]).
856    pub fn glyph_private_hints(&self, glyph_id: u16) -> Option<&PrivateHints> {
857        if glyph_id >= self.maxp.num_glyphs {
858            return None;
859        }
860        match &self.cff {
861            CffFlavour::Cff1(c) => c.private_hints_for_glyph(glyph_id),
862            CffFlavour::Cff2(_) => Some(default_private_hints()),
863        }
864    }
865
866    // ---- per-glyph derived metrics ---------------------------------------
867
868    /// Per-glyph bounding box in font units, derived by decoding the
869    /// glyph's charstring and walking every emitted point + control
870    /// point. Returns `None` if the glyph has no outline (e.g.
871    /// `.notdef` in some fonts, or any glyph whose `endchar` is
872    /// reached without emitting a path).
873    ///
874    /// This is a convenience over [`Font::glyph_outline`] for callers
875    /// that only want the metrics — but note it still does the full
876    /// charstring decode, so callers that need both should prefer
877    /// `glyph_outline().bounds` directly to avoid duplicating work.
878    pub fn glyph_bbox(&self, glyph_id: u16) -> Result<Option<BBox>, Error> {
879        let outline = self.glyph_outline(glyph_id)?;
880        if outline.is_empty() {
881            Ok(None)
882        } else {
883            Ok(Some(outline.bounds))
884        }
885    }
886
887    // ---- table-directory enumeration -------------------------------------
888
889    /// Iterate all `(tag, length)` pairs present in the sfnt table
890    /// directory, in on-disk order (which the spec requires to be
891    /// ascending by tag). Useful for diagnostics, dumping a font's
892    /// table inventory, or deciding whether to fall back to an
893    /// alternative table.
894    pub fn table_tags(&self) -> impl Iterator<Item = ([u8; 4], u32)> + '_ {
895        self.dir.tag_list()
896    }
897
898    /// Raw byte slice for the sfnt table with `tag`, or `None` if the
899    /// table is absent. The slice is borrowed from the original font
900    /// bytes; the layout is exactly what the OpenType spec specifies
901    /// for that table.
902    pub fn table_data(&self, tag: &[u8; 4]) -> Option<&'a [u8]> {
903        self.dir.find(tag, self.bytes)
904    }
905
906    /// `true` if the font carries a table with `tag`.
907    pub fn has_table(&self, tag: &[u8; 4]) -> bool {
908        self.dir.find(tag, self.bytes).is_some()
909    }
910
911    // ---- `post` PostScript table ------------------------------------------
912
913    /// Borrow the parsed `post` table, if present. The table is one of
914    /// OpenType's nine required tables (per `otff` spec) but some
915    /// real-world stripped-down fonts omit it.
916    ///
917    /// For OpenType-CFF1 (this crate's only supported flavour) the
918    /// spec mandates `post` version 3.0; the table still carries the
919    /// 32-byte header (italic angle / underline / fixed-pitch / VM
920    /// hints) regardless of version, and version 2.0 adds the
921    /// PostScript-name array.
922    pub fn post(&self) -> Option<&PostTable<'a>> {
923        self.post.as_ref()
924    }
925
926    /// `post` table format discriminant, if present.
927    pub fn post_format(&self) -> Option<PostFormat> {
928        self.post.as_ref().map(PostTable::format)
929    }
930
931    /// Italic angle in degrees from the `post` table, if present.
932    /// Equivalent to [`Font::italic_angle`] (sourced from CFF Top
933    /// DICT) when both are populated; the spec recommends they match
934    /// but does not require it.
935    pub fn post_italic_angle(&self) -> Option<f64> {
936        self.post.as_ref().map(PostTable::italic_angle)
937    }
938
939    /// Underline position in font units from the `post` table. The
940    /// spec defines this as the y-coordinate of the *top* of the
941    /// underline (CFF Top DICT's `UnderlinePosition` operates on the
942    /// same coordinate definition).
943    pub fn post_underline_position(&self) -> Option<i16> {
944        self.post.as_ref().map(PostTable::underline_position)
945    }
946
947    /// Underline stroke thickness in font units from the `post`
948    /// table.
949    pub fn post_underline_thickness(&self) -> Option<i16> {
950        self.post.as_ref().map(PostTable::underline_thickness)
951    }
952
953    /// `post.isFixedPitch` — `true` when the font is monospaced.
954    /// `None` if `post` is absent. Note the on-disk field is a
955    /// `uint32` and any non-zero value rounds up to `true`.
956    pub fn post_is_fixed_pitch(&self) -> Option<bool> {
957        self.post.as_ref().map(PostTable::is_fixed_pitch)
958    }
959
960    /// Glyph name for `glyph_id` from the `post` table, if the table
961    /// is present in format 2.0 *and* the glyph maps to a non-
962    /// standard Pascal string. For format-2.0 glyphs that map to the
963    /// 258-entry standard Macintosh set (`glyphNameIndex < 258`),
964    /// this returns `None` because the standard-Macintosh glyph-name
965    /// list is not yet staged in `docs/text/opentype/` — see the
966    /// module-level docs in `tables::post` and the round-187 report
967    /// for the docs gap. Callers wanting names that work for every
968    /// CFF1 glyph should prefer [`Font::glyph_name`] (CFF charset
969    /// → strings, which has no docs gap).
970    pub fn post_glyph_name(&self, glyph_id: u16) -> Option<&'a [u8]> {
971        let post = self.post.as_ref()?;
972        let idx = post.name_index(glyph_id)?;
973        if idx < 258 {
974            // Standard-Mac name — table not staged. See module docs.
975            return None;
976        }
977        post.name_string(idx - 258)
978    }
979
980    // ---- `OS/2` table ------------------------------------------------------
981
982    /// Borrow the parsed `OS/2` table, if present. Required by the
983    /// OpenType spec but occasionally omitted from stripped-down
984    /// fonts; absence surfaces as `None` (and the per-field
985    /// convenience getters below return `None` in lock-step).
986    pub fn os2(&self) -> Option<&Os2Table> {
987        self.os2.as_ref()
988    }
989
990    /// `OS/2` table version (0..=5), if the table is present.
991    pub fn os2_version(&self) -> Option<u16> {
992        self.os2.as_ref().map(Os2Table::version)
993    }
994
995    /// `OS/2.usWeightClass` (1..=1000; 400 = Regular, 700 = Bold per
996    /// the spec's common values).
997    pub fn weight_class(&self) -> Option<u16> {
998        self.os2.as_ref().map(Os2Table::weight_class)
999    }
1000
1001    /// `OS/2.usWidthClass` (1..=9; 5 = Medium).
1002    pub fn width_class(&self) -> Option<u16> {
1003        self.os2.as_ref().map(Os2Table::width_class)
1004    }
1005
1006    /// `usWidthClass` interpreted as the spec's "% of normal" scale
1007    /// (50, 62.5, …, 200) — convenient for driving the variable-font
1008    /// `wdth` axis.
1009    pub fn width_class_percent(&self) -> Option<f32> {
1010        self.os2.as_ref().map(Os2Table::width_class_percent)
1011    }
1012
1013    /// `OS/2.fsType` raw embedding-licensing bitfield.
1014    pub fn fs_type(&self) -> Option<u16> {
1015        self.os2.as_ref().map(Os2Table::fs_type)
1016    }
1017
1018    /// `OS/2.fsType` bits 0..3 decoded into the named permission.
1019    pub fn embedding_permission(&self) -> Option<EmbeddingPermission> {
1020        self.os2.as_ref().map(Os2Table::embedding_permission)
1021    }
1022
1023    /// `OS/2.fsSelection.ITALIC` (bit 0). The spec requires this to
1024    /// agree with `head.macStyle` bit 1.
1025    pub fn is_italic(&self) -> Option<bool> {
1026        self.os2.as_ref().map(Os2Table::is_italic)
1027    }
1028
1029    /// `OS/2.fsSelection.BOLD` (bit 5). The spec requires this to
1030    /// agree with `head.macStyle` bit 0.
1031    pub fn is_bold(&self) -> Option<bool> {
1032        self.os2.as_ref().map(Os2Table::is_bold)
1033    }
1034
1035    /// `OS/2.fsSelection.REGULAR` (bit 6).
1036    pub fn is_regular(&self) -> Option<bool> {
1037        self.os2.as_ref().map(Os2Table::is_regular)
1038    }
1039
1040    /// `OS/2.fsSelection.USE_TYPO_METRICS` (bit 7, v4+).
1041    pub fn use_typo_metrics(&self) -> Option<bool> {
1042        self.os2.as_ref().map(Os2Table::use_typo_metrics)
1043    }
1044
1045    /// `OS/2.fsSelection.OBLIQUE` (bit 9, v4+).
1046    pub fn is_oblique(&self) -> Option<bool> {
1047        self.os2.as_ref().map(Os2Table::is_oblique)
1048    }
1049
1050    /// Four-byte registered vendor tag (`OS/2.achVendID`), interpreted
1051    /// as ASCII when possible.
1052    pub fn vendor_id(&self) -> Option<&str> {
1053        self.os2.as_ref().and_then(Os2Table::ach_vend_id_str)
1054    }
1055
1056    /// 10-byte PANOSE classification (`OS/2.panose`).
1057    pub fn panose(&self) -> Option<&[u8; 10]> {
1058        self.os2.as_ref().map(Os2Table::panose)
1059    }
1060
1061    /// `OS/2.sTypoAscender` — typographic ascender (v0-full or
1062    /// later). Combine with [`Font::typo_descender`] +
1063    /// [`Font::typo_line_gap`] for default line spacing when
1064    /// [`Font::use_typo_metrics`] is set.
1065    pub fn typo_ascender(&self) -> Option<i16> {
1066        self.os2.as_ref().and_then(Os2Table::typo_ascender)
1067    }
1068
1069    /// `OS/2.sTypoDescender` — typically negative.
1070    pub fn typo_descender(&self) -> Option<i16> {
1071        self.os2.as_ref().and_then(Os2Table::typo_descender)
1072    }
1073
1074    /// `OS/2.sTypoLineGap`.
1075    pub fn typo_line_gap(&self) -> Option<i16> {
1076        self.os2.as_ref().and_then(Os2Table::typo_line_gap)
1077    }
1078
1079    /// `OS/2.usWinAscent` — Windows GDI clipping ascender.
1080    pub fn win_ascent(&self) -> Option<u16> {
1081        self.os2.as_ref().and_then(Os2Table::win_ascent)
1082    }
1083
1084    /// `OS/2.usWinDescent` — Windows GDI clipping descender (positive).
1085    pub fn win_descent(&self) -> Option<u16> {
1086        self.os2.as_ref().and_then(Os2Table::win_descent)
1087    }
1088
1089    /// `OS/2.sxHeight` (v2+) — height of lowercase `x`.
1090    pub fn x_height(&self) -> Option<i16> {
1091        self.os2.as_ref().and_then(Os2Table::x_height)
1092    }
1093
1094    /// `OS/2.sCapHeight` (v2+) — height of uppercase letters.
1095    pub fn cap_height(&self) -> Option<i16> {
1096        self.os2.as_ref().and_then(Os2Table::cap_height)
1097    }
1098
1099    /// `OS/2.usDefaultChar` (v2+).
1100    pub fn default_char(&self) -> Option<u16> {
1101        self.os2.as_ref().and_then(Os2Table::default_char)
1102    }
1103
1104    /// `OS/2.usBreakChar` (v2+); conventionally `0x0020` (space).
1105    pub fn break_char(&self) -> Option<u16> {
1106        self.os2.as_ref().and_then(Os2Table::break_char)
1107    }
1108
1109    /// `OS/2.usMaxContext` (v2+) — maximum target-glyph context length
1110    /// for any GSUB / GPOS lookup. `1` means single-glyph only.
1111    pub fn max_context(&self) -> Option<u16> {
1112        self.os2.as_ref().and_then(Os2Table::max_context)
1113    }
1114
1115    // ---- `GDEF` table -----------------------------------------------------
1116
1117    /// Borrow the parsed `GDEF` table, if present.
1118    ///
1119    /// GDEF is optional per the OpenType spec — a font without any
1120    /// GSUB / GPOS layout lookups can legitimately omit it, and many
1121    /// stripped-down system fonts do. Absence surfaces as `None`
1122    /// rather than rejecting the whole font.
1123    pub fn gdef(&self) -> Option<&GdefTable<'a>> {
1124        self.gdef.as_ref()
1125    }
1126
1127    /// `GDEF` `(majorVersion, minorVersion)` pair (`(1, 0)`, `(1, 2)`,
1128    /// or `(1, 3)`), if the table is present.
1129    pub fn gdef_version(&self) -> Option<(u16, u16)> {
1130        self.gdef.as_ref().map(GdefTable::version)
1131    }
1132
1133    /// Spec [`GlyphClass`] for `glyph_id`, from `GDEF.GlyphClassDef`.
1134    ///
1135    /// `None` when `GDEF` is absent, the GlyphClassDef sub-table is
1136    /// absent, or the glyph is unclassified (the spec's class-0 default
1137    /// for any glyph not covered by the on-disk records).
1138    pub fn glyph_class(&self, glyph_id: u16) -> Option<GlyphClass> {
1139        self.gdef.as_ref().and_then(|g| g.glyph_class(glyph_id))
1140    }
1141
1142    /// Mark-attachment class for `glyph_id`, from
1143    /// `GDEF.MarkAttachClassDef`. Returns `0` if the table is absent,
1144    /// the sub-table is absent, or the glyph is unclassified — the
1145    /// "unfiltered" semantics `LookupFlag.markAttachmentType` uses.
1146    pub fn mark_attach_class(&self, glyph_id: u16) -> u16 {
1147        self.gdef
1148            .as_ref()
1149            .map(|g| g.mark_attach_class(glyph_id))
1150            .unwrap_or(0)
1151    }
1152
1153    // ---- `GSUB` / `GPOS` layout tables ------------------------------------
1154
1155    /// Borrow the parsed `GSUB` (Glyph Substitution Table) view, if
1156    /// present.
1157    ///
1158    /// GSUB is optional per the OpenType spec — a font that performs
1159    /// no glyph substitution legitimately omits it. The view surfaces
1160    /// the header (`majorVersion` + `minorVersion` +
1161    /// `featureVariationsOffset`) and `ScriptList` / `FeatureList` /
1162    /// `LookupList` walks. Decoding the per-lookup substitution
1163    /// subtable formats (GsubLookupType 1–8) is deferred to a future
1164    /// round.
1165    pub fn gsub(&self) -> Option<&GsubTable<'a>> {
1166        self.gsub.as_ref()
1167    }
1168
1169    /// `GSUB` `(majorVersion, minorVersion)`, if the table is present.
1170    pub fn gsub_version(&self) -> Option<(u16, u16)> {
1171        self.gsub.as_ref().map(GsubTable::version)
1172    }
1173
1174    /// Borrow the parsed `GPOS` (Glyph Positioning Table) view, if
1175    /// present.
1176    ///
1177    /// GPOS is optional per the OpenType spec — a font with no
1178    /// kerning or other positioning lookups legitimately omits it.
1179    /// The view surfaces the header and `ScriptList` / `FeatureList`
1180    /// / `LookupList` walks. Decoding the per-lookup positioning
1181    /// subtable formats (GposLookupType 1–9: SinglePos, PairPos,
1182    /// CursivePos, MarkBasePos, MarkLigPos, MarkMarkPos,
1183    /// ContextPos, ChainContextPos, Extension) is deferred to a
1184    /// future round.
1185    pub fn gpos(&self) -> Option<&GposTable<'a>> {
1186        self.gpos.as_ref()
1187    }
1188
1189    /// `GPOS` `(majorVersion, minorVersion)`, if the table is present.
1190    pub fn gpos_version(&self) -> Option<(u16, u16)> {
1191        self.gpos.as_ref().map(GposTable::version)
1192    }
1193
1194    // ---- `name` table -----------------------------------------------------
1195
1196    /// Borrow the parsed `name` table view. Use this for callers that
1197    /// want to iterate every `NameRecord` directly via
1198    /// `name().records()` or to test for version-1 language-tag
1199    /// support via `name().version()` / `name().lang_tag(id)`.
1200    pub fn name(&self) -> &NameTable<'a> {
1201        &self.name
1202    }
1203
1204    /// `name` table version (`0` for platform-specific language IDs
1205    /// only, `1` when language-tag records are present).
1206    pub fn name_version(&self) -> u16 {
1207        self.name.version()
1208    }
1209
1210    /// Resolve a name-record `languageID >= 0x8000` to its
1211    /// version-1 BCP 47 language-tag string (per `otspec-name.html`
1212    /// "naming table version 1"). Returns `None` on a version-0 table
1213    /// (which has no language-tag records), for IDs `< 0x8000` (which
1214    /// are platform-specific numeric IDs, not tags), and for IDs
1215    /// outside the `[0x8000, 0x8000 + langTagCount)` declared range
1216    /// (which the spec says "should not be used").
1217    pub fn name_lang_tag(&self, language_id: u16) -> Option<String> {
1218        self.name.lang_tag(language_id)
1219    }
1220
1221    /// Generic lookup by standard `NameId`, picking the best-ranked
1222    /// encoding (Windows / Unicode BMP English first). Sibling of
1223    /// [`Font::family_name`] / [`Font::full_name`] for callers that
1224    /// want any of the 26 spec-defined name IDs without a separate
1225    /// helper.
1226    pub fn name_string(&self, name_id: NameId) -> Option<&str> {
1227        self.name.get(name_id)
1228    }
1229
1230    /// Designer name (name ID 9).
1231    pub fn designer(&self) -> Option<&str> {
1232        self.name.get(NameId::Designer)
1233    }
1234
1235    /// Manufacturer name (name ID 8).
1236    pub fn manufacturer(&self) -> Option<&str> {
1237        self.name.get(NameId::Manufacturer)
1238    }
1239
1240    /// Typeface description (name ID 10).
1241    pub fn description(&self) -> Option<&str> {
1242        self.name.get(NameId::Description)
1243    }
1244
1245    /// Vendor URL (name ID 11).
1246    pub fn vendor_url(&self) -> Option<&str> {
1247        self.name.get(NameId::VendorUrl)
1248    }
1249
1250    /// Designer URL (name ID 12).
1251    pub fn designer_url(&self) -> Option<&str> {
1252        self.name.get(NameId::DesignerUrl)
1253    }
1254
1255    /// License description (name ID 13).
1256    pub fn license(&self) -> Option<&str> {
1257        self.name.get(NameId::License)
1258    }
1259
1260    /// License-info URL (name ID 14).
1261    pub fn license_url(&self) -> Option<&str> {
1262        self.name.get(NameId::LicenseUrl)
1263    }
1264
1265    /// Trademark string (name ID 7).
1266    pub fn trademark(&self) -> Option<&str> {
1267        self.name.get(NameId::Trademark)
1268    }
1269
1270    /// Sample text (name ID 19).
1271    pub fn sample_text(&self) -> Option<&str> {
1272        self.name.get(NameId::SampleText)
1273    }
1274
1275    /// Typographic Family name (name ID 16; "Preferred Family" in
1276    /// earlier spec text). The unconstrained extended-family grouping
1277    /// used by applications that look past the 4-style style-linking
1278    /// `font_family` cap.
1279    pub fn typographic_family(&self) -> Option<&str> {
1280        self.name.get(NameId::TypographicFamily)
1281    }
1282
1283    /// Typographic Subfamily name (name ID 17; "Preferred Subfamily"
1284    /// in earlier spec text).
1285    pub fn typographic_subfamily(&self) -> Option<&str> {
1286        self.name.get(NameId::TypographicSubfamily)
1287    }
1288
1289    /// WWS Family name (name ID 21). Provides a WWS-conformant family
1290    /// name when name IDs 16 / 17 carry extra non-WWS attributes; see
1291    /// `OS/2.fsSelection` bit 8.
1292    pub fn wws_family(&self) -> Option<&str> {
1293        self.name.get(NameId::WwsFamily)
1294    }
1295
1296    /// WWS Subfamily name (name ID 22).
1297    pub fn wws_subfamily(&self) -> Option<&str> {
1298        self.name.get(NameId::WwsSubfamily)
1299    }
1300
1301    /// Variations PostScript Name Prefix (name ID 25; variable fonts).
1302    pub fn variations_ps_name_prefix(&self) -> Option<&str> {
1303        self.name.get(NameId::VariationsPsNamePrefix)
1304    }
1305
1306    /// Unique font identifier from the `name` table (name ID 3).
1307    /// Distinct from [`Font::unique_id`] (which is the CFF Top DICT's
1308    /// legacy PostScript `UniqueID` integer).
1309    pub fn unique_font_id(&self) -> Option<&str> {
1310        self.name.get(NameId::UniqueId)
1311    }
1312
1313    // ---- Adobe Glyph List (AGL) integration ------------------------------
1314
1315    /// Resolve a PostScript glyph name to a glyph id by routing through
1316    /// the **Adobe Glyph List (AGL 2.0)** name → Unicode codepoint
1317    /// table (`crate::agl`) and then through the font's own `cmap`.
1318    ///
1319    /// This is the right tool when callers have a PostScript glyph
1320    /// name in hand (e.g. parsed from a PDF content stream, or from a
1321    /// `post`-format-2.0 Pascal-string entry) and need to map back to
1322    /// a glyph id without first decoding the name into a Unicode
1323    /// scalar.
1324    ///
1325    /// Two-step semantics:
1326    ///
1327    /// 1. Look up `name` in AGL via [`crate::agl::name_to_codepoint`].
1328    ///    `None` if the name isn't in AGL.
1329    /// 2. Map that codepoint to a glyph id via the font's `cmap`. `None`
1330    ///    if the font doesn't encode that codepoint.
1331    ///
1332    /// The AGL Specification's §6 component-name decomposition
1333    /// (`f_f_i` → `ffi`, `uniXXXX` → `U+XXXX`) is **not** applied —
1334    /// the AGL spec document itself is not staged under
1335    /// `docs/text/opentype/`. Callers that need the §6 algorithm can
1336    /// implement it in their own code on top of this exact-match
1337    /// lookup.
1338    pub fn glyph_id_from_agl_name(&self, name: &str) -> Option<u16> {
1339        let cp = agl::name_to_codepoint(name)?;
1340        self.glyph_index(cp)
1341    }
1342
1343    /// Canonical Adobe Glyph List name for `glyph_id`, if any.
1344    ///
1345    /// Resolution order, mirroring "use the font's own knowledge
1346    /// first, then fall back to the standard":
1347    ///
1348    /// 1. The CFF charset → Strings name (the same lookup as
1349    ///    [`Font::glyph_name`]). For CFF1 fonts this surfaces the
1350    ///    font's authored PostScript name regardless of whether it
1351    ///    happens to be an AGL entry. Always `None` for CFF2 fonts
1352    ///    (CFF2 has no Charset / Strings).
1353    /// 2. The `post` table version-2.0 Pascal-string tail (the same
1354    ///    lookup as [`Font::post_glyph_name`]); decoded as UTF-8 and
1355    ///    returned only when the on-disk bytes are valid UTF-8.
1356    /// 3. The AGL reverse-lookup table — if the glyph is reachable
1357    ///    from a `cmap` entry, the AGL name of that codepoint.
1358    ///
1359    /// `None` only when none of the three sources have a name for
1360    /// this glyph.
1361    pub fn agl_glyph_name(&self, glyph_id: u16) -> Option<&str> {
1362        if glyph_id >= self.maxp.num_glyphs {
1363            return None;
1364        }
1365        // 1. CFF charset → Strings.
1366        if let Some(name) = self.glyph_name(glyph_id) {
1367            return Some(name);
1368        }
1369        // 2. post-format-2.0 Pascal-string tail (UTF-8-clean only).
1370        if let Some(bytes) = self.post_glyph_name(glyph_id) {
1371            if let Ok(s) = std::str::from_utf8(bytes) {
1372                return Some(s);
1373            }
1374        }
1375        // 3. AGL reverse lookup keyed on the glyph's `cmap`
1376        //    codepoint. The CmapTable doesn't expose a reverse
1377        //    iterator, so we walk the BMP only — the AGL itself is
1378        //    BMP-only (no astral entries), so any astral glyph would
1379        //    never match anyway.
1380        for cp in 0u32..0x1_0000 {
1381            if let Some(c) = char::from_u32(cp) {
1382                if self.cmap.lookup(cp) == Some(glyph_id) {
1383                    if let Some(name) = agl::codepoint_to_name(c) {
1384                        return Some(name);
1385                    }
1386                    // Found the codepoint but it's not in AGL; keep
1387                    // scanning in case another encoded codepoint maps
1388                    // to the same glyph and *is* in AGL.
1389                }
1390            }
1391        }
1392        None
1393    }
1394}