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

            
10
use azul_css::{AzString, U8Vec};
11
use rust_fontconfig::{FcFontCache, OwnedFontSource};
12

            
13
pub 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
2415
    pub fn build_font_cache() -> FcFontCache {
25
2415
        FcFontCache::build()
26
2415
    }
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::*;
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
}
57
pub 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
        pub fn new(font_metrics: LayoutFontMetrics) -> Self {
88
            MockFont {
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
        pub 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
        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
        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
        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

            
123
pub 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
66850
    pub(crate) fn monotonic_now_nanos() -> u64 {
167
        // Safe: `Instant::elapsed` against the same launch instant is
168
        // monotonic and never overflows in any realistic process
169
        // lifetime (>500 years).
170
        use std::sync::OnceLock;
171
        use std::time::Instant;
172
        static LAUNCH: OnceLock<Instant> = OnceLock::new();
173
66850
        let start = LAUNCH.get_or_init(Instant::now);
174
66850
        start.elapsed().as_nanos() as u64
175
66850
    }
176

            
177
    /// Glyph-outline decoder state. See the
178
    /// [`ParsedFont::loca_glyf`] field docs for the full description.
179
    #[derive(Clone)]
180
    pub(crate) enum LocaGlyfState {
181
        /// Ready to decode immediately, or known to have no outline
182
        /// data. `None` covers both CFF fonts and fonts where the
183
        /// loca+glyf parse failed.
184
        ///
185
        /// This variant *cannot* be evicted by
186
        /// [`crate::text3::cache::FontManager::evict_unused`]: there
187
        /// are no source bytes retained to re-decode from. The eager
188
        /// `from_bytes` path (tests, `with_source_bytes` PDF callers)
189
        /// produces this variant.
190
        Loaded(Option<Arc<std::sync::Mutex<LocaGlyf>>>),
191
        /// Font bytes retained for lazy `LocaGlyf` construction.
192
        ///
193
        /// `loaded` is `Mutex<Option<…>>` (not `OnceLock`) so an
194
        /// idle eviction can clear it back to `None`; the next
195
        /// `get_or_decode_glyph` will re-parse from `bytes`. Two-step
196
        /// double-check pattern in `resolve_loca_glyf` keeps the
197
        /// expensive `LocaGlyf::load` outside the critical section.
198
        Deferred {
199
            bytes: Arc<rust_fontconfig::FontBytes>,
200
            font_index: usize,
201
            loaded: Arc<std::sync::Mutex<Option<Arc<std::sync::Mutex<LocaGlyf>>>>>,
202
        },
203
    }
204

            
205
    /// Adapter that collects allsorts outline commands into our `GlyphOutline` format.
206
    ///
207
    /// Implements `OutlineSink` so it can be passed to `GlyfVisitorContext::visit()`.
208
    /// This handles composite glyph resolution, transforms, and variable font
209
    /// deltas automatically via allsorts internals.
210
    struct GlyphOutlineCollector {
211
        contours: Vec<GlyphOutline>,
212
        current_contour: Vec<GlyphOutlineOperation>,
213
    }
214

            
215
    impl GlyphOutlineCollector {
216
25095
        fn new() -> Self {
217
25095
            Self {
218
25095
                contours: Vec::new(),
219
25095
                current_contour: Vec::new(),
220
25095
            }
221
25095
        }
222

            
223
25095
        fn into_outlines(mut self) -> Vec<GlyphOutline> {
224
25095
            if !self.current_contour.is_empty() {
225
                self.contours.push(GlyphOutline {
226
                    operations: std::mem::take(&mut self.current_contour).into(),
227
                });
228
25095
            }
229
25095
            self.contours
230
25095
        }
231
    }
232

            
233
    impl OutlineSink for GlyphOutlineCollector {
234
33495
        fn move_to(&mut self, to: Vector2F) {
235
33495
            if !self.current_contour.is_empty() {
236
                self.contours.push(GlyphOutline {
237
                    operations: std::mem::take(&mut self.current_contour).into(),
238
                });
239
33495
            }
240
33495
            self.current_contour.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo {
241
33495
                x: to.x() as i16,
242
33495
                y: to.y() as i16,
243
33495
            }));
244
33495
        }
245

            
246
247765
        fn line_to(&mut self, to: Vector2F) {
247
247765
            self.current_contour.push(GlyphOutlineOperation::LineTo(OutlineLineTo {
248
247765
                x: to.x() as i16,
249
247765
                y: to.y() as i16,
250
247765
            }));
251
247765
        }
252

            
253
229600
        fn quadratic_curve_to(&mut self, ctrl: Vector2F, to: Vector2F) {
254
229600
            self.current_contour.push(GlyphOutlineOperation::QuadraticCurveTo(
255
229600
                OutlineQuadTo {
256
229600
                    ctrl_1_x: ctrl.x() as i16,
257
229600
                    ctrl_1_y: ctrl.y() as i16,
258
229600
                    end_x: to.x() as i16,
259
229600
                    end_y: to.y() as i16,
260
229600
                },
261
229600
            ));
262
229600
        }
263

            
264
        fn cubic_curve_to(&mut self, ctrl: LineSegment2F, to: Vector2F) {
265
            self.current_contour.push(GlyphOutlineOperation::CubicCurveTo(
266
                OutlineCubicTo {
267
                    ctrl_1_x: ctrl.from_x() as i16,
268
                    ctrl_1_y: ctrl.from_y() as i16,
269
                    ctrl_2_x: ctrl.to_x() as i16,
270
                    ctrl_2_y: ctrl.to_y() as i16,
271
                    end_x: to.x() as i16,
272
                    end_y: to.y() as i16,
273
                },
274
            ));
275
        }
276

            
277
33495
        fn close(&mut self) {
278
33495
            self.current_contour.push(GlyphOutlineOperation::ClosePath);
279
33495
            self.contours.push(GlyphOutline {
280
33495
                operations: std::mem::take(&mut self.current_contour).into(),
281
33495
            });
282
33495
        }
283
    }
284

            
285
    /// Parsed font data with all required tables for text layout and PDF generation.
286
    ///
287
    /// This struct holds the parsed representation of a TrueType/OpenType font,
288
    /// including glyph outlines, metrics, and shaping tables. It's used for:
289
    /// - Text layout (via GSUB/GPOS tables)
290
    /// - Glyph rendering (via glyf/CFF outlines)
291
    /// - PDF font embedding (via font metrics and subsetting)
292
    pub struct ParsedFont {
293
        /// Hash of the font bytes for caching and equality checks.
294
        pub hash: u64,
295
        /// Layout-specific font metrics (ascent, descent, line gap).
296
        pub font_metrics: LayoutFontMetrics,
297
        /// PDF-specific detailed font metrics from HEAD, HHEA, OS/2 tables.
298
        pub pdf_font_metrics: PdfFontMetrics,
299
        /// Total number of glyphs in the font (from maxp table).
300
        pub num_glyphs: u16,
301
        /// Horizontal header table (hhea) containing global horizontal metrics.
302
        pub hhea_table: HheaTable,
303
        /// Offset+length into original_bytes for hmtx table (lazy: no copy).
304
        pub hmtx_range: (usize, usize),
305
        /// Offset+length into original_bytes for vmtx table (lazy: no copy).
306
        pub vmtx_range: (usize, usize),
307
        /// Vertical header table (vhea), same format as hhea. None if font has no vertical metrics.
308
        pub vhea_table: Option<HheaTable>,
309
        /// Maximum profile table (maxp) containing glyph count and memory hints.
310
        pub maxp_table: MaxpTable,
311
        /// Raw GSUB table bytes, kept as a `Vec<u8>` (tens to low-hundreds
312
        /// of KiB) so the parsed `GsubCache` can be built on first shape
313
        /// call instead of up-front. Access via [`ParsedFont::gsub`] —
314
        /// that getter populates `gsub_cache_lazy` via `OnceLock` and
315
        /// returns a borrow.
316
        pub(crate) gsub_bytes: Option<Vec<u8>>,
317
        /// Lazy GSUB cache: populated on first [`ParsedFont::gsub`] call.
318
        /// `None` means "font has no GSUB table" *after* init attempt;
319
        /// the `OnceLock` wrapper distinguishes "not yet initialised"
320
        /// from "initialised to None".
321
        pub(crate) gsub_cache_lazy: std::sync::OnceLock<Option<GsubCache>>,
322
        /// Raw GPOS table bytes. Same lazy-parse arrangement as
323
        /// `gsub_bytes` — see [`ParsedFont::gpos`].
324
        pub(crate) gpos_bytes: Option<Vec<u8>>,
325
        /// Lazy GPOS cache, populated on first [`ParsedFont::gpos`] call.
326
        pub(crate) gpos_cache_lazy: std::sync::OnceLock<Option<GposCache>>,
327
        /// Glyph definition table (GDEF) for glyph classification.
328
        pub opt_gdef_table: Option<Arc<GDEFTable>>,
329
        /// Legacy kerning table (kern) for fonts without GPOS.
330
        pub opt_kern_table: Option<Arc<KernTable>>,
331
        /// Monotonic-clock nanos at the most recent
332
        /// [`ParsedFont::get_or_decode_glyph`] / `gsub()` / `gpos()`
333
        /// call. `0` means "never touched". Used by
334
        /// [`crate::text3::cache::FontManager::evict_unused`] to
335
        /// decide which `LocaGlyfState::Deferred` faces to release.
336
        pub(crate) last_used: Arc<std::sync::atomic::AtomicU64>,
337
        /// `true` if this font is a variable font (carries a `gvar`
338
        /// table). Cached at parse time so [`decode_glyph_inner`]
339
        /// can short-circuit the variable-context construction for
340
        /// the common non-variable case. Variable-glyph delta
341
        /// application requires the source bytes to be retained,
342
        /// so it only fires on the `LocaGlyfState::Deferred` path.
343
        pub(crate) is_variable_font: bool,
344
        /// Lazy outline cache. Populated on first
345
        /// [`ParsedFont::get_or_decode_glyph`] call per `gid`; entries
346
        /// are wrapped in `Arc` so callers can hold them without
347
        /// keeping the lock. The space glyph (and `.notdef` when
348
        /// present) are pre-inserted by `from_bytes_internal` so the
349
        /// shaper's cmap-miss path has something to render without
350
        /// racing with a decode.
351
        ///
352
        /// Tests that previously walked the public `glyph_records_decoded`
353
        /// `BTreeMap` field now call
354
        /// [`ParsedFont::prime_glyph_cache`] (decodes every glyph into
355
        /// this cache) followed by
356
        /// [`ParsedFont::for_each_decoded_glyph`] /
357
        /// [`ParsedFont::glyph_cache_snapshot`] to walk the result.
358
        pub(crate) glyph_cache: Arc<std::sync::RwLock<BTreeMap<u16, Arc<OwnedGlyph>>>>,
359
        /// Glyph outline decoder state.
360
        ///
361
        /// - `Loaded(Some(arc))`: `LocaGlyf` is already loaded (owning
362
        ///   its own `Box<[u8]>` copy of the loca+glyf tables) and
363
        ///   ready to decode glyphs. Produced by the eager `from_bytes`
364
        ///   constructor path (tests).
365
        /// - `Loaded(None)`: the font has no usable loca+glyf (CFF, or
366
        ///   a parse failure). Glyph outlines won't decode; the hmtx
367
        ///   advance fallback fills in the blanks.
368
        /// - `Deferred`: we retain an `Arc<[u8]>` to the full font file
369
        ///   and the `font_index`; the first `get_or_decode_glyph` call
370
        ///   parses a fresh `FontData` / `TableProvider` from those
371
        ///   bytes and loads `LocaGlyf`, storing the result in the
372
        ///   `OnceLock`. Fonts that get resolved into a chain but are
373
        ///   never actually rasterized pay zero decode cost — this is
374
        ///   the big win for pages like `excel.html` where 20+ fallback
375
        ///   faces load but only a handful are touched.
376
        pub(crate) loca_glyf: LocaGlyfState,
377
        /// Cached width of the space character in font units.
378
        pub space_width: Option<usize>,
379
        /// Character-to-glyph mapping (cmap subtable).
380
        pub cmap_subtable: Option<OwnedCmapSubtable>,
381
        /// Mock font data for testing (replaces real font behavior).
382
        pub mock: Option<Box<MockFont>>,
383
        /// Reverse mapping: glyph_id -> cluster text (handles ligatures like "fi").
384
        pub reverse_glyph_cache: std::collections::BTreeMap<u16, String>,
385
        /// Original font bytes — only retained for callers that need to
386
        /// reconstruct or subset the font (PDF export). Layout / shaping /
387
        /// raster never read this, so `ParsedFont::from_bytes` leaves it
388
        /// as `None` by default and callers opt in via
389
        /// [`ParsedFont::with_source_bytes`]. Shared across faces of the
390
        /// same `.ttc` via the `Arc<FontBytes>` that
391
        /// [`rust_fontconfig::FcFontCache::get_font_bytes`] returns —
392
        /// for disk fonts the backing is an mmap so untouched pages
393
        /// don't count toward RSS.
394
        pub original_bytes: Option<std::sync::Arc<rust_fontconfig::FontBytes>>,
395
        /// Font index within collection (0 for single-font files).
396
        pub original_index: usize,
397
        /// GID to CID mapping for CFF fonts (required for PDF embedding).
398
        pub index_to_cid: BTreeMap<u16, u16>,
399
        /// Font type (TrueType outlines or OpenType CFF).
400
        pub font_type: FontType,
401
        /// PostScript font name from the NAME table.
402
        pub font_name: Option<String>,
403
        /// TrueType bytecode hinting instance (mutable interpreter state).
404
        /// Wrapped in Mutex because hinting mutates internal state.
405
        /// None for CFF fonts or fonts without hinting data.
406
        pub hint_instance: Option<std::sync::Mutex<allsorts::hinting::HintInstance>>,
407
    }
