Skip to main content

azul_layout/
font.rs

1//! Font parsing, metrics extraction, and subsetting.
2//!
3//! This module provides the core font infrastructure for text layout and PDF generation:
4//! - `loading`: System font cache construction and font reload errors
5//! - `mock`: Mock font implementation for testing without real font files
6//! - `parsed`: Full font parsing via allsorts (outlines, metrics, shaping tables, subsetting)
7
8#![cfg(feature = "font_loading")]
9
10use azul_css::{AzString, U8Vec};
11use rust_fontconfig::{FcFontCache, OwnedFontSource};
12
13pub mod loading {
14    #![cfg(feature = "std")]
15    #![cfg(feature = "font_loading")]
16    #![cfg_attr(not(feature = "std"), no_std)]
17
18    use std::io::Error as IoError;
19
20    use azul_css::{AzString, StringVec, U8Vec};
21    use rust_fontconfig::FcFontCache;
22
23    #[cfg(not(miri))]
24    #[must_use] pub fn build_font_cache() -> FcFontCache {
25        FcFontCache::build()
26    }
27
28    #[cfg(miri)]
29    pub fn build_font_cache() -> FcFontCache {
30        FcFontCache::default()
31    }
32
33    #[derive(Debug)]
34    pub enum FontReloadError {
35        Io(IoError, AzString),
36        FontNotFound(AzString),
37        FontLoadingNotActive(AzString),
38    }
39
40    impl Clone for FontReloadError {
41        fn clone(&self) -> Self {
42            use self::FontReloadError::{Io, FontNotFound, FontLoadingNotActive};
43            match self {
44                Io(err, path) => Io(IoError::new(err.kind(), "Io Error"), path.clone()),
45                FontNotFound(id) => FontNotFound(id.clone()),
46                FontLoadingNotActive(id) => FontLoadingNotActive(id.clone()),
47            }
48        }
49    }
50
51    azul_core::impl_display!(FontReloadError, {
52        Io(err, path_buf) => format!("Could not load \"{}\" - IO error: {}", path_buf.as_str(), err),
53        FontNotFound(id) => format!("Could not locate system font: \"{:?}\" found", id),
54        FontLoadingNotActive(id) => format!("Could not load system font: \"{:?}\": crate was not compiled with --features=\"font_loading\"", id)
55    });
56}
57pub mod mock {
58    //! Mock font implementation for testing text layout.
59    //!
60    //! Provides a `MockFont` that simulates font behavior without requiring
61    //! actual font files, useful for unit testing text layout functionality.
62
63    use std::collections::BTreeMap;
64
65    use crate::text3::cache::LayoutFontMetrics;
66
67    /// A mock font implementation for testing text layout without real fonts.
68    ///
69    /// This allows testing text shaping, layout, and rendering code paths
70    /// without needing to load actual TrueType/OpenType font files.
71    #[derive(Debug, Clone)]
72    pub struct MockFont {
73        /// Font metrics (ascent, descent, etc.).
74        pub font_metrics: LayoutFontMetrics,
75        /// Width of the space character in font units.
76        pub space_width: Option<usize>,
77        /// Horizontal advance widths keyed by glyph ID.
78        pub glyph_advances: BTreeMap<u16, u16>,
79        /// Glyph bounding box sizes (width, height) keyed by glyph ID.
80        pub glyph_sizes: BTreeMap<u16, (i32, i32)>,
81        /// Unicode codepoint to glyph ID mapping.
82        pub glyph_indices: BTreeMap<u32, u16>,
83    }
84
85    impl MockFont {
86        /// Creates a new `MockFont` with the given font metrics.
87        #[must_use] pub const fn new(font_metrics: LayoutFontMetrics) -> Self {
88            Self {
89                font_metrics,
90                space_width: Some(10),
91                glyph_advances: BTreeMap::new(),
92                glyph_sizes: BTreeMap::new(),
93                glyph_indices: BTreeMap::new(),
94            }
95        }
96
97        /// Sets the space character width.
98        #[must_use] pub const fn with_space_width(mut self, width: usize) -> Self {
99            self.space_width = Some(width);
100            self
101        }
102
103        /// Adds a horizontal advance value for a glyph.
104        #[must_use] pub fn with_glyph_advance(mut self, glyph_index: u16, advance: u16) -> Self {
105            self.glyph_advances.insert(glyph_index, advance);
106            self
107        }
108
109        /// Adds a bounding box size for a glyph.
110        #[must_use] pub fn with_glyph_size(mut self, glyph_index: u16, size: (i32, i32)) -> Self {
111            self.glyph_sizes.insert(glyph_index, size);
112            self
113        }
114
115        /// Adds a Unicode codepoint to glyph ID mapping.
116        #[must_use] pub fn with_glyph_index(mut self, unicode: u32, index: u16) -> Self {
117            self.glyph_indices.insert(unicode, index);
118            self
119        }
120    }
121}
122
123pub mod parsed {
124    use core::fmt;
125    use std::{collections::BTreeMap, sync::Arc};
126
127    use allsorts::{
128        binary::read::ReadScope,
129        font_data::FontData,
130        layout::{GDEFTable, LayoutCache, LayoutCacheData, GPOS, GSUB},
131        outline::{OutlineBuilder, OutlineSink},
132        pathfinder_geometry::{line_segment::LineSegment2F, vector::Vector2F},
133        subset::{subset as allsorts_subset, whole_font, CmapTarget, SubsetProfile},
134        tables::{
135            cmap::owned::CmapSubtable as OwnedCmapSubtable,
136            glyf::{
137                Glyph, GlyfVisitorContext, LocaGlyf, Point,
138                VariableGlyfContext, VariableGlyfContextStore,
139            },
140            kern::owned::KernTable,
141            FontTableProvider, HheaTable, MaxpTable,
142        },
143        tag,
144    };
145    use azul_core::resources::{
146        GlyphOutline, GlyphOutlineOperation, OutlineCubicTo, OutlineLineTo, OutlineMoveTo,
147        OutlineQuadTo, OwnedGlyphBoundingBox,
148    };
149    use azul_css::props::basic::FontMetrics as CssFontMetrics;
150
151    // Mock font module for testing
152    pub use crate::font::mock::MockFont;
153    use crate::text3::cache::LayoutFontMetrics;
154
155    /// Cached GSUB table for glyph substitution operations.
156    pub type GsubCache = Arc<LayoutCacheData<GSUB>>;
157    /// Cached GPOS table for glyph positioning operations.
158    pub type GposCache = Arc<LayoutCacheData<GPOS>>;
159
160    /// Monotonic-clock nanos since process start. Used to timestamp
161    /// `ParsedFont.last_used` for LRU eviction. Cheap (single
162    /// `Instant::now`); resolution is plenty fine for "did this
163    /// face get touched in the last N seconds" decisions. Exposed
164    /// `pub(crate)` so `FontManager::evict_unused` reads from the
165    /// same clock as `last_used` writes.
166    #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
167    pub(crate) fn monotonic_now_nanos() -> u64 {
168        // Safe: `Instant::elapsed` against the same launch instant is
169        // monotonic and never overflows in any realistic process
170        // lifetime (>500 years).
171        use std::sync::OnceLock;
172        use std::time::Instant;
173        static LAUNCH: OnceLock<Instant> = OnceLock::new();
174        let start = LAUNCH.get_or_init(Instant::now);
175        start.elapsed().as_nanos() as u64
176    }
177
178    /// Glyph-outline decoder state. See the
179    /// [`ParsedFont::loca_glyf`] field docs for the full description.
180    #[derive(Clone)]
181    pub(crate) enum LocaGlyfState {
182        /// Ready to decode immediately, or known to have no outline
183        /// data. `None` covers both CFF fonts and fonts where the
184        /// loca+glyf parse failed.
185        ///
186        /// This variant *cannot* be evicted by
187        /// [`crate::text3::cache::FontManager::evict_unused`]: there
188        /// are no source bytes retained to re-decode from. The eager
189        /// `from_bytes` path (tests, `with_source_bytes` PDF callers)
190        /// produces this variant.
191        Loaded(Option<Arc<std::sync::Mutex<LocaGlyf>>>),
192        /// Font bytes retained for lazy `LocaGlyf` construction.
193        ///
194        /// `loaded` is `Mutex<Option<…>>` (not `OnceLock`) so an
195        /// idle eviction can clear it back to `None`; the next
196        /// `get_or_decode_glyph` will re-parse from `bytes`. Two-step
197        /// double-check pattern in `resolve_loca_glyf` keeps the
198        /// expensive `LocaGlyf::load` outside the critical section.
199        Deferred {
200            bytes: Arc<rust_fontconfig::FontBytes>,
201            font_index: usize,
202            loaded: Arc<std::sync::Mutex<Option<Arc<std::sync::Mutex<LocaGlyf>>>>>,
203        },
204    }
205
206    /// Adapter that collects allsorts outline commands into our `GlyphOutline` format.
207    ///
208    /// Implements `OutlineSink` so it can be passed to `GlyfVisitorContext::visit()`.
209    /// This handles composite glyph resolution, transforms, and variable font
210    /// deltas automatically via allsorts internals.
211    struct GlyphOutlineCollector {
212        contours: Vec<GlyphOutline>,
213        current_contour: Vec<GlyphOutlineOperation>,
214    }
215
216    impl GlyphOutlineCollector {
217        const fn new() -> Self {
218            Self {
219                contours: Vec::new(),
220                current_contour: Vec::new(),
221            }
222        }
223
224        fn into_outlines(mut self) -> Vec<GlyphOutline> {
225            if !self.current_contour.is_empty() {
226                self.contours.push(GlyphOutline {
227                    operations: std::mem::take(&mut self.current_contour).into(),
228                });
229            }
230            self.contours
231        }
232    }
233
234    impl OutlineSink for GlyphOutlineCollector {
235        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
236        fn move_to(&mut self, to: Vector2F) {
237            if !self.current_contour.is_empty() {
238                self.contours.push(GlyphOutline {
239                    operations: std::mem::take(&mut self.current_contour).into(),
240                });
241            }
242            self.current_contour.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo {
243                x: to.x() as i16,
244                y: to.y() as i16,
245            }));
246        }
247
248        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
249        fn line_to(&mut self, to: Vector2F) {
250            self.current_contour.push(GlyphOutlineOperation::LineTo(OutlineLineTo {
251                x: to.x() as i16,
252                y: to.y() as i16,
253            }));
254        }
255
256        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
257        fn quadratic_curve_to(&mut self, ctrl: Vector2F, to: Vector2F) {
258            self.current_contour.push(GlyphOutlineOperation::QuadraticCurveTo(
259                OutlineQuadTo {
260                    ctrl_1_x: ctrl.x() as i16,
261                    ctrl_1_y: ctrl.y() as i16,
262                    end_x: to.x() as i16,
263                    end_y: to.y() as i16,
264                },
265            ));
266        }
267
268        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
269        fn cubic_curve_to(&mut self, ctrl: LineSegment2F, to: Vector2F) {
270            self.current_contour.push(GlyphOutlineOperation::CubicCurveTo(
271                OutlineCubicTo {
272                    ctrl_1_x: ctrl.from_x() as i16,
273                    ctrl_1_y: ctrl.from_y() as i16,
274                    ctrl_2_x: ctrl.to_x() as i16,
275                    ctrl_2_y: ctrl.to_y() as i16,
276                    end_x: to.x() as i16,
277                    end_y: to.y() as i16,
278                },
279            ));
280        }
281
282        fn close(&mut self) {
283            self.current_contour.push(GlyphOutlineOperation::ClosePath);
284            self.contours.push(GlyphOutline {
285                operations: std::mem::take(&mut self.current_contour).into(),
286            });
287        }
288    }
289
290    /// Parsed font data with all required tables for text layout and PDF generation.
291    ///
292    /// This struct holds the parsed representation of a TrueType/OpenType font,
293    /// including glyph outlines, metrics, and shaping tables. It's used for:
294    /// - Text layout (via GSUB/GPOS tables)
295    /// - Glyph rendering (via glyf/CFF outlines)
296    /// - PDF font embedding (via font metrics and subsetting)
297    pub struct ParsedFont {
298        /// Hash of the font bytes for caching and equality checks.
299        pub hash: u64,
300        /// Layout-specific font metrics (ascent, descent, line gap).
301        pub font_metrics: LayoutFontMetrics,
302        /// PDF-specific detailed font metrics from HEAD, HHEA, OS/2 tables.
303        pub pdf_font_metrics: PdfFontMetrics,
304        /// Total number of glyphs in the font (from maxp table).
305        pub num_glyphs: u16,
306        /// Horizontal header table (hhea) containing global horizontal metrics.
307        pub hhea_table: HheaTable,
308        /// Offset+length into `original_bytes` for hmtx table (lazy: no copy).
309        pub hmtx_range: (usize, usize),
310        /// Offset+length into `original_bytes` for vmtx table (lazy: no copy).
311        pub vmtx_range: (usize, usize),
312        /// Vertical header table (vhea), same format as hhea. None if font has no vertical metrics.
313        pub vhea_table: Option<HheaTable>,
314        /// Maximum profile table (maxp) containing glyph count and memory hints.
315        pub maxp_table: MaxpTable,
316        /// Raw GSUB table bytes, kept as a `Vec<u8>` (tens to low-hundreds
317        /// of KiB) so the parsed `GsubCache` can be built on first shape
318        /// call instead of up-front. Access via [`ParsedFont::gsub`] —
319        /// that getter populates `gsub_cache_lazy` via `OnceLock` and
320        /// returns a borrow.
321        pub(crate) gsub_bytes: Option<Vec<u8>>,
322        /// Lazy GSUB cache: populated on first [`ParsedFont::gsub`] call.
323        /// `None` means "font has no GSUB table" *after* init attempt;
324        /// the `OnceLock` wrapper distinguishes "not yet initialised"
325        /// from "initialised to None".
326        pub(crate) gsub_cache_lazy: std::sync::OnceLock<Option<GsubCache>>,
327        /// Raw GPOS table bytes. Same lazy-parse arrangement as
328        /// `gsub_bytes` — see [`ParsedFont::gpos`].
329        pub(crate) gpos_bytes: Option<Vec<u8>>,
330        /// Lazy GPOS cache, populated on first [`ParsedFont::gpos`] call.
331        pub(crate) gpos_cache_lazy: std::sync::OnceLock<Option<GposCache>>,
332        /// Glyph definition table (GDEF) for glyph classification.
333        pub opt_gdef_table: Option<Arc<GDEFTable>>,
334        /// Legacy kerning table (kern) for fonts without GPOS.
335        pub opt_kern_table: Option<Arc<KernTable>>,
336        /// Monotonic-clock nanos at the most recent
337        /// [`ParsedFont::get_or_decode_glyph`] / `gsub()` / `gpos()`
338        /// call. `0` means "never touched". Used by
339        /// [`crate::text3::cache::FontManager::evict_unused`] to
340        /// decide which `LocaGlyfState::Deferred` faces to release.
341        pub(crate) last_used: Arc<std::sync::atomic::AtomicU64>,
342        /// `true` if this font is a variable font (carries a `gvar`
343        /// table). Cached at parse time so [`decode_glyph_inner`]
344        /// can short-circuit the variable-context construction for
345        /// the common non-variable case. Variable-glyph delta
346        /// application requires the source bytes to be retained,
347        /// so it only fires on the `LocaGlyfState::Deferred` path.
348        pub(crate) is_variable_font: bool,
349        /// Lazy outline cache. Populated on first
350        /// [`ParsedFont::get_or_decode_glyph`] call per `gid`; entries
351        /// are wrapped in `Arc` so callers can hold them without
352        /// keeping the lock. The space glyph (and `.notdef` when
353        /// present) are pre-inserted by `from_bytes_internal` so the
354        /// shaper's cmap-miss path has something to render without
355        /// racing with a decode.
356        ///
357        /// Tests that previously walked the public `glyph_records_decoded`
358        /// `BTreeMap` field now call
359        /// [`ParsedFont::prime_glyph_cache`] (decodes every glyph into
360        /// this cache) followed by
361        /// [`ParsedFont::for_each_decoded_glyph`] /
362        /// [`ParsedFont::glyph_cache_snapshot`] to walk the result.
363        // [az-web-lift] queue RwLock spins in lock_contended in single-threaded lifted wasm
364        // (only the pure-Rust queue RwLock is lifted; Mutex is Leaf-stubbed). Reuse
365        // rust_fontconfig::StLock (no-atomic single-threaded bypass). One of the 3 RwLocks total.
366        pub(crate) glyph_cache: Arc<rust_fontconfig::StLock<BTreeMap<u16, Arc<OwnedGlyph>>>>,
367        /// Glyph outline decoder state.
368        ///
369        /// - `Loaded(Some(arc))`: `LocaGlyf` is already loaded (owning
370        ///   its own `Box<[u8]>` copy of the loca+glyf tables) and
371        ///   ready to decode glyphs. Produced by the eager `from_bytes`
372        ///   constructor path (tests).
373        /// - `Loaded(None)`: the font has no usable loca+glyf (CFF, or
374        ///   a parse failure). Glyph outlines won't decode; the hmtx
375        ///   advance fallback fills in the blanks.
376        /// - `Deferred`: we retain an `Arc<[u8]>` to the full font file
377        ///   and the `font_index`; the first `get_or_decode_glyph` call
378        ///   parses a fresh `FontData` / `TableProvider` from those
379        ///   bytes and loads `LocaGlyf`, storing the result in the
380        ///   `OnceLock`. Fonts that get resolved into a chain but are
381        ///   never actually rasterized pay zero decode cost — this is
382        ///   the big win for pages like `excel.html` where 20+ fallback
383        ///   faces load but only a handful are touched.
384        pub(crate) loca_glyf: LocaGlyfState,
385        /// Cached width of the space character in font units.
386        pub space_width: Option<usize>,
387        /// Character-to-glyph mapping (cmap subtable).
388        pub cmap_subtable: Option<OwnedCmapSubtable>,
389        /// Mock font data for testing (replaces real font behavior).
390        pub mock: Option<Box<MockFont>>,
391        /// Reverse mapping: `glyph_id` -> cluster text (handles ligatures like "fi").
392        pub reverse_glyph_cache: BTreeMap<u16, String>,
393        /// Original font bytes — only retained for callers that need to
394        /// reconstruct or subset the font (PDF export). Layout / shaping /
395        /// raster never read this, so `ParsedFont::from_bytes` leaves it
396        /// as `None` by default and callers opt in via
397        /// [`ParsedFont::with_source_bytes`]. Shared across faces of the
398        /// same `.ttc` via the `Arc<FontBytes>` that
399        /// [`rust_fontconfig::FcFontCache::get_font_bytes`] returns —
400        /// for disk fonts the backing is an mmap so untouched pages
401        /// don't count toward RSS.
402        pub original_bytes: Option<Arc<rust_fontconfig::FontBytes>>,
403        /// Font index within collection (0 for single-font files).
404        pub original_index: usize,
405        /// GID to CID mapping for CFF fonts (required for PDF embedding).
406        pub index_to_cid: BTreeMap<u16, u16>,
407        /// Font type (TrueType outlines or OpenType CFF).
408        pub font_type: FontType,
409        /// PostScript font name from the NAME table.
410        pub font_name: Option<String>,
411        /// TrueType bytecode hinting instance (mutable interpreter state).
412        /// Wrapped in Mutex because hinting mutates internal state.
413        /// None for CFF fonts or fonts without hinting data.
414        pub hint_instance: Option<std::sync::Mutex<allsorts::hinting::HintInstance>>,
415    }
416
417    impl Clone for ParsedFont {
418        fn clone(&self) -> Self {
419            Self {
420                hash: self.hash,
421                font_metrics: self.font_metrics,
422                pdf_font_metrics: self.pdf_font_metrics,
423                num_glyphs: self.num_glyphs,
424                hhea_table: self.hhea_table.clone(),
425                hmtx_range: self.hmtx_range,
426                vmtx_range: self.vmtx_range,
427                vhea_table: self.vhea_table.clone(),
428                maxp_table: self.maxp_table.clone(),
429                // OnceLock<T: Clone>: Clone preserves the init state, so
430                // a clone of a parsed cache skips re-parse on first
431                // access. The raw bytes we keep around for lazy init
432                // are cloned too.
433                gsub_bytes: self.gsub_bytes.clone(),
434                gsub_cache_lazy: self.gsub_cache_lazy.clone(),
435                gpos_bytes: self.gpos_bytes.clone(),
436                gpos_cache_lazy: self.gpos_cache_lazy.clone(),
437                opt_gdef_table: self.opt_gdef_table.clone(),
438                opt_kern_table: self.opt_kern_table.clone(),
439                // Share the lazy cache and loca_glyf across clones: cheap
440                // Arc bump, amortises glyph decode across clones of the
441                // same face.
442                last_used: Arc::clone(&self.last_used),
443                is_variable_font: self.is_variable_font,
444                glyph_cache: Arc::clone(&self.glyph_cache),
445                // `LocaGlyfState` is `Clone` — for `Loaded` this is an
446                // `Arc::clone`; for `Deferred` it's an `Arc::clone` of
447                // the bytes + the `OnceLock`, so a clone of a face
448                // that's already decoded glyphs carries the decode.
449                loca_glyf: self.loca_glyf.clone(),
450                space_width: self.space_width,
451                cmap_subtable: self.cmap_subtable.clone(),
452                mock: self.mock.clone(),
453                reverse_glyph_cache: self.reverse_glyph_cache.clone(),
454                // Arc clone — O(1), just bumps refcount; no byte copy.
455                original_bytes: self.original_bytes.clone(),
456                original_index: self.original_index,
457                index_to_cid: self.index_to_cid.clone(),
458                font_type: self.font_type.clone(),
459                font_name: self.font_name.clone(),
460                // HintInstance has mutable interpreter state and is not Clone.
461                // Clones are used for PDF/serialization where hinting isn't needed.
462                hint_instance: None,
463            }
464        }
465    }
466
467    /// Distinguishes TrueType fonts from OpenType CFF fonts.
468    ///
469    /// This affects how glyph outlines are extracted and how the font
470    /// is embedded in PDF documents.
471    #[derive(Debug, Clone, PartialEq, Eq)]
472    pub enum FontType {
473        /// TrueType font with quadratic Bézier outlines in glyf table.
474        TrueType,
475        /// OpenType font with cubic Bézier outlines in CFF table.
476        /// Contains the serialized CFF data for PDF embedding.
477        OpenTypeCFF(Vec<u8>),
478    }
479
480    /// PDF-specific font metrics from HEAD, HHEA, and OS/2 tables.
481    ///
482    /// These metrics are used for PDF font descriptors and accurate
483    /// text positioning in generated PDF documents.
484    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
485    #[repr(C)]
486    pub struct PdfFontMetrics {
487        // -- HEAD table fields --
488        /// Font units per em-square (typically 1000 or 2048).
489        pub units_per_em: u16,
490        /// Font flags (italic, bold, fixed-pitch, etc.).
491        pub font_flags: u16,
492        /// Minimum x-coordinate across all glyphs.
493        pub x_min: i16,
494        /// Minimum y-coordinate across all glyphs.
495        pub y_min: i16,
496        /// Maximum x-coordinate across all glyphs.
497        pub x_max: i16,
498        /// Maximum y-coordinate across all glyphs.
499        pub y_max: i16,
500
501        // -- HHEA table fields --
502        /// Typographic ascender (distance above baseline).
503        pub ascender: i16,
504        /// Typographic descender (distance below baseline, usually negative).
505        pub descender: i16,
506        /// Recommended line gap between lines of text.
507        pub line_gap: i16,
508        /// Maximum horizontal advance width across all glyphs.
509        pub advance_width_max: u16,
510        /// Caret slope rise for italic angle calculation.
511        pub caret_slope_rise: i16,
512        /// Caret slope run for italic angle calculation.
513        pub caret_slope_run: i16,
514
515        // -- OS/2 table fields (0 if table not present) --
516        /// Average width of lowercase letters.
517        pub x_avg_char_width: i16,
518        /// Visual weight class (100-900, 400=normal, 700=bold).
519        pub us_weight_class: u16,
520        /// Visual width class (1-9, 5=normal).
521        pub us_width_class: u16,
522        /// Thickness of strikeout stroke in font units.
523        pub y_strikeout_size: i16,
524        /// Vertical position of strikeout stroke.
525        pub y_strikeout_position: i16,
526    }
527
528    impl Default for PdfFontMetrics {
529        fn default() -> Self {
530            Self::zero()
531        }
532    }
533
534    impl PdfFontMetrics {
535        /// Returns zeroed metrics with `units_per_em` set to 1000 (standard PostScript default)
536        /// to avoid division-by-zero in scaling calculations.
537        #[must_use] pub const fn zero() -> Self {
538            Self {
539                units_per_em: 1000,
540                font_flags: 0,
541                x_min: 0,
542                y_min: 0,
543                x_max: 0,
544                y_max: 0,
545                ascender: 0,
546                descender: 0,
547                line_gap: 0,
548                advance_width_max: 0,
549                caret_slope_rise: 0,
550                caret_slope_run: 0,
551                x_avg_char_width: 0,
552                us_weight_class: 0,
553                us_width_class: 0,
554                y_strikeout_size: 0,
555                y_strikeout_position: 0,
556            }
557        }
558    }
559
560    /// Result of font subsetting operation.
561    ///
562    /// Contains the subsetted font bytes and a mapping from original
563    /// glyph IDs to new glyph IDs in the subset.
564    #[derive(Debug, Clone)]
565    pub struct SubsetFont {
566        /// The subsetted font file bytes (smaller than original).
567        pub bytes: Vec<u8>,
568        /// Mapping: original glyph ID -> (new subset glyph ID, source character).
569        pub glyph_mapping: BTreeMap<u16, (u16, char)>,
570    }
571
572    impl SubsetFont {
573        /// Return the changed text so that when rendering with the subset font (instead of the
574        /// original) the renderer will end up at the same glyph IDs as if we used the original text
575        /// on the original font
576        #[must_use] pub fn subset_text(&self, text: &str) -> String {
577            text.chars()
578                .filter_map(|c| {
579                    self.glyph_mapping.values().find_map(|(ngid, ch)| {
580                        if *ch == c {
581                            char::from_u32(u32::from(*ngid))
582                        } else {
583                            None
584                        }
585                    })
586                })
587                .collect()
588        }
589    }
590
591    /// Hash-based equality: two fonts are considered equal if their content hash matches.
592    /// This is a performance optimization — hash collisions are possible but vanishingly
593    /// unlikely (~1/2^64).
594    impl PartialEq for ParsedFont {
595        fn eq(&self, other: &Self) -> bool {
596            self.hash == other.hash
597        }
598    }
599
600    impl Eq for ParsedFont {}
601
602    const FONT_B64_START: &str = "data:font/ttf;base64,";
603
604    impl serde::Serialize for ParsedFont {
605        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
606            use base64::Engine;
607            let s = format!(
608                "{FONT_B64_START}{}",
609                base64::prelude::BASE64_STANDARD.encode(self.to_bytes(None).unwrap_or_default())
610            );
611            s.serialize(serializer)
612        }
613    }
614
615    impl<'de> serde::Deserialize<'de> for ParsedFont {
616        fn deserialize<D: serde::Deserializer<'de>>(
617            deserializer: D,
618        ) -> Result<Self, D::Error> {
619            use base64::Engine;
620            let s = String::deserialize(deserializer)?;
621            let b64 = s.strip_prefix(FONT_B64_START).and_then(|b| base64::prelude::BASE64_STANDARD.decode(b).ok());
622
623            let mut warnings = Vec::new();
624            Self::from_bytes(&b64.unwrap_or_default(), 0, &mut warnings).ok_or_else(|| {
625                serde::de::Error::custom(format!("Font deserialization error: {warnings:?}"))
626            })
627        }
628    }
629
630    impl fmt::Debug for ParsedFont {
631        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632            f.debug_struct("ParsedFont")
633                .field("hash", &self.hash)
634                .field("font_metrics", &self.font_metrics)
635                .field("num_glyphs", &self.num_glyphs)
636                .field("hhea_table", &self.hhea_table)
637                .field(
638                    "hmtx_range",
639                    &format_args!("<{} bytes>", self.hmtx_range.1),
640                )
641                .field("maxp_table", &self.maxp_table)
642                .field(
643                    "glyph_cache",
644                    &format_args!(
645                        "{} entries (lazy)",
646                        self.glyph_cache.read().map(|m| m.len()).unwrap_or(0),
647                    ),
648                )
649                .field("space_width", &self.space_width)
650                .field("cmap_subtable", &self.cmap_subtable)
651                .finish_non_exhaustive()
652        }
653    }
654
655    /// Warning or error message generated during font parsing.
656    #[derive(Debug, Clone, PartialEq, Eq)]
657    pub struct FontParseWarning {
658        /// Severity level of this warning.
659        pub severity: FontParseWarningSeverity,
660        /// Human-readable description of the issue.
661        pub message: String,
662    }
663
664    /// Severity level for font parsing warnings.
665    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
666    pub enum FontParseWarningSeverity {
667        /// Informational message (not an error).
668        Info,
669        /// Warning that may affect font rendering.
670        Warning,
671        /// Error that prevents proper font usage.
672        Error,
673    }
674
675    impl FontParseWarning {
676        /// Creates an info-level message.
677        #[must_use] pub const fn info(message: String) -> Self {
678            Self {
679                severity: FontParseWarningSeverity::Info,
680                message,
681            }
682        }
683
684        /// Creates a warning-level message.
685        #[must_use] pub const fn warning(message: String) -> Self {
686            Self {
687                severity: FontParseWarningSeverity::Warning,
688                message,
689            }
690        }
691
692        /// Creates an error-level message.
693        #[must_use] pub const fn error(message: String) -> Self {
694            Self {
695                severity: FontParseWarningSeverity::Error,
696                message,
697            }
698        }
699    }
700
701    // WEB-LIFT FIX (2026-06-02): a `FontTableProvider` that scans the sfnt table directory
702    // by hand from the raw font bytes. allsorts' `OffsetTableFontProvider` produces garbage
703    // on the remill/web backend: (1) `ReadArray::read_item`'s nested-tuple `TableRecord` read
704    // returns `table_tag = 0` for EVERY record (proven: tags[7]=0x0000 while the bytes there
705    // are 0x68656164 'head'); (2) even a hand-rolled scan added to the *allsorts crate* sees a
706    // bad `self.scope.data()` (the ReadScope fat-pointer mis-lifts through provider
707    // construction, or allsorts-crate code lifts differently). This provider lives in
708    // azul-layout — whose identical byte reads PROVABLY work (the `from_provider` probe read
709    // num_tables=15 from these same `font_bytes`) — and reads the slice directly. KEEP.
710    #[inline]
711    fn manual_be16(d: &[u8], o: usize) -> u32 {
712        (u32::from(d[o]) << 8) | u32::from(d[o + 1])
713    }
714    #[inline]
715    fn manual_be32(d: &[u8], o: usize) -> u32 {
716        (u32::from(d[o]) << 24)
717            | (u32::from(d[o + 1]) << 16)
718            | (u32::from(d[o + 2]) << 8)
719            | u32::from(d[o + 3])
720    }
721
722    struct ManualTableProvider<'a> {
723        data: &'a [u8],
724        dir: usize, // byte offset of the first table record (offset-table base + 12)
725        num: usize, // number of table records
726    }
727
728    impl<'a> ManualTableProvider<'a> {
729        fn new(data: &'a [u8], font_index: usize) -> Option<Self> {
730            if data.len() < 12 {
731                return None;
732            }
733            let base = if manual_be32(data, 0) == 0x7474_6366 {
734                // 'ttcf' (TrueType Collection): the font_index'th offset-table offset.
735                let num_fonts = manual_be32(data, 8) as usize;
736                if font_index >= num_fonts || 12 + font_index * 4 + 4 > data.len() {
737                    return None;
738                }
739                manual_be32(data, 12 + font_index * 4) as usize
740            } else {
741                0 // single font: offset table at the start
742            };
743            if base + 12 > data.len() {
744                return None;
745            }
746            Some(ManualTableProvider {
747                data,
748                dir: base + 12,
749                num: manual_be16(data, base + 4) as usize,
750            })
751        }
752    }
753
754    impl FontTableProvider for ManualTableProvider<'_> {
755        fn table_data(
756            &self,
757            tag: u32,
758        ) -> Result<Option<std::borrow::Cow<'_, [u8]>>, allsorts::error::ParseError> {
759            let mut i = 0;
760            while i < self.num {
761                let r = self.dir + i * 16;
762                if r + 16 > self.data.len() {
763                    break;
764                }
765                if manual_be32(self.data, r) == tag {
766                    let off = manual_be32(self.data, r + 8) as usize;
767                    let len = manual_be32(self.data, r + 12) as usize;
768                    return Ok(off
769                        .checked_add(len)
770                        .filter(|&e| e <= self.data.len())
771                        .map(|e| std::borrow::Cow::Borrowed(&self.data[off..e])));
772                }
773                i += 1;
774            }
775            Ok(None)
776        }
777
778        fn has_table(&self, tag: u32) -> bool {
779            self.table_data(tag).ok().flatten().is_some()
780        }
781
782        fn table_tags(&self) -> Option<Vec<u32>> {
783            // DIAG (REVERT): sentinel 0xFADE as tags[0] proves THIS provider ran; then
784            // self.num pushes let me see if the usize field survived; then the real reads
785            // show if self.data (slice field) survived the struct move through generics.
786            let mut tags = Vec::with_capacity(self.num + 1);
787            tags.push(0x0000_FADE);
788            let mut i = 0;
789            while i < self.num {
790                let r = self.dir + i * 16;
791                if r + 4 > self.data.len() {
792                    break;
793                }
794                tags.push(manual_be32(self.data, r));
795                i += 1;
796            }
797            Some(tags)
798        }
799    }
800
801    impl allsorts::tables::SfntVersion for ManualTableProvider<'_> {
802        fn sfnt_version(&self) -> u32 {
803            let base = self.dir.saturating_sub(12);
804            if base + 4 <= self.data.len() {
805                manual_be32(self.data, base)
806            } else {
807                0
808            }
809        }
810    }
811
812    impl ParsedFont {
813        /// Parse a font from bytes using allsorts
814        ///
815        /// # Arguments
816        /// * `font_bytes` - The font file data
817        /// * `font_index` - Index of the font in a font collection (0 for single fonts)
818        /// * `warnings` - Optional vector to collect parsing warnings
819        ///
820        /// # Returns
821        /// `Some(ParsedFont)` if parsing succeeds, `None` otherwise
822        ///
823        /// Note: Outlines are decoded lazily by `get_or_decode_glyph`;
824        /// `LocaGlyf::load` runs eagerly here. Use `from_bytes_shared`
825        /// for the lazy-LocaGlyf production path.
826        pub fn from_bytes(
827            font_bytes: &[u8],
828            font_index: usize,
829            warnings: &mut Vec<FontParseWarning>,
830        ) -> Option<Self> {
831            // `from_bytes` keeps the eager-LocaGlyf behaviour for the
832            // small number of callers (mainly tests) that don't have
833            // an `Arc<[u8]>` to keep alive for the lazy path.
834            let mut font = Self::from_bytes_internal(font_bytes, font_index, warnings, false)?;
835            // Retain an owned copy of the source bytes so the face can later be
836            // subset/embedded (PDF export, save->parse roundtrips). Callers pass a
837            // borrowed slice that may not outlive us, so we own it here. Mirrors
838            // `from_bytes_shared`, which retains the caller's `Arc<FontBytes>`.
839            if font.original_bytes.is_none() {
840                font.original_bytes = Some(Arc::new(
841                    rust_fontconfig::FontBytes::Owned(Arc::from(font_bytes.to_vec())),
842                ));
843            }
844            Some(font)
845        }
846
847        /// Shared implementation of `from_bytes` / `from_bytes_shared`.
848        ///
849        /// `defer_loca_glyf = true` skips the `LocaGlyf::load` call
850        /// here so the caller (`from_bytes_shared`) can install a
851        /// `LocaGlyfState::Deferred` slot that re-parses on first
852        /// glyph decode. Saves the load-then-drop cycle the previous
853        /// arrangement paid (`from_bytes_shared` used to call
854        /// `from_bytes` and immediately replace the loaded `LocaGlyf`
855        /// with a Deferred slot, throwing away ~hundreds of KiB of
856        /// loca+glyf bytes per face for fonts in the chain that get
857        /// loaded but never rasterized).
858        fn from_bytes_internal(
859            font_bytes: &[u8],
860            font_index: usize,
861            warnings: &mut Vec<FontParseWarning>,
862            defer_loca_glyf: bool,
863        ) -> Option<Self> {
864            use allsorts::{binary::read::ReadScope, font_data::FontData};
865            fn provider_err(font_index: usize, e: impl fmt::Display) -> FontParseWarning {
866                FontParseWarning::error(format!(
867                    "Failed to get table provider for font index {font_index}: {e}"
868                ))
869            }
870
871            let scope = ReadScope::new(font_bytes);
872            let font_file = match scope.read::<FontData<'_>>() {
873                Ok(ff) => ff,
874                Err(e) => {
875                    warnings.push(FontParseWarning::error(format!(
876                        "Failed to read font data: {e}"
877                    )));
878                    return None;
879                }
880            };
881            // FIX (2026-06-02): route OpenType fonts through the CONCRETE provider
882            // (`OffsetTableFontProvider`) instead of `FontData::table_provider`'s
883            // `Box<dyn FontTableProvider>`. On the lifted/web backend the trait-object
884            // VTABLE dispatch (allsorts font_data.rs:45 `self.provider.table_data(tag)`)
885            // mis-lifts: the vtable's fn-pointers are untranslated native addresses, so the
886            // indirect-call dispatcher routes the dyn call to the WRONG `table_data` impl,
887            // which returns a `Cow::Owned` garbage buffer → `HeadTable::read` errors → font
888            // parse returns None → text measures height 0. A concrete provider makes every
889            // `table_data` a DIRECT (monomorphized) call, which lifts correctly. Woff/Woff2
890            // keep the dyn path (they're not used on the web backend's embedded TTF).
891            match font_file {
892                FontData::OpenType(otf) => {
893                    // Prefer the hand-rolled provider (reads font_bytes directly) over
894                    // allsorts' OffsetTableFontProvider, whose lifted table reads are garbage
895                    // on the web backend. Fall back to allsorts only if the manual layout
896                    // parse can't recognise the sfnt (e.g. an unusual TTC).
897                    if let Some(mp) = ManualTableProvider::new(font_bytes, font_index) {
898                        Self::from_provider(mp, font_bytes, font_index, warnings, defer_loca_glyf)
899                    } else {
900                        match otf.table_provider(font_index) {
901                            Ok(p) => Self::from_provider(
902                                p,
903                                font_bytes,
904                                font_index,
905                                warnings,
906                                defer_loca_glyf,
907                            ),
908                            Err(e) => {
909                                warnings.push(provider_err(font_index, e));
910                                None
911                            }
912                        }
913                    }
914                }
915                other => match other.table_provider(font_index) {
916                    Ok(p) => {
917                        Self::from_provider(p, font_bytes, font_index, warnings, defer_loca_glyf)
918                    }
919                    Err(e) => {
920                        warnings.push(provider_err(font_index, e));
921                        None
922                    }
923                },
924            }
925        }
926
927        /// Build a `ParsedFont` from a concrete [`FontTableProvider`]. Split out of
928        /// `from_bytes_internal` (2026-06-02) so OpenType fonts use the concrete
929        /// `OffsetTableFontProvider` (direct `table_data` calls that lift correctly on
930        /// the web backend) rather than `FontData::table_provider`'s `Box<dyn>`, whose
931        /// trait-object vtable dispatch mis-lifts (wrong impl → Owned garbage → parse fail).
932        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
933        #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
934        fn from_provider<P: FontTableProvider>(
935            provider: P,
936            font_bytes: &[u8],
937            font_index: usize,
938            warnings: &mut Vec<FontParseWarning>,
939            defer_loca_glyf: bool,
940        ) -> Option<Self> {
941            use std::{
942                collections::hash_map::DefaultHasher,
943                hash::{Hash, Hasher},
944            };
945
946            use allsorts::{
947                binary::read::ReadScope,
948                tables::{
949                    cmap::{owned::CmapSubtable as OwnedCmapSubtable, CmapSubtable},
950                    FontTableProvider, HeadTable, HheaTable, MaxpTable,
951                },
952                tag,
953            };
954
955            // Extract font name from NAME table early (before provider is moved).
956            // WEB-LIFT FIX (2026-06-02): NameTable::string_for_id decodes the NAME strings via
957            // `encoding_rs` (Mac Roman / UTF-16 charset state machines), whose jump-tables
958            // are NOT devirt'd by the remill lift → MISSING_BLOCK trap (proven: trap in
959            // encoding_rs::Decoder::decode_to_utf8). font_name is OPTIONAL metadata (NOT used
960            // for layout/metrics/shaping — those are binary head/hhea/maxp/cmap/glyf), so skip
961            // the NAME-string decode on the web backend to avoid encoding_rs entirely.
962            #[cfg(feature = "web_lift")]
963            let font_name: Option<String> = None;
964            #[cfg(not(feature = "web_lift"))]
965            let font_name = provider.table_data(tag::NAME).ok().and_then(|name_data| {
966                ReadScope::new(&name_data?)
967                    .read::<allsorts::tables::NameTable<'_>>()
968                    .ok()
969                    .and_then(|name_table| {
970                        name_table.string_for_id(allsorts::tables::NameTable::POSTSCRIPT_NAME)
971                    })
972            });
973
974            // DIAG (2026-06-02, REVERT): pinpoint the web font-parse-fails root — does HEAD
975            // fail because table_data can't find/return the table (directory mis-lift) or
976            // because HeadTable::read errors (table-read mis-lift)? Surfaced via warnings.
977            let head_table = match provider.table_data(tag::HEAD) {
978                Ok(Some(head_cow)) => {
979                    // DIAG: is the HEAD table data CORRECT (magicNumber 0x5F0F3CF5 @ off 12 →
980                    // HeadTable::read mis-lifts) or WRONG bytes (directory offset mis-lift)?
981                    let bb = head_cow.as_ref();
982                    let magic = if bb.len() >= 16 {
983                        (u32::from(bb[12]) << 24) | (u32::from(bb[13]) << 16)
984                            | (u32::from(bb[14]) << 8) | u32::from(bb[15])
985                    } else { 0 };
986                    if let Ok(h) = ReadScope::new(&head_cow).read::<HeadTable>() { h } else {
987                        // DIAG: surface the sliced offset (how wrong) as hex — "HO" + 8 hex
988                        // of (head_cow.ptr - font_bytes.ptr). garbage→offset-read mis-lift;
989                        // 00000000→base; plausible-but-wrong→record mapping. "RF"=bytes-OK.
990                        let m = if magic == 0x5F0F_3CF5 {
991                            "RF000000".to_string()
992                        } else {
993                            let off = (head_cow.as_ref().as_ptr() as usize)
994                                .wrapping_sub(font_bytes.as_ptr() as usize);
995                            let mut msg = String::new();
996                            // B=Borrowed(slice of font_bytes, ptr-arith/base mis-lift) vs
997                            // O=Owned(decompressed/copied Vec — wrong path for plain TTF).
998                            msg.push(if matches!(head_cow, std::borrow::Cow::Borrowed(_)) { 'B' } else { 'O' });
999                            msg.push_str("HO");
1000                            let mut sh: i32 = 28;
1001                            while sh >= 0 {
1002                                let d = ((off >> sh) & 0xf) as u8;
1003                                msg.push((if d < 10 { b'0' + d } else { b'a' + d - 10 }) as char);
1004                                sh -= 4;
1005                            }
1006                            msg
1007                        };
1008                        warnings.push(FontParseWarning::error(m));
1009                        return None;
1010                    }
1011                }
1012                Ok(None) => {
1013                    // DIAG (REVERT): bytes+len+read_item-count+dir all proved OK (N0fr0fc0fg1)
1014                    // yet find_table_record(HEAD)=None though 'head' is rec[7] on disk. So
1015                    // either read_item's table_tag FIELD is garbage, or tag::HEAD mis-lifts, or
1016                    // the u32 == mis-lifts. t7 = tags[7] (should be 0x68656164 'head' low16
1017                    // =6164); H = tag::HEAD low16 (should be 6164); f = ANY tag==HEAD via an
1018                    // indexed compare loop (NOT .iter().any). "T<4h t7>H<4h HEAD>f<0|1>".
1019                    //   T6164 H6164 f1 → values+compare OK (won't reach here — HEAD found)
1020                    //   T6164 H6164 f0 → the u32 == comparison mis-lifts
1021                    //   T!=6164        → read_item table_tag FIELD garbage (tuple read mis-lift)
1022                    //   H!=6164        → tag::HEAD const mis-lifts
1023                    #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1024                    fn hx(m: &mut String, val: u32, nibbles: i32) {
1025                        let mut sh = (nibbles - 1) * 4;
1026                        while sh >= 0 {
1027                            let d = ((val >> sh) & 0xf) as u8;
1028                            m.push((if d < 10 { b'0' + d } else { b'a' + d - 10 }) as char);
1029                            sh -= 4;
1030                        }
1031                    }
1032                    // DECISIVE: tags[8] (head, file off 124) reads 0 but tags[2] (off 28) is OK.
1033                    // Read the SAME offsets from from_provider's LOCAL font_bytes param (proven
1034                    // correct at off 4/12). If local@124 = 0x6865 'he' but provider tags[8]=0 ⇒
1035                    // STORED-SLICE issue (provider self.data fat-ptr mis-lifts) → read locally.
1036                    // If local@124 = 0 ⇒ the font CONST is only PARTIALLY MIRRORED into the wasm
1037                    // (deep data-mirror gap) → table data is simply absent. local@93596 (=0x16f9c,
1038                    // head TABLE data start) further maps the mirror: 'he'/nonzero vs 0.
1039                    let loc124 = if font_bytes.len() >= 126 {
1040                        (u32::from(font_bytes[124]) << 8) | u32::from(font_bytes[125])
1041                    } else {
1042                        0xEEEE
1043                    };
1044                    let loc_head = if font_bytes.len() >= 93598 {
1045                        (u32::from(font_bytes[93596]) << 8) | u32::from(font_bytes[93597])
1046                    } else {
1047                        0xEEEE
1048                    };
1049                    let mut m = String::from("L"); // local font_bytes[124..126] (head dir record):
1050                    hx(&mut m, loc124 & 0xffff, 4); // 6865 'he' = mirrored; 0000 = not
1051                    m.push('H'); // local font_bytes[93596..] (head TABLE data, deep):
1052                    hx(&mut m, loc_head & 0xffff, 4);
1053                    warnings.push(FontParseWarning::error(m));
1054                    return None;
1055                }
1056                Err(_) => {
1057                    warnings.push(FontParseWarning::error("HEAD_DATAERR".to_string()));
1058                    return None;
1059                }
1060            };
1061
1062            let maxp_table = provider
1063                .table_data(tag::MAXP)
1064                .ok()
1065                .and_then(|maxp_data| ReadScope::new(&maxp_data?).read::<MaxpTable>().ok())
1066                .unwrap_or(MaxpTable {
1067                    num_glyphs: 0,
1068                    version1_sub_table: None,
1069                });
1070
1071            let num_glyphs = maxp_table.num_glyphs as usize;
1072
1073            // Compute byte offset+length into font_bytes for hmtx/vmtx
1074            // instead of copying the table data. The provider returns a
1075            // borrowed slice for OpenType fonts, so we can derive the
1076            // offset via pointer arithmetic.
1077            let hmtx_range = provider
1078                .table_data(tag::HMTX)
1079                .ok()
1080                .and_then(|cow_opt| {
1081                    let cow = cow_opt?;
1082                    match cow {
1083                        std::borrow::Cow::Borrowed(slice) => {
1084                            let base = font_bytes.as_ptr() as usize;
1085                            let ptr = slice.as_ptr() as usize;
1086                            let offset = ptr.checked_sub(base)?;
1087                            if offset + slice.len() <= font_bytes.len() {
1088                                Some((offset, slice.len()))
1089                            } else {
1090                                None
1091                            }
1092                        }
1093                        std::borrow::Cow::Owned(_) => None,
1094                    }
1095                })
1096                .unwrap_or((0, 0));
1097
1098            let vmtx_range = provider
1099                .table_data(tag::VMTX)
1100                .ok()
1101                .and_then(|s| {
1102                    let slice = s?;
1103                    let base = font_bytes.as_ptr() as usize;
1104                    let ptr = slice.as_ptr() as usize;
1105                    let offset = ptr.checked_sub(base)?;
1106                    if offset + slice.len() <= font_bytes.len() {
1107                        Some((offset, slice.len()))
1108                    } else {
1109                        None
1110                    }
1111                })
1112                .unwrap_or((0, 0));
1113
1114            // Parse vhea table (same format as hhea, used for vertical metrics)
1115            let vhea_table = provider
1116                .table_data(tag::VHEA)
1117                .ok()
1118                .and_then(|vhea_data| ReadScope::new(&vhea_data?).read::<HheaTable>().ok());
1119
1120            // hhea is required per the OpenType spec; return None if missing
1121            let hhea_table = provider
1122                .table_data(tag::HHEA)
1123                .ok()
1124                .and_then(|hhea_data| ReadScope::new(&hhea_data?).read::<HheaTable>().ok())?;
1125
1126            // Build layout-specific font metrics
1127            let font_metrics = LayoutFontMetrics {
1128                units_per_em: if head_table.units_per_em == 0 {
1129                    1000
1130                } else {
1131                    head_table.units_per_em
1132                },
1133                ascent: f32::from(hhea_table.ascender),
1134                descent: f32::from(hhea_table.descender),
1135                line_gap: f32::from(hhea_table.line_gap),
1136                x_height: None, // will be populated from OS/2 table via from_font_metrics if available
1137                cap_height: None,
1138            };
1139
1140            // Build PDF-specific font metrics
1141            let pdf_font_metrics =
1142                Self::parse_pdf_font_metrics(font_bytes, font_index, &head_table, &hhea_table);
1143
1144            // Use allsorts LocaGlyf for on-demand outline extraction. We
1145            // *load* LocaGlyf eagerly (it owns ~tens of KiB of loca +
1146            // ~hundreds of KiB of glyf bytes) but we *don't* decode any
1147            // glyph outlines up front — that's the big RSS win. Glyphs
1148            // are decoded by `ParsedFont::get_or_decode_glyph` on first
1149            // access from the CPU/GPU rasterizer.
1150            //
1151            // When `defer_loca_glyf` is set (production lazy path via
1152            // `from_bytes_shared`), we skip `LocaGlyf::load` here too —
1153            // the caller will overwrite the slot with
1154            // `LocaGlyfState::Deferred` carrying the source bytes
1155            // `Arc<[u8]>`, and the load happens on the first
1156            // `get_or_decode_glyph` call. This avoids parsing
1157            // ~hundreds of KiB per face for fonts that get resolved
1158            // into a chain but never actually rasterized (typical
1159            // for fallback fonts in CSS chains).
1160            let has_glyf = provider.has_table(tag::GLYF) && provider.has_table(tag::LOCA);
1161            // Cache `has_gvar` before `provider` gets moved into
1162            // `allsorts::font::Font::new(provider)` further down —
1163            // it's the cheapest way to detect a variable font and
1164            // avoids the borrow-after-move that a later
1165            // `provider.has_table(tag::GVAR)` would incur.
1166            let has_gvar = provider.has_table(tag::GVAR);
1167            let loca_glyf_opt: Option<Arc<std::sync::Mutex<LocaGlyf>>> = if has_glyf
1168                && !defer_loca_glyf
1169            {
1170                match LocaGlyf::load(&provider) {
1171                    Ok(lg) => Some(Arc::new(std::sync::Mutex::new(lg))),
1172                    Err(e) => {
1173                        warnings.push(FontParseWarning::warning(format!(
1174                            "Failed to load LocaGlyf: {e} — falling back to hmtx-only"
1175                        )));
1176                        None
1177                    }
1178                }
1179            } else {
1180                None
1181            };
1182
1183            // Lazy `glyph_cache` starts empty; the space-glyph stub
1184            // below pre-inserts gid 0 / space so the shaper's
1185            // cmap-miss fallback has something to render without
1186            // racing with a decode.
1187
1188            let mut font_data_impl = allsorts::font::Font::new(provider).ok()?;
1189
1190            // Create TrueType hinting instance from font tables.
1191            // [az-web-lift] Skip on the web build. The lifted layout never grid-fits glyphs to a
1192            // pixel raster (it measures + ships a display list to JS), so hinting is never used.
1193            // Building it (HintInstance::new) runs the allsorts bytecode Interpreter
1194            // (Interpreter::new + ::dispatch — a large un-devirt'd opcode jump table the remill
1195            // lift can't resolve, plus ~700 op_* fns of closure bloat). This is INDEPENDENT of the
1196            // lift's jump-table devirt: even with a perfect lift, web has no use for hinting, and
1197            // hinted advances are lower-quality output than the plain scaled advance. Native keeps
1198            // real hinting unchanged.
1199            #[cfg(feature = "web_lift")]
1200            let hint_instance: Option<std::sync::Mutex<allsorts::hinting::HintInstance>> = None;
1201            #[cfg(not(feature = "web_lift"))]
1202            let hint_instance = allsorts::hinting::HintInstance::new(
1203                &font_data_impl.font_table_provider
1204            ).ok().flatten().map(std::sync::Mutex::new);
1205
1206            // Stash raw GSUB/GPOS bytes for lazy parse. Typical fonts
1207            // have ~tens of KiB of GSUB + a few-to-tens of KiB of GPOS —
1208            // dwarfed by glyph outlines — so we keep the bytes around
1209            // and only spend `LayoutTable::read` + `new_layout_cache`
1210            // cycles when the shaper actually needs them (via
1211            // `ParsedFont::gsub` / `::gpos`). For an ASCII run where no
1212            // substitution / kerning is required, we skip both entirely.
1213            let gsub_bytes = font_data_impl
1214                .font_table_provider
1215                .table_data(tag::GSUB)
1216                .ok()
1217                .flatten()
1218                .map(std::borrow::Cow::into_owned);
1219            let gpos_bytes = font_data_impl
1220                .font_table_provider
1221                .table_data(tag::GPOS)
1222                .ok()
1223                .flatten()
1224                .map(std::borrow::Cow::into_owned);
1225            let opt_gdef_table = font_data_impl.gdef_table().ok().and_then(|o| o);
1226            let num_glyphs = font_data_impl.num_glyphs();
1227
1228            let opt_kern_table = font_data_impl
1229                .kern_table()
1230                .ok()
1231                .and_then(|s| s);
1232
1233            let cmap_data = font_data_impl.cmap_subtable_data();
1234            let cmap_subtable = ReadScope::new(cmap_data);
1235            let cmap_subtable = cmap_subtable
1236                .read::<CmapSubtable<'_>>()
1237                .ok()
1238                .and_then(|s| s.to_owned());
1239
1240            // Font identity hash — used by `PartialEq` for ParsedFont.
1241            //
1242            // Previously we did `font_bytes.hash(&mut hasher)` over
1243            // the full mmap. That touched every page of the file
1244            // (a 40 MiB `.ttc` walked byte-for-byte) so the "lazy
1245            // mmap" ended up *fully resident* the moment we built
1246            // a `ParsedFont`. Cold RSS jumped ~40 MiB from this
1247            // single line.
1248            //
1249            // The hash doesn't need to be cryptographic — it just
1250            // has to disambiguate two `ParsedFont`s. `(len, first
1251            // 4 KiB, last 4 KiB, font_index)` is plenty unique and
1252            // only faults in the two header / trailer pages, which
1253            // shaping is going to need anyway.
1254            let mut hasher = DefaultHasher::new();
1255            (font_bytes.len() as u64).hash(&mut hasher);
1256            let head_len = font_bytes.len().min(4096);
1257            font_bytes[..head_len].hash(&mut hasher);
1258            let tail_start = font_bytes.len().saturating_sub(4096);
1259            font_bytes[tail_start..].hash(&mut hasher);
1260            font_index.hash(&mut hasher);
1261            let hash = hasher.finish();
1262
1263            let mut font = Self {
1264                hash,
1265                font_metrics,
1266                pdf_font_metrics,
1267                num_glyphs,
1268                hhea_table,
1269                hmtx_range,
1270                vmtx_range,
1271                vhea_table,
1272                maxp_table,
1273                gsub_bytes,
1274                gsub_cache_lazy: std::sync::OnceLock::new(),
1275                gpos_bytes,
1276                gpos_cache_lazy: std::sync::OnceLock::new(),
1277                opt_gdef_table,
1278                opt_kern_table,
1279                cmap_subtable,
1280                last_used: Arc::new(std::sync::atomic::AtomicU64::new(0)),
1281                is_variable_font: has_gvar,
1282                glyph_cache: Arc::new(rust_fontconfig::StLock::new(BTreeMap::new())),
1283                // Eager path: `from_bytes` loaded LocaGlyf immediately
1284                // (or set None if the font has no loca+glyf). Lazy
1285                // callers use `from_bytes_shared` which replaces this
1286                // with `LocaGlyfState::Deferred` before returning.
1287                loca_glyf: LocaGlyfState::Loaded(loca_glyf_opt),
1288                space_width: None,
1289                mock: None,
1290                reverse_glyph_cache: BTreeMap::new(),
1291                // Don't retain the source bytes by default — layout and
1292                // raster don't need them. PDF subsetting / `to_bytes`
1293                // callers opt in via `with_source_bytes`.
1294                original_bytes: None,
1295                original_index: font_index,
1296                index_to_cid: BTreeMap::new(), // Will be filled for CFF fonts
1297                font_type: FontType::TrueType, // Default, will be updated if CFF
1298                font_name,
1299                hint_instance,
1300            };
1301
1302            // Calculate space width
1303            let space_width = font.get_space_width_internal();
1304
1305            // Pre-decode the space glyph straight into the lazy
1306            // `glyph_cache`. Space typically has no outline, so the
1307            // decoder's outline visitor returns nothing useful and
1308            // we'd spin re-decoding it every shape — short-circuit
1309            // here with a hand-rolled record carrying the hmtx
1310            // advance.
1311            let _ = (|| {
1312                let space_gid = font.lookup_glyph_index(' ' as u32)?;
1313                {
1314                    // StLock::read() is infallible (Result<_, Infallible>);
1315                    // kept in a tight block so the guard drops at scope end.
1316                    let Ok(cache) = font.glyph_cache.read();
1317                    if cache.contains_key(&space_gid) {
1318                        return None;
1319                    }
1320                }
1321                let space_width_val = space_width?;
1322                // Only pre-cache when we actually know a non-zero advance. During
1323                // `from_bytes_internal` the source bytes are not attached yet, so
1324                // `hmtx` is unreadable and `get_space_width_internal` reads back 0;
1325                // caching that would pin every space to a 0 advance for the life of
1326                // the face. Skip it and let the space decode lazily once bytes are
1327                // attached (mock fonts that carry a real space width still cache).
1328                if space_width_val == 0 {
1329                    return None;
1330                }
1331                let space_record = OwnedGlyph {
1332                    bounding_box: OwnedGlyphBoundingBox {
1333                        max_x: 0,
1334                        max_y: 0,
1335                        min_x: 0,
1336                        min_y: 0,
1337                    },
1338                    horz_advance: space_width_val as u16,
1339                    outline: Vec::new(),
1340                    phantom_points: None,
1341                    raw_points: None,
1342                    raw_on_curve: None,
1343                    raw_contour_ends: None,
1344                    instructions: None,
1345                };
1346                {
1347                    // StLock::write() is infallible (Result<_, Infallible>).
1348                    let Ok(mut cache) = font.glyph_cache.write();
1349                    cache.insert(space_gid, Arc::new(space_record));
1350                }
1351                Some(())
1352            })();
1353
1354            font.space_width = space_width;
1355
1356            Some(font)
1357        }
1358
1359        /// Attach the source font bytes to this `ParsedFont`, enabling
1360        /// [`ParsedFont::to_bytes`] and [`ParsedFont::subset`] (both of
1361        /// which the layout / shaping path never calls).
1362        ///
1363        /// Takes an `Arc<FontBytes>` so the same file's bytes can be
1364        /// shared across every face of a `.ttc` at zero extra cost —
1365        /// pair with [`rust_fontconfig::FcFontCache::get_font_bytes`].
1366        /// For ad-hoc PDF callers that have raw heap bytes, wrap them
1367        /// via `Arc::new(FontBytes::Owned(Arc::from(vec)))`.
1368        #[must_use]
1369        pub fn with_source_bytes(mut self, bytes: Arc<rust_fontconfig::FontBytes>) -> Self {
1370            self.original_bytes = Some(bytes);
1371            self
1372        }
1373
1374        /// Lazy-friendly constructor — identical to
1375        /// [`ParsedFont::from_bytes`] except that `LocaGlyf` is
1376        /// **not** loaded during the call. Instead, the supplied
1377        /// `Arc<[u8]>` is retained and `LocaGlyf::load` runs the first
1378        /// time [`get_or_decode_glyph`] needs glyph outlines for this
1379        /// face.
1380        ///
1381        /// Fonts that get resolved into a CSS fallback chain but are
1382        /// never actually rasterized (common on desktop — e.g. every
1383        /// face of HelveticaNeue.ttc loads, but only one or two are
1384        /// shaped) then pay zero loca/glyf cost.
1385        ///
1386        /// Production callers (the reftest harness, `LayoutWindow`,
1387        /// `cpurender`) should prefer this constructor. Tests that
1388        /// inspect `glyph_records_decoded` directly and don't want
1389        /// a lazy path keep using `from_bytes`.
1390        pub fn from_bytes_shared(
1391            bytes: Arc<rust_fontconfig::FontBytes>,
1392            font_index: usize,
1393            warnings: &mut Vec<FontParseWarning>,
1394        ) -> Option<Self> {
1395            // Skip the eager LocaGlyf::load via `defer_loca_glyf=true`
1396            // — saves the load-then-drop cycle the prior arrangement
1397            // paid (when this called `from_bytes`, allocated
1398            // ~hundreds of KiB of loca+glyf bytes, then immediately
1399            // replaced the slot with `Deferred` and dropped them).
1400            // `bytes.as_ref()` derefs FontBytes → &[u8] (mmap or owned
1401            // — same code path).
1402            let mut font = Self::from_bytes_internal(bytes.as_ref(), font_index, warnings, true)?;
1403            font.original_bytes = Some(bytes.clone());
1404            font.loca_glyf = LocaGlyfState::Deferred {
1405                bytes,
1406                font_index,
1407                loaded: Arc::new(std::sync::Mutex::new(None)),
1408            };
1409            Some(font)
1410        }
1411
1412        /// Resolve the current face's `LocaGlyf`, loading it lazily
1413        /// on first call when `loca_glyf` is `Deferred`. Returns
1414        /// `None` when the font has no usable loca+glyf (CFF fonts
1415        /// or parse failures).
1416        fn resolve_loca_glyf(&self) -> Option<Arc<std::sync::Mutex<LocaGlyf>>> {
1417            use allsorts::{
1418                binary::read::ReadScope,
1419                font_data::FontData,
1420                tables::FontTableProvider,
1421            };
1422            match &self.loca_glyf {
1423                LocaGlyfState::Loaded(inner) => inner.clone(),
1424                LocaGlyfState::Deferred { bytes, font_index, loaded } => {
1425                    // Fast path: cached LocaGlyf is present.
1426                    if let Ok(guard) = loaded.lock() {
1427                        if let Some(arc) = guard.as_ref() {
1428                            return Some(Arc::clone(arc));
1429                        }
1430                    }
1431                    let _p = crate::probe::Probe::span("resolve_loca_glyf");
1432
1433                    // Slow path: parse provider + load LocaGlyf without
1434                    // holding the slot's lock (allsorts can take a
1435                    // millisecond or two on a fresh load). Re-check
1436                    // after acquiring the write lock so a parallel
1437                    // decoder doesn't double-load.
1438                    let scope = ReadScope::new(bytes.as_slice());
1439                    let font_data = scope.read::<FontData<'_>>().ok()?;
1440                    let provider = font_data.table_provider(*font_index).ok()?;
1441                    // Gate on table presence to match the `from_bytes`
1442                    // has_glyf check; avoids a spurious warning on
1443                    // CFF fonts that sneak into the Deferred path.
1444                    if !provider.has_table(tag::GLYF) || !provider.has_table(tag::LOCA) {
1445                        return None;
1446                    }
1447                    // Zero-copy: keep `glyf` as a view into the already-resident
1448                    // (mmap'd) font bytes instead of copying the whole table
1449                    // (~20 MB for a large font) onto the heap. `bytes` is the
1450                    // exact buffer `provider` reads from, so load_shared can
1451                    // anchor the glyf range inside it (falling back to an owned
1452                    // copy if it ever can't). Audit §3.3a.
1453                    let owner: Arc<dyn AsRef<[u8]> + Send + Sync> = bytes.clone();
1454                    let new_arc = LocaGlyf::load_shared(&provider, owner)
1455                        .ok()
1456                        .map(|lg| Arc::new(std::sync::Mutex::new(lg)))?;
1457
1458                    if let Ok(mut guard) = loaded.lock() {
1459                        if let Some(existing) = guard.as_ref() {
1460                            return Some(Arc::clone(existing));
1461                        }
1462                        *guard = Some(Arc::clone(&new_arc));
1463                    }
1464                    Some(new_arc)
1465                }
1466            }
1467        }
1468
1469        /// Source bytes for PDF subsetting / table extraction.
1470        ///
1471        /// Looks in two places:
1472        /// - `original_bytes` (set by [`ParsedFont::with_source_bytes`]
1473        ///   for legacy PDF-first construction).
1474        /// - `LocaGlyfState::Deferred.bytes` (set by
1475        ///   [`ParsedFont::from_bytes_shared`] — the production lazy
1476        ///   path, which already retains an `Arc<[u8]>` for the lazy
1477        ///   loca/glyf loader).
1478        ///
1479        /// Returns `None` only for `ParsedFont`s built via the eager
1480        /// `from_bytes` path without an explicit `with_source_bytes`
1481        /// call — i.e. unit tests that load a font and don't touch
1482        /// PDF.
1483        pub fn source_bytes_for_subset(&self) -> Option<Arc<rust_fontconfig::FontBytes>> {
1484            if let Some(bytes) = &self.original_bytes {
1485                return Some(Arc::clone(bytes));
1486            }
1487            if let LocaGlyfState::Deferred { bytes, .. } = &self.loca_glyf {
1488                return Some(Arc::clone(bytes));
1489            }
1490            None
1491        }
1492
1493        /// Read the monotonic-clock nanos timestamp of the most
1494        /// recent [`get_or_decode_glyph`] call on this face, or `0`
1495        /// if it's never been touched.
1496        pub fn last_used_nanos(&self) -> u64 {
1497            self.last_used.load(std::sync::atomic::Ordering::Relaxed)
1498        }
1499
1500        /// Drop the cached `LocaGlyf` for this face if it's
1501        /// `Deferred`-with-bytes-retained — so the next
1502        /// [`get_or_decode_glyph`] re-parses from `bytes`. No-op for
1503        /// `Loaded` faces (no source bytes to fall back to).
1504        ///
1505        /// Used by [`crate::text3::cache::FontManager::evict_unused`]
1506        /// and exposed publicly so embedders can free memory under
1507        /// pressure on fonts they no longer need to render.
1508        pub fn evict_loca_glyf(&self) -> bool {
1509            match &self.loca_glyf {
1510                LocaGlyfState::Deferred { loaded, .. } => {
1511                    if let Ok(mut guard) = loaded.lock() {
1512                        if guard.is_some() {
1513                            *guard = None;
1514                            return true;
1515                        }
1516                    }
1517                    false
1518                }
1519                LocaGlyfState::Loaded(_) => false,
1520            }
1521        }
1522
1523        /// Fetch the parsed GSUB cache if this font has one, parsing
1524        /// it from the retained `gsub_bytes` on first access.
1525        ///
1526        /// Moved out of the eager `from_bytes` path because most text
1527        /// runs never trigger GSUB — plain ASCII without ligatures is
1528        /// handled entirely by the cmap + hmtx fast path. Building
1529        /// `LayoutCacheData<GSUB>` up front reserved ~0.5–2 MiB per
1530        /// face just to throw it away on pages that don't shape
1531        /// complex scripts.
1532        pub fn gsub(&self) -> Option<&GsubCache> {
1533            self.gsub_cache_lazy
1534                .get_or_init(|| {
1535                    use allsorts::{
1536                        binary::read::ReadScope,
1537                        layout::{new_layout_cache, LayoutTable, GSUB},
1538                    };
1539                    let bytes = self.gsub_bytes.as_ref()?;
1540                    ReadScope::new(bytes)
1541                        .read::<LayoutTable<GSUB>>()
1542                        .ok()
1543                        .map(new_layout_cache)
1544                })
1545                .as_ref()
1546        }
1547
1548        /// Fetch the parsed GPOS cache if this font has one, parsing
1549        /// it from the retained `gpos_bytes` on first access. See
1550        /// [`ParsedFont::gsub`] for the motivation.
1551        pub fn gpos(&self) -> Option<&GposCache> {
1552            self.gpos_cache_lazy
1553                .get_or_init(|| {
1554                    use allsorts::{
1555                        binary::read::ReadScope,
1556                        layout::{new_layout_cache, LayoutTable, GPOS},
1557                    };
1558                    let bytes = self.gpos_bytes.as_ref()?;
1559                    ReadScope::new(bytes)
1560                        .read::<LayoutTable<GPOS>>()
1561                        .ok()
1562                        .map(new_layout_cache)
1563                })
1564                .as_ref()
1565        }
1566
1567        /// Fetch an `OwnedGlyph` for `gid`, decoding it on first access.
1568        ///
1569        /// Cached in the `Arc<RwLock<…>>` `glyph_cache` so subsequent
1570        /// calls (including across clones of this `ParsedFont`) hit the
1571        /// cache. Returns `None` when `gid >= num_glyphs` or the font
1572        /// has no loca+glyf and no hmtx entry for the glyph. For CFF
1573        /// fonts the returned record has an empty outline and an advance
1574        /// pulled from hmtx — matching the pre-lazy behaviour.
1575        ///
1576        /// Called on the rasterizer hot path; performance budget is a
1577        /// few µs per unique glyph (first hit) and an Arc bump + `BTreeMap`
1578        /// lookup (cache hits). The write lock is held only across the
1579        /// decode, not across the caller's use of the returned Arc.
1580        pub fn get_or_decode_glyph(&self, gid: u16) -> Option<Arc<OwnedGlyph>> {
1581            use std::sync::Arc;
1582            if usize::from(gid) >= self.num_glyphs as usize {
1583                return None;
1584            }
1585            // Bump the LRU timestamp so `FontManager::evict_unused`
1586            // can tell this face is still in use. Cheap atomic store
1587            // (Relaxed — eviction reads the same atomic and tolerates
1588            // a slightly stale value, which only causes "evict, then
1589            // re-load on next access" — never an incorrect render).
1590            self.last_used
1591                .store(monotonic_now_nanos(), std::sync::atomic::Ordering::Relaxed);
1592
1593            // Fast path: cache hit.
1594            {
1595                // StLock::read() is infallible; tight block drops the read
1596                // guard before the write lock below (deadlock avoidance).
1597                let Ok(cache) = self.glyph_cache.read();
1598                if let Some(existing) = cache.get(&gid) {
1599                    return Some(Arc::clone(existing));
1600                }
1601            }
1602
1603            // Miss: decode. We drop the read lock before taking the
1604            // write lock to avoid deadlock, and we re-check on the way
1605            // in because another thread may have decoded the same glyph
1606            // in between.
1607            let record = self.decode_glyph_inner(gid);
1608            let arc = Arc::new(record);
1609            {
1610                // StLock::write() is infallible (Result<_, Infallible>).
1611                let Ok(mut cache) = self.glyph_cache.write();
1612                cache
1613                    .entry(gid)
1614                    .or_insert_with(|| Arc::clone(&arc));
1615                // If another thread beat us to the insert, return theirs
1616                // so all callers observe the same Arc.
1617                if let Some(winner) = cache.get(&gid) {
1618                    return Some(Arc::clone(winner));
1619                }
1620            }
1621            Some(arc)
1622        }
1623
1624        /// Eagerly decode every glyph into the lazy `glyph_cache`,
1625        /// restoring the pre-lazy "every glyph is materialised at
1626        /// construction time" behaviour. Used by tests that iterate
1627        /// or compare against reference tooling, and by embedders
1628        /// that want a walkable view without driving every shape
1629        /// through `get_or_decode_glyph`.
1630        ///
1631        /// After `prime_glyph_cache`, callers can use
1632        /// [`ParsedFont::for_each_decoded_glyph`] or
1633        /// [`ParsedFont::glyph_cache_snapshot`] to observe the
1634        /// populated cache.
1635        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1636        pub fn prime_glyph_cache(&mut self) {
1637            let n = self.num_glyphs as usize;
1638            for glyph_index in 0..n {
1639                let gid = glyph_index as u16;
1640                drop(self.get_or_decode_glyph(gid));
1641            }
1642        }
1643
1644        /// Walk every entry currently in the lazy `glyph_cache`,
1645        /// invoking `f(gid, &OwnedGlyph)` for each. Holds a read
1646        /// lock for the duration; do not call back into the font
1647        /// from `f`. The cache is populated on demand by
1648        /// [`ParsedFont::get_or_decode_glyph`] (and bulk-prefilled
1649        /// by [`ParsedFont::prime_glyph_cache`]).
1650        pub fn for_each_decoded_glyph<F: FnMut(u16, &OwnedGlyph)>(&self, mut f: F) {
1651            {
1652                // StLock::read() is infallible (Result<_, Infallible>).
1653                let Ok(cache) = self.glyph_cache.read();
1654                for (gid, glyph) in cache.iter() {
1655                    f(*gid, glyph.as_ref());
1656                }
1657            }
1658        }
1659
1660        /// Snapshot of the currently-decoded glyphs as a
1661        /// `BTreeMap<u16, Arc<OwnedGlyph>>`. Cheap (clones the
1662        /// Arcs, not the records). Used by callers that want to
1663        /// hand the map off across an API boundary; for in-place
1664        /// iteration prefer [`ParsedFont::for_each_decoded_glyph`].
1665        pub fn glyph_cache_snapshot(&self) -> BTreeMap<u16, Arc<OwnedGlyph>> {
1666            self.glyph_cache
1667                .read()
1668                .map(|c| c.clone())
1669                .unwrap_or_default()
1670        }
1671
1672        /// Core decode routine: produces one `OwnedGlyph` for `gid` by
1673        /// locking `loca_glyf` and running allsorts' outline visitor +
1674        /// raw-simple-glyph extraction. Factored out so both
1675        /// [`get_or_decode_glyph`] and [`prime_glyph_cache`] share it.
1676        ///
1677        /// Always returns an `OwnedGlyph` — if anything in the decode
1678        /// chain fails, falls back to an empty-outline record with the
1679        /// `hmtx` advance. This mirrors the pre-lazy behaviour where
1680        /// every gid ended up in `glyph_records_decoded`.
1681        fn hmtx_bytes(&self) -> &[u8] {
1682            let (off, len) = self.hmtx_range;
1683            if len == 0 { return &[]; }
1684            self.original_bytes.as_ref()
1685                .map_or(&[], |b| &b.as_ref()[off..off+len])
1686        }
1687
1688        fn vmtx_bytes(&self) -> &[u8] {
1689            let (off, len) = self.vmtx_range;
1690            if len == 0 { return &[]; }
1691            self.original_bytes.as_ref()
1692                .map_or(&[], |b| &b.as_ref()[off..off+len])
1693        }
1694
1695        #[allow(clippy::cast_possible_wrap)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1696        fn decode_glyph_inner(&self, gid: u16) -> OwnedGlyph {
1697            let _p = crate::probe::Probe::span("decode_glyph");
1698            // [az-web-lift] use get_horizontal_advance (reads hmtx directly on the web build)
1699            // instead of allsorts::glyph_info::advance, whose lifted ReadArray parse has an
1700            // un-devirt'd jump table → MISSING_BLOCK → OOB during measure.
1701            let horz_advance = self.get_horizontal_advance(gid);
1702
1703            let mut record = OwnedGlyph {
1704                horz_advance,
1705                bounding_box: OwnedGlyphBoundingBox {
1706                    min_x: 0,
1707                    min_y: 0,
1708                    max_x: horz_advance as i16,
1709                    max_y: 0,
1710                },
1711                outline: Vec::new(),
1712                phantom_points: None,
1713                raw_points: None,
1714                raw_on_curve: None,
1715                raw_contour_ends: None,
1716                instructions: None,
1717            };
1718
1719            // Resolve the `LocaGlyf` for this face. For `Loaded` that's
1720            // a cheap `Arc::clone`; for `Deferred` this is where the
1721            // actual `LocaGlyf::load` happens on first access, paid once
1722            // per face that ever decodes a glyph.
1723            let Some(loca_glyf_arc) = self.resolve_loca_glyf() else {
1724                // No usable loca+glyf → CFF / OpenType-PostScript font
1725                // (Noto Sans/Serif CJK and most .otf). Decode the glyph
1726                // from the `CFF ` table instead; the TrueType-only glyf
1727                // path below can't see these, which left every CFF glyph
1728                // blank on the cpurender/headless path (CJK rendered as
1729                // empty space with the hmtx advance still reserved).
1730                self.decode_cff_glyph_into(gid, &mut record);
1731                return record;
1732            };
1733            let Ok(mut loca_glyf) = loca_glyf_arc.lock() else {
1734                return record;
1735            };
1736
1737            // Visit the outline. If this is a variable font (gvar
1738            // table present) AND we still have source bytes (only
1739            // the `LocaGlyfState::Deferred` path retains them), we
1740            // re-derive a `VariableGlyfContext` here so default-
1741            // instance vs designed-instance differences land in
1742            // the decoded outline. The chained `if let` pattern
1743            // keeps `provider` and `store` in scope for the
1744            // visit, which the borrow checker requires (the
1745            // store's `Cow::Borrowed(&[u8])` tables tie its
1746            // lifetime to the provider).
1747            //
1748            // Eager-`from_bytes` faces (no retained bytes) and
1749            // non-variable fonts skip the var-context machinery
1750            // and decode the default instance — same behaviour as
1751            // before R4.
1752            // [az-web-lift] The lifted web layout NEVER rasterizes (it measures + positions, then
1753            // ships a display list to JS) — so glyph OUTLINES + TrueType hinting raw-points are
1754            // never needed in wasm. Decoding them (allsorts GlyfVisitorContext::visit +
1755            // GlyphOutlineCollector::into_outlines, whose GlyphOutlineOperation match is a 5-arm
1756            // jump table the remill lift doesn't devirtualize → MISSING_BLOCK → OOB) crashes the
1757            // measure pass. Skip BOTH decode passes on the web build; the record keeps its hmtx
1758            // advance/metrics (set above) which is all text measurement needs.
1759            if !cfg!(feature = "web_lift") {
1760            let mut outline_done = false;
1761            if self.is_variable_font {
1762                if let LocaGlyfState::Deferred { bytes, .. } = &self.loca_glyf {
1763                    let scope = ReadScope::new(bytes);
1764                    if let Ok(font_data) =
1765                        scope.read::<FontData<'_>>()
1766                    {
1767                        if let Ok(provider) = font_data.table_provider(self.original_index) {
1768                            if let Ok(store) = VariableGlyfContextStore::read(&provider) {
1769                                if let Ok(var_ctx) = VariableGlyfContext::new(&store) {
1770                                    let mut visitor = GlyfVisitorContext::new(
1771                                        &mut loca_glyf,
1772                                        Some(var_ctx),
1773                                    );
1774                                    let mut collector = GlyphOutlineCollector::new();
1775                                    if visitor.visit(gid, None, &mut collector).is_ok() {
1776                                        record.outline = collector.into_outlines();
1777                                        let (min_x, min_y, max_x, max_y) =
1778                                            compute_outline_bbox(&record.outline);
1779                                        record.bounding_box = OwnedGlyphBoundingBox {
1780                                            min_x,
1781                                            min_y,
1782                                            max_x,
1783                                            max_y,
1784                                        };
1785                                        outline_done = true;
1786                                    }
1787                                }
1788                            }
1789                        }
1790                    }
1791                }
1792            }
1793            if !outline_done {
1794                let mut visitor =
1795                    GlyfVisitorContext::new(&mut loca_glyf, None);
1796                let mut collector = GlyphOutlineCollector::new();
1797                if visitor.visit(gid, None, &mut collector).is_ok() {
1798                    record.outline = collector.into_outlines();
1799                    let (min_x, min_y, max_x, max_y) =
1800                        compute_outline_bbox(&record.outline);
1801                    record.bounding_box = OwnedGlyphBoundingBox {
1802                        min_x,
1803                        min_y,
1804                        max_x,
1805                        max_y,
1806                    };
1807                }
1808            }
1809
1810            // Second pass: pull raw SimpleGlyph data for TrueType
1811            // bytecode hinting. LocaGlyf caches the `Arc<Glyph>`
1812            // internally so this lookup is cheap after the first call.
1813            if let Ok(glyph_arc) = loca_glyf.glyph(gid) {
1814                // `is_on_curve` moved onto the `SimpleGlyphFlagExt` trait in
1815                // allsorts 0.17 (SimpleGlyphFlags is now a BitFlags alias).
1816                use allsorts::tables::glyf::SimpleGlyphFlagExt;
1817                if let allsorts::tables::glyf::Glyph::Simple(sg) = glyph_arc.as_ref() {
1818                    record.raw_points = Some(
1819                        sg.coordinates.iter().map(|(_, pt)| (pt.0, pt.1)).collect(),
1820                    );
1821                    record.raw_on_curve = Some(
1822                        sg.coordinates.iter().map(|(f, _)| f.is_on_curve()).collect(),
1823                    );
1824                    record.raw_contour_ends = Some(sg.end_pts_of_contours.clone());
1825                    record.instructions = Some(sg.instructions.to_vec());
1826                }
1827            }
1828            } // [az-web-lift] end skip glyph outline/hinting decode on web
1829
1830            record
1831        }
1832
1833        /// Decode a single glyph outline from the `CFF ` (OpenType
1834        /// PostScript) table into `record`. Used for fonts with no `glyf`
1835        /// table — `decode_glyph_inner`'s TrueType path returns an empty
1836        /// outline for them, so without this every CFF glyph rasterised as
1837        /// blank on the CPU renderer. Notably this hit ALL CJK text: the
1838        /// installed Noto Sans/Serif CJK fonts are CID-keyed CFF. allsorts'
1839        /// `CFFOutlines` feeds the same `GlyphOutlineCollector` the glyf
1840        /// path uses and resolves CID-keyed local subrs internally.
1841        fn decode_cff_glyph_into(&self, gid: u16, record: &mut OwnedGlyph) {
1842            use allsorts::cff::{outline::CFFOutlines, CFF};
1843
1844            let Some(ref original) = self.original_bytes else {
1845                return;
1846            };
1847            let bytes: &[u8] = original.as_slice();
1848            let Ok(font_data) = ReadScope::new(bytes).read::<FontData<'_>>() else {
1849                return;
1850            };
1851            let Ok(provider) = font_data.table_provider(self.original_index) else {
1852                return;
1853            };
1854            let Ok(Some(cff_data)) = provider.table_data(tag::CFF) else {
1855                return;
1856            };
1857            let Ok(cff) = ReadScope::new(&cff_data).read::<CFF<'_>>() else {
1858                return;
1859            };
1860            let mut outlines = CFFOutlines { table: &cff };
1861            let mut collector = GlyphOutlineCollector::new();
1862            if outlines.visit(gid, None, &mut collector).is_ok() {
1863                record.outline = collector.into_outlines();
1864                let (min_x, min_y, max_x, max_y) = compute_outline_bbox(&record.outline);
1865                record.bounding_box = OwnedGlyphBoundingBox {
1866                    min_x,
1867                    min_y,
1868                    max_x,
1869                    max_y,
1870                };
1871            }
1872        }
1873
1874        /// Parse PDF-specific font metrics from HEAD, HHEA, and OS/2 tables
1875        fn parse_pdf_font_metrics(
1876            font_bytes: &[u8],
1877            font_index: usize,
1878            head_table: &allsorts::tables::HeadTable,
1879            hhea_table: &HheaTable,
1880        ) -> PdfFontMetrics {
1881            use allsorts::{
1882                binary::read::ReadScope,
1883                font_data::FontData,
1884                tables::{os2::Os2, FontTableProvider},
1885                tag,
1886            };
1887
1888            let scope = ReadScope::new(font_bytes);
1889            let font_file = scope.read::<FontData<'_>>().ok();
1890            let provider = font_file
1891                .as_ref()
1892                .and_then(|ff| ff.table_provider(font_index).ok());
1893
1894            let os2_table = provider
1895                .as_ref()
1896                .and_then(|p| p.table_data(tag::OS_2).ok())
1897                .and_then(|os2_data| {
1898                    let data = os2_data?;
1899                    let scope = ReadScope::new(&data);
1900                    scope.read_dep::<Os2>(data.len()).ok()
1901                });
1902
1903            // Base metrics from HEAD and HHEA (always present)
1904            let base = PdfFontMetrics {
1905                units_per_em: head_table.units_per_em,
1906                font_flags: head_table.flags,
1907                x_min: head_table.x_min,
1908                y_min: head_table.y_min,
1909                x_max: head_table.x_max,
1910                y_max: head_table.y_max,
1911                ascender: hhea_table.ascender,
1912                descender: hhea_table.descender,
1913                line_gap: hhea_table.line_gap,
1914                advance_width_max: hhea_table.advance_width_max,
1915                caret_slope_rise: hhea_table.caret_slope_rise,
1916                caret_slope_run: hhea_table.caret_slope_run,
1917                ..PdfFontMetrics::zero()
1918            };
1919
1920            // Add OS/2 metrics if available
1921            os2_table
1922                .map_or(base, |os2| PdfFontMetrics {
1923                    x_avg_char_width: os2.x_avg_char_width,
1924                    us_weight_class: os2.us_weight_class,
1925                    us_width_class: os2.us_width_class,
1926                    y_strikeout_size: os2.y_strikeout_size,
1927                    y_strikeout_position: os2.y_strikeout_position,
1928                    ..base
1929                })
1930        }
1931
1932        /// Returns the width of the space character in font units.
1933        ///
1934        /// This is used internally for text layout calculations.
1935        /// Returns `None` if the font has no space glyph or its width cannot be determined.
1936        fn get_space_width_internal(&self) -> Option<usize> {
1937            if let Some(mock) = self.mock.as_ref() {
1938                return mock.space_width;
1939            }
1940            let glyph_index = self.lookup_glyph_index(' ' as u32)?;
1941
1942            // [az-web-lift] use get_horizontal_advance (direct hmtx on web) instead of
1943            // allsorts::glyph_info::advance (un-devirt'd jump table → OOB).
1944            Some(self.get_horizontal_advance(glyph_index) as usize)
1945        }
1946
1947        /// Look up the glyph index for a Unicode codepoint
1948        pub fn lookup_glyph_index(&self, codepoint: u32) -> Option<u16> {
1949            let cmap = self.cmap_subtable.as_ref()?;
1950            cmap.map_glyph(codepoint).ok().flatten()
1951        }
1952
1953        /// Get the horizontal advance width for a glyph in font units.
1954        ///
1955        /// Pulled straight from the `hmtx` table — no glyph-outline
1956        /// decode. Called once per shaped glyph per layout pass, so
1957        /// avoiding the lazy decode here is a meaningful win over
1958        /// routing through `get_or_decode_glyph`.
1959        pub fn get_horizontal_advance(&self, glyph_index: u16) -> u16 {
1960            if let Some(mock) = self.mock.as_ref() {
1961                return mock.glyph_advances.get(&glyph_index).copied().unwrap_or(0);
1962            }
1963            // [az-web-lift] Read the hmtx advance DIRECTLY (a plain longHorMetric table lookup)
1964            // instead of allsorts::glyph_info::advance, whose lifted binary `ReadArray` parse has
1965            // an un-devirt'd jump table → MISSING_BLOCK → OOB during text measure. Identical result
1966            // for non-variable fonts (the web fallback font is non-variable); native keeps the
1967            // allsorts path (variable-font deltas etc.).
1968            #[cfg(feature = "web_lift")]
1969            {
1970                let hmtx = self.hmtx_bytes();
1971                let num = usize::from(self.hhea_table.num_h_metrics);
1972                if num == 0 {
1973                    return 0;
1974                }
1975                let idx = (glyph_index as usize).min(num - 1);
1976                let off = idx * 4;
1977                return if off + 2 <= hmtx.len() {
1978                    ((hmtx[off] as u16) << 8) | (hmtx[off + 1] as u16)
1979                } else {
1980                    0
1981                };
1982            }
1983            #[cfg(not(feature = "web_lift"))]
1984            {
1985                allsorts::glyph_info::advance(
1986                    &self.maxp_table,
1987                    &self.hhea_table,
1988                    self.hmtx_bytes(),
1989                    glyph_index,
1990                )
1991                .unwrap_or_default()
1992            }
1993        }
1994
1995        /// Get the hinted advance width in pixels for a glyph at the given ppem.
1996        ///
1997        /// For glyphs with outlines, runs TrueType bytecode hinting to get the
1998        /// grid-fitted advance from phantom points. For glyphs without outlines
1999        /// (e.g. space), rounds the scaled advance to the pixel grid, matching
2000        /// `FreeType`'s behavior.
2001        ///
2002        /// Returns `None` if hinting is not available or fails.
2003        #[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
2004        pub fn get_hinted_advance_px(&self, glyph_index: u16, ppem: u16) -> Option<f32> {
2005            // [az-web-lift] No pixel grid-fitting on the web (measure-only): return None so the
2006            // caller falls back to the plain scaled advance. Hard-cfg (not a runtime `if cfg!`)
2007            // so the whole hinting body — get_or_decode_glyph's outline path AND set_ppem →
2008            // allsorts Interpreter::dispatch (opcode jump table → OOB) — is removed from the lift
2009            // closure entirely. SEPARATE concern from the transpiler's jump-table devirt: web has
2010            // no use for hinted advances regardless of lift quality. Native is unchanged.
2011            #[cfg(feature = "web_lift")]
2012            {
2013                let _ = (glyph_index, ppem);
2014                None
2015            }
2016            #[cfg(not(feature = "web_lift"))]
2017            {
2018            use allsorts::hinting::f26dot6::{compute_scale, F26Dot6};
2019            let glyph = self.get_or_decode_glyph(glyph_index)?;
2020
2021            let upem = self.font_metrics.units_per_em;
2022            if upem == 0 || ppem == 0 {
2023                return None;
2024            }
2025
2026            // Check if we even have a hint instance
2027            let hint_mutex = self.hint_instance.as_ref()?;
2028
2029            let scale = compute_scale(ppem, upem);
2030            // Round the LIVE hmtx advance, not the decoded glyph's cached
2031            // `horz_advance`. The space glyph is eagerly pre-cached during
2032            // `from_bytes_internal` before `original_bytes` is attached, so its
2033            // cached advance can be a stale 0; and this function only ever rounds
2034            // the scaled hmtx advance to the pixel grid anyway (see below), never
2035            // the hinted phantom point. For every correctly-decoded glyph the two
2036            // are identical, so this is a no-op except for the stale-space case.
2037            let hmtx_advance = self.get_horizontal_advance(glyph_index);
2038            let adv_f26dot6 = F26Dot6::from_funits(i32::from(hmtx_advance), scale);
2039
2040            // For glyphs with outline data, run bytecode hinting
2041            if let (Some(raw_points), Some(raw_on_curve), Some(raw_contour_ends)) = (
2042                glyph.raw_points.as_ref(),
2043                glyph.raw_on_curve.as_ref(),
2044                glyph.raw_contour_ends.as_ref(),
2045            ) {
2046                let instructions = glyph.instructions.as_deref().unwrap_or(&[]);
2047                let mut hint = hint_mutex.lock().ok()?;
2048                hint.set_ppem(ppem, f64::from(ppem)).ok()?;
2049                drop(hint);
2050
2051                let points_f26dot6: Vec<(i32, i32)> = raw_points
2052                    .iter()
2053                    .map(|&(x, y)| {
2054                        let sx = F26Dot6::from_funits(i32::from(x), scale);
2055                        let sy = F26Dot6::from_funits(i32::from(y), scale);
2056                        (sx.to_bits(), sy.to_bits())
2057                    })
2058                    .collect();
2059            }
2060
2061            // Use the scaled advance rounded to pixel grid, NOT the hinted
2062            // phantom point.  Some glyph programs apply ClearType-specific SHPIX
2063            // adjustments to the advance phantom point that are wrong for
2064            // non-ClearType rendering.  The rounded scaled advance matches
2065            // FreeType's DEFAULT mode advance output (and, for glyphs without an
2066            // outline such as space, FreeType's phantom-point pre-rounding).
2067            let rounded = (adv_f26dot6.to_bits() + 32) & !63;
2068            Some(rounded as f32 / 64.0)
2069            } // [az-web-lift] end #[cfg(not(web_lift))] hinting body
2070        }
2071
2072        /// Get the number of glyphs in this font
2073        pub const fn num_glyphs(&self) -> u16 {
2074            self.num_glyphs
2075        }
2076
2077        /// Check if this font has a glyph for the given codepoint
2078        pub fn has_glyph(&self, codepoint: u32) -> bool {
2079            self.lookup_glyph_index(codepoint).is_some()
2080        }
2081
2082        /// Get vertical metrics for a glyph (for vertical text layout).
2083        ///
2084        /// Uses vhea+vmtx tables (same binary format as hhea+hmtx).
2085        /// Returns None if font has no vertical metrics tables.
2086        #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2087        pub fn get_vertical_metrics(
2088            &self,
2089            glyph_id: u16,
2090        ) -> Option<crate::text3::cache::VerticalMetrics> {
2091            let vhea = self.vhea_table.as_ref()?;
2092            if self.vmtx_range.1 == 0 {
2093                return None;
2094            }
2095            let vert_advance = f32::from(allsorts::glyph_info::advance(
2096                &self.maxp_table, vhea, self.vmtx_bytes(), glyph_id,
2097            ).ok()?);
2098
2099            let units_per_em = f32::from(self.font_metrics.units_per_em);
2100            let scale = if units_per_em > 0.0 { 1.0 / units_per_em } else { 0.001 };
2101
2102            // Vertical bearing: approximate from glyph bbox if available
2103            let (bearing_x, bearing_y) = self.get_or_decode_glyph(glyph_id)
2104                .map_or((0.0, 0.0), |g| {
2105                    let bbox = &g.bounding_box;
2106                    // tsb (top side bearing): origin_y - max_y
2107                    // lsb for vertical: center the glyph horizontally
2108                    let width = f32::from(bbox.max_x - bbox.min_x);
2109                    (-(width / 2.0) * scale, (vert_advance * scale) - (f32::from(bbox.max_y) * scale))
2110                });
2111
2112            Some(crate::text3::cache::VerticalMetrics {
2113                advance: vert_advance * scale,
2114                bearing_x,
2115                bearing_y,
2116                origin_y: self.font_metrics.ascent * scale,
2117            })
2118        }
2119
2120        /// Get layout-specific font metrics
2121        #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2122        pub fn get_font_metrics(&self) -> LayoutFontMetrics {
2123            // Ensure descent is positive (OpenType may have negative descent)
2124            let descent = if self.font_metrics.descent > 0.0 {
2125                self.font_metrics.descent
2126            } else {
2127                -self.font_metrics.descent
2128            };
2129
2130            LayoutFontMetrics {
2131                ascent: self.font_metrics.ascent,
2132                descent,
2133                line_gap: self.font_metrics.line_gap,
2134                units_per_em: self.font_metrics.units_per_em,
2135                x_height: self.font_metrics.x_height,
2136                cap_height: self.font_metrics.cap_height,
2137            }
2138        }
2139
2140        /// Convert the `ParsedFont` back to bytes using `allsorts::whole_font`
2141        /// This reconstructs the entire font from the parsed data
2142        ///
2143        /// Source bytes come from either the explicit
2144        /// [`ParsedFont::with_source_bytes`] handle (PDF-first
2145        /// construction) *or* the `LocaGlyfState::Deferred` slot
2146        /// installed by [`ParsedFont::from_bytes_shared`]. The
2147        /// production lazy path retains bytes for the lazy `LocaGlyf`
2148        /// loader, so PDF subsetting Just Works without an extra
2149        /// `with_source_bytes` call.
2150        ///
2151        /// # Arguments
2152        /// * `tags` - Optional list of specific table tags to include (None = all tables)
2153        /// # Errors
2154        ///
2155        /// Returns an error string if serializing the font fails.
2156        pub fn to_bytes(&self, tags: Option<&[u32]>) -> Result<Vec<u8>, String> {
2157            let source = self.source_bytes_for_subset().ok_or_else(|| {
2158                "ParsedFont::to_bytes requires source bytes; construct via \
2159                 ParsedFont::from_bytes_shared (production lazy path) or \
2160                 attach via ParsedFont::with_source_bytes"
2161                    .to_string()
2162            })?;
2163            let scope = ReadScope::new(source.as_slice());
2164            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
2165            let provider = font_file
2166                .table_provider(self.original_index)
2167                .map_err(|e| e.to_string())?;
2168
2169            let tags_to_use = tags.unwrap_or(&[
2170                tag::CMAP,
2171                tag::HEAD,
2172                tag::HHEA,
2173                tag::HMTX,
2174                tag::MAXP,
2175                tag::NAME,
2176                tag::OS_2,
2177                tag::POST,
2178                tag::GLYF,
2179                tag::LOCA,
2180            ]);
2181
2182            whole_font(&provider, tags_to_use).map_err(|e| e.to_string())
2183        }
2184
2185        /// Create a subset font containing only the specified glyph IDs
2186        /// Returns the subset font bytes and a mapping from old to new glyph IDs
2187        ///
2188        /// # Arguments
2189        /// * `glyph_ids` - The glyph IDs to include in the subset (glyph 0/.notdef is always
2190        ///   included)
2191        /// * `cmap_target` - Target cmap format (Unicode for web, `MacRoman` for compatibility)
2192        ///
2193        /// # Returns
2194        /// A tuple of (`subset_font_bytes`, `glyph_mapping`) where `glyph_mapping` maps
2195        /// `original_glyph_id` -> (`new_glyph_id`, `original_char`)
2196        #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
2197        /// # Errors
2198        ///
2199        /// Returns an error string if subsetting the font fails.
2200        pub fn subset(
2201            &self,
2202            glyph_ids: &[(u16, char)],
2203            cmap_target: CmapTarget,
2204        ) -> Result<(Vec<u8>, BTreeMap<u16, (u16, char)>), String> {
2205            let source = self.source_bytes_for_subset().ok_or_else(|| {
2206                "ParsedFont::subset requires source bytes; construct via \
2207                 ParsedFont::from_bytes_shared (production lazy path) or \
2208                 attach via ParsedFont::with_source_bytes"
2209                    .to_string()
2210            })?;
2211            let scope = ReadScope::new(source.as_slice());
2212            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
2213            let provider = font_file
2214                .table_provider(self.original_index)
2215                .map_err(|e| e.to_string())?;
2216
2217            // Build glyph mapping: original_id -> (new_id, char)
2218            let glyph_mapping: BTreeMap<u16, (u16, char)> = glyph_ids
2219                .iter()
2220                .enumerate()
2221                .map(|(new_id, &(original_id, ch))| (original_id, (new_id as u16, ch)))
2222                .collect();
2223
2224            // Extract just the glyph IDs for subsetting
2225            let ids: Vec<u16> = glyph_ids.iter().map(|(id, _)| *id).collect();
2226
2227            // Use PDF profile for embedding fonts in PDFs
2228            let font_bytes = allsorts_subset(&provider, &ids, &SubsetProfile::Pdf, cmap_target)
2229                .map_err(|e| format!("Subset error: {e:?}"))?;
2230
2231            Ok((font_bytes, glyph_mapping))
2232        }
2233
2234        /// Get the width of a glyph in font units (internal, unscaled)
2235        pub fn get_glyph_width_internal(&self, glyph_index: u16) -> Option<usize> {
2236            allsorts::glyph_info::advance(
2237                &self.maxp_table,
2238                &self.hhea_table,
2239                self.hmtx_bytes(),
2240                glyph_index,
2241            )
2242            .ok()
2243            .map(|s| s as usize)
2244        }
2245
2246        /// Get the width of the space character (unscaled font units)
2247        #[inline]
2248        pub const fn get_space_width(&self) -> Option<usize> {
2249            self.space_width
2250        }
2251
2252        /// Add glyph-to-text mapping to reverse cache
2253        /// This should be called during text shaping when we know both the source text and
2254        /// resulting glyphs
2255        pub fn cache_glyph_mapping(&mut self, glyph_id: u16, cluster_text: &str) {
2256            self.reverse_glyph_cache
2257                .insert(glyph_id, cluster_text.to_string());
2258        }
2259
2260        /// Get the cluster text that produced a specific glyph ID
2261        /// Returns the original text that was shaped into this glyph (handles ligatures correctly)
2262        pub fn get_glyph_cluster_text(&self, glyph_id: u16) -> Option<&str> {
2263            self.reverse_glyph_cache.get(&glyph_id).map(String::as_str)
2264        }
2265
2266        /// Get the first character from the cluster text for a glyph ID
2267        /// This is useful for PDF `ToUnicode` `CMap` generation which requires single character
2268        /// mappings
2269        pub fn get_glyph_primary_char(&self, glyph_id: u16) -> Option<char> {
2270            self.reverse_glyph_cache
2271                .get(&glyph_id)
2272                .and_then(|text| text.chars().next())
2273        }
2274
2275        /// Clear the reverse glyph cache (useful for memory management)
2276        pub fn clear_glyph_cache(&mut self) {
2277            self.reverse_glyph_cache.clear();
2278        }
2279
2280        /// Get the bounding box size of a glyph (unscaled units) - for PDF
2281        /// Returns (width, height) in font units
2282        pub fn get_glyph_bbox_size(&self, glyph_index: u16) -> Option<(i32, i32)> {
2283            let g = self.get_or_decode_glyph(glyph_index)?;
2284            let glyph_width = i32::from(g.horz_advance);
2285            let glyph_height = i32::from(g.bounding_box.max_y) - i32::from(g.bounding_box.min_y);
2286            Some((glyph_width, glyph_height))
2287        }
2288    }
2289
2290    /// Compute the bounding box from collected glyph outlines.
2291    fn compute_outline_bbox(outlines: &[GlyphOutline]) -> (i16, i16, i16, i16) {
2292        let mut min_x = i16::MAX;
2293        let mut min_y = i16::MAX;
2294        let mut max_x = i16::MIN;
2295        let mut max_y = i16::MIN;
2296        let mut has_points = false;
2297
2298        for outline in outlines {
2299            for op in outline.operations.as_slice() {
2300                let points: &[(i16, i16)] = match op {
2301                    GlyphOutlineOperation::MoveTo(m) => &[(m.x, m.y)],
2302                    GlyphOutlineOperation::LineTo(l) => &[(l.x, l.y)],
2303                    GlyphOutlineOperation::QuadraticCurveTo(q) => {
2304                        // Check both control and end point for bbox
2305                        min_x = min_x.min(q.ctrl_1_x).min(q.end_x);
2306                        min_y = min_y.min(q.ctrl_1_y).min(q.end_y);
2307                        max_x = max_x.max(q.ctrl_1_x).max(q.end_x);
2308                        max_y = max_y.max(q.ctrl_1_y).max(q.end_y);
2309                        has_points = true;
2310                        continue;
2311                    }
2312                    GlyphOutlineOperation::CubicCurveTo(c) => {
2313                        min_x = min_x.min(c.ctrl_1_x).min(c.ctrl_2_x).min(c.end_x);
2314                        min_y = min_y.min(c.ctrl_1_y).min(c.ctrl_2_y).min(c.end_y);
2315                        max_x = max_x.max(c.ctrl_1_x).max(c.ctrl_2_x).max(c.end_x);
2316                        max_y = max_y.max(c.ctrl_1_y).max(c.ctrl_2_y).max(c.end_y);
2317                        has_points = true;
2318                        continue;
2319                    }
2320                    GlyphOutlineOperation::ClosePath => continue,
2321                };
2322                for &(x, y) in points {
2323                    min_x = min_x.min(x);
2324                    min_y = min_y.min(y);
2325                    max_x = max_x.max(x);
2326                    max_y = max_y.max(y);
2327                    has_points = true;
2328                }
2329            }
2330        }
2331
2332        if has_points {
2333            (min_x, min_y, max_x, max_y)
2334        } else {
2335            (0, 0, 0, 0)
2336        }
2337    }
2338
2339    #[derive(Debug, Clone)]
2340    pub struct OwnedGlyph {
2341        pub bounding_box: OwnedGlyphBoundingBox,
2342        pub horz_advance: u16,
2343        pub outline: Vec<GlyphOutline>,
2344        pub phantom_points: Option<[Point; 4]>,
2345        /// Raw TrueType points in font units (for hinting). None for composite/CFF glyphs.
2346        pub raw_points: Option<Vec<(i16, i16)>>,
2347        /// On-curve flags for each raw point.
2348        pub raw_on_curve: Option<Vec<bool>>,
2349        /// Contour end-point indices (TrueType).
2350        pub raw_contour_ends: Option<Vec<u16>>,
2351        /// Per-glyph TrueType hinting instructions.
2352        pub instructions: Option<Vec<u8>>,
2353    }
2354
2355    // --- ParsedFontTrait Implementation for ParsedFont ---
2356
2357    impl crate::text3::cache::ShallowClone for ParsedFont {
2358        fn shallow_clone(&self) -> Self {
2359            self.clone() // ParsedFont::clone uses Arc internally, so it's shallow
2360        }
2361    }
2362
2363    impl crate::text3::cache::ParsedFontTrait for ParsedFont {
2364        fn shape_text(
2365            &self,
2366            text: &str,
2367            script: crate::font_traits::Script,
2368            language: crate::font_traits::Language,
2369            direction: crate::font_traits::BidiDirection,
2370            style: &crate::font_traits::StyleProperties,
2371        ) -> Result<Vec<crate::font_traits::Glyph>, crate::font_traits::LayoutError> {
2372            // Call the existing shape_text_for_parsed_font method (defined in default.rs)
2373            crate::text3::default::shape_text_for_parsed_font(
2374                self, text, script, language, direction, style,
2375            )
2376        }
2377
2378        fn get_hash(&self) -> u64 {
2379            self.hash
2380        }
2381
2382        fn get_glyph_size(
2383            &self,
2384            glyph_id: u16,
2385            font_size_px: f32,
2386        ) -> Option<azul_core::geom::LogicalSize> {
2387            self.get_or_decode_glyph(glyph_id).map(|record| {
2388                let units_per_em = f32::from(self.font_metrics.units_per_em);
2389                let scale_factor = if units_per_em > 0.0 {
2390                    font_size_px / units_per_em
2391                } else {
2392                    0.01
2393                };
2394                let bbox = &record.bounding_box;
2395                azul_core::geom::LogicalSize {
2396                    width: f32::from(bbox.max_x - bbox.min_x) * scale_factor,
2397                    height: f32::from(bbox.max_y - bbox.min_y) * scale_factor,
2398                }
2399            })
2400        }
2401
2402        fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
2403            let glyph_id = self.lookup_glyph_index('-' as u32)?;
2404            let advance_units = self.get_horizontal_advance(glyph_id);
2405            let scale_factor = if self.font_metrics.units_per_em > 0 {
2406                font_size / f32::from(self.font_metrics.units_per_em)
2407            } else {
2408                return None;
2409            };
2410            let scaled_advance = f32::from(advance_units) * scale_factor;
2411            Some((glyph_id, scaled_advance))
2412        }
2413
2414        fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
2415            let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
2416            let advance_units = self.get_horizontal_advance(glyph_id);
2417            let scale_factor = if self.font_metrics.units_per_em > 0 {
2418                font_size / f32::from(self.font_metrics.units_per_em)
2419            } else {
2420                return None;
2421            };
2422            let scaled_advance = f32::from(advance_units) * scale_factor;
2423            Some((glyph_id, scaled_advance))
2424        }
2425
2426        fn has_glyph(&self, codepoint: u32) -> bool {
2427            self.lookup_glyph_index(codepoint).is_some()
2428        }
2429
2430        fn get_vertical_metrics(
2431            &self,
2432            glyph_id: u16,
2433        ) -> Option<crate::text3::cache::VerticalMetrics> {
2434            self.get_vertical_metrics(glyph_id)
2435        }
2436
2437        fn get_font_metrics(&self) -> LayoutFontMetrics {
2438            self.font_metrics
2439        }
2440
2441        fn num_glyphs(&self) -> u16 {
2442            self.num_glyphs
2443        }
2444
2445        fn get_space_width(&self) -> Option<usize> {
2446            self.space_width
2447        }
2448    }
2449
2450    /// Build an agg-rust `PathStorage` from an `OwnedGlyph` outline (in font units, Y-up → Y-down).
2451    ///
2452    /// Returns `None` if the glyph has no outline operations (e.g. space).
2453    /// The caller is responsible for applying scale and translation transforms.
2454    #[cfg(feature = "cpurender")]
2455    #[must_use] pub fn build_glyph_path(glyph: &OwnedGlyph) -> Option<agg_rust::path_storage::PathStorage> {
2456        use agg_rust::{basics::PATH_FLAGS_NONE, path_storage::PathStorage};
2457
2458        let mut path = PathStorage::new();
2459        let mut has_ops = false;
2460        for outline in &glyph.outline {
2461            for op in outline.operations.as_slice() {
2462                has_ops = true;
2463                match op {
2464                    GlyphOutlineOperation::MoveTo(OutlineMoveTo { x, y }) => {
2465                        path.move_to(f64::from(*x), -f64::from(*y));
2466                    }
2467                    GlyphOutlineOperation::LineTo(OutlineLineTo { x, y }) => {
2468                        path.line_to(f64::from(*x), -f64::from(*y));
2469                    }
2470                    GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
2471                        ctrl_1_x, ctrl_1_y, end_x, end_y,
2472                    }) => {
2473                        path.curve3(
2474                            f64::from(*ctrl_1_x), -f64::from(*ctrl_1_y),
2475                            f64::from(*end_x), -f64::from(*end_y),
2476                        );
2477                    }
2478                    GlyphOutlineOperation::CubicCurveTo(OutlineCubicTo {
2479                        ctrl_1_x, ctrl_1_y, ctrl_2_x, ctrl_2_y, end_x, end_y,
2480                    }) => {
2481                        path.curve4(
2482                            f64::from(*ctrl_1_x), -f64::from(*ctrl_1_y),
2483                            f64::from(*ctrl_2_x), -f64::from(*ctrl_2_y),
2484                            f64::from(*end_x), -f64::from(*end_y),
2485                        );
2486                    }
2487                    GlyphOutlineOperation::ClosePath => {
2488                        path.close_polygon(PATH_FLAGS_NONE);
2489                    }
2490                }
2491            }
2492        }
2493        if !has_ops {
2494            return None;
2495        }
2496        Some(path)
2497    }
2498}