408

            
409
    impl Clone for ParsedFont {
410
        fn clone(&self) -> Self {
411
            ParsedFont {
412
                hash: self.hash,
413
                font_metrics: self.font_metrics.clone(),
414
                pdf_font_metrics: self.pdf_font_metrics,
415
                num_glyphs: self.num_glyphs,
416
                hhea_table: self.hhea_table.clone(),
417
                hmtx_range: self.hmtx_range,
418
                vmtx_range: self.vmtx_range,
419
                vhea_table: self.vhea_table.clone(),
420
                maxp_table: self.maxp_table.clone(),
421
                // OnceLock<T: Clone>: Clone preserves the init state, so
422
                // a clone of a parsed cache skips re-parse on first
423
                // access. The raw bytes we keep around for lazy init
424
                // are cloned too.
425
                gsub_bytes: self.gsub_bytes.clone(),
426
                gsub_cache_lazy: self.gsub_cache_lazy.clone(),
427
                gpos_bytes: self.gpos_bytes.clone(),
428
                gpos_cache_lazy: self.gpos_cache_lazy.clone(),
429
                opt_gdef_table: self.opt_gdef_table.clone(),
430
                opt_kern_table: self.opt_kern_table.clone(),
431
                // Share the lazy cache and loca_glyf across clones: cheap
432
                // Arc bump, amortises glyph decode across clones of the
433
                // same face.
434
                last_used: Arc::clone(&self.last_used),
435
                is_variable_font: self.is_variable_font,
436
                glyph_cache: Arc::clone(&self.glyph_cache),
437
                // `LocaGlyfState` is `Clone` — for `Loaded` this is an
438
                // `Arc::clone`; for `Deferred` it's an `Arc::clone` of
439
                // the bytes + the `OnceLock`, so a clone of a face
440
                // that's already decoded glyphs carries the decode.
441
                loca_glyf: self.loca_glyf.clone(),
442
                space_width: self.space_width,
443
                cmap_subtable: self.cmap_subtable.clone(),
444
                mock: self.mock.clone(),
445
                reverse_glyph_cache: self.reverse_glyph_cache.clone(),
446
                // Arc clone — O(1), just bumps refcount; no byte copy.
447
                original_bytes: self.original_bytes.clone(),
448
                original_index: self.original_index,
449
                index_to_cid: self.index_to_cid.clone(),
450
                font_type: self.font_type.clone(),
451
                font_name: self.font_name.clone(),
452
                // HintInstance has mutable interpreter state and is not Clone.
453
                // Clones are used for PDF/serialization where hinting isn't needed.
454
                hint_instance: None,
455
            }
456
        }
457
    }
458

            
459
    /// Distinguishes TrueType fonts from OpenType CFF fonts.
460
    ///
461
    /// This affects how glyph outlines are extracted and how the font
462
    /// is embedded in PDF documents.
463
    #[derive(Debug, Clone, PartialEq)]
464
    pub enum FontType {
465
        /// TrueType font with quadratic Bézier outlines in glyf table.
466
        TrueType,
467
        /// OpenType font with cubic Bézier outlines in CFF table.
468
        /// Contains the serialized CFF data for PDF embedding.
469
        OpenTypeCFF(Vec<u8>),
470
    }
471

            
472
    /// PDF-specific font metrics from HEAD, HHEA, and OS/2 tables.
473
    ///
474
    /// These metrics are used for PDF font descriptors and accurate
475
    /// text positioning in generated PDF documents.
476
    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
477
    #[repr(C)]
478
    pub struct PdfFontMetrics {
479
        // -- HEAD table fields --
480
        /// Font units per em-square (typically 1000 or 2048).
481
        pub units_per_em: u16,
482
        /// Font flags (italic, bold, fixed-pitch, etc.).
483
        pub font_flags: u16,
484
        /// Minimum x-coordinate across all glyphs.
485
        pub x_min: i16,
486
        /// Minimum y-coordinate across all glyphs.
487
        pub y_min: i16,
488
        /// Maximum x-coordinate across all glyphs.
489
        pub x_max: i16,
490
        /// Maximum y-coordinate across all glyphs.
491
        pub y_max: i16,
492

            
493
        // -- HHEA table fields --
494
        /// Typographic ascender (distance above baseline).
495
        pub ascender: i16,
496
        /// Typographic descender (distance below baseline, usually negative).
497
        pub descender: i16,
498
        /// Recommended line gap between lines of text.
499
        pub line_gap: i16,
500
        /// Maximum horizontal advance width across all glyphs.
501
        pub advance_width_max: u16,
502
        /// Caret slope rise for italic angle calculation.
503
        pub caret_slope_rise: i16,
504
        /// Caret slope run for italic angle calculation.
505
        pub caret_slope_run: i16,
506

            
507
        // -- OS/2 table fields (0 if table not present) --
508
        /// Average width of lowercase letters.
509
        pub x_avg_char_width: i16,
510
        /// Visual weight class (100-900, 400=normal, 700=bold).
511
        pub us_weight_class: u16,
512
        /// Visual width class (1-9, 5=normal).
513
        pub us_width_class: u16,
514
        /// Thickness of strikeout stroke in font units.
515
        pub y_strikeout_size: i16,
516
        /// Vertical position of strikeout stroke.
517
        pub y_strikeout_position: i16,
518
    }
519

            
520
    impl Default for PdfFontMetrics {
521
        fn default() -> Self {
522
            PdfFontMetrics::zero()
523
        }
524
    }
525

            
526
    impl PdfFontMetrics {
527
        /// Returns zeroed metrics with `units_per_em` set to 1000 (standard PostScript default)
528
        /// to avoid division-by-zero in scaling calculations.
529
21210
        pub const fn zero() -> Self {
530
21210
            PdfFontMetrics {
531
21210
                units_per_em: 1000,
532
21210
                font_flags: 0,
533
21210
                x_min: 0,
534
21210
                y_min: 0,
535
21210
                x_max: 0,
536
21210
                y_max: 0,
537
21210
                ascender: 0,
538
21210
                descender: 0,
539
21210
                line_gap: 0,
540
21210
                advance_width_max: 0,
541
21210
                caret_slope_rise: 0,
542
21210
                caret_slope_run: 0,
543
21210
                x_avg_char_width: 0,
544
21210
                us_weight_class: 0,
545
21210
                us_width_class: 0,
546
21210
                y_strikeout_size: 0,
547
21210
                y_strikeout_position: 0,
548
21210
            }
549
21210
        }
550
    }
551

            
552
    /// Result of font subsetting operation.
553
    ///
554
    /// Contains the subsetted font bytes and a mapping from original
555
    /// glyph IDs to new glyph IDs in the subset.
556
    #[derive(Debug, Clone)]
557
    pub struct SubsetFont {
558
        /// The subsetted font file bytes (smaller than original).
559
        pub bytes: Vec<u8>,
560
        /// Mapping: original glyph ID -> (new subset glyph ID, source character).
561
        pub glyph_mapping: BTreeMap<u16, (u16, char)>,
562
    }
563

            
564
    impl SubsetFont {
565
        /// Return the changed text so that when rendering with the subset font (instead of the
566
        /// original) the renderer will end up at the same glyph IDs as if we used the original text
567
        /// on the original font
568
        pub fn subset_text(&self, text: &str) -> String {
569
            text.chars()
570
                .filter_map(|c| {
571
                    self.glyph_mapping.values().find_map(|(ngid, ch)| {
572
                        if *ch == c {
573
                            char::from_u32(*ngid as u32)
574
                        } else {
575
                            None
576
                        }
577
                    })
578
                })
579
                .collect()
580
        }
581
    }
582

            
583
    /// Hash-based equality: two fonts are considered equal if their content hash matches.
584
    /// This is a performance optimization — hash collisions are possible but vanishingly
585
    /// unlikely (~1/2^64).
586
    impl PartialEq for ParsedFont {
587
        fn eq(&self, other: &Self) -> bool {
588
            self.hash == other.hash
589
        }
590
    }
591

            
592
    impl Eq for ParsedFont {}
593

            
594
    const FONT_B64_START: &str = "data:font/ttf;base64,";
595

            
596
    impl serde::Serialize for ParsedFont {
597
        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
598
            use base64::Engine;
599
            let s = format!(
600
                "{FONT_B64_START}{}",
601
                base64::prelude::BASE64_STANDARD.encode(&self.to_bytes(None).unwrap_or_default())
602
            );
603
            s.serialize(serializer)
604
        }
605
    }
606

            
607
    impl<'de> serde::Deserialize<'de> for ParsedFont {
608
        fn deserialize<D: serde::Deserializer<'de>>(
609
            deserializer: D,
610
        ) -> Result<ParsedFont, D::Error> {
611
            use base64::Engine;
612
            let s = String::deserialize(deserializer)?;
613
            let b64 = if s.starts_with(FONT_B64_START) {
614
                let b = &s[FONT_B64_START.len()..];
615
                base64::prelude::BASE64_STANDARD.decode(&b).ok()
616
            } else {
617
                None
618
            };
619

            
620
            let mut warnings = Vec::new();
621
            ParsedFont::from_bytes(&b64.unwrap_or_default(), 0, &mut warnings).ok_or_else(|| {
622
                serde::de::Error::custom(format!("Font deserialization error: {warnings:?}"))
623
            })
624
        }
625
    }
626

            
627
    impl fmt::Debug for ParsedFont {
628
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629
            f.debug_struct("ParsedFont")
630
                .field("hash", &self.hash)
631
                .field("font_metrics", &self.font_metrics)
632
                .field("num_glyphs", &self.num_glyphs)
633
                .field("hhea_table", &self.hhea_table)
634
                .field(
635
                    "hmtx_range",
636
                    &format_args!("<{} bytes>", self.hmtx_range.1),
637
                )
638
                .field("maxp_table", &self.maxp_table)
639
                .field(
640
                    "glyph_cache",
641
                    &format_args!(
642
                        "{} entries (lazy)",
643
                        self.glyph_cache.read().map(|m| m.len()).unwrap_or(0),
644
                    ),
645
                )
646
                .field("space_width", &self.space_width)
647
                .field("cmap_subtable", &self.cmap_subtable)
648
                .finish()
649
        }
650
    }
651

            
652
    /// Warning or error message generated during font parsing.
653
    #[derive(Debug, Clone, PartialEq, Eq)]
654
    pub struct FontParseWarning {
655
        /// Severity level of this warning.
656
        pub severity: FontParseWarningSeverity,
657
        /// Human-readable description of the issue.
658
        pub message: String,
659
    }
660

            
661
    /// Severity level for font parsing warnings.
662
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
663
    pub enum FontParseWarningSeverity {
664
        /// Informational message (not an error).
665
        Info,
666
        /// Warning that may affect font rendering.
667
        Warning,
668
        /// Error that prevents proper font usage.
669
        Error,
670
    }
671

            
672
    impl FontParseWarning {
673
        /// Creates an info-level message.
674
        pub fn info(message: String) -> Self {
675
            Self {
676
                severity: FontParseWarningSeverity::Info,
677
                message,
678
            }
679
        }
680

            
681
        /// Creates a warning-level message.
682
        pub fn warning(message: String) -> Self {
683
            Self {
684
                severity: FontParseWarningSeverity::Warning,
685
                message,
686
            }
687
        }
688

            
689
        /// Creates an error-level message.
690
        pub fn error(message: String) -> Self {
691
            Self {
692
                severity: FontParseWarningSeverity::Error,
693
                message,
694
            }
695
        }
696
    }
697

            
698
    impl ParsedFont {
699
        /// Parse a font from bytes using allsorts
700
        ///
701
        /// # Arguments
702
        /// * `font_bytes` - The font file data
703
        /// * `font_index` - Index of the font in a font collection (0 for single fonts)
704
        /// * `warnings` - Optional vector to collect parsing warnings
705
        ///
706
        /// # Returns
707
        /// `Some(ParsedFont)` if parsing succeeds, `None` otherwise
708
        ///
709
        /// Note: Outlines are decoded lazily by `get_or_decode_glyph`;
710
        /// `LocaGlyf::load` runs eagerly here. Use `from_bytes_shared`
711
        /// for the lazy-LocaGlyf production path.
712
35
        pub fn from_bytes(
713
35
            font_bytes: &[u8],
714
35
            font_index: usize,
715
35
            warnings: &mut Vec<FontParseWarning>,
716
35
        ) -> Option<Self> {
717
            // `from_bytes` keeps the eager-LocaGlyf behaviour for the
718
            // small number of callers (mainly tests) that don't have
719
            // an `Arc<[u8]>` to keep alive for the lazy path.
720
35
            let mut font = Self::from_bytes_internal(font_bytes, font_index, warnings, false)?;
721
            // Retain an owned copy of the source bytes so the face can later be
722
            // subset/embedded (PDF export, save->parse roundtrips). Callers pass a
723
            // borrowed slice that may not outlive us, so we own it here. Mirrors
724
            // `from_bytes_shared`, which retains the caller's `Arc<FontBytes>`.
725
35
            if font.original_bytes.is_none() {
726
35
                font.original_bytes = Some(std::sync::Arc::new(
727
35
                    rust_fontconfig::FontBytes::Owned(std::sync::Arc::from(font_bytes.to_vec())),
728
35
                ));
729
35
            }
730
35
            Some(font)
731
35
        }
732

            
733
        /// Shared implementation of `from_bytes` / `from_bytes_shared`.
734
        ///
735
        /// `defer_loca_glyf = true` skips the `LocaGlyf::load` call
736
        /// here so the caller (`from_bytes_shared`) can install a
737
        /// `LocaGlyfState::Deferred` slot that re-parses on first
738
        /// glyph decode. Saves the load-then-drop cycle the previous
739
        /// arrangement paid (`from_bytes_shared` used to call
740
        /// `from_bytes` and immediately replace the loaded LocaGlyf
741
        /// with a Deferred slot, throwing away ~hundreds of KiB of
742
        /// loca+glyf bytes per face for fonts in the chain that get
743
        /// loaded but never rasterized).
744
21210
        fn from_bytes_internal(
745
21210
            font_bytes: &[u8],
746
21210
            font_index: usize,
747
21210
            warnings: &mut Vec<FontParseWarning>,
748
21210
            defer_loca_glyf: bool,
749
21210
        ) -> Option<Self> {
750
            use std::{
751
                collections::hash_map::DefaultHasher,
752
                hash::{Hash, Hasher},
753
            };
754

            
755
            use allsorts::{
756
                binary::read::ReadScope,
757
                font_data::FontData,
758
                tables::{
759
                    cmap::{owned::CmapSubtable as OwnedCmapSubtable, CmapSubtable},
760
                    FontTableProvider, HeadTable, HheaTable, MaxpTable,
761
                },
762
                tag,
763
            };
764

            
765
21210
            let scope = ReadScope::new(font_bytes);
766
21210
            let font_file = match scope.read::<FontData<'_>>() {
767
21210
                Ok(ff) => ff,
768
                Err(e) => {
769
                    warnings.push(FontParseWarning::error(format!(
770
                        "Failed to read font data: {}",
771
                        e
772
                    )));
773
                    return None;
774
                }
775
            };
776
21210
            let provider = match font_file.table_provider(font_index) {
777
21210
                Ok(p) => p,
778
                Err(e) => {
779
                    warnings.push(FontParseWarning::error(format!(
780
                        "Failed to get table provider for font index {}: {}",
781
                        font_index, e
782
                    )));
783
                    return None;
784
                }
785
            };
786

            
787
            // Extract font name from NAME table early (before provider is moved)
788
21210
            let font_name = provider.table_data(tag::NAME).ok().and_then(|name_data| {
789
21210
                ReadScope::new(&name_data?)
790
21210
                    .read::<allsorts::tables::NameTable>()
791
21210
                    .ok()
792
21210
                    .and_then(|name_table| {
793
21210
                        name_table.string_for_id(allsorts::tables::NameTable::POSTSCRIPT_NAME)
794
21210
                    })
795
21210
            });
796

            
797
21210
            let head_table = provider
798
21210
                .table_data(tag::HEAD)
799
21210
                .ok()
800
21210
                .and_then(|head_data| ReadScope::new(&head_data?).read::<HeadTable>().ok())?;
801

            
802
21210
            let maxp_table = provider
803
21210
                .table_data(tag::MAXP)
804
21210
                .ok()
805
21210
                .and_then(|maxp_data| ReadScope::new(&maxp_data?).read::<MaxpTable>().ok())
806
21210
                .unwrap_or(MaxpTable {
807
21210
                    num_glyphs: 0,
808
21210
                    version1_sub_table: None,
809
21210
                });
810

            
811
21210
            let num_glyphs = maxp_table.num_glyphs as usize;
812

            
813
            // Compute byte offset+length into font_bytes for hmtx/vmtx
814
            // instead of copying the table data. The provider returns a
815
            // borrowed slice for OpenType fonts, so we can derive the
816
            // offset via pointer arithmetic.
817
21210
            let hmtx_range = provider
818
21210
                .table_data(tag::HMTX)
819
21210
                .ok()
820
21210
                .and_then(|cow_opt| {
821
21210
                    let cow = cow_opt?;
822
21210
                    match cow {
823
21210
                        std::borrow::Cow::Borrowed(slice) => {
824
21210
                            let base = font_bytes.as_ptr() as usize;
825
21210
                            let ptr = slice.as_ptr() as usize;
826
21210
                            let offset = ptr.checked_sub(base)?;
827
21210
                            if offset + slice.len() <= font_bytes.len() {
828
21210
                                Some((offset, slice.len()))
829
                            } else {
830
                                None
831
                            }
832
                        }
833
                        std::borrow::Cow::Owned(_) => None,
834
                    }
835
21210
                })
836
21210
                .unwrap_or((0, 0));
837

            
838
21210
            let vmtx_range = provider
839
21210
                .table_data(tag::VMTX)
840
21210
                .ok()
841
21210
                .and_then(|s| {
842
21210
                    let slice = s?;
843
3500
                    let base = font_bytes.as_ptr() as usize;
844
3500
                    let ptr = slice.as_ptr() as usize;
845
3500
                    let offset = ptr.checked_sub(base)?;
846
3500
                    if offset + slice.len() <= font_bytes.len() {
847
3500
                        Some((offset, slice.len()))
848
                    } else {
849
                        None
850
                    }
851
21210
                })
852
21210
                .unwrap_or((0, 0));
853

            
854
            // Parse vhea table (same format as hhea, used for vertical metrics)
855
21210
            let vhea_table = provider
856
21210
                .table_data(tag::VHEA)
857
21210
                .ok()
858
21210
                .and_then(|vhea_data| ReadScope::new(&vhea_data?).read::<HheaTable>().ok());
859

            
860
            // hhea is required per the OpenType spec; return None if missing
861
21210
            let hhea_table = provider
862
21210
                .table_data(tag::HHEA)
863
21210
                .ok()
864
21210
                .and_then(|hhea_data| ReadScope::new(&hhea_data?).read::<HheaTable>().ok())?;
865

            
866
            // Build layout-specific font metrics
867
21210
            let font_metrics = LayoutFontMetrics {
868
21210
                units_per_em: if head_table.units_per_em == 0 {
869
                    1000
870
                } else {
871
21210
                    head_table.units_per_em
872
                },
873
21210
                ascent: hhea_table.ascender as f32,
874
21210
                descent: hhea_table.descender as f32,
875
21210
                line_gap: hhea_table.line_gap as f32,
876
21210
                x_height: None, // will be populated from OS/2 table via from_font_metrics if available
877
21210
                cap_height: None,
878
            };
879

            
880
            // Build PDF-specific font metrics
881
21210
            let pdf_font_metrics =
882
21210
                Self::parse_pdf_font_metrics(font_bytes, font_index, &head_table, &hhea_table);
883

            
884
            // Use allsorts LocaGlyf for on-demand outline extraction. We
885
            // *load* LocaGlyf eagerly (it owns ~tens of KiB of loca +
886
            // ~hundreds of KiB of glyf bytes) but we *don't* decode any
887
            // glyph outlines up front — that's the big RSS win. Glyphs
888
            // are decoded by `ParsedFont::get_or_decode_glyph` on first
889
            // access from the CPU/GPU rasterizer.
890
            //
891
            // When `defer_loca_glyf` is set (production lazy path via
892
            // `from_bytes_shared`), we skip `LocaGlyf::load` here too —
893
            // the caller will overwrite the slot with
894
            // `LocaGlyfState::Deferred` carrying the source bytes
895
            // `Arc<[u8]>`, and the load happens on the first
896
            // `get_or_decode_glyph` call. This avoids parsing
897
            // ~hundreds of KiB per face for fonts that get resolved
898
            // into a chain but never actually rasterized (typical
899
            // for fallback fonts in CSS chains).
900
21210
            let has_glyf = provider.has_table(tag::GLYF) && provider.has_table(tag::LOCA);
901
            // Cache `has_gvar` before `provider` gets moved into
902
            // `allsorts::font::Font::new(provider)` further down —
903
            // it's the cheapest way to detect a variable font and
904
            // avoids the borrow-after-move that a later
905
            // `provider.has_table(tag::GVAR)` would incur.
906
21210
            let has_gvar = provider.has_table(tag::GVAR);
907
21210
            let loca_glyf_opt: Option<Arc<std::sync::Mutex<LocaGlyf>>> = if has_glyf
908
17710
                && !defer_loca_glyf
909
            {
910
35
                match LocaGlyf::load(&provider) {
911
35
                    Ok(lg) => Some(Arc::new(std::sync::Mutex::new(lg))),
912
                    Err(e) => {
913
                        warnings.push(FontParseWarning::warning(format!(
914
                            "Failed to load LocaGlyf: {} — falling back to hmtx-only", e
915
                        )));
916
                        None
917
                    }
918
                }
919
            } else {
920
21175
                None
921
            };
922

            
923
            // Lazy `glyph_cache` starts empty; the space-glyph stub
924
            // below pre-inserts gid 0 / space so the shaper's
925
            // cmap-miss fallback has something to render without
926
            // racing with a decode.
927

            
928
21210
            let mut font_data_impl = allsorts::font::Font::new(provider).ok()?;
929

            
930
            // Create TrueType hinting instance from font tables
931
21210
            let hint_instance = allsorts::hinting::HintInstance::new(
932
21210
                &font_data_impl.font_table_provider
933
21210
            ).ok().flatten().map(|h| std::sync::Mutex::new(h));
934

            
935
            // Stash raw GSUB/GPOS bytes for lazy parse. Typical fonts
936
            // have ~tens of KiB of GSUB + a few-to-tens of KiB of GPOS —
937
            // dwarfed by glyph outlines — so we keep the bytes around
938
            // and only spend `LayoutTable::read` + `new_layout_cache`
939
            // cycles when the shaper actually needs them (via
940
            // `ParsedFont::gsub` / `::gpos`). For an ASCII run where no
941
            // substitution / kerning is required, we skip both entirely.
942
21210
            let gsub_bytes = font_data_impl
943
21210
                .font_table_provider
944
21210
                .table_data(tag::GSUB)
945
21210
                .ok()
946
21210
                .flatten()
947
21210
                .map(|c| c.into_owned());
948
21210
            let gpos_bytes = font_data_impl
949
21210
                .font_table_provider
950
21210
                .table_data(tag::GPOS)
951
21210
                .ok()
952
21210
                .flatten()
953
21210
                .map(|c| c.into_owned());
954
21210
            let opt_gdef_table = font_data_impl.gdef_table().ok().and_then(|o| o);
955
21210
            let num_glyphs = font_data_impl.num_glyphs();
956

            
957
21210
            let opt_kern_table = font_data_impl
958
21210
                .kern_table()
959
21210
                .ok()
960
21210
                .and_then(|s| Some(s?.to_owned()));
961

            
962
21210
            let cmap_data = font_data_impl.cmap_subtable_data();
963
21210
            let cmap_subtable = ReadScope::new(cmap_data);
964
21210
            let cmap_subtable = cmap_subtable
965
21210
                .read::<CmapSubtable<'_>>()
966
21210
                .ok()
967
21210
                .and_then(|s| s.to_owned());
968

            
969
            // Font identity hash — used by `PartialEq` for ParsedFont.
970
            //
971
            // Previously we did `font_bytes.hash(&mut hasher)` over
972
            // the full mmap. That touched every page of the file
973
            // (a 40 MiB `.ttc` walked byte-for-byte) so the "lazy
974
            // mmap" ended up *fully resident* the moment we built
975
            // a `ParsedFont`. Cold RSS jumped ~40 MiB from this
976
            // single line.
977
            //
978
            // The hash doesn't need to be cryptographic — it just
979
            // has to disambiguate two `ParsedFont`s. `(len, first
980
            // 4 KiB, last 4 KiB, font_index)` is plenty unique and
981
            // only faults in the two header / trailer pages, which
982
            // shaping is going to need anyway.
983
21210
            let mut hasher = DefaultHasher::new();
984
21210
            (font_bytes.len() as u64).hash(&mut hasher);
985
21210
            let head_len = font_bytes.len().min(4096);
986
21210
            font_bytes[..head_len].hash(&mut hasher);
987
21210
            let tail_start = font_bytes.len().saturating_sub(4096);
988
21210
            font_bytes[tail_start..].hash(&mut hasher);
989
21210
            font_index.hash(&mut hasher);
990
21210
            let hash = hasher.finish();
991

            
992
21210
            let mut font = ParsedFont {
993
21210
                hash,
994
21210
                font_metrics,
995
21210
                pdf_font_metrics,
996
21210
                num_glyphs,
997
21210
                hhea_table,
998
21210
                hmtx_range,
999
21210
                vmtx_range,
21210
                vhea_table,
21210
                maxp_table,
21210
                gsub_bytes,
21210
                gsub_cache_lazy: std::sync::OnceLock::new(),
21210
                gpos_bytes,
21210
                gpos_cache_lazy: std::sync::OnceLock::new(),
21210
                opt_gdef_table,
21210
                opt_kern_table,
21210
                cmap_subtable,
21210
                last_used: Arc::new(std::sync::atomic::AtomicU64::new(0)),
21210
                is_variable_font: has_gvar,
21210
                glyph_cache: Arc::new(std::sync::RwLock::new(BTreeMap::new())),
21210
                // Eager path: `from_bytes` loaded LocaGlyf immediately
21210
                // (or set None if the font has no loca+glyf). Lazy
21210
                // callers use `from_bytes_shared` which replaces this
21210
                // with `LocaGlyfState::Deferred` before returning.
21210
                loca_glyf: LocaGlyfState::Loaded(loca_glyf_opt),
21210
                space_width: None,
21210
                mock: None,
21210
                reverse_glyph_cache: BTreeMap::new(),
21210
                // Don't retain the source bytes by default — layout and
21210
                // raster don't need them. PDF subsetting / `to_bytes`
21210
                // callers opt in via `with_source_bytes`.
21210
                original_bytes: None,
21210
                original_index: font_index,
21210
                index_to_cid: BTreeMap::new(), // Will be filled for CFF fonts
21210
                font_type: FontType::TrueType, // Default, will be updated if CFF
21210
                font_name,
21210
                hint_instance,
21210
            };
            // Calculate space width
21210
            let space_width = font.get_space_width_internal();
            // Pre-decode the space glyph straight into the lazy
            // `glyph_cache`. Space typically has no outline, so the
            // decoder's outline visitor returns nothing useful and
            // we'd spin re-decoding it every shape — short-circuit
            // here with a hand-rolled record carrying the hmtx
            // advance.
21210
            let _ = (|| {
21210
                let space_gid = font.lookup_glyph_index(' ' as u32)?;
21210
                if let Ok(cache) = font.glyph_cache.read() {
21210
                    if cache.contains_key(&space_gid) {
                        return None;
21210
                    }
                }
21210
                let space_width_val = space_width?;
                let space_record = OwnedGlyph {
                    bounding_box: OwnedGlyphBoundingBox {
                        max_x: 0,
                        max_y: 0,
                        min_x: 0,
                        min_y: 0,
                    },
                    horz_advance: space_width_val as u16,
                    outline: Vec::new(),
                    phantom_points: None,
                    raw_points: None,
                    raw_on_curve: None,
                    raw_contour_ends: None,
                    instructions: None,
                };
                if let Ok(mut cache) = font.glyph_cache.write() {
                    cache.insert(space_gid, Arc::new(space_record));
                }
                Some(())
            })();
21210
            font.space_width = space_width;
21210
            Some(font)
21210
        }
        /// Attach the source font bytes to this `ParsedFont`, enabling
        /// [`ParsedFont::to_bytes`] and [`ParsedFont::subset`] (both of
        /// which the layout / shaping path never calls).
        ///
        /// Takes an `Arc<FontBytes>` so the same file's bytes can be
        /// shared across every face of a `.ttc` at zero extra cost —
        /// pair with [`rust_fontconfig::FcFontCache::get_font_bytes`].
        /// For ad-hoc PDF callers that have raw heap bytes, wrap them
        /// via `Arc::new(FontBytes::Owned(Arc::from(vec)))`.
        pub fn with_source_bytes(mut self, bytes: std::sync::Arc<rust_fontconfig::FontBytes>) -> Self {
            self.original_bytes = Some(bytes);
            self
        }
        /// Lazy-friendly constructor — identical to
        /// [`ParsedFont::from_bytes`] except that `LocaGlyf` is
        /// **not** loaded during the call. Instead, the supplied
        /// `Arc<[u8]>` is retained and `LocaGlyf::load` runs the first
        /// time [`get_or_decode_glyph`] needs glyph outlines for this
        /// face.
        ///
        /// Fonts that get resolved into a CSS fallback chain but are
        /// never actually rasterized (common on desktop — e.g. every
        /// face of HelveticaNeue.ttc loads, but only one or two are
        /// shaped) then pay zero loca/glyf cost.
        ///
        /// Production callers (the reftest harness, `LayoutWindow`,
        /// `cpurender`) should prefer this constructor. Tests that
        /// inspect `glyph_records_decoded` directly and don't want
        /// a lazy path keep using `from_bytes`.
21175
        pub fn from_bytes_shared(
21175
            bytes: std::sync::Arc<rust_fontconfig::FontBytes>,
21175
            font_index: usize,
21175
            warnings: &mut Vec<FontParseWarning>,
21175
        ) -> Option<Self> {
            // Skip the eager LocaGlyf::load via `defer_loca_glyf=true`
            // — saves the load-then-drop cycle the prior arrangement
            // paid (when this called `from_bytes`, allocated
            // ~hundreds of KiB of loca+glyf bytes, then immediately
            // replaced the slot with `Deferred` and dropped them).
            // `bytes.as_ref()` derefs FontBytes → &[u8] (mmap or owned
            // — same code path).
21175
            let mut font = Self::from_bytes_internal(bytes.as_ref(), font_index, warnings, true)?;
21175
            font.original_bytes = Some(bytes.clone());
21175
            font.loca_glyf = LocaGlyfState::Deferred {
21175
                bytes,
21175
                font_index,
21175
                loaded: Arc::new(std::sync::Mutex::new(None)),
21175
            };
21175
            Some(font)
21175
        }
        /// Resolve the current face's `LocaGlyf`, loading it lazily
        /// on first call when `loca_glyf` is `Deferred`. Returns
        /// `None` when the font has no usable loca+glyf (CFF fonts
        /// or parse failures).
25095
        fn resolve_loca_glyf(&self) -> Option<Arc<std::sync::Mutex<LocaGlyf>>> {
25095
            match &self.loca_glyf {
                LocaGlyfState::Loaded(inner) => inner.clone(),
25095
                LocaGlyfState::Deferred { bytes, font_index, loaded } => {
                    // Fast path: cached LocaGlyf is present.
25095
                    if let Ok(guard) = loaded.lock() {
25095
                        if let Some(arc) = guard.as_ref() {
22400
                            return Some(Arc::clone(arc));
2695
                        }
                    }
2695
                    let _p = crate::probe::Probe::span("resolve_loca_glyf");
                    // Slow path: parse provider + load LocaGlyf without
                    // holding the slot's lock (allsorts can take a
                    // millisecond or two on a fresh load). Re-check
                    // after acquiring the write lock so a parallel
                    // decoder doesn't double-load.
                    use allsorts::{
                        binary::read::ReadScope,
                        font_data::FontData,
                        tables::FontTableProvider,
                    };
2695
                    let scope = ReadScope::new(bytes.as_slice());
2695
                    let font_data = scope.read::<FontData<'_>>().ok()?;
2695
                    let provider = font_data.table_provider(*font_index).ok()?;
                    // Gate on table presence to match the `from_bytes`
                    // has_glyf check; avoids a spurious warning on
                    // CFF fonts that sneak into the Deferred path.
2695
                    if !provider.has_table(tag::GLYF) || !provider.has_table(tag::LOCA) {
                        return None;
2695
                    }
2695
                    let new_arc = LocaGlyf::load(&provider)
2695
                        .ok()
2695
                        .map(|lg| Arc::new(std::sync::Mutex::new(lg)))?;
2695
                    if let Ok(mut guard) = loaded.lock() {
2695
                        if let Some(existing) = guard.as_ref() {
                            return Some(Arc::clone(existing));
2695
                        }
2695
                        *guard = Some(Arc::clone(&new_arc));
                    }
2695
                    Some(new_arc)
                }
            }
25095
        }
        /// Source bytes for PDF subsetting / table extraction.
        ///
        /// Looks in two places:
        /// - `original_bytes` (set by [`ParsedFont::with_source_bytes`]
        ///   for legacy PDF-first construction).
        /// - `LocaGlyfState::Deferred.bytes` (set by
        ///   [`ParsedFont::from_bytes_shared`] — the production lazy
        ///   path, which already retains an `Arc<[u8]>` for the lazy
        ///   loca/glyf loader).
        ///
        /// Returns `None` only for `ParsedFont`s built via the eager
        /// `from_bytes` path without an explicit `with_source_bytes`
        /// call — i.e. unit tests that load a font and don't touch
        /// PDF.
105
        pub fn source_bytes_for_subset(&self) -> Option<std::sync::Arc<rust_fontconfig::FontBytes>> {
105
            if let Some(bytes) = &self.original_bytes {
105
                return Some(std::sync::Arc::clone(bytes));
            }
            if let LocaGlyfState::Deferred { bytes, .. } = &self.loca_glyf {
                return Some(std::sync::Arc::clone(bytes));
            }
            None
105
        }
        /// Read the monotonic-clock nanos timestamp of the most
        /// recent [`get_or_decode_glyph`] call on this face, or `0`
        /// if it's never been touched.
        pub fn last_used_nanos(&self) -> u64 {
            self.last_used.load(std::sync::atomic::Ordering::Relaxed)
        }
        /// Drop the cached `LocaGlyf` for this face if it's
        /// `Deferred`-with-bytes-retained — so the next
        /// [`get_or_decode_glyph`] re-parses from `bytes`. No-op for
        /// `Loaded` faces (no source bytes to fall back to).
        ///
        /// Used by [`crate::text3::cache::FontManager::evict_unused`]
        /// and exposed publicly so embedders can free memory under
        /// pressure on fonts they no longer need to render.
        pub fn evict_loca_glyf(&self) -> bool {
            match &self.loca_glyf {
                LocaGlyfState::Deferred { loaded, .. } => {
                    if let Ok(mut guard) = loaded.lock() {
                        if guard.is_some() {
                            *guard = None;
                            return true;
                        }
                    }
                    false
                }
                LocaGlyfState::Loaded(_) => false,
            }
        }
        /// Fetch the parsed GSUB cache if this font has one, parsing
        /// it from the retained `gsub_bytes` on first access.
        ///
        /// Moved out of the eager `from_bytes` path because most text
        /// runs never trigger GSUB — plain ASCII without ligatures is
        /// handled entirely by the cmap + hmtx fast path. Building
        /// `LayoutCacheData<GSUB>` up front reserved ~0.5–2 MiB per
        /// face just to throw it away on pages that don't shape
        /// complex scripts.
5495
        pub fn gsub(&self) -> Option<&GsubCache> {
5495
            self.gsub_cache_lazy
5495
                .get_or_init(|| {
                    use allsorts::{
                        binary::read::ReadScope,
                        layout::{new_layout_cache, LayoutTable, GSUB},
                    };
2695
                    let bytes = self.gsub_bytes.as_ref()?;
2695
                    ReadScope::new(bytes)
2695
                        .read::<LayoutTable<GSUB>>()
2695
                        .ok()
2695
                        .map(new_layout_cache)
2695
                })
5495
                .as_ref()
5495
        }
        /// Fetch the parsed GPOS cache if this font has one, parsing
        /// it from the retained `gpos_bytes` on first access. See
        /// [`ParsedFont::gsub`] for the motivation.
5495
        pub fn gpos(&self) -> Option<&GposCache> {
5495
            self.gpos_cache_lazy
5495
                .get_or_init(|| {
                    use allsorts::{
                        binary::read::ReadScope,
                        layout::{new_layout_cache, LayoutTable, GPOS},
                    };
2695
                    let bytes = self.gpos_bytes.as_ref()?;
2695
                    ReadScope::new(bytes)
2695
                        .read::<LayoutTable<GPOS>>()
2695
                        .ok()
2695
                        .map(new_layout_cache)
2695
                })
5495
                .as_ref()
5495
        }
        /// Fetch an `OwnedGlyph` for `gid`, decoding it on first access.
        ///
        /// Cached in the `Arc<RwLock<…>>` `glyph_cache` so subsequent
        /// calls (including across clones of this `ParsedFont`) hit the
        /// cache. Returns `None` when `gid >= num_glyphs` or the font
        /// has no loca+glyf and no hmtx entry for the glyph. For CFF
        /// fonts the returned record has an empty outline and an advance
        /// pulled from hmtx — matching the pre-lazy behaviour.
        ///
        /// Called on the rasterizer hot path; performance budget is a
        /// few µs per unique glyph (first hit) and an Arc bump + BTreeMap
        /// lookup (cache hits). The write lock is held only across the
        /// decode, not across the caller's use of the returned Arc.
66850
        pub fn get_or_decode_glyph(&self, gid: u16) -> Option<std::sync::Arc<OwnedGlyph>> {
            use std::sync::Arc;
66850
            if usize::from(gid) >= self.num_glyphs.min(u16::MAX) as usize {
                return None;
66850
            }
            // Bump the LRU timestamp so `FontManager::evict_unused`
            // can tell this face is still in use. Cheap atomic store
            // (Relaxed — eviction reads the same atomic and tolerates
            // a slightly stale value, which only causes "evict, then
            // re-load on next access" — never an incorrect render).
66850
            self.last_used
66850
                .store(monotonic_now_nanos(), std::sync::atomic::Ordering::Relaxed);
            // Fast path: cache hit.
66850
            if let Ok(cache) = self.glyph_cache.read() {
66850
                if let Some(existing) = cache.get(&gid) {
41755
                    return Some(Arc::clone(existing));
25095
                }
            }
            // Miss: decode. We drop the read lock before taking the
            // write lock to avoid deadlock, and we re-check on the way
            // in because another thread may have decoded the same glyph
            // in between.
25095
            let record = self.decode_glyph_inner(gid);
25095
            let arc = Arc::new(record);
25095
            if let Ok(mut cache) = self.glyph_cache.write() {
25095
                cache
25095
                    .entry(gid)
25095
                    .or_insert_with(|| Arc::clone(&arc));
                // If another thread beat us to the insert, return theirs
                // so all callers observe the same Arc.
25095
                if let Some(winner) = cache.get(&gid) {
25095
                    return Some(Arc::clone(winner));
                }
            }
            Some(arc)
66850
        }
        /// Eagerly decode every glyph into the lazy `glyph_cache`,
        /// restoring the pre-lazy "every glyph is materialised at
        /// construction time" behaviour. Used by tests that iterate
        /// or compare against reference tooling, and by embedders
        /// that want a walkable view without driving every shape
        /// through `get_or_decode_glyph`.
        ///
        /// After `prime_glyph_cache`, callers can use
        /// [`ParsedFont::for_each_decoded_glyph`] or
        /// [`ParsedFont::glyph_cache_snapshot`] to observe the
        /// populated cache.
        pub fn prime_glyph_cache(&mut self) {
            let n = self.num_glyphs.min(u16::MAX) as usize;
            for glyph_index in 0..n {
                let gid = glyph_index as u16;
                let _ = self.get_or_decode_glyph(gid);
            }
        }
        /// Walk every entry currently in the lazy `glyph_cache`,
        /// invoking `f(gid, &OwnedGlyph)` for each. Holds a read
        /// lock for the duration; do not call back into the font
        /// from `f`. The cache is populated on demand by
        /// [`ParsedFont::get_or_decode_glyph`] (and bulk-prefilled
        /// by [`ParsedFont::prime_glyph_cache`]).
        pub fn for_each_decoded_glyph<F: FnMut(u16, &OwnedGlyph)>(&self, mut f: F) {
            if let Ok(cache) = self.glyph_cache.read() {
                for (gid, glyph) in cache.iter() {
                    f(*gid, glyph.as_ref());
                }
            }
        }
        /// Snapshot of the currently-decoded glyphs as a
        /// `BTreeMap<u16, Arc<OwnedGlyph>>`. Cheap (clones the
        /// Arcs, not the records). Used by callers that want to
        /// hand the map off across an API boundary; for in-place
        /// iteration prefer [`ParsedFont::for_each_decoded_glyph`].
        pub fn glyph_cache_snapshot(&self) -> BTreeMap<u16, Arc<OwnedGlyph>> {
            self.glyph_cache
                .read()
                .map(|c| c.clone())
                .unwrap_or_default()
        }
        /// Core decode routine: produces one `OwnedGlyph` for `gid` by
        /// locking `loca_glyf` and running allsorts' outline visitor +
        /// raw-simple-glyph extraction. Factored out so both
        /// [`get_or_decode_glyph`] and [`prime_glyph_cache`] share it.
        ///
        /// Always returns an `OwnedGlyph` — if anything in the decode
        /// chain fails, falls back to an empty-outline record with the
        /// `hmtx` advance. This mirrors the pre-lazy behaviour where
        /// every gid ended up in `glyph_records_decoded`.
106890
        fn hmtx_bytes(&self) -> &[u8] {
106890
            let (off, len) = self.hmtx_range;
106890
            if len == 0 { return &[]; }
106890
            self.original_bytes.as_ref()
106890
                .map(|b| &b.as_ref()[off..off+len])
106890
                .unwrap_or(&[])
106890
        }
        fn vmtx_bytes(&self) -> &[u8] {
            let (off, len) = self.vmtx_range;
            if len == 0 { return &[]; }
            self.original_bytes.as_ref()
                .map(|b| &b.as_ref()[off..off+len])
                .unwrap_or(&[])
        }
25095
        fn decode_glyph_inner(&self, gid: u16) -> OwnedGlyph {
25095
            let _p = crate::probe::Probe::span("decode_glyph");
25095
            let horz_advance = allsorts::glyph_info::advance(
25095
                &self.maxp_table,
25095
                &self.hhea_table,
25095
                self.hmtx_bytes(),
25095
                gid,
            )
25095
            .unwrap_or_default();
25095
            let mut record = OwnedGlyph {
25095
                horz_advance,
25095
                bounding_box: OwnedGlyphBoundingBox {
25095
                    min_x: 0,
25095
                    min_y: 0,
25095
                    max_x: horz_advance as i16,
25095
                    max_y: 0,
25095
                },
25095
                outline: Vec::new(),
25095
                phantom_points: None,
25095
                raw_points: None,
25095
                raw_on_curve: None,
25095
                raw_contour_ends: None,
25095
                instructions: None,
25095
            };
            // Resolve the `LocaGlyf` for this face. For `Loaded` that's
            // a cheap `Arc::clone`; for `Deferred` this is where the
            // actual `LocaGlyf::load` happens on first access, paid once
            // per face that ever decodes a glyph.
25095
            let Some(loca_glyf_arc) = self.resolve_loca_glyf() else {
                return record;
            };
25095
            let Ok(mut loca_glyf) = loca_glyf_arc.lock() else {
                return record;
            };
            // Visit the outline. If this is a variable font (gvar
            // table present) AND we still have source bytes (only
            // the `LocaGlyfState::Deferred` path retains them), we
            // re-derive a `VariableGlyfContext` here so default-
            // instance vs designed-instance differences land in
            // the decoded outline. The chained `if let` pattern
            // keeps `provider` and `store` in scope for the
            // visit, which the borrow checker requires (the
            // store's `Cow::Borrowed(&[u8])` tables tie its
            // lifetime to the provider).
            //
            // Eager-`from_bytes` faces (no retained bytes) and
            // non-variable fonts skip the var-context machinery
            // and decode the default instance — same behaviour as
            // before R4.
25095
            let mut outline_done = false;
25095
            if self.is_variable_font {
                if let LocaGlyfState::Deferred { bytes, .. } = &self.loca_glyf {
                    let scope = allsorts::binary::read::ReadScope::new(bytes);
                    if let Ok(font_data) =
                        scope.read::<allsorts::font_data::FontData<'_>>()
                    {
                        if let Ok(provider) = font_data.table_provider(self.original_index) {
                            if let Ok(store) = VariableGlyfContextStore::read(&provider) {
                                if let Ok(var_ctx) = VariableGlyfContext::new(&store) {
                                    let mut visitor = GlyfVisitorContext::new(
                                        &mut *loca_glyf,
                                        Some(var_ctx),
                                    );
                                    let mut collector = GlyphOutlineCollector::new();
                                    if visitor.visit(gid, None, &mut collector).is_ok() {
                                        record.outline = collector.into_outlines();
                                        let (min_x, min_y, max_x, max_y) =
                                            compute_outline_bbox(&record.outline);
                                        record.bounding_box = OwnedGlyphBoundingBox {
                                            min_x,
                                            min_y,
                                            max_x,
                                            max_y,
                                        };
                                        outline_done = true;
                                    }
                                }
                            }
                        }
                    }
                }
25095
            }
25095
            if !outline_done {
25095
                let mut visitor =
25095
                    GlyfVisitorContext::new(&mut *loca_glyf, None);
25095
                let mut collector = GlyphOutlineCollector::new();
25095
                if visitor.visit(gid, None, &mut collector).is_ok() {
25095
                    record.outline = collector.into_outlines();
25095
                    let (min_x, min_y, max_x, max_y) =
25095
                        compute_outline_bbox(&record.outline);
25095
                    record.bounding_box = OwnedGlyphBoundingBox {
25095
                        min_x,
25095
                        min_y,
25095
                        max_x,
25095
                        max_y,
25095
                    };
25095
                }
            }
            // Second pass: pull raw SimpleGlyph data for TrueType
            // bytecode hinting. LocaGlyf caches the `Arc<Glyph>`
            // internally so this lookup is cheap after the first call.
25095
            if let Ok(glyph_arc) = loca_glyf.glyph(gid) {
25095
                if let allsorts::tables::glyf::Glyph::Simple(sg) = glyph_arc.as_ref() {
23625
                    record.raw_points = Some(
612255
                        sg.coordinates.iter().map(|(_, pt)| (pt.0, pt.1)).collect(),
                    );
23625
                    record.raw_on_curve = Some(
612255
                        sg.coordinates.iter().map(|(f, _)| f.is_on_curve()).collect(),
                    );
23625
                    record.raw_contour_ends = Some(sg.end_pts_of_contours.clone());
23625
                    record.instructions = Some(sg.instructions.to_vec());
1470
                }
            }
25095
            record
25095
        }
        /// Parse PDF-specific font metrics from HEAD, HHEA, and OS/2 tables
21210
        fn parse_pdf_font_metrics(
21210
            font_bytes: &[u8],
21210
            font_index: usize,
21210
            head_table: &allsorts::tables::HeadTable,
21210
            hhea_table: &allsorts::tables::HheaTable,
21210
        ) -> PdfFontMetrics {
            use allsorts::{
                binary::read::ReadScope,
                font_data::FontData,
                tables::{os2::Os2, FontTableProvider},
                tag,
            };
21210
            let scope = ReadScope::new(font_bytes);
21210
            let font_file = scope.read::<FontData<'_>>().ok();
21210
            let provider = font_file
21210
                .as_ref()
21210
                .and_then(|ff| ff.table_provider(font_index).ok());
21210
            let os2_table = provider
21210
                .as_ref()
21210
                .and_then(|p| p.table_data(tag::OS_2).ok())
21210
                .and_then(|os2_data| {
21210
                    let data = os2_data?;
21210
                    let scope = ReadScope::new(&data);
21210
                    scope.read_dep::<Os2>(data.len()).ok()
21210
                });
            // Base metrics from HEAD and HHEA (always present)
21210
            let base = PdfFontMetrics {
21210
                units_per_em: head_table.units_per_em,
21210
                font_flags: head_table.flags,
21210
                x_min: head_table.x_min,
21210
                y_min: head_table.y_min,
21210
                x_max: head_table.x_max,
21210
                y_max: head_table.y_max,
21210
                ascender: hhea_table.ascender,
21210
                descender: hhea_table.descender,
21210
                line_gap: hhea_table.line_gap,
21210
                advance_width_max: hhea_table.advance_width_max,
21210
                caret_slope_rise: hhea_table.caret_slope_rise,
21210
                caret_slope_run: hhea_table.caret_slope_run,
21210
                ..PdfFontMetrics::zero()
21210
            };
            // Add OS/2 metrics if available
21210
            os2_table
21210
                .map(|os2| PdfFontMetrics {
21210
                    x_avg_char_width: os2.x_avg_char_width,
21210
                    us_weight_class: os2.us_weight_class,
21210
                    us_width_class: os2.us_width_class,
21210
                    y_strikeout_size: os2.y_strikeout_size,
21210
                    y_strikeout_position: os2.y_strikeout_position,
                    ..base
21210
                })
21210
                .unwrap_or(base)
21210
        }
        /// Returns the width of the space character in font units.
        ///
        /// This is used internally for text layout calculations.
        /// Returns `None` if the font has no space glyph or its width cannot be determined.
21210
        fn get_space_width_internal(&self) -> Option<usize> {
21210
            if let Some(mock) = self.mock.as_ref() {
                return mock.space_width;
21210
            }
21210
            let glyph_index = self.lookup_glyph_index(' ' as u32)?;
21210
            allsorts::glyph_info::advance(
21210
                &self.maxp_table,
21210
                &self.hhea_table,
21210
                self.hmtx_bytes(),
21210
                glyph_index,
            )
21210
            .ok()
21210
            .map(|s| s as usize)
21210
        }
        /// Look up the glyph index for a Unicode codepoint
108955
        pub fn lookup_glyph_index(&self, codepoint: u32) -> Option<u16> {
108955
            let cmap = self.cmap_subtable.as_ref()?;
108955
            cmap.map_glyph(codepoint).ok().flatten()
108955
        }
        /// Get the horizontal advance width for a glyph in font units.
        ///
        /// Pulled straight from the `hmtx` table — no glyph-outline
        /// decode. Called once per shaped glyph per layout pass, so
        /// avoiding the lazy decode here is a meaningful win over
        /// routing through `get_or_decode_glyph`.
60585
        pub fn get_horizontal_advance(&self, glyph_index: u16) -> u16 {
60585
            if let Some(mock) = self.mock.as_ref() {
                return mock.glyph_advances.get(&glyph_index).copied().unwrap_or(0);
60585
            }
60585
            allsorts::glyph_info::advance(
60585
                &self.maxp_table,
60585
                &self.hhea_table,
60585
                self.hmtx_bytes(),
60585
                glyph_index,
            )
60585
            .unwrap_or_default()
60585
        }
        /// Get the hinted advance width in pixels for a glyph at the given ppem.
        ///
        /// For glyphs with outlines, runs TrueType bytecode hinting to get the
        /// grid-fitted advance from phantom points. For glyphs without outlines
        /// (e.g. space), rounds the scaled advance to the pixel grid, matching
        /// FreeType's behavior.
        ///
        /// Returns `None` if hinting is not available or fails.
60585
        pub fn get_hinted_advance_px(&self, glyph_index: u16, ppem: u16) -> Option<f32> {
60585
            let glyph = self.get_or_decode_glyph(glyph_index)?;
60585
            let upem = self.font_metrics.units_per_em;
60585
            if upem == 0 || ppem == 0 {
                return None;
60585
            }
            // Check if we even have a hint instance
60585
            let _hint_mutex = self.hint_instance.as_ref()?;
            use allsorts::hinting::f26dot6::{compute_scale, F26Dot6};
60585
            let scale = compute_scale(ppem, upem);
60585
            let adv_f26dot6 = F26Dot6::from_funits(glyph.horz_advance as i32, scale);
            // For glyphs with outline data, run bytecode hinting
53025
            if let (Some(raw_points), Some(raw_on_curve), Some(raw_contour_ends)) = (
60585
                glyph.raw_points.as_ref(),
60585
                glyph.raw_on_curve.as_ref(),
60585
                glyph.raw_contour_ends.as_ref(),
            ) {
53025
                let instructions = glyph.instructions.as_deref().unwrap_or(&[]);
53025
                let mut hint = _hint_mutex.lock().ok()?;
53025
                hint.set_ppem(ppem, ppem as f64).ok()?;
53025
                let points_f26dot6: Vec<(i32, i32)> = raw_points
53025
                    .iter()
1351140
                    .map(|&(x, y)| {
1351140
                        let sx = F26Dot6::from_funits(x as i32, scale);
1351140
                        let sy = F26Dot6::from_funits(y as i32, scale);
1351140
                        (sx.to_bits(), sy.to_bits())
1351140
                    })
53025
                    .collect();
                // Use the scaled advance rounded to pixel grid, NOT the hinted
                // phantom point.  Some glyph programs apply ClearType-specific
                // SHPIX adjustments to the advance phantom point that are wrong
                // for non-ClearType rendering.  The rounded scaled advance matches
                // FreeType's DEFAULT mode advance output.
53025
                let rounded = (adv_f26dot6.to_bits() + 32) & !63;
53025
                Some(rounded as f32 / 64.0)
            } else {
                // No outline (e.g. space): use scaled advance, rounded to grid
                // (matching FreeType's phantom point pre-rounding)
7560
                let rounded = (adv_f26dot6.to_bits() + 32) & !63;
7560
                Some(rounded as f32 / 64.0)
            }
60585
        }
        /// Get the number of glyphs in this font
5495
        pub fn num_glyphs(&self) -> u16 {
5495
            self.num_glyphs
5495
        }
        /// Check if this font has a glyph for the given codepoint
        pub fn has_glyph(&self, codepoint: u32) -> bool {
            self.lookup_glyph_index(codepoint).is_some()
        }
        /// Get vertical metrics for a glyph (for vertical text layout).
        ///
        /// Uses vhea+vmtx tables (same binary format as hhea+hmtx).
        /// Returns None if font has no vertical metrics tables.
60585
        pub fn get_vertical_metrics(
60585
            &self,
60585
            glyph_id: u16,
60585
        ) -> Option<crate::text3::cache::VerticalMetrics> {
60585
            let vhea = self.vhea_table.as_ref()?;
            if self.vmtx_range.1 == 0 {
                return None;
            }
            let vert_advance = allsorts::glyph_info::advance(
                &self.maxp_table, vhea, self.vmtx_bytes(), glyph_id,
            ).ok()? as f32;
            let units_per_em = self.font_metrics.units_per_em as f32;
            let scale = if units_per_em > 0.0 { 1.0 / units_per_em } else { 0.001 };
            // Vertical bearing: approximate from glyph bbox if available
            let (bearing_x, bearing_y) = self.get_or_decode_glyph(glyph_id)
                .map(|g| {
                    let bbox = &g.bounding_box;
                    // tsb (top side bearing): origin_y - max_y
                    // lsb for vertical: center the glyph horizontally
                    let width = (bbox.max_x - bbox.min_x) as f32;
                    (-(width / 2.0) * scale, (vert_advance * scale) - (bbox.max_y as f32 * scale))
                })
                .unwrap_or((0.0, 0.0));
            Some(crate::text3::cache::VerticalMetrics {
                advance: vert_advance * scale,
                bearing_x,
                bearing_y,
                origin_y: self.font_metrics.ascent * scale,
            })
60585
        }
        /// Get layout-specific font metrics
        pub fn get_font_metrics(&self) -> crate::text3::cache::LayoutFontMetrics {
            // Ensure descent is positive (OpenType may have negative descent)
            let descent = if self.font_metrics.descent > 0.0 {
                self.font_metrics.descent
            } else {
                -self.font_metrics.descent
            };
            crate::text3::cache::LayoutFontMetrics {
                ascent: self.font_metrics.ascent,
                descent,
                line_gap: self.font_metrics.line_gap,
                units_per_em: self.font_metrics.units_per_em,
                x_height: self.font_metrics.x_height,
                cap_height: self.font_metrics.cap_height,
            }
        }
        /// Convert the ParsedFont back to bytes using allsorts::whole_font
        /// This reconstructs the entire font from the parsed data
        ///
        /// Source bytes come from either the explicit
        /// [`ParsedFont::with_source_bytes`] handle (PDF-first
        /// construction) *or* the `LocaGlyfState::Deferred` slot
        /// installed by [`ParsedFont::from_bytes_shared`]. The
        /// production lazy path retains bytes for the lazy LocaGlyf
        /// loader, so PDF subsetting Just Works without an extra
        /// `with_source_bytes` call.
        ///
        /// # Arguments
        /// * `tags` - Optional list of specific table tags to include (None = all tables)
        pub fn to_bytes(&self, tags: Option<&[u32]>) -> Result<Vec<u8>, String> {
            let source = self.source_bytes_for_subset().ok_or_else(|| {
                "ParsedFont::to_bytes requires source bytes; construct via \
                 ParsedFont::from_bytes_shared (production lazy path) or \
                 attach via ParsedFont::with_source_bytes"
                    .to_string()
            })?;
            let scope = ReadScope::new(source.as_slice());
            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
            let provider = font_file
                .table_provider(self.original_index)
                .map_err(|e| e.to_string())?;
            let tags_to_use = tags.unwrap_or(&[
                tag::CMAP,
                tag::HEAD,
                tag::HHEA,
                tag::HMTX,
                tag::MAXP,
                tag::NAME,
                tag::OS_2,
                tag::POST,
                tag::GLYF,
                tag::LOCA,
            ]);
            whole_font(&provider, tags_to_use).map_err(|e| e.to_string())
        }
        /// Create a subset font containing only the specified glyph IDs
        /// Returns the subset font bytes and a mapping from old to new glyph IDs
        ///
        /// # Arguments
        /// * `glyph_ids` - The glyph IDs to include in the subset (glyph 0/.notdef is always
        ///   included)
        /// * `cmap_target` - Target cmap format (Unicode for web, MacRoman for compatibility)
        ///
        /// # Returns
        /// A tuple of (subset_font_bytes, glyph_mapping) where glyph_mapping maps
        /// original_glyph_id -> (new_glyph_id, original_char)
        pub fn subset(
            &self,
            glyph_ids: &[(u16, char)],
            cmap_target: CmapTarget,
        ) -> Result<(Vec<u8>, BTreeMap<u16, (u16, char)>), String> {
            let source = self.source_bytes_for_subset().ok_or_else(|| {
                "ParsedFont::subset requires source bytes; construct via \
                 ParsedFont::from_bytes_shared (production lazy path) or \
                 attach via ParsedFont::with_source_bytes"
                    .to_string()
            })?;
            let scope = ReadScope::new(source.as_slice());
            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
            let provider = font_file
                .table_provider(self.original_index)
                .map_err(|e| e.to_string())?;
            // Build glyph mapping: original_id -> (new_id, char)
            let glyph_mapping: BTreeMap<u16, (u16, char)> = glyph_ids
                .iter()
                .enumerate()
                .map(|(new_id, &(original_id, ch))| (original_id, (new_id as u16, ch)))
                .collect();
            // Extract just the glyph IDs for subsetting
            let ids: Vec<u16> = glyph_ids.iter().map(|(id, _)| *id).collect();
            // Use PDF profile for embedding fonts in PDFs
            let font_bytes = allsorts_subset(&provider, &ids, &SubsetProfile::Pdf, cmap_target)
                .map_err(|e| format!("Subset error: {:?}", e))?;
            Ok((font_bytes, glyph_mapping))
        }
        /// Get the width of a glyph in font units (internal, unscaled)
        pub fn get_glyph_width_internal(&self, glyph_index: u16) -> Option<usize> {
            allsorts::glyph_info::advance(
                &self.maxp_table,
                &self.hhea_table,
                self.hmtx_bytes(),
                glyph_index,
            )
            .ok()
            .map(|s| s as usize)
        }
        /// Get the width of the space character (unscaled font units)
        #[inline]
        pub const fn get_space_width(&self) -> Option<usize> {
            self.space_width
        }
        /// Add glyph-to-text mapping to reverse cache
        /// This should be called during text shaping when we know both the source text and
        /// resulting glyphs
        pub fn cache_glyph_mapping(&mut self, glyph_id: u16, cluster_text: &str) {
            self.reverse_glyph_cache
                .insert(glyph_id, cluster_text.to_string());
        }
        /// Get the cluster text that produced a specific glyph ID
        /// Returns the original text that was shaped into this glyph (handles ligatures correctly)
        pub fn get_glyph_cluster_text(&self, glyph_id: u16) -> Option<&str> {
            self.reverse_glyph_cache.get(&glyph_id).map(|s| s.as_str())
        }
        /// Get the first character from the cluster text for a glyph ID
        /// This is useful for PDF ToUnicode CMap generation which requires single character
        /// mappings
        pub fn get_glyph_primary_char(&self, glyph_id: u16) -> Option<char> {
            self.reverse_glyph_cache
                .get(&glyph_id)
                .and_then(|text| text.chars().next())
        }
        /// Clear the reverse glyph cache (useful for memory management)
        pub fn clear_glyph_cache(&mut self) {
            self.reverse_glyph_cache.clear();
        }
        /// Get the bounding box size of a glyph (unscaled units) - for PDF
        /// Returns (width, height) in font units
        pub fn get_glyph_bbox_size(&self, glyph_index: u16) -> Option<(i32, i32)> {
            let g = self.get_or_decode_glyph(glyph_index)?;
            let glyph_width = g.horz_advance as i32;
            let glyph_height = g.bounding_box.max_y as i32 - g.bounding_box.min_y as i32;
            Some((glyph_width, glyph_height))
        }
    }
    /// Compute the bounding box from collected glyph outlines.
25095
    fn compute_outline_bbox(outlines: &[GlyphOutline]) -> (i16, i16, i16, i16) {
25095
        let mut min_x = i16::MAX;
25095
        let mut min_y = i16::MAX;
25095
        let mut max_x = i16::MIN;
25095
        let mut max_y = i16::MIN;
25095
        let mut has_points = false;
58590
        for outline in outlines {
544355
            for op in outline.operations.as_slice() {
544355
                let points: &[(i16, i16)] = match op {
33495
                    GlyphOutlineOperation::MoveTo(m) => &[(m.x, m.y)],
247765
                    GlyphOutlineOperation::LineTo(l) => &[(l.x, l.y)],
229600
                    GlyphOutlineOperation::QuadraticCurveTo(q) => {
                        // Check both control and end point for bbox
229600
                        min_x = min_x.min(q.ctrl_1_x).min(q.end_x);
229600
                        min_y = min_y.min(q.ctrl_1_y).min(q.end_y);
229600
                        max_x = max_x.max(q.ctrl_1_x).max(q.end_x);
229600
                        max_y = max_y.max(q.ctrl_1_y).max(q.end_y);
229600
                        has_points = true;
229600
                        continue;
                    }
                    GlyphOutlineOperation::CubicCurveTo(c) => {
                        min_x = min_x.min(c.ctrl_1_x).min(c.ctrl_2_x).min(c.end_x);
                        min_y = min_y.min(c.ctrl_1_y).min(c.ctrl_2_y).min(c.end_y);
                        max_x = max_x.max(c.ctrl_1_x).max(c.ctrl_2_x).max(c.end_x);
                        max_y = max_y.max(c.ctrl_1_y).max(c.ctrl_2_y).max(c.end_y);
                        has_points = true;
                        continue;
                    }
33495
                    GlyphOutlineOperation::ClosePath => continue,
                };
562520
                for &(x, y) in points {
281260
                    min_x = min_x.min(x);
281260
                    min_y = min_y.min(y);
281260
                    max_x = max_x.max(x);
281260
                    max_y = max_y.max(y);
281260
                    has_points = true;
281260
                }
            }
        }
25095
        if has_points {
23625
            (min_x, min_y, max_x, max_y)
        } else {
1470
            (0, 0, 0, 0)
        }
25095
    }
    #[derive(Debug, Clone)]
    pub struct OwnedGlyph {
        pub bounding_box: OwnedGlyphBoundingBox,
        pub horz_advance: u16,
        pub outline: Vec<GlyphOutline>,
        pub phantom_points: Option<[Point; 4]>,
        /// Raw TrueType points in font units (for hinting). None for composite/CFF glyphs.
        pub raw_points: Option<Vec<(i16, i16)>>,
        /// On-curve flags for each raw point.
        pub raw_on_curve: Option<Vec<bool>>,
        /// Contour end-point indices (TrueType).
        pub raw_contour_ends: Option<Vec<u16>>,
        /// Per-glyph TrueType hinting instructions.
        pub instructions: Option<Vec<u8>>,
    }
    // --- ParsedFontTrait Implementation for ParsedFont ---
    impl crate::text3::cache::ShallowClone for ParsedFont {
        fn shallow_clone(&self) -> Self {
            self.clone() // ParsedFont::clone uses Arc internally, so it's shallow
        }
    }
    impl crate::text3::cache::ParsedFontTrait for ParsedFont {
        fn shape_text(
            &self,
            text: &str,
            script: crate::font_traits::Script,
            language: crate::font_traits::Language,
            direction: crate::font_traits::BidiDirection,
            style: &crate::font_traits::StyleProperties,
        ) -> Result<Vec<crate::font_traits::Glyph>, crate::font_traits::LayoutError> {
            // Call the existing shape_text_for_parsed_font method (defined in default.rs)
            crate::text3::default::shape_text_for_parsed_font(
                self, text, script, language, direction, style,
            )
        }
        fn get_hash(&self) -> u64 {
            self.hash
        }
        fn get_glyph_size(
            &self,
            glyph_id: u16,
            font_size_px: f32,
        ) -> Option<azul_core::geom::LogicalSize> {
            self.get_or_decode_glyph(glyph_id).map(|record| {
                let units_per_em = self.font_metrics.units_per_em as f32;
                let scale_factor = if units_per_em > 0.0 {
                    font_size_px / units_per_em
                } else {
                    0.01
                };
                let bbox = &record.bounding_box;
                azul_core::geom::LogicalSize {
                    width: (bbox.max_x - bbox.min_x) as f32 * scale_factor,
                    height: (bbox.max_y - bbox.min_y) as f32 * scale_factor,
                }
            })
        }
        fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
            let glyph_id = self.lookup_glyph_index('-' as u32)?;
            let advance_units = self.get_horizontal_advance(glyph_id);
            let scale_factor = if self.font_metrics.units_per_em > 0 {
                font_size / (self.font_metrics.units_per_em as f32)
            } else {
                return None;
            };
            let scaled_advance = advance_units as f32 * scale_factor;
            Some((glyph_id, scaled_advance))
        }
        fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
            let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
            let advance_units = self.get_horizontal_advance(glyph_id);
            let scale_factor = if self.font_metrics.units_per_em > 0 {
                font_size / (self.font_metrics.units_per_em as f32)
            } else {
                return None;
            };
            let scaled_advance = advance_units as f32 * scale_factor;
            Some((glyph_id, scaled_advance))
        }
        fn has_glyph(&self, codepoint: u32) -> bool {
            self.lookup_glyph_index(codepoint).is_some()
        }
        fn get_vertical_metrics(
            &self,
            glyph_id: u16,
        ) -> Option<crate::text3::cache::VerticalMetrics> {
            self.get_vertical_metrics(glyph_id)
        }
        fn get_font_metrics(&self) -> crate::text3::cache::LayoutFontMetrics {
            self.font_metrics.clone()
        }
        fn num_glyphs(&self) -> u16 {
            self.num_glyphs
        }
        fn get_space_width(&self) -> Option<usize> {
            self.space_width
        }
    }
    /// Build an agg-rust PathStorage from an OwnedGlyph outline (in font units, Y-up → Y-down).
    ///
    /// Returns `None` if the glyph has no outline operations (e.g. space).
    /// The caller is responsible for applying scale and translation transforms.
    #[cfg(feature = "cpurender")]
105
    pub fn build_glyph_path(glyph: &OwnedGlyph) -> Option<agg_rust::path_storage::PathStorage> {
        use agg_rust::{basics::PATH_FLAGS_NONE, path_storage::PathStorage};
105
        let mut path = PathStorage::new();
105
        let mut has_ops = false;
105
        for outline in &glyph.outline {
            for op in outline.operations.as_slice() {
                has_ops = true;
                match op {
                    GlyphOutlineOperation::MoveTo(OutlineMoveTo { x, y }) => {
                        path.move_to(*x as f64, -(*y as f64));
                    }
                    GlyphOutlineOperation::LineTo(OutlineLineTo { x, y }) => {
                        path.line_to(*x as f64, -(*y as f64));
                    }
                    GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
                        ctrl_1_x, ctrl_1_y, end_x, end_y,
                    }) => {
                        path.curve3(
                            *ctrl_1_x as f64, -(*ctrl_1_y as f64),
                            *end_x as f64, -(*end_y as f64),
                        );
                    }
                    GlyphOutlineOperation::CubicCurveTo(OutlineCubicTo {
                        ctrl_1_x, ctrl_1_y, ctrl_2_x, ctrl_2_y, end_x, end_y,
                    }) => {
                        path.curve4(
                            *ctrl_1_x as f64, -(*ctrl_1_y as f64),
                            *ctrl_2_x as f64, -(*ctrl_2_y as f64),
                            *end_x as f64, -(*end_y as f64),
                        );
                    }
                    GlyphOutlineOperation::ClosePath => {
                        path.close_polygon(PATH_FLAGS_NONE);
                    }
                }
            }
        }
105
        if !has_ops {
105
            return None;
        }
        Some(path)
105
    }
}