azul_layout/font.rs
1//! Font parsing, metrics extraction, and subsetting.
2//!
3//! This module provides the core font infrastructure for text layout and PDF generation:
4//! - `loading`: System font cache construction and font reload errors
5//! - `mock`: Mock font implementation for testing without real font files
6//! - `parsed`: Full font parsing via allsorts (outlines, metrics, shaping tables, subsetting)
7
8#![cfg(feature = "font_loading")]
9
10use azul_css::{AzString, U8Vec};
11use rust_fontconfig::{FcFontCache, OwnedFontSource};
12
13pub mod loading {
14 #![cfg(feature = "std")]
15 #![cfg(feature = "font_loading")]
16 #![cfg_attr(not(feature = "std"), no_std)]
17
18 use std::io::Error as IoError;
19
20 use azul_css::{AzString, StringVec, U8Vec};
21 use rust_fontconfig::FcFontCache;
22
23 #[cfg(not(miri))]
24 #[must_use] pub fn build_font_cache() -> FcFontCache {
25 FcFontCache::build()
26 }
27
28 #[cfg(miri)]
29 pub fn build_font_cache() -> FcFontCache {
30 FcFontCache::default()
31 }
32
33 #[derive(Debug)]
34 pub enum FontReloadError {
35 Io(IoError, AzString),
36 FontNotFound(AzString),
37 FontLoadingNotActive(AzString),
38 }
39
40 impl Clone for FontReloadError {
41 fn clone(&self) -> Self {
42 use self::FontReloadError::{Io, FontNotFound, FontLoadingNotActive};
43 match self {
44 Io(err, path) => Io(IoError::new(err.kind(), "Io Error"), path.clone()),
45 FontNotFound(id) => FontNotFound(id.clone()),
46 FontLoadingNotActive(id) => FontLoadingNotActive(id.clone()),
47 }
48 }
49 }
50
51 azul_core::impl_display!(FontReloadError, {
52 Io(err, path_buf) => format!("Could not load \"{}\" - IO error: {}", path_buf.as_str(), err),
53 FontNotFound(id) => format!("Could not locate system font: \"{:?}\" found", id),
54 FontLoadingNotActive(id) => format!("Could not load system font: \"{:?}\": crate was not compiled with --features=\"font_loading\"", id)
55 });
56}
57pub mod mock {
58 //! Mock font implementation for testing text layout.
59 //!
60 //! Provides a `MockFont` that simulates font behavior without requiring
61 //! actual font files, useful for unit testing text layout functionality.
62
63 use std::collections::BTreeMap;
64
65 use crate::text3::cache::LayoutFontMetrics;
66
67 /// A mock font implementation for testing text layout without real fonts.
68 ///
69 /// This allows testing text shaping, layout, and rendering code paths
70 /// without needing to load actual TrueType/OpenType font files.
71 #[derive(Debug, Clone)]
72 pub struct MockFont {
73 /// Font metrics (ascent, descent, etc.).
74 pub font_metrics: LayoutFontMetrics,
75 /// Width of the space character in font units.
76 pub space_width: Option<usize>,
77 /// Horizontal advance widths keyed by glyph ID.
78 pub glyph_advances: BTreeMap<u16, u16>,
79 /// Glyph bounding box sizes (width, height) keyed by glyph ID.
80 pub glyph_sizes: BTreeMap<u16, (i32, i32)>,
81 /// Unicode codepoint to glyph ID mapping.
82 pub glyph_indices: BTreeMap<u32, u16>,
83 }
84
85 impl MockFont {
86 /// Creates a new `MockFont` with the given font metrics.
87 #[must_use] pub const fn new(font_metrics: LayoutFontMetrics) -> Self {
88 Self {
89 font_metrics,
90 space_width: Some(10),
91 glyph_advances: BTreeMap::new(),
92 glyph_sizes: BTreeMap::new(),
93 glyph_indices: BTreeMap::new(),
94 }
95 }
96
97 /// Sets the space character width.
98 #[must_use] pub const fn with_space_width(mut self, width: usize) -> Self {
99 self.space_width = Some(width);
100 self
101 }
102
103 /// Adds a horizontal advance value for a glyph.
104 #[must_use] pub fn with_glyph_advance(mut self, glyph_index: u16, advance: u16) -> Self {
105 self.glyph_advances.insert(glyph_index, advance);
106 self
107 }
108
109 /// Adds a bounding box size for a glyph.
110 #[must_use] pub fn with_glyph_size(mut self, glyph_index: u16, size: (i32, i32)) -> Self {
111 self.glyph_sizes.insert(glyph_index, size);
112 self
113 }
114
115 /// Adds a Unicode codepoint to glyph ID mapping.
116 #[must_use] pub fn with_glyph_index(mut self, unicode: u32, index: u16) -> Self {
117 self.glyph_indices.insert(unicode, index);
118 self
119 }
120 }
121}
122
123pub mod parsed {
124 use core::fmt;
125 use std::{collections::BTreeMap, sync::Arc};
126
127 use allsorts::{
128 binary::read::ReadScope,
129 font_data::FontData,
130 layout::{GDEFTable, LayoutCache, LayoutCacheData, GPOS, GSUB},
131 outline::{OutlineBuilder, OutlineSink},
132 pathfinder_geometry::{line_segment::LineSegment2F, vector::Vector2F},
133 subset::{subset as allsorts_subset, whole_font, CmapTarget, SubsetProfile},
134 tables::{
135 cmap::owned::CmapSubtable as OwnedCmapSubtable,
136 glyf::{
137 Glyph, GlyfVisitorContext, LocaGlyf, Point,
138 VariableGlyfContext, VariableGlyfContextStore,
139 },
140 kern::owned::KernTable,
141 FontTableProvider, HheaTable, MaxpTable,
142 },
143 tag,
144 };
145 use azul_core::resources::{
146 GlyphOutline, GlyphOutlineOperation, OutlineCubicTo, OutlineLineTo, OutlineMoveTo,
147 OutlineQuadTo, OwnedGlyphBoundingBox,
148 };
149 use azul_css::props::basic::FontMetrics as CssFontMetrics;
150
151 // Mock font module for testing
152 pub use crate::font::mock::MockFont;
153 use crate::text3::cache::LayoutFontMetrics;
154
155 /// Cached GSUB table for glyph substitution operations.
156 pub type GsubCache = Arc<LayoutCacheData<GSUB>>;
157 /// Cached GPOS table for glyph positioning operations.
158 pub type GposCache = Arc<LayoutCacheData<GPOS>>;
159
160 /// The `wght` variation axis `(min, default, max)` in user units.
161 ///
162 /// `None` when the font has no variable `wght` axis. Used to expand a
163 /// variable font into per-weight STATIC instances so the ordinary (static)
164 /// weight-selection path can pick the right one — no changes to
165 /// shaping/decode/PDF needed.
166 #[must_use]
167 pub fn read_wght_axis(bytes: &[u8], index: usize) -> Option<(f32, f32, f32)> {
168 let font_file = ReadScope::new(bytes).read::<FontData<'_>>().ok()?;
169 let provider = font_file.table_provider(index).ok()?;
170 let fvar_data = provider.read_table_data(tag::FVAR).ok()?;
171 let fvar = ReadScope::new(&fvar_data)
172 .read::<allsorts::tables::variable_fonts::fvar::FvarTable<'_>>()
173 .ok()?;
174 // Bind before returning so the (borrowing) axes() iterator is dropped at
175 // the end of this statement, not after `provider`/`fvar` at block end.
176 let axis = fvar.axes().find(|a| a.axis_tag == tag::WGHT);
177 axis.map(|a| {
178 (
179 f32::from(a.min_value),
180 f32::from(a.default_value),
181 f32::from(a.max_value),
182 )
183 })
184 }
185
186 /// Bake a self-contained STATIC instance of a variable font at `wght`.
187 ///
188 /// All other axes are left at their default. Returns fresh TTF bytes that
189 /// parse and embed exactly like any static font, or `None` if the font is
190 /// not a bakeable variable font.
191 #[must_use]
192 pub fn bake_weight_instance(bytes: &[u8], index: usize, wght: f32) -> Option<Vec<u8>> {
193 use allsorts::tables::Fixed;
194 let font_file = ReadScope::new(bytes).read::<FontData<'_>>().ok()?;
195 let provider = font_file.table_provider(index).ok()?;
196 let fvar_data = provider.read_table_data(tag::FVAR).ok()?;
197 let fvar = ReadScope::new(&fvar_data)
198 .read::<allsorts::tables::variable_fonts::fvar::FvarTable<'_>>()
199 .ok()?;
200 let user: Vec<Fixed> = fvar
201 .axes()
202 .map(|a| {
203 if a.axis_tag == tag::WGHT {
204 Fixed::from(wght)
205 } else {
206 a.default_value
207 }
208 })
209 .collect();
210 allsorts::variations::instance(&provider, &user)
211 .ok()
212 .map(|(baked, _tuple)| baked)
213 }
214
215 /// Monotonic-clock nanos since process start. Used to timestamp
216 /// `ParsedFont.last_used` for LRU eviction. Cheap (single
217 /// `Instant::now`); resolution is plenty fine for "did this
218 /// face get touched in the last N seconds" decisions. Exposed
219 /// `pub(crate)` so `FontManager::evict_unused` reads from the
220 /// same clock as `last_used` writes.
221 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
222 #[cfg(not(target_family = "wasm"))]
223 pub(crate) fn monotonic_now_nanos() -> u64 {
224 // Safe: `Instant::elapsed` against the same launch instant is
225 // monotonic and never overflows in any realistic process
226 // lifetime (>500 years).
227 use std::sync::OnceLock;
228 use std::time::Instant;
229 static LAUNCH: OnceLock<Instant> = OnceLock::new();
230 let start = LAUNCH.get_or_init(Instant::now);
231 start.elapsed().as_nanos() as u64
232 }
233
234 /// On browser wasm `std::time::Instant::now()` panics ("time not
235 /// implemented on this platform") — it took the whole printpdf wasm demo
236 /// down with it on the first shaped glyph: every `last_used` store on a
237 /// font touch aborted the module. LRU eviction only needs *ordering*, not
238 /// wall time, and a shared atomic counter is exactly as monotonic.
239 #[cfg(target_family = "wasm")]
240 pub(crate) fn monotonic_now_nanos() -> u64 {
241 use std::sync::atomic::{AtomicU64, Ordering};
242 static TICK: AtomicU64 = AtomicU64::new(1);
243 TICK.fetch_add(1, Ordering::Relaxed)
244 }
245
246 /// Glyph-outline decoder state. See the
247 /// [`ParsedFont::loca_glyf`] field docs for the full description.
248 #[derive(Clone)]
249 pub(crate) enum LocaGlyfState {
250 /// Ready to decode immediately, or known to have no outline
251 /// data. `None` covers both CFF fonts and fonts where the
252 /// loca+glyf parse failed.
253 ///
254 /// This variant *cannot* be evicted by
255 /// [`crate::text3::cache::FontManager::evict_unused`]: there
256 /// are no source bytes retained to re-decode from. The eager
257 /// `from_bytes` path (tests, `with_source_bytes` PDF callers)
258 /// produces this variant.
259 Loaded(Option<Arc<std::sync::Mutex<LocaGlyf>>>),
260 /// Font bytes retained for lazy `LocaGlyf` construction.
261 ///
262 /// `loaded` is `Mutex<Option<…>>` (not `OnceLock`) so an
263 /// idle eviction can clear it back to `None`; the next
264 /// `get_or_decode_glyph` will re-parse from `bytes`. Two-step
265 /// double-check pattern in `resolve_loca_glyf` keeps the
266 /// expensive `LocaGlyf::load` outside the critical section.
267 Deferred {
268 bytes: Arc<rust_fontconfig::FontBytes>,
269 font_index: usize,
270 loaded: Arc<std::sync::Mutex<Option<Arc<std::sync::Mutex<LocaGlyf>>>>>,
271 },
272 }
273
274 /// Adapter that collects allsorts outline commands into our `GlyphOutline` format.
275 ///
276 /// Implements `OutlineSink` so it can be passed to `GlyfVisitorContext::visit()`.
277 /// This handles composite glyph resolution, transforms, and variable font
278 /// deltas automatically via allsorts internals.
279 struct GlyphOutlineCollector {
280 contours: Vec<GlyphOutline>,
281 current_contour: Vec<GlyphOutlineOperation>,
282 }
283
284 impl GlyphOutlineCollector {
285 const fn new() -> Self {
286 Self {
287 contours: Vec::new(),
288 current_contour: Vec::new(),
289 }
290 }
291
292 fn into_outlines(mut self) -> Vec<GlyphOutline> {
293 if !self.current_contour.is_empty() {
294 self.contours.push(GlyphOutline {
295 operations: std::mem::take(&mut self.current_contour).into(),
296 });
297 }
298 self.contours
299 }
300 }
301
302 impl OutlineSink for GlyphOutlineCollector {
303 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
304 fn move_to(&mut self, to: Vector2F) {
305 if !self.current_contour.is_empty() {
306 self.contours.push(GlyphOutline {
307 operations: std::mem::take(&mut self.current_contour).into(),
308 });
309 }
310 self.current_contour.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo {
311 x: to.x() as i16,
312 y: to.y() as i16,
313 }));
314 }
315
316 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
317 fn line_to(&mut self, to: Vector2F) {
318 self.current_contour.push(GlyphOutlineOperation::LineTo(OutlineLineTo {
319 x: to.x() as i16,
320 y: to.y() as i16,
321 }));
322 }
323
324 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
325 fn quadratic_curve_to(&mut self, ctrl: Vector2F, to: Vector2F) {
326 self.current_contour.push(GlyphOutlineOperation::QuadraticCurveTo(
327 OutlineQuadTo {
328 ctrl_1_x: ctrl.x() as i16,
329 ctrl_1_y: ctrl.y() as i16,
330 end_x: to.x() as i16,
331 end_y: to.y() as i16,
332 },
333 ));
334 }
335
336 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
337 fn cubic_curve_to(&mut self, ctrl: LineSegment2F, to: Vector2F) {
338 self.current_contour.push(GlyphOutlineOperation::CubicCurveTo(
339 OutlineCubicTo {
340 ctrl_1_x: ctrl.from_x() as i16,
341 ctrl_1_y: ctrl.from_y() as i16,
342 ctrl_2_x: ctrl.to_x() as i16,
343 ctrl_2_y: ctrl.to_y() as i16,
344 end_x: to.x() as i16,
345 end_y: to.y() as i16,
346 },
347 ));
348 }
349
350 fn close(&mut self) {
351 self.current_contour.push(GlyphOutlineOperation::ClosePath);
352 self.contours.push(GlyphOutline {
353 operations: std::mem::take(&mut self.current_contour).into(),
354 });
355 }
356 }
357
358 /// Parsed font data with all required tables for text layout and PDF generation.
359 ///
360 /// This struct holds the parsed representation of a TrueType/OpenType font,
361 /// including glyph outlines, metrics, and shaping tables. It's used for:
362 /// - Text layout (via GSUB/GPOS tables)
363 /// - Glyph rendering (via glyf/CFF outlines)
364 /// - PDF font embedding (via font metrics and subsetting)
365 pub struct ParsedFont {
366 /// Hash of the font bytes for caching and equality checks.
367 pub hash: u64,
368 /// Layout-specific font metrics (ascent, descent, line gap).
369 pub font_metrics: LayoutFontMetrics,
370 /// PDF-specific detailed font metrics from HEAD, HHEA, OS/2 tables.
371 pub pdf_font_metrics: PdfFontMetrics,
372 /// Total number of glyphs in the font (from maxp table).
373 pub num_glyphs: u16,
374 /// Horizontal header table (hhea) containing global horizontal metrics.
375 pub hhea_table: HheaTable,
376 /// Offset+length into `original_bytes` for hmtx table (lazy: no copy).
377 pub hmtx_range: (usize, usize),
378 /// Offset+length into `original_bytes` for vmtx table (lazy: no copy).
379 pub vmtx_range: (usize, usize),
380 /// Vertical header table (vhea), same format as hhea. None if font has no vertical metrics.
381 pub vhea_table: Option<HheaTable>,
382 /// Maximum profile table (maxp) containing glyph count and memory hints.
383 pub maxp_table: MaxpTable,
384 /// Raw GSUB table bytes, kept as a `Vec<u8>` (tens to low-hundreds
385 /// of KiB) so the parsed `GsubCache` can be built on first shape
386 /// call instead of up-front. Access via [`ParsedFont::gsub`] —
387 /// that getter populates `gsub_cache_lazy` via `OnceLock` and
388 /// returns a borrow.
389 pub(crate) gsub_bytes: Option<Vec<u8>>,
390 /// Lazy GSUB cache: populated on first [`ParsedFont::gsub`] call.
391 /// `None` means "font has no GSUB table" *after* init attempt;
392 /// the `OnceLock` wrapper distinguishes "not yet initialised"
393 /// from "initialised to None".
394 pub(crate) gsub_cache_lazy: std::sync::OnceLock<Option<GsubCache>>,
395 /// Raw GPOS table bytes. Same lazy-parse arrangement as
396 /// `gsub_bytes` — see [`ParsedFont::gpos`].
397 pub(crate) gpos_bytes: Option<Vec<u8>>,
398 /// Lazy GPOS cache, populated on first [`ParsedFont::gpos`] call.
399 pub(crate) gpos_cache_lazy: std::sync::OnceLock<Option<GposCache>>,
400 /// Glyph definition table (GDEF) for glyph classification.
401 pub opt_gdef_table: Option<Arc<GDEFTable>>,
402 /// Legacy kerning table (kern) for fonts without GPOS.
403 pub opt_kern_table: Option<Arc<KernTable>>,
404 /// Monotonic-clock nanos at the most recent
405 /// [`ParsedFont::get_or_decode_glyph`] / `gsub()` / `gpos()`
406 /// call. `0` means "never touched". Used by
407 /// [`crate::text3::cache::FontManager::evict_unused`] to
408 /// decide which `LocaGlyfState::Deferred` faces to release.
409 pub(crate) last_used: Arc<std::sync::atomic::AtomicU64>,
410 /// `true` if this font is a variable font (carries a `gvar`
411 /// table). Cached at parse time so [`decode_glyph_inner`]
412 /// can short-circuit the variable-context construction for
413 /// the common non-variable case. Variable-glyph delta
414 /// application requires the source bytes to be retained,
415 /// so it only fires on the `LocaGlyfState::Deferred` path.
416 pub(crate) is_variable_font: bool,
417 /// Lazy outline cache. Populated on first
418 /// [`ParsedFont::get_or_decode_glyph`] call per `gid`; entries
419 /// are wrapped in `Arc` so callers can hold them without
420 /// keeping the lock. The space glyph (and `.notdef` when
421 /// present) are pre-inserted by `from_bytes_internal` so the
422 /// shaper's cmap-miss path has something to render without
423 /// racing with a decode.
424 ///
425 /// Tests that previously walked the public `glyph_records_decoded`
426 /// `BTreeMap` field now call
427 /// [`ParsedFont::prime_glyph_cache`] (decodes every glyph into
428 /// this cache) followed by
429 /// [`ParsedFont::for_each_decoded_glyph`] /
430 /// [`ParsedFont::glyph_cache_snapshot`] to walk the result.
431 // [az-web-lift] queue RwLock spins in lock_contended in single-threaded lifted wasm
432 // (only the pure-Rust queue RwLock is lifted; Mutex is Leaf-stubbed). Reuse
433 // rust_fontconfig::StLock (no-atomic single-threaded bypass). One of the 3 RwLocks total.
434 pub(crate) glyph_cache: Arc<rust_fontconfig::StLock<BTreeMap<u16, Arc<OwnedGlyph>>>>,
435 /// Glyph outline decoder state.
436 ///
437 /// - `Loaded(Some(arc))`: `LocaGlyf` is already loaded (owning
438 /// its own `Box<[u8]>` copy of the loca+glyf tables) and
439 /// ready to decode glyphs. Produced by the eager `from_bytes`
440 /// constructor path (tests).
441 /// - `Loaded(None)`: the font has no usable loca+glyf (CFF, or
442 /// a parse failure). Glyph outlines won't decode; the hmtx
443 /// advance fallback fills in the blanks.
444 /// - `Deferred`: we retain an `Arc<[u8]>` to the full font file
445 /// and the `font_index`; the first `get_or_decode_glyph` call
446 /// parses a fresh `FontData` / `TableProvider` from those
447 /// bytes and loads `LocaGlyf`, storing the result in the
448 /// `OnceLock`. Fonts that get resolved into a chain but are
449 /// never actually rasterized pay zero decode cost — this is
450 /// the big win for pages like `excel.html` where 20+ fallback
451 /// faces load but only a handful are touched.
452 pub(crate) loca_glyf: LocaGlyfState,
453 /// Cached width of the space character in font units.
454 pub space_width: Option<usize>,
455 /// Character-to-glyph mapping (cmap subtable).
456 pub cmap_subtable: Option<OwnedCmapSubtable>,
457 /// Mock font data for testing (replaces real font behavior).
458 pub mock: Option<Box<MockFont>>,
459 /// Reverse mapping: `glyph_id` -> cluster text (handles ligatures like "fi").
460 pub reverse_glyph_cache: BTreeMap<u16, String>,
461 /// Original font bytes — only retained for callers that need to
462 /// reconstruct or subset the font (PDF export). Layout / shaping /
463 /// raster never read this, so `ParsedFont::from_bytes` leaves it
464 /// as `None` by default and callers opt in via
465 /// [`ParsedFont::with_source_bytes`]. Shared across faces of the
466 /// same `.ttc` via the `Arc<FontBytes>` that
467 /// [`rust_fontconfig::FcFontCache::get_font_bytes`] returns —
468 /// for disk fonts the backing is an mmap so untouched pages
469 /// don't count toward RSS.
470 pub original_bytes: Option<Arc<rust_fontconfig::FontBytes>>,
471 /// Font index within collection (0 for single-font files).
472 pub original_index: usize,
473 /// GID to CID mapping for CFF fonts (required for PDF embedding).
474 pub index_to_cid: BTreeMap<u16, u16>,
475 /// Font type (TrueType outlines or OpenType CFF).
476 pub font_type: FontType,
477 /// PostScript font name from the NAME table.
478 pub font_name: Option<String>,
479 /// TrueType bytecode hinting instance (mutable interpreter state).
480 /// Wrapped in Mutex because hinting mutates internal state.
481 /// None for CFF fonts or fonts without hinting data.
482 pub hint_instance: Option<std::sync::Mutex<allsorts::hinting::HintInstance>>,
483 }
484
485 impl Clone for ParsedFont {
486 fn clone(&self) -> Self {
487 Self {
488 hash: self.hash,
489 font_metrics: self.font_metrics,
490 pdf_font_metrics: self.pdf_font_metrics,
491 num_glyphs: self.num_glyphs,
492 hhea_table: self.hhea_table.clone(),
493 hmtx_range: self.hmtx_range,
494 vmtx_range: self.vmtx_range,
495 vhea_table: self.vhea_table.clone(),
496 maxp_table: self.maxp_table.clone(),
497 // OnceLock<T: Clone>: Clone preserves the init state, so
498 // a clone of a parsed cache skips re-parse on first
499 // access. The raw bytes we keep around for lazy init
500 // are cloned too.
501 gsub_bytes: self.gsub_bytes.clone(),
502 gsub_cache_lazy: self.gsub_cache_lazy.clone(),
503 gpos_bytes: self.gpos_bytes.clone(),
504 gpos_cache_lazy: self.gpos_cache_lazy.clone(),
505 opt_gdef_table: self.opt_gdef_table.clone(),
506 opt_kern_table: self.opt_kern_table.clone(),
507 // Share the lazy cache and loca_glyf across clones: cheap
508 // Arc bump, amortises glyph decode across clones of the
509 // same face.
510 last_used: Arc::clone(&self.last_used),
511 is_variable_font: self.is_variable_font,
512 glyph_cache: Arc::clone(&self.glyph_cache),
513 // `LocaGlyfState` is `Clone` — for `Loaded` this is an
514 // `Arc::clone`; for `Deferred` it's an `Arc::clone` of
515 // the bytes + the `OnceLock`, so a clone of a face
516 // that's already decoded glyphs carries the decode.
517 loca_glyf: self.loca_glyf.clone(),
518 space_width: self.space_width,
519 cmap_subtable: self.cmap_subtable.clone(),
520 mock: self.mock.clone(),
521 reverse_glyph_cache: self.reverse_glyph_cache.clone(),
522 // Arc clone — O(1), just bumps refcount; no byte copy.
523 original_bytes: self.original_bytes.clone(),
524 original_index: self.original_index,
525 index_to_cid: self.index_to_cid.clone(),
526 font_type: self.font_type.clone(),
527 font_name: self.font_name.clone(),
528 // HintInstance has mutable interpreter state and is not Clone.
529 // Clones are used for PDF/serialization where hinting isn't needed.
530 hint_instance: None,
531 }
532 }
533 }
534
535 /// Distinguishes TrueType fonts from OpenType CFF fonts.
536 ///
537 /// This affects how glyph outlines are extracted and how the font
538 /// is embedded in PDF documents.
539 #[derive(Debug, Clone, PartialEq, Eq)]
540 pub enum FontType {
541 /// TrueType font with quadratic Bézier outlines in glyf table.
542 TrueType,
543 /// OpenType font with cubic Bézier outlines in CFF table.
544 /// Contains the serialized CFF data for PDF embedding.
545 OpenTypeCFF(Vec<u8>),
546 }
547
548 /// PDF-specific font metrics from HEAD, HHEA, and OS/2 tables.
549 ///
550 /// These metrics are used for PDF font descriptors and accurate
551 /// text positioning in generated PDF documents.
552 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
553 #[repr(C)]
554 pub struct PdfFontMetrics {
555 // -- HEAD table fields --
556 /// Font units per em-square (typically 1000 or 2048).
557 pub units_per_em: u16,
558 /// Font flags (italic, bold, fixed-pitch, etc.).
559 pub font_flags: u16,
560 /// Minimum x-coordinate across all glyphs.
561 pub x_min: i16,
562 /// Minimum y-coordinate across all glyphs.
563 pub y_min: i16,
564 /// Maximum x-coordinate across all glyphs.
565 pub x_max: i16,
566 /// Maximum y-coordinate across all glyphs.
567 pub y_max: i16,
568
569 // -- HHEA table fields --
570 /// Typographic ascender (distance above baseline).
571 pub ascender: i16,
572 /// Typographic descender (distance below baseline, usually negative).
573 pub descender: i16,
574 /// Recommended line gap between lines of text.
575 pub line_gap: i16,
576 /// Maximum horizontal advance width across all glyphs.
577 pub advance_width_max: u16,
578 /// Caret slope rise for italic angle calculation.
579 pub caret_slope_rise: i16,
580 /// Caret slope run for italic angle calculation.
581 pub caret_slope_run: i16,
582
583 // -- OS/2 table fields (0 if table not present) --
584 /// Average width of lowercase letters.
585 pub x_avg_char_width: i16,
586 /// Visual weight class (100-900, 400=normal, 700=bold).
587 pub us_weight_class: u16,
588 /// Visual width class (1-9, 5=normal).
589 pub us_width_class: u16,
590 /// Thickness of strikeout stroke in font units.
591 pub y_strikeout_size: i16,
592 /// Vertical position of strikeout stroke.
593 pub y_strikeout_position: i16,
594 }
595
596 impl Default for PdfFontMetrics {
597 fn default() -> Self {
598 Self::zero()
599 }
600 }
601
602 impl PdfFontMetrics {
603 /// Returns zeroed metrics with `units_per_em` set to 1000 (standard PostScript default)
604 /// to avoid division-by-zero in scaling calculations.
605 #[must_use] pub const fn zero() -> Self {
606 Self {
607 units_per_em: 1000,
608 font_flags: 0,
609 x_min: 0,
610 y_min: 0,
611 x_max: 0,
612 y_max: 0,
613 ascender: 0,
614 descender: 0,
615 line_gap: 0,
616 advance_width_max: 0,
617 caret_slope_rise: 0,
618 caret_slope_run: 0,
619 x_avg_char_width: 0,
620 us_weight_class: 0,
621 us_width_class: 0,
622 y_strikeout_size: 0,
623 y_strikeout_position: 0,
624 }
625 }
626 }
627
628 /// Result of font subsetting operation.
629 ///
630 /// Contains the subsetted font bytes and a mapping from original
631 /// glyph IDs to new glyph IDs in the subset.
632 #[derive(Debug, Clone)]
633 pub struct SubsetFont {
634 /// The subsetted font file bytes (smaller than original).
635 pub bytes: Vec<u8>,
636 /// Mapping: original glyph ID -> (new subset glyph ID, source character).
637 pub glyph_mapping: BTreeMap<u16, (u16, char)>,
638 }
639
640 impl SubsetFont {
641 /// Return the changed text so that when rendering with the subset font (instead of the
642 /// original) the renderer will end up at the same glyph IDs as if we used the original text
643 /// on the original font
644 #[must_use] pub fn subset_text(&self, text: &str) -> String {
645 text.chars()
646 .filter_map(|c| {
647 self.glyph_mapping.values().find_map(|(ngid, ch)| {
648 if *ch == c {
649 char::from_u32(u32::from(*ngid))
650 } else {
651 None
652 }
653 })
654 })
655 .collect()
656 }
657 }
658
659 /// Hash-based equality: two fonts are considered equal if their content hash matches.
660 /// This is a performance optimization — hash collisions are possible but vanishingly
661 /// unlikely (~1/2^64).
662 impl PartialEq for ParsedFont {
663 fn eq(&self, other: &Self) -> bool {
664 self.hash == other.hash
665 }
666 }
667
668 impl Eq for ParsedFont {}
669
670 const FONT_B64_START: &str = "data:font/ttf;base64,";
671
672 impl serde::Serialize for ParsedFont {
673 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
674 use base64::Engine;
675 let s = format!(
676 "{FONT_B64_START}{}",
677 base64::prelude::BASE64_STANDARD.encode(self.to_bytes(None).unwrap_or_default())
678 );
679 s.serialize(serializer)
680 }
681 }
682
683 impl<'de> serde::Deserialize<'de> for ParsedFont {
684 fn deserialize<D: serde::Deserializer<'de>>(
685 deserializer: D,
686 ) -> Result<Self, D::Error> {
687 use base64::Engine;
688 let s = String::deserialize(deserializer)?;
689 let b64 = s.strip_prefix(FONT_B64_START).and_then(|b| base64::prelude::BASE64_STANDARD.decode(b).ok());
690
691 let mut warnings = Vec::new();
692 Self::from_bytes(&b64.unwrap_or_default(), 0, &mut warnings).ok_or_else(|| {
693 serde::de::Error::custom(format!("Font deserialization error: {warnings:?}"))
694 })
695 }
696 }
697
698 impl fmt::Debug for ParsedFont {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 f.debug_struct("ParsedFont")
701 .field("hash", &self.hash)
702 .field("font_metrics", &self.font_metrics)
703 .field("num_glyphs", &self.num_glyphs)
704 .field("hhea_table", &self.hhea_table)
705 .field(
706 "hmtx_range",
707 &format_args!("<{} bytes>", self.hmtx_range.1),
708 )
709 .field("maxp_table", &self.maxp_table)
710 .field(
711 "glyph_cache",
712 &format_args!(
713 "{} entries (lazy)",
714 self.glyph_cache.read().map(|m| m.len()).unwrap_or(0),
715 ),
716 )
717 .field("space_width", &self.space_width)
718 .field("cmap_subtable", &self.cmap_subtable)
719 .finish_non_exhaustive()
720 }
721 }
722
723 /// Warning or error message generated during font parsing.
724 #[derive(Debug, Clone, PartialEq, Eq)]
725 pub struct FontParseWarning {
726 /// Severity level of this warning.
727 pub severity: FontParseWarningSeverity,
728 /// Human-readable description of the issue.
729 pub message: String,
730 }
731
732 /// Severity level for font parsing warnings.
733 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
734 pub enum FontParseWarningSeverity {
735 /// Informational message (not an error).
736 Info,
737 /// Warning that may affect font rendering.
738 Warning,
739 /// Error that prevents proper font usage.
740 Error,
741 }
742
743 impl FontParseWarning {
744 /// Creates an info-level message.
745 #[must_use] pub const fn info(message: String) -> Self {
746 Self {
747 severity: FontParseWarningSeverity::Info,
748 message,
749 }
750 }
751
752 /// Creates a warning-level message.
753 #[must_use] pub const fn warning(message: String) -> Self {
754 Self {
755 severity: FontParseWarningSeverity::Warning,
756 message,
757 }
758 }
759
760 /// Creates an error-level message.
761 #[must_use] pub const fn error(message: String) -> Self {
762 Self {
763 severity: FontParseWarningSeverity::Error,
764 message,
765 }
766 }
767 }
768
769 // WEB-LIFT FIX (2026-06-02): a `FontTableProvider` that scans the sfnt table directory
770 // by hand from the raw font bytes. allsorts' `OffsetTableFontProvider` produces garbage
771 // on the remill/web backend: (1) `ReadArray::read_item`'s nested-tuple `TableRecord` read
772 // returns `table_tag = 0` for EVERY record (proven: tags[7]=0x0000 while the bytes there
773 // are 0x68656164 'head'); (2) even a hand-rolled scan added to the *allsorts crate* sees a
774 // bad `self.scope.data()` (the ReadScope fat-pointer mis-lifts through provider
775 // construction, or allsorts-crate code lifts differently). This provider lives in
776 // azul-layout — whose identical byte reads PROVABLY work (the `from_provider` probe read
777 // num_tables=15 from these same `font_bytes`) — and reads the slice directly. KEEP.
778 #[inline]
779 fn manual_be16(d: &[u8], o: usize) -> u32 {
780 (u32::from(d[o]) << 8) | u32::from(d[o + 1])
781 }
782 #[inline]
783 fn manual_be32(d: &[u8], o: usize) -> u32 {
784 (u32::from(d[o]) << 24)
785 | (u32::from(d[o + 1]) << 16)
786 | (u32::from(d[o + 2]) << 8)
787 | u32::from(d[o + 3])
788 }
789
790 struct ManualTableProvider<'a> {
791 data: &'a [u8],
792 dir: usize, // byte offset of the first table record (offset-table base + 12)
793 num: usize, // number of table records
794 }
795
796 impl<'a> ManualTableProvider<'a> {
797 fn new(data: &'a [u8], font_index: usize) -> Option<Self> {
798 if data.len() < 12 {
799 return None;
800 }
801 let base = if manual_be32(data, 0) == 0x7474_6366 {
802 // 'ttcf' (TrueType Collection): the font_index'th offset-table offset.
803 let num_fonts = manual_be32(data, 8) as usize;
804 if font_index >= num_fonts || 12 + font_index * 4 + 4 > data.len() {
805 return None;
806 }
807 manual_be32(data, 12 + font_index * 4) as usize
808 } else {
809 0 // single font: offset table at the start
810 };
811 if base + 12 > data.len() {
812 return None;
813 }
814 Some(ManualTableProvider {
815 data,
816 dir: base + 12,
817 num: manual_be16(data, base + 4) as usize,
818 })
819 }
820 }
821
822 impl FontTableProvider for ManualTableProvider<'_> {
823 fn table_data(
824 &self,
825 tag: u32,
826 ) -> Result<Option<std::borrow::Cow<'_, [u8]>>, allsorts::error::ParseError> {
827 let mut i = 0;
828 while i < self.num {
829 let r = self.dir + i * 16;
830 if r + 16 > self.data.len() {
831 break;
832 }
833 if manual_be32(self.data, r) == tag {
834 let off = manual_be32(self.data, r + 8) as usize;
835 let len = manual_be32(self.data, r + 12) as usize;
836 return Ok(off
837 .checked_add(len)
838 .filter(|&e| e <= self.data.len())
839 .map(|e| std::borrow::Cow::Borrowed(&self.data[off..e])));
840 }
841 i += 1;
842 }
843 Ok(None)
844 }
845
846 fn has_table(&self, tag: u32) -> bool {
847 self.table_data(tag).ok().flatten().is_some()
848 }
849
850 fn table_tags(&self) -> Option<Vec<u32>> {
851 // DIAG (REVERT): sentinel 0xFADE as tags[0] proves THIS provider ran; then
852 // self.num pushes let me see if the usize field survived; then the real reads
853 // show if self.data (slice field) survived the struct move through generics.
854 let mut tags = Vec::with_capacity(self.num + 1);
855 tags.push(0x0000_FADE);
856 let mut i = 0;
857 while i < self.num {
858 let r = self.dir + i * 16;
859 if r + 4 > self.data.len() {
860 break;
861 }
862 tags.push(manual_be32(self.data, r));
863 i += 1;
864 }
865 Some(tags)
866 }
867 }
868
869 impl allsorts::tables::SfntVersion for ManualTableProvider<'_> {
870 fn sfnt_version(&self) -> u32 {
871 let base = self.dir.saturating_sub(12);
872 if base + 4 <= self.data.len() {
873 manual_be32(self.data, base)
874 } else {
875 0
876 }
877 }
878 }
879
880 impl ParsedFont {
881 /// Parse a font from bytes using allsorts
882 ///
883 /// # Arguments
884 /// * `font_bytes` - The font file data
885 /// * `font_index` - Index of the font in a font collection (0 for single fonts)
886 /// * `warnings` - Optional vector to collect parsing warnings
887 ///
888 /// # Returns
889 /// `Some(ParsedFont)` if parsing succeeds, `None` otherwise
890 ///
891 /// Note: Outlines are decoded lazily by `get_or_decode_glyph`;
892 /// `LocaGlyf::load` runs eagerly here. Use `from_bytes_shared`
893 /// for the lazy-LocaGlyf production path.
894 pub fn from_bytes(
895 font_bytes: &[u8],
896 font_index: usize,
897 warnings: &mut Vec<FontParseWarning>,
898 ) -> Option<Self> {
899 // `from_bytes` keeps the eager-LocaGlyf behaviour for the
900 // small number of callers (mainly tests) that don't have
901 // an `Arc<[u8]>` to keep alive for the lazy path.
902 let mut font = Self::from_bytes_internal(font_bytes, font_index, warnings, false)?;
903 // Retain an owned copy of the source bytes so the face can later be
904 // subset/embedded (PDF export, save->parse roundtrips). Callers pass a
905 // borrowed slice that may not outlive us, so we own it here. Mirrors
906 // `from_bytes_shared`, which retains the caller's `Arc<FontBytes>`.
907 if font.original_bytes.is_none() {
908 font.original_bytes = Some(Arc::new(
909 rust_fontconfig::FontBytes::Owned(Arc::from(font_bytes.to_vec())),
910 ));
911 }
912 Some(font)
913 }
914
915 /// Shared implementation of `from_bytes` / `from_bytes_shared`.
916 ///
917 /// `defer_loca_glyf = true` skips the `LocaGlyf::load` call
918 /// here so the caller (`from_bytes_shared`) can install a
919 /// `LocaGlyfState::Deferred` slot that re-parses on first
920 /// glyph decode. Saves the load-then-drop cycle the previous
921 /// arrangement paid (`from_bytes_shared` used to call
922 /// `from_bytes` and immediately replace the loaded `LocaGlyf`
923 /// with a Deferred slot, throwing away ~hundreds of KiB of
924 /// loca+glyf bytes per face for fonts in the chain that get
925 /// loaded but never rasterized).
926 fn from_bytes_internal(
927 font_bytes: &[u8],
928 font_index: usize,
929 warnings: &mut Vec<FontParseWarning>,
930 defer_loca_glyf: bool,
931 ) -> Option<Self> {
932 use allsorts::{binary::read::ReadScope, font_data::FontData};
933 fn provider_err(font_index: usize, e: impl fmt::Display) -> FontParseWarning {
934 FontParseWarning::error(format!(
935 "Failed to get table provider for font index {font_index}: {e}"
936 ))
937 }
938
939 let scope = ReadScope::new(font_bytes);
940 let font_file = match scope.read::<FontData<'_>>() {
941 Ok(ff) => ff,
942 Err(e) => {
943 warnings.push(FontParseWarning::error(format!(
944 "Failed to read font data: {e}"
945 )));
946 return None;
947 }
948 };
949 // FIX (2026-06-02): route OpenType fonts through the CONCRETE provider
950 // (`OffsetTableFontProvider`) instead of `FontData::table_provider`'s
951 // `Box<dyn FontTableProvider>`. On the lifted/web backend the trait-object
952 // VTABLE dispatch (allsorts font_data.rs:45 `self.provider.table_data(tag)`)
953 // mis-lifts: the vtable's fn-pointers are untranslated native addresses, so the
954 // indirect-call dispatcher routes the dyn call to the WRONG `table_data` impl,
955 // which returns a `Cow::Owned` garbage buffer → `HeadTable::read` errors → font
956 // parse returns None → text measures height 0. A concrete provider makes every
957 // `table_data` a DIRECT (monomorphized) call, which lifts correctly. Woff/Woff2
958 // keep the dyn path (they're not used on the web backend's embedded TTF).
959 match font_file {
960 FontData::OpenType(otf) => {
961 // Prefer the hand-rolled provider (reads font_bytes directly) over
962 // allsorts' OffsetTableFontProvider, whose lifted table reads are garbage
963 // on the web backend. Fall back to allsorts only if the manual layout
964 // parse can't recognise the sfnt (e.g. an unusual TTC).
965 if let Some(mp) = ManualTableProvider::new(font_bytes, font_index) {
966 Self::from_provider(mp, font_bytes, font_index, warnings, defer_loca_glyf)
967 } else {
968 match otf.table_provider(font_index) {
969 Ok(p) => Self::from_provider(
970 p,
971 font_bytes,
972 font_index,
973 warnings,
974 defer_loca_glyf,
975 ),
976 Err(e) => {
977 warnings.push(provider_err(font_index, e));
978 None
979 }
980 }
981 }
982 }
983 other => match other.table_provider(font_index) {
984 Ok(p) => {
985 Self::from_provider(p, font_bytes, font_index, warnings, defer_loca_glyf)
986 }
987 Err(e) => {
988 warnings.push(provider_err(font_index, e));
989 None
990 }
991 },
992 }
993 }
994
995 /// Build a `ParsedFont` from a concrete [`FontTableProvider`]. Split out of
996 /// `from_bytes_internal` (2026-06-02) so OpenType fonts use the concrete
997 /// `OffsetTableFontProvider` (direct `table_data` calls that lift correctly on
998 /// the web backend) rather than `FontData::table_provider`'s `Box<dyn>`, whose
999 /// trait-object vtable dispatch mis-lifts (wrong impl → Owned garbage → parse fail).
1000 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1001 #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1002 fn from_provider<P: FontTableProvider>(
1003 provider: P,
1004 font_bytes: &[u8],
1005 font_index: usize,
1006 warnings: &mut Vec<FontParseWarning>,
1007 defer_loca_glyf: bool,
1008 ) -> Option<Self> {
1009 use std::{
1010 collections::hash_map::DefaultHasher,
1011 hash::{Hash, Hasher},
1012 };
1013
1014 use allsorts::{
1015 binary::read::ReadScope,
1016 tables::{
1017 cmap::{owned::CmapSubtable as OwnedCmapSubtable, CmapSubtable},
1018 FontTableProvider, HeadTable, HheaTable, MaxpTable,
1019 },
1020 tag,
1021 };
1022
1023 // Extract font name from NAME table early (before provider is moved).
1024 // WEB-LIFT FIX (2026-06-02): NameTable::string_for_id decodes the NAME strings via
1025 // `encoding_rs` (Mac Roman / UTF-16 charset state machines), whose jump-tables
1026 // are NOT devirt'd by the remill lift → MISSING_BLOCK trap (proven: trap in
1027 // encoding_rs::Decoder::decode_to_utf8). font_name is OPTIONAL metadata (NOT used
1028 // for layout/metrics/shaping — those are binary head/hhea/maxp/cmap/glyf), so skip
1029 // the NAME-string decode on the web backend to avoid encoding_rs entirely.
1030 #[cfg(feature = "web_lift")]
1031 let font_name: Option<String> = None;
1032 #[cfg(not(feature = "web_lift"))]
1033 let font_name = provider.table_data(tag::NAME).ok().and_then(|name_data| {
1034 ReadScope::new(&name_data?)
1035 .read::<allsorts::tables::NameTable<'_>>()
1036 .ok()
1037 .and_then(|name_table| {
1038 name_table.string_for_id(allsorts::tables::NameTable::POSTSCRIPT_NAME)
1039 })
1040 });
1041
1042 // DIAG (2026-06-02, REVERT): pinpoint the web font-parse-fails root — does HEAD
1043 // fail because table_data can't find/return the table (directory mis-lift) or
1044 // because HeadTable::read errors (table-read mis-lift)? Surfaced via warnings.
1045 let head_table = match provider.table_data(tag::HEAD) {
1046 Ok(Some(head_cow)) => {
1047 // DIAG: is the HEAD table data CORRECT (magicNumber 0x5F0F3CF5 @ off 12 →
1048 // HeadTable::read mis-lifts) or WRONG bytes (directory offset mis-lift)?
1049 let bb = head_cow.as_ref();
1050 let magic = if bb.len() >= 16 {
1051 (u32::from(bb[12]) << 24) | (u32::from(bb[13]) << 16)
1052 | (u32::from(bb[14]) << 8) | u32::from(bb[15])
1053 } else { 0 };
1054 if let Ok(h) = ReadScope::new(&head_cow).read::<HeadTable>() { h } else {
1055 // DIAG: surface the sliced offset (how wrong) as hex — "HO" + 8 hex
1056 // of (head_cow.ptr - font_bytes.ptr). garbage→offset-read mis-lift;
1057 // 00000000→base; plausible-but-wrong→record mapping. "RF"=bytes-OK.
1058 let m = if magic == 0x5F0F_3CF5 {
1059 "RF000000".to_string()
1060 } else {
1061 let off = (head_cow.as_ref().as_ptr() as usize)
1062 .wrapping_sub(font_bytes.as_ptr() as usize);
1063 let mut msg = String::new();
1064 // B=Borrowed(slice of font_bytes, ptr-arith/base mis-lift) vs
1065 // O=Owned(decompressed/copied Vec — wrong path for plain TTF).
1066 msg.push(if matches!(head_cow, std::borrow::Cow::Borrowed(_)) { 'B' } else { 'O' });
1067 msg.push_str("HO");
1068 let mut sh: i32 = 28;
1069 while sh >= 0 {
1070 let d = ((off >> sh) & 0xf) as u8;
1071 msg.push((if d < 10 { b'0' + d } else { b'a' + d - 10 }) as char);
1072 sh -= 4;
1073 }
1074 msg
1075 };
1076 warnings.push(FontParseWarning::error(m));
1077 return None;
1078 }
1079 }
1080 Ok(None) => {
1081 // DIAG (REVERT): bytes+len+read_item-count+dir all proved OK (N0fr0fc0fg1)
1082 // yet find_table_record(HEAD)=None though 'head' is rec[7] on disk. So
1083 // either read_item's table_tag FIELD is garbage, or tag::HEAD mis-lifts, or
1084 // the u32 == mis-lifts. t7 = tags[7] (should be 0x68656164 'head' low16
1085 // =6164); H = tag::HEAD low16 (should be 6164); f = ANY tag==HEAD via an
1086 // indexed compare loop (NOT .iter().any). "T<4h t7>H<4h HEAD>f<0|1>".
1087 // T6164 H6164 f1 → values+compare OK (won't reach here — HEAD found)
1088 // T6164 H6164 f0 → the u32 == comparison mis-lifts
1089 // T!=6164 → read_item table_tag FIELD garbage (tuple read mis-lift)
1090 // H!=6164 → tag::HEAD const mis-lifts
1091 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1092 fn hx(m: &mut String, val: u32, nibbles: i32) {
1093 let mut sh = (nibbles - 1) * 4;
1094 while sh >= 0 {
1095 let d = ((val >> sh) & 0xf) as u8;
1096 m.push((if d < 10 { b'0' + d } else { b'a' + d - 10 }) as char);
1097 sh -= 4;
1098 }
1099 }
1100 // DECISIVE: tags[8] (head, file off 124) reads 0 but tags[2] (off 28) is OK.
1101 // Read the SAME offsets from from_provider's LOCAL font_bytes param (proven
1102 // correct at off 4/12). If local@124 = 0x6865 'he' but provider tags[8]=0 ⇒
1103 // STORED-SLICE issue (provider self.data fat-ptr mis-lifts) → read locally.
1104 // If local@124 = 0 ⇒ the font CONST is only PARTIALLY MIRRORED into the wasm
1105 // (deep data-mirror gap) → table data is simply absent. local@93596 (=0x16f9c,
1106 // head TABLE data start) further maps the mirror: 'he'/nonzero vs 0.
1107 let loc124 = if font_bytes.len() >= 126 {
1108 (u32::from(font_bytes[124]) << 8) | u32::from(font_bytes[125])
1109 } else {
1110 0xEEEE
1111 };
1112 let loc_head = if font_bytes.len() >= 93598 {
1113 (u32::from(font_bytes[93596]) << 8) | u32::from(font_bytes[93597])
1114 } else {
1115 0xEEEE
1116 };
1117 let mut m = String::from("L"); // local font_bytes[124..126] (head dir record):
1118 hx(&mut m, loc124 & 0xffff, 4); // 6865 'he' = mirrored; 0000 = not
1119 m.push('H'); // local font_bytes[93596..] (head TABLE data, deep):
1120 hx(&mut m, loc_head & 0xffff, 4);
1121 warnings.push(FontParseWarning::error(m));
1122 return None;
1123 }
1124 Err(_) => {
1125 warnings.push(FontParseWarning::error("HEAD_DATAERR".to_string()));
1126 return None;
1127 }
1128 };
1129
1130 let maxp_table = provider
1131 .table_data(tag::MAXP)
1132 .ok()
1133 .and_then(|maxp_data| ReadScope::new(&maxp_data?).read::<MaxpTable>().ok())
1134 .unwrap_or(MaxpTable {
1135 num_glyphs: 0,
1136 version1_sub_table: None,
1137 });
1138
1139 let num_glyphs = maxp_table.num_glyphs as usize;
1140
1141 // Compute byte offset+length into font_bytes for hmtx/vmtx
1142 // instead of copying the table data. The provider returns a
1143 // borrowed slice for OpenType fonts, so we can derive the
1144 // offset via pointer arithmetic.
1145 let hmtx_range = provider
1146 .table_data(tag::HMTX)
1147 .ok()
1148 .and_then(|cow_opt| {
1149 let cow = cow_opt?;
1150 match cow {
1151 std::borrow::Cow::Borrowed(slice) => {
1152 let base = font_bytes.as_ptr() as usize;
1153 let ptr = slice.as_ptr() as usize;
1154 let offset = ptr.checked_sub(base)?;
1155 if offset + slice.len() <= font_bytes.len() {
1156 Some((offset, slice.len()))
1157 } else {
1158 None
1159 }
1160 }
1161 std::borrow::Cow::Owned(_) => None,
1162 }
1163 })
1164 .unwrap_or((0, 0));
1165
1166 let vmtx_range = provider
1167 .table_data(tag::VMTX)
1168 .ok()
1169 .and_then(|s| {
1170 let slice = s?;
1171 let base = font_bytes.as_ptr() as usize;
1172 let ptr = slice.as_ptr() as usize;
1173 let offset = ptr.checked_sub(base)?;
1174 if offset + slice.len() <= font_bytes.len() {
1175 Some((offset, slice.len()))
1176 } else {
1177 None
1178 }
1179 })
1180 .unwrap_or((0, 0));
1181
1182 // Parse vhea table (same format as hhea, used for vertical metrics)
1183 let vhea_table = provider
1184 .table_data(tag::VHEA)
1185 .ok()
1186 .and_then(|vhea_data| ReadScope::new(&vhea_data?).read::<HheaTable>().ok());
1187
1188 // hhea is required per the OpenType spec; return None if missing
1189 let hhea_table = provider
1190 .table_data(tag::HHEA)
1191 .ok()
1192 .and_then(|hhea_data| ReadScope::new(&hhea_data?).read::<HheaTable>().ok())?;
1193
1194 // Build layout-specific font metrics
1195 let font_metrics = LayoutFontMetrics {
1196 units_per_em: if head_table.units_per_em == 0 {
1197 1000
1198 } else {
1199 head_table.units_per_em
1200 },
1201 ascent: f32::from(hhea_table.ascender),
1202 descent: f32::from(hhea_table.descender),
1203 line_gap: f32::from(hhea_table.line_gap),
1204 x_height: None, // will be populated from OS/2 table via from_font_metrics if available
1205 cap_height: None,
1206 };
1207
1208 // Build PDF-specific font metrics
1209 let pdf_font_metrics =
1210 Self::parse_pdf_font_metrics(font_bytes, font_index, &head_table, &hhea_table);
1211
1212 // Use allsorts LocaGlyf for on-demand outline extraction. We
1213 // *load* LocaGlyf eagerly (it owns ~tens of KiB of loca +
1214 // ~hundreds of KiB of glyf bytes) but we *don't* decode any
1215 // glyph outlines up front — that's the big RSS win. Glyphs
1216 // are decoded by `ParsedFont::get_or_decode_glyph` on first
1217 // access from the CPU/GPU rasterizer.
1218 //
1219 // When `defer_loca_glyf` is set (production lazy path via
1220 // `from_bytes_shared`), we skip `LocaGlyf::load` here too —
1221 // the caller will overwrite the slot with
1222 // `LocaGlyfState::Deferred` carrying the source bytes
1223 // `Arc<[u8]>`, and the load happens on the first
1224 // `get_or_decode_glyph` call. This avoids parsing
1225 // ~hundreds of KiB per face for fonts that get resolved
1226 // into a chain but never actually rasterized (typical
1227 // for fallback fonts in CSS chains).
1228 let has_glyf = provider.has_table(tag::GLYF) && provider.has_table(tag::LOCA);
1229 // Cache `has_gvar` before `provider` gets moved into
1230 // `allsorts::font::Font::new(provider)` further down —
1231 // it's the cheapest way to detect a variable font and
1232 // avoids the borrow-after-move that a later
1233 // `provider.has_table(tag::GVAR)` would incur.
1234 let has_gvar = provider.has_table(tag::GVAR);
1235 let loca_glyf_opt: Option<Arc<std::sync::Mutex<LocaGlyf>>> = if has_glyf
1236 && !defer_loca_glyf
1237 {
1238 match LocaGlyf::load(&provider) {
1239 Ok(lg) => Some(Arc::new(std::sync::Mutex::new(lg))),
1240 Err(e) => {
1241 warnings.push(FontParseWarning::warning(format!(
1242 "Failed to load LocaGlyf: {e} — falling back to hmtx-only"
1243 )));
1244 None
1245 }
1246 }
1247 } else {
1248 None
1249 };
1250
1251 // Lazy `glyph_cache` starts empty; the space-glyph stub
1252 // below pre-inserts gid 0 / space so the shaper's
1253 // cmap-miss fallback has something to render without
1254 // racing with a decode.
1255
1256 let mut font_data_impl = allsorts::font::Font::new(provider).ok()?;
1257
1258 // Create TrueType hinting instance from font tables.
1259 // [az-web-lift] Skip on the web build. The lifted layout never grid-fits glyphs to a
1260 // pixel raster (it measures + ships a display list to JS), so hinting is never used.
1261 // Building it (HintInstance::new) runs the allsorts bytecode Interpreter
1262 // (Interpreter::new + ::dispatch — a large un-devirt'd opcode jump table the remill
1263 // lift can't resolve, plus ~700 op_* fns of closure bloat). This is INDEPENDENT of the
1264 // lift's jump-table devirt: even with a perfect lift, web has no use for hinting, and
1265 // hinted advances are lower-quality output than the plain scaled advance. Native keeps
1266 // real hinting unchanged.
1267 #[cfg(feature = "web_lift")]
1268 let hint_instance: Option<std::sync::Mutex<allsorts::hinting::HintInstance>> = None;
1269 #[cfg(not(feature = "web_lift"))]
1270 let hint_instance = allsorts::hinting::HintInstance::new(
1271 &font_data_impl.font_table_provider
1272 ).ok().flatten().map(std::sync::Mutex::new);
1273
1274 // Stash raw GSUB/GPOS bytes for lazy parse. Typical fonts
1275 // have ~tens of KiB of GSUB + a few-to-tens of KiB of GPOS —
1276 // dwarfed by glyph outlines — so we keep the bytes around
1277 // and only spend `LayoutTable::read` + `new_layout_cache`
1278 // cycles when the shaper actually needs them (via
1279 // `ParsedFont::gsub` / `::gpos`). For an ASCII run where no
1280 // substitution / kerning is required, we skip both entirely.
1281 let gsub_bytes = font_data_impl
1282 .font_table_provider
1283 .table_data(tag::GSUB)
1284 .ok()
1285 .flatten()
1286 .map(std::borrow::Cow::into_owned);
1287 let gpos_bytes = font_data_impl
1288 .font_table_provider
1289 .table_data(tag::GPOS)
1290 .ok()
1291 .flatten()
1292 .map(std::borrow::Cow::into_owned);
1293 let opt_gdef_table = font_data_impl.gdef_table().ok().and_then(|o| o);
1294 let num_glyphs = font_data_impl.num_glyphs();
1295
1296 let opt_kern_table = font_data_impl
1297 .kern_table()
1298 .ok()
1299 .and_then(|s| s);
1300
1301 let cmap_data = font_data_impl.cmap_subtable_data();
1302 let cmap_subtable = ReadScope::new(cmap_data);
1303 let cmap_subtable = cmap_subtable
1304 .read::<CmapSubtable<'_>>()
1305 .ok()
1306 .and_then(|s| s.to_owned());
1307
1308 // Font identity hash — used by `PartialEq` for ParsedFont.
1309 //
1310 // Previously we did `font_bytes.hash(&mut hasher)` over
1311 // the full mmap. That touched every page of the file
1312 // (a 40 MiB `.ttc` walked byte-for-byte) so the "lazy
1313 // mmap" ended up *fully resident* the moment we built
1314 // a `ParsedFont`. Cold RSS jumped ~40 MiB from this
1315 // single line.
1316 //
1317 // The hash doesn't need to be cryptographic — it just
1318 // has to disambiguate two `ParsedFont`s. `(len, first
1319 // 4 KiB, last 4 KiB, font_index)` is plenty unique and
1320 // only faults in the two header / trailer pages, which
1321 // shaping is going to need anyway.
1322 let mut hasher = DefaultHasher::new();
1323 (font_bytes.len() as u64).hash(&mut hasher);
1324 let head_len = font_bytes.len().min(4096);
1325 font_bytes[..head_len].hash(&mut hasher);
1326 let tail_start = font_bytes.len().saturating_sub(4096);
1327 font_bytes[tail_start..].hash(&mut hasher);
1328 font_index.hash(&mut hasher);
1329 let hash = hasher.finish();
1330
1331 let mut font = Self {
1332 hash,
1333 font_metrics,
1334 pdf_font_metrics,
1335 num_glyphs,
1336 hhea_table,
1337 hmtx_range,
1338 vmtx_range,
1339 vhea_table,
1340 maxp_table,
1341 gsub_bytes,
1342 gsub_cache_lazy: std::sync::OnceLock::new(),
1343 gpos_bytes,
1344 gpos_cache_lazy: std::sync::OnceLock::new(),
1345 opt_gdef_table,
1346 opt_kern_table,
1347 cmap_subtable,
1348 last_used: Arc::new(std::sync::atomic::AtomicU64::new(0)),
1349 is_variable_font: has_gvar,
1350 glyph_cache: Arc::new(rust_fontconfig::StLock::new(BTreeMap::new())),
1351 // Eager path: `from_bytes` loaded LocaGlyf immediately
1352 // (or set None if the font has no loca+glyf). Lazy
1353 // callers use `from_bytes_shared` which replaces this
1354 // with `LocaGlyfState::Deferred` before returning.
1355 loca_glyf: LocaGlyfState::Loaded(loca_glyf_opt),
1356 space_width: None,
1357 mock: None,
1358 reverse_glyph_cache: BTreeMap::new(),
1359 // Don't retain the source bytes by default — layout and
1360 // raster don't need them. PDF subsetting / `to_bytes`
1361 // callers opt in via `with_source_bytes`.
1362 original_bytes: None,
1363 original_index: font_index,
1364 index_to_cid: BTreeMap::new(), // Will be filled for CFF fonts
1365 font_type: FontType::TrueType, // Default, will be updated if CFF
1366 font_name,
1367 hint_instance,
1368 };
1369
1370 // Calculate space width
1371 let space_width = font.get_space_width_internal();
1372
1373 // Pre-decode the space glyph straight into the lazy
1374 // `glyph_cache`. Space typically has no outline, so the
1375 // decoder's outline visitor returns nothing useful and
1376 // we'd spin re-decoding it every shape — short-circuit
1377 // here with a hand-rolled record carrying the hmtx
1378 // advance.
1379 let _ = (|| {
1380 let space_gid = font.lookup_glyph_index(' ' as u32)?;
1381 {
1382 // StLock::read() is infallible (Result<_, Infallible>);
1383 // kept in a tight block so the guard drops at scope end.
1384 let Ok(cache) = font.glyph_cache.read();
1385 if cache.contains_key(&space_gid) {
1386 return None;
1387 }
1388 }
1389 let space_width_val = space_width?;
1390 // Only pre-cache when we actually know a non-zero advance. During
1391 // `from_bytes_internal` the source bytes are not attached yet, so
1392 // `hmtx` is unreadable and `get_space_width_internal` reads back 0;
1393 // caching that would pin every space to a 0 advance for the life of
1394 // the face. Skip it and let the space decode lazily once bytes are
1395 // attached (mock fonts that carry a real space width still cache).
1396 if space_width_val == 0 {
1397 return None;
1398 }
1399 let space_record = OwnedGlyph {
1400 bounding_box: OwnedGlyphBoundingBox {
1401 max_x: 0,
1402 max_y: 0,
1403 min_x: 0,
1404 min_y: 0,
1405 },
1406 horz_advance: space_width_val as u16,
1407 outline: Vec::new(),
1408 phantom_points: None,
1409 raw_points: None,
1410 raw_on_curve: None,
1411 raw_contour_ends: None,
1412 instructions: None,
1413 };
1414 {
1415 // StLock::write() is infallible (Result<_, Infallible>).
1416 let Ok(mut cache) = font.glyph_cache.write();
1417 cache.insert(space_gid, Arc::new(space_record));
1418 }
1419 Some(())
1420 })();
1421
1422 font.space_width = space_width;
1423
1424 Some(font)
1425 }
1426
1427 /// Attach the source font bytes to this `ParsedFont`, enabling
1428 /// [`ParsedFont::to_bytes`] and [`ParsedFont::subset`] (both of
1429 /// which the layout / shaping path never calls).
1430 ///
1431 /// Takes an `Arc<FontBytes>` so the same file's bytes can be
1432 /// shared across every face of a `.ttc` at zero extra cost —
1433 /// pair with [`rust_fontconfig::FcFontCache::get_font_bytes`].
1434 /// For ad-hoc PDF callers that have raw heap bytes, wrap them
1435 /// via `Arc::new(FontBytes::Owned(Arc::from(vec)))`.
1436 #[must_use]
1437 pub fn with_source_bytes(mut self, bytes: Arc<rust_fontconfig::FontBytes>) -> Self {
1438 self.original_bytes = Some(bytes);
1439 self
1440 }
1441
1442 /// Lazy-friendly constructor — identical to
1443 /// [`ParsedFont::from_bytes`] except that `LocaGlyf` is
1444 /// **not** loaded during the call. Instead, the supplied
1445 /// `Arc<[u8]>` is retained and `LocaGlyf::load` runs the first
1446 /// time [`get_or_decode_glyph`] needs glyph outlines for this
1447 /// face.
1448 ///
1449 /// Fonts that get resolved into a CSS fallback chain but are
1450 /// never actually rasterized (common on desktop — e.g. every
1451 /// face of HelveticaNeue.ttc loads, but only one or two are
1452 /// shaped) then pay zero loca/glyf cost.
1453 ///
1454 /// Production callers (the reftest harness, `LayoutWindow`,
1455 /// `cpurender`) should prefer this constructor. Tests that
1456 /// inspect `glyph_records_decoded` directly and don't want
1457 /// a lazy path keep using `from_bytes`.
1458 pub fn from_bytes_shared(
1459 bytes: Arc<rust_fontconfig::FontBytes>,
1460 font_index: usize,
1461 warnings: &mut Vec<FontParseWarning>,
1462 ) -> Option<Self> {
1463 // Skip the eager LocaGlyf::load via `defer_loca_glyf=true`
1464 // — saves the load-then-drop cycle the prior arrangement
1465 // paid (when this called `from_bytes`, allocated
1466 // ~hundreds of KiB of loca+glyf bytes, then immediately
1467 // replaced the slot with `Deferred` and dropped them).
1468 // `bytes.as_ref()` derefs FontBytes → &[u8] (mmap or owned
1469 // — same code path).
1470 let mut font = Self::from_bytes_internal(bytes.as_ref(), font_index, warnings, true)?;
1471 font.original_bytes = Some(bytes.clone());
1472 font.loca_glyf = LocaGlyfState::Deferred {
1473 bytes,
1474 font_index,
1475 loaded: Arc::new(std::sync::Mutex::new(None)),
1476 };
1477 Some(font)
1478 }
1479
1480 /// Resolve the current face's `LocaGlyf`, loading it lazily
1481 /// on first call when `loca_glyf` is `Deferred`. Returns
1482 /// `None` when the font has no usable loca+glyf (CFF fonts
1483 /// or parse failures).
1484 fn resolve_loca_glyf(&self) -> Option<Arc<std::sync::Mutex<LocaGlyf>>> {
1485 use allsorts::{
1486 binary::read::ReadScope,
1487 font_data::FontData,
1488 tables::FontTableProvider,
1489 };
1490 match &self.loca_glyf {
1491 LocaGlyfState::Loaded(inner) => inner.clone(),
1492 LocaGlyfState::Deferred { bytes, font_index, loaded } => {
1493 // Fast path: cached LocaGlyf is present.
1494 if let Ok(guard) = loaded.lock() {
1495 if let Some(arc) = guard.as_ref() {
1496 return Some(Arc::clone(arc));
1497 }
1498 }
1499 let _p = crate::probe::Probe::span("resolve_loca_glyf");
1500
1501 // Slow path: parse provider + load LocaGlyf without
1502 // holding the slot's lock (allsorts can take a
1503 // millisecond or two on a fresh load). Re-check
1504 // after acquiring the write lock so a parallel
1505 // decoder doesn't double-load.
1506 let scope = ReadScope::new(bytes.as_slice());
1507 let font_data = scope.read::<FontData<'_>>().ok()?;
1508 let provider = font_data.table_provider(*font_index).ok()?;
1509 // Gate on table presence to match the `from_bytes`
1510 // has_glyf check; avoids a spurious warning on
1511 // CFF fonts that sneak into the Deferred path.
1512 if !provider.has_table(tag::GLYF) || !provider.has_table(tag::LOCA) {
1513 return None;
1514 }
1515 // Zero-copy: keep `glyf` as a view into the already-resident
1516 // (mmap'd) font bytes instead of copying the whole table
1517 // (~20 MB for a large font) onto the heap. `bytes` is the
1518 // exact buffer `provider` reads from, so load_shared can
1519 // anchor the glyf range inside it (falling back to an owned
1520 // copy if it ever can't). Audit §3.3a.
1521 let owner: Arc<dyn AsRef<[u8]> + Send + Sync> = bytes.clone();
1522 let new_arc = LocaGlyf::load_shared(&provider, owner)
1523 .ok()
1524 .map(|lg| Arc::new(std::sync::Mutex::new(lg)))?;
1525
1526 if let Ok(mut guard) = loaded.lock() {
1527 if let Some(existing) = guard.as_ref() {
1528 return Some(Arc::clone(existing));
1529 }
1530 *guard = Some(Arc::clone(&new_arc));
1531 }
1532 Some(new_arc)
1533 }
1534 }
1535 }
1536
1537 /// Source bytes for PDF subsetting / table extraction.
1538 ///
1539 /// Looks in two places:
1540 /// - `original_bytes` (set by [`ParsedFont::with_source_bytes`]
1541 /// for legacy PDF-first construction).
1542 /// - `LocaGlyfState::Deferred.bytes` (set by
1543 /// [`ParsedFont::from_bytes_shared`] — the production lazy
1544 /// path, which already retains an `Arc<[u8]>` for the lazy
1545 /// loca/glyf loader).
1546 ///
1547 /// Returns `None` only for `ParsedFont`s built via the eager
1548 /// `from_bytes` path without an explicit `with_source_bytes`
1549 /// call — i.e. unit tests that load a font and don't touch
1550 /// PDF.
1551 pub fn source_bytes_for_subset(&self) -> Option<Arc<rust_fontconfig::FontBytes>> {
1552 if let Some(bytes) = &self.original_bytes {
1553 return Some(Arc::clone(bytes));
1554 }
1555 if let LocaGlyfState::Deferred { bytes, .. } = &self.loca_glyf {
1556 return Some(Arc::clone(bytes));
1557 }
1558 None
1559 }
1560
1561 /// Read the monotonic-clock nanos timestamp of the most
1562 /// recent [`get_or_decode_glyph`] call on this face, or `0`
1563 /// if it's never been touched.
1564 pub fn last_used_nanos(&self) -> u64 {
1565 self.last_used.load(std::sync::atomic::Ordering::Relaxed)
1566 }
1567
1568 /// Drop the cached `LocaGlyf` for this face if it's
1569 /// `Deferred`-with-bytes-retained — so the next
1570 /// [`get_or_decode_glyph`] re-parses from `bytes`. No-op for
1571 /// `Loaded` faces (no source bytes to fall back to).
1572 ///
1573 /// Used by [`crate::text3::cache::FontManager::evict_unused`]
1574 /// and exposed publicly so embedders can free memory under
1575 /// pressure on fonts they no longer need to render.
1576 pub fn evict_loca_glyf(&self) -> bool {
1577 match &self.loca_glyf {
1578 LocaGlyfState::Deferred { loaded, .. } => {
1579 if let Ok(mut guard) = loaded.lock() {
1580 if guard.is_some() {
1581 *guard = None;
1582 return true;
1583 }
1584 }
1585 false
1586 }
1587 LocaGlyfState::Loaded(_) => false,
1588 }
1589 }
1590
1591 /// Fetch the parsed GSUB cache if this font has one, parsing
1592 /// it from the retained `gsub_bytes` on first access.
1593 ///
1594 /// Moved out of the eager `from_bytes` path because most text
1595 /// runs never trigger GSUB — plain ASCII without ligatures is
1596 /// handled entirely by the cmap + hmtx fast path. Building
1597 /// `LayoutCacheData<GSUB>` up front reserved ~0.5–2 MiB per
1598 /// face just to throw it away on pages that don't shape
1599 /// complex scripts.
1600 pub fn gsub(&self) -> Option<&GsubCache> {
1601 self.gsub_cache_lazy
1602 .get_or_init(|| {
1603 use allsorts::{
1604 binary::read::ReadScope,
1605 layout::{new_layout_cache, LayoutTable, GSUB},
1606 };
1607 let bytes = self.gsub_bytes.as_ref()?;
1608 ReadScope::new(bytes)
1609 .read::<LayoutTable<GSUB>>()
1610 .ok()
1611 .map(new_layout_cache)
1612 })
1613 .as_ref()
1614 }
1615
1616 /// Fetch the parsed GPOS cache if this font has one, parsing
1617 /// it from the retained `gpos_bytes` on first access. See
1618 /// [`ParsedFont::gsub`] for the motivation.
1619 pub fn gpos(&self) -> Option<&GposCache> {
1620 self.gpos_cache_lazy
1621 .get_or_init(|| {
1622 use allsorts::{
1623 binary::read::ReadScope,
1624 layout::{new_layout_cache, LayoutTable, GPOS},
1625 };
1626 let bytes = self.gpos_bytes.as_ref()?;
1627 ReadScope::new(bytes)
1628 .read::<LayoutTable<GPOS>>()
1629 .ok()
1630 .map(new_layout_cache)
1631 })
1632 .as_ref()
1633 }
1634
1635 /// Fetch an `OwnedGlyph` for `gid`, decoding it on first access.
1636 ///
1637 /// Cached in the `Arc<RwLock<…>>` `glyph_cache` so subsequent
1638 /// calls (including across clones of this `ParsedFont`) hit the
1639 /// cache. Returns `None` when `gid >= num_glyphs` or the font
1640 /// has no loca+glyf and no hmtx entry for the glyph. For CFF
1641 /// fonts the returned record has an empty outline and an advance
1642 /// pulled from hmtx — matching the pre-lazy behaviour.
1643 ///
1644 /// Called on the rasterizer hot path; performance budget is a
1645 /// few µs per unique glyph (first hit) and an Arc bump + `BTreeMap`
1646 /// lookup (cache hits). The write lock is held only across the
1647 /// decode, not across the caller's use of the returned Arc.
1648 pub fn get_or_decode_glyph(&self, gid: u16) -> Option<Arc<OwnedGlyph>> {
1649 use std::sync::Arc;
1650 if usize::from(gid) >= self.num_glyphs as usize {
1651 return None;
1652 }
1653 // Bump the LRU timestamp so `FontManager::evict_unused`
1654 // can tell this face is still in use. Cheap atomic store
1655 // (Relaxed — eviction reads the same atomic and tolerates
1656 // a slightly stale value, which only causes "evict, then
1657 // re-load on next access" — never an incorrect render).
1658 self.last_used
1659 .store(monotonic_now_nanos(), std::sync::atomic::Ordering::Relaxed);
1660
1661 // Fast path: cache hit.
1662 {
1663 // StLock::read() is infallible; tight block drops the read
1664 // guard before the write lock below (deadlock avoidance).
1665 let Ok(cache) = self.glyph_cache.read();
1666 if let Some(existing) = cache.get(&gid) {
1667 return Some(Arc::clone(existing));
1668 }
1669 }
1670
1671 // Miss: decode. We drop the read lock before taking the
1672 // write lock to avoid deadlock, and we re-check on the way
1673 // in because another thread may have decoded the same glyph
1674 // in between.
1675 let record = self.decode_glyph_inner(gid);
1676 let arc = Arc::new(record);
1677 {
1678 // StLock::write() is infallible (Result<_, Infallible>).
1679 let Ok(mut cache) = self.glyph_cache.write();
1680 cache
1681 .entry(gid)
1682 .or_insert_with(|| Arc::clone(&arc));
1683 // If another thread beat us to the insert, return theirs
1684 // so all callers observe the same Arc.
1685 if let Some(winner) = cache.get(&gid) {
1686 return Some(Arc::clone(winner));
1687 }
1688 }
1689 Some(arc)
1690 }
1691
1692 /// Eagerly decode every glyph into the lazy `glyph_cache`,
1693 /// restoring the pre-lazy "every glyph is materialised at
1694 /// construction time" behaviour. Used by tests that iterate
1695 /// or compare against reference tooling, and by embedders
1696 /// that want a walkable view without driving every shape
1697 /// through `get_or_decode_glyph`.
1698 ///
1699 /// After `prime_glyph_cache`, callers can use
1700 /// [`ParsedFont::for_each_decoded_glyph`] or
1701 /// [`ParsedFont::glyph_cache_snapshot`] to observe the
1702 /// populated cache.
1703 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1704 pub fn prime_glyph_cache(&mut self) {
1705 let n = self.num_glyphs as usize;
1706 for glyph_index in 0..n {
1707 let gid = glyph_index as u16;
1708 drop(self.get_or_decode_glyph(gid));
1709 }
1710 }
1711
1712 /// Walk every entry currently in the lazy `glyph_cache`,
1713 /// invoking `f(gid, &OwnedGlyph)` for each. Holds a read
1714 /// lock for the duration; do not call back into the font
1715 /// from `f`. The cache is populated on demand by
1716 /// [`ParsedFont::get_or_decode_glyph`] (and bulk-prefilled
1717 /// by [`ParsedFont::prime_glyph_cache`]).
1718 pub fn for_each_decoded_glyph<F: FnMut(u16, &OwnedGlyph)>(&self, mut f: F) {
1719 {
1720 // StLock::read() is infallible (Result<_, Infallible>).
1721 let Ok(cache) = self.glyph_cache.read();
1722 for (gid, glyph) in cache.iter() {
1723 f(*gid, glyph.as_ref());
1724 }
1725 }
1726 }
1727
1728 /// Snapshot of the currently-decoded glyphs as a
1729 /// `BTreeMap<u16, Arc<OwnedGlyph>>`. Cheap (clones the
1730 /// Arcs, not the records). Used by callers that want to
1731 /// hand the map off across an API boundary; for in-place
1732 /// iteration prefer [`ParsedFont::for_each_decoded_glyph`].
1733 pub fn glyph_cache_snapshot(&self) -> BTreeMap<u16, Arc<OwnedGlyph>> {
1734 self.glyph_cache
1735 .read()
1736 .map(|c| c.clone())
1737 .unwrap_or_default()
1738 }
1739
1740 /// Core decode routine: produces one `OwnedGlyph` for `gid` by
1741 /// locking `loca_glyf` and running allsorts' outline visitor +
1742 /// raw-simple-glyph extraction. Factored out so both
1743 /// [`get_or_decode_glyph`] and [`prime_glyph_cache`] share it.
1744 ///
1745 /// Always returns an `OwnedGlyph` — if anything in the decode
1746 /// chain fails, falls back to an empty-outline record with the
1747 /// `hmtx` advance. This mirrors the pre-lazy behaviour where
1748 /// every gid ended up in `glyph_records_decoded`.
1749 fn hmtx_bytes(&self) -> &[u8] {
1750 let (off, len) = self.hmtx_range;
1751 if len == 0 { return &[]; }
1752 self.original_bytes.as_ref()
1753 .map_or(&[], |b| &b.as_ref()[off..off+len])
1754 }
1755
1756 fn vmtx_bytes(&self) -> &[u8] {
1757 let (off, len) = self.vmtx_range;
1758 if len == 0 { return &[]; }
1759 self.original_bytes.as_ref()
1760 .map_or(&[], |b| &b.as_ref()[off..off+len])
1761 }
1762
1763 #[allow(clippy::cast_possible_wrap)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1764 fn decode_glyph_inner(&self, gid: u16) -> OwnedGlyph {
1765 let _p = crate::probe::Probe::span("decode_glyph");
1766 // [az-web-lift] use get_horizontal_advance (reads hmtx directly on the web build)
1767 // instead of allsorts::glyph_info::advance, whose lifted ReadArray parse has an
1768 // un-devirt'd jump table → MISSING_BLOCK → OOB during measure.
1769 let horz_advance = self.get_horizontal_advance(gid);
1770
1771 let mut record = OwnedGlyph {
1772 horz_advance,
1773 bounding_box: OwnedGlyphBoundingBox {
1774 min_x: 0,
1775 min_y: 0,
1776 max_x: horz_advance as i16,
1777 max_y: 0,
1778 },
1779 outline: Vec::new(),
1780 phantom_points: None,
1781 raw_points: None,
1782 raw_on_curve: None,
1783 raw_contour_ends: None,
1784 instructions: None,
1785 };
1786
1787 // Resolve the `LocaGlyf` for this face. For `Loaded` that's
1788 // a cheap `Arc::clone`; for `Deferred` this is where the
1789 // actual `LocaGlyf::load` happens on first access, paid once
1790 // per face that ever decodes a glyph.
1791 let Some(loca_glyf_arc) = self.resolve_loca_glyf() else {
1792 // No usable loca+glyf → CFF / OpenType-PostScript font
1793 // (Noto Sans/Serif CJK and most .otf). Decode the glyph
1794 // from the `CFF ` table instead; the TrueType-only glyf
1795 // path below can't see these, which left every CFF glyph
1796 // blank on the cpurender/headless path (CJK rendered as
1797 // empty space with the hmtx advance still reserved).
1798 self.decode_cff_glyph_into(gid, &mut record);
1799 return record;
1800 };
1801 let Ok(mut loca_glyf) = loca_glyf_arc.lock() else {
1802 return record;
1803 };
1804
1805 // Visit the outline. If this is a variable font (gvar
1806 // table present) AND we still have source bytes (only
1807 // the `LocaGlyfState::Deferred` path retains them), we
1808 // re-derive a `VariableGlyfContext` here so default-
1809 // instance vs designed-instance differences land in
1810 // the decoded outline. The chained `if let` pattern
1811 // keeps `provider` and `store` in scope for the
1812 // visit, which the borrow checker requires (the
1813 // store's `Cow::Borrowed(&[u8])` tables tie its
1814 // lifetime to the provider).
1815 //
1816 // Eager-`from_bytes` faces (no retained bytes) and
1817 // non-variable fonts skip the var-context machinery
1818 // and decode the default instance — same behaviour as
1819 // before R4.
1820 // [az-web-lift] The lifted web layout NEVER rasterizes (it measures + positions, then
1821 // ships a display list to JS) — so glyph OUTLINES + TrueType hinting raw-points are
1822 // never needed in wasm. Decoding them (allsorts GlyfVisitorContext::visit +
1823 // GlyphOutlineCollector::into_outlines, whose GlyphOutlineOperation match is a 5-arm
1824 // jump table the remill lift doesn't devirtualize → MISSING_BLOCK → OOB) crashes the
1825 // measure pass. Skip BOTH decode passes on the web build; the record keeps its hmtx
1826 // advance/metrics (set above) which is all text measurement needs.
1827 if !cfg!(feature = "web_lift") {
1828 let mut outline_done = false;
1829 if self.is_variable_font {
1830 if let LocaGlyfState::Deferred { bytes, .. } = &self.loca_glyf {
1831 let scope = ReadScope::new(bytes);
1832 if let Ok(font_data) =
1833 scope.read::<FontData<'_>>()
1834 {
1835 if let Ok(provider) = font_data.table_provider(self.original_index) {
1836 if let Ok(store) = VariableGlyfContextStore::read(&provider) {
1837 if let Ok(var_ctx) = VariableGlyfContext::new(&store) {
1838 let mut visitor = GlyfVisitorContext::new(
1839 &mut loca_glyf,
1840 Some(var_ctx),
1841 );
1842 let mut collector = GlyphOutlineCollector::new();
1843 if visitor.visit(gid, None, &mut collector).is_ok() {
1844 record.outline = collector.into_outlines();
1845 let (min_x, min_y, max_x, max_y) =
1846 compute_outline_bbox(&record.outline);
1847 record.bounding_box = OwnedGlyphBoundingBox {
1848 min_x,
1849 min_y,
1850 max_x,
1851 max_y,
1852 };
1853 outline_done = true;
1854 }
1855 }
1856 }
1857 }
1858 }
1859 }
1860 }
1861 if !outline_done {
1862 let mut visitor =
1863 GlyfVisitorContext::new(&mut loca_glyf, None);
1864 let mut collector = GlyphOutlineCollector::new();
1865 if visitor.visit(gid, None, &mut collector).is_ok() {
1866 record.outline = collector.into_outlines();
1867 let (min_x, min_y, max_x, max_y) =
1868 compute_outline_bbox(&record.outline);
1869 record.bounding_box = OwnedGlyphBoundingBox {
1870 min_x,
1871 min_y,
1872 max_x,
1873 max_y,
1874 };
1875 }
1876 }
1877
1878 // Second pass: pull raw SimpleGlyph data for TrueType
1879 // bytecode hinting. LocaGlyf caches the `Arc<Glyph>`
1880 // internally so this lookup is cheap after the first call.
1881 if let Ok(glyph_arc) = loca_glyf.glyph(gid) {
1882 // `is_on_curve` moved onto the `SimpleGlyphFlagExt` trait in
1883 // allsorts 0.17 (SimpleGlyphFlags is now a BitFlags alias).
1884 use allsorts::tables::glyf::SimpleGlyphFlagExt;
1885 if let allsorts::tables::glyf::Glyph::Simple(sg) = glyph_arc.as_ref() {
1886 record.raw_points = Some(
1887 sg.coordinates.iter().map(|(_, pt)| (pt.0, pt.1)).collect(),
1888 );
1889 record.raw_on_curve = Some(
1890 sg.coordinates.iter().map(|(f, _)| f.is_on_curve()).collect(),
1891 );
1892 record.raw_contour_ends = Some(sg.end_pts_of_contours.clone());
1893 record.instructions = Some(sg.instructions.to_vec());
1894 }
1895 }
1896 } // [az-web-lift] end skip glyph outline/hinting decode on web
1897
1898 record
1899 }
1900
1901 /// Decode a single glyph outline from the `CFF ` (OpenType
1902 /// PostScript) table into `record`. Used for fonts with no `glyf`
1903 /// table — `decode_glyph_inner`'s TrueType path returns an empty
1904 /// outline for them, so without this every CFF glyph rasterised as
1905 /// blank on the CPU renderer. Notably this hit ALL CJK text: the
1906 /// installed Noto Sans/Serif CJK fonts are CID-keyed CFF. allsorts'
1907 /// `CFFOutlines` feeds the same `GlyphOutlineCollector` the glyf
1908 /// path uses and resolves CID-keyed local subrs internally.
1909 fn decode_cff_glyph_into(&self, gid: u16, record: &mut OwnedGlyph) {
1910 use allsorts::cff::{outline::CFFOutlines, CFF};
1911
1912 let Some(ref original) = self.original_bytes else {
1913 return;
1914 };
1915 let bytes: &[u8] = original.as_slice();
1916 let Ok(font_data) = ReadScope::new(bytes).read::<FontData<'_>>() else {
1917 return;
1918 };
1919 let Ok(provider) = font_data.table_provider(self.original_index) else {
1920 return;
1921 };
1922 let Ok(Some(cff_data)) = provider.table_data(tag::CFF) else {
1923 return;
1924 };
1925 let Ok(cff) = ReadScope::new(&cff_data).read::<CFF<'_>>() else {
1926 return;
1927 };
1928 let mut outlines = CFFOutlines { table: &cff };
1929 let mut collector = GlyphOutlineCollector::new();
1930 if outlines.visit(gid, None, &mut collector).is_ok() {
1931 record.outline = collector.into_outlines();
1932 let (min_x, min_y, max_x, max_y) = compute_outline_bbox(&record.outline);
1933 record.bounding_box = OwnedGlyphBoundingBox {
1934 min_x,
1935 min_y,
1936 max_x,
1937 max_y,
1938 };
1939 }
1940 }
1941
1942 /// Parse PDF-specific font metrics from HEAD, HHEA, and OS/2 tables
1943 fn parse_pdf_font_metrics(
1944 font_bytes: &[u8],
1945 font_index: usize,
1946 head_table: &allsorts::tables::HeadTable,
1947 hhea_table: &HheaTable,
1948 ) -> PdfFontMetrics {
1949 use allsorts::{
1950 binary::read::ReadScope,
1951 font_data::FontData,
1952 tables::{os2::Os2, FontTableProvider},
1953 tag,
1954 };
1955
1956 let scope = ReadScope::new(font_bytes);
1957 let font_file = scope.read::<FontData<'_>>().ok();
1958 let provider = font_file
1959 .as_ref()
1960 .and_then(|ff| ff.table_provider(font_index).ok());
1961
1962 let os2_table = provider
1963 .as_ref()
1964 .and_then(|p| p.table_data(tag::OS_2).ok())
1965 .and_then(|os2_data| {
1966 let data = os2_data?;
1967 let scope = ReadScope::new(&data);
1968 scope.read_dep::<Os2>(data.len()).ok()
1969 });
1970
1971 // Base metrics from HEAD and HHEA (always present)
1972 let base = PdfFontMetrics {
1973 units_per_em: head_table.units_per_em,
1974 font_flags: head_table.flags,
1975 x_min: head_table.x_min,
1976 y_min: head_table.y_min,
1977 x_max: head_table.x_max,
1978 y_max: head_table.y_max,
1979 ascender: hhea_table.ascender,
1980 descender: hhea_table.descender,
1981 line_gap: hhea_table.line_gap,
1982 advance_width_max: hhea_table.advance_width_max,
1983 caret_slope_rise: hhea_table.caret_slope_rise,
1984 caret_slope_run: hhea_table.caret_slope_run,
1985 ..PdfFontMetrics::zero()
1986 };
1987
1988 // Add OS/2 metrics if available
1989 os2_table
1990 .map_or(base, |os2| PdfFontMetrics {
1991 x_avg_char_width: os2.x_avg_char_width,
1992 us_weight_class: os2.us_weight_class,
1993 us_width_class: os2.us_width_class,
1994 y_strikeout_size: os2.y_strikeout_size,
1995 y_strikeout_position: os2.y_strikeout_position,
1996 ..base
1997 })
1998 }
1999
2000 /// Returns the width of the space character in font units.
2001 ///
2002 /// This is used internally for text layout calculations.
2003 /// Returns `None` if the font has no space glyph or its width cannot be determined.
2004 fn get_space_width_internal(&self) -> Option<usize> {
2005 if let Some(mock) = self.mock.as_ref() {
2006 return mock.space_width;
2007 }
2008 let glyph_index = self.lookup_glyph_index(' ' as u32)?;
2009
2010 // [az-web-lift] use get_horizontal_advance (direct hmtx on web) instead of
2011 // allsorts::glyph_info::advance (un-devirt'd jump table → OOB).
2012 Some(self.get_horizontal_advance(glyph_index) as usize)
2013 }
2014
2015 /// Look up the glyph index for a Unicode codepoint
2016 pub fn lookup_glyph_index(&self, codepoint: u32) -> Option<u16> {
2017 let cmap = self.cmap_subtable.as_ref()?;
2018 cmap.map_glyph(codepoint).ok().flatten()
2019 }
2020
2021 /// Get the horizontal advance width for a glyph in font units.
2022 ///
2023 /// Pulled straight from the `hmtx` table — no glyph-outline
2024 /// decode. Called once per shaped glyph per layout pass, so
2025 /// avoiding the lazy decode here is a meaningful win over
2026 /// routing through `get_or_decode_glyph`.
2027 pub fn get_horizontal_advance(&self, glyph_index: u16) -> u16 {
2028 if let Some(mock) = self.mock.as_ref() {
2029 return mock.glyph_advances.get(&glyph_index).copied().unwrap_or(0);
2030 }
2031 // [az-web-lift] Read the hmtx advance DIRECTLY (a plain longHorMetric table lookup)
2032 // instead of allsorts::glyph_info::advance, whose lifted binary `ReadArray` parse has
2033 // an un-devirt'd jump table → MISSING_BLOCK → OOB during text measure. Identical result
2034 // for non-variable fonts (the web fallback font is non-variable); native keeps the
2035 // allsorts path (variable-font deltas etc.).
2036 #[cfg(feature = "web_lift")]
2037 {
2038 let hmtx = self.hmtx_bytes();
2039 let num = usize::from(self.hhea_table.num_h_metrics);
2040 if num == 0 {
2041 return 0;
2042 }
2043 let idx = (glyph_index as usize).min(num - 1);
2044 let off = idx * 4;
2045 return if off + 2 <= hmtx.len() {
2046 ((hmtx[off] as u16) << 8) | (hmtx[off + 1] as u16)
2047 } else {
2048 0
2049 };
2050 }
2051 #[cfg(not(feature = "web_lift"))]
2052 {
2053 allsorts::glyph_info::advance(
2054 &self.maxp_table,
2055 &self.hhea_table,
2056 self.hmtx_bytes(),
2057 glyph_index,
2058 )
2059 .unwrap_or_default()
2060 }
2061 }
2062
2063 /// Get the hinted advance width in pixels for a glyph at the given ppem.
2064 ///
2065 /// For glyphs with outlines, runs TrueType bytecode hinting to get the
2066 /// grid-fitted advance from phantom points. For glyphs without outlines
2067 /// (e.g. space), rounds the scaled advance to the pixel grid, matching
2068 /// `FreeType`'s behavior.
2069 ///
2070 /// Returns `None` if hinting is not available or fails.
2071 #[allow(clippy::cast_precision_loss)] // bounded graphics/coord/font/fixed-point/debug-marker cast
2072 pub fn get_hinted_advance_px(&self, glyph_index: u16, ppem: u16) -> Option<f32> {
2073 // [az-web-lift] No pixel grid-fitting on the web (measure-only): return None so the
2074 // caller falls back to the plain scaled advance. Hard-cfg (not a runtime `if cfg!`)
2075 // so the whole hinting body — get_or_decode_glyph's outline path AND set_ppem →
2076 // allsorts Interpreter::dispatch (opcode jump table → OOB) — is removed from the lift
2077 // closure entirely. SEPARATE concern from the transpiler's jump-table devirt: web has
2078 // no use for hinted advances regardless of lift quality. Native is unchanged.
2079 #[cfg(feature = "web_lift")]
2080 {
2081 let _ = (glyph_index, ppem);
2082 None
2083 }
2084 #[cfg(not(feature = "web_lift"))]
2085 {
2086 use allsorts::hinting::f26dot6::{compute_scale, F26Dot6};
2087 let glyph = self.get_or_decode_glyph(glyph_index)?;
2088
2089 let upem = self.font_metrics.units_per_em;
2090 if upem == 0 || ppem == 0 {
2091 return None;
2092 }
2093
2094 // Check if we even have a hint instance
2095 let hint_mutex = self.hint_instance.as_ref()?;
2096
2097 let scale = compute_scale(ppem, upem);
2098 // Round the LIVE hmtx advance, not the decoded glyph's cached
2099 // `horz_advance`. The space glyph is eagerly pre-cached during
2100 // `from_bytes_internal` before `original_bytes` is attached, so its
2101 // cached advance can be a stale 0; and this function only ever rounds
2102 // the scaled hmtx advance to the pixel grid anyway (see below), never
2103 // the hinted phantom point. For every correctly-decoded glyph the two
2104 // are identical, so this is a no-op except for the stale-space case.
2105 let hmtx_advance = self.get_horizontal_advance(glyph_index);
2106 let adv_f26dot6 = F26Dot6::from_funits(i32::from(hmtx_advance), scale);
2107
2108 // For glyphs with outline data, run bytecode hinting
2109 if let (Some(raw_points), Some(raw_on_curve), Some(raw_contour_ends)) = (
2110 glyph.raw_points.as_ref(),
2111 glyph.raw_on_curve.as_ref(),
2112 glyph.raw_contour_ends.as_ref(),
2113 ) {
2114 let instructions = glyph.instructions.as_deref().unwrap_or(&[]);
2115 let mut hint = hint_mutex.lock().ok()?;
2116 hint.set_ppem(ppem, f64::from(ppem)).ok()?;
2117 drop(hint);
2118
2119 let points_f26dot6: Vec<(i32, i32)> = raw_points
2120 .iter()
2121 .map(|&(x, y)| {
2122 let sx = F26Dot6::from_funits(i32::from(x), scale);
2123 let sy = F26Dot6::from_funits(i32::from(y), scale);
2124 (sx.to_bits(), sy.to_bits())
2125 })
2126 .collect();
2127 }
2128
2129 // Use the scaled advance rounded to pixel grid, NOT the hinted
2130 // phantom point. Some glyph programs apply ClearType-specific SHPIX
2131 // adjustments to the advance phantom point that are wrong for
2132 // non-ClearType rendering. The rounded scaled advance matches
2133 // FreeType's DEFAULT mode advance output (and, for glyphs without an
2134 // outline such as space, FreeType's phantom-point pre-rounding).
2135 let rounded = (adv_f26dot6.to_bits() + 32) & !63;
2136 Some(rounded as f32 / 64.0)
2137 } // [az-web-lift] end #[cfg(not(web_lift))] hinting body
2138 }
2139
2140 /// Get the number of glyphs in this font
2141 pub const fn num_glyphs(&self) -> u16 {
2142 self.num_glyphs
2143 }
2144
2145 /// Check if this font has a glyph for the given codepoint
2146 pub fn has_glyph(&self, codepoint: u32) -> bool {
2147 self.lookup_glyph_index(codepoint).is_some()
2148 }
2149
2150 /// Get vertical metrics for a glyph (for vertical text layout).
2151 ///
2152 /// Uses vhea+vmtx tables (same binary format as hhea+hmtx).
2153 /// Returns None if font has no vertical metrics tables.
2154 #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2155 pub fn get_vertical_metrics(
2156 &self,
2157 glyph_id: u16,
2158 ) -> Option<crate::text3::cache::VerticalMetrics> {
2159 let vhea = self.vhea_table.as_ref()?;
2160 if self.vmtx_range.1 == 0 {
2161 return None;
2162 }
2163 let vert_advance = f32::from(allsorts::glyph_info::advance(
2164 &self.maxp_table, vhea, self.vmtx_bytes(), glyph_id,
2165 ).ok()?);
2166
2167 let units_per_em = f32::from(self.font_metrics.units_per_em);
2168 let scale = if units_per_em > 0.0 { 1.0 / units_per_em } else { 0.001 };
2169
2170 // Vertical bearing: approximate from glyph bbox if available
2171 let (bearing_x, bearing_y) = self.get_or_decode_glyph(glyph_id)
2172 .map_or((0.0, 0.0), |g| {
2173 let bbox = &g.bounding_box;
2174 // tsb (top side bearing): origin_y - max_y
2175 // lsb for vertical: center the glyph horizontally
2176 let width = f32::from(bbox.max_x - bbox.min_x);
2177 (-(width / 2.0) * scale, (vert_advance * scale) - (f32::from(bbox.max_y) * scale))
2178 });
2179
2180 Some(crate::text3::cache::VerticalMetrics {
2181 advance: vert_advance * scale,
2182 bearing_x,
2183 bearing_y,
2184 origin_y: self.font_metrics.ascent * scale,
2185 })
2186 }
2187
2188 /// Get layout-specific font metrics
2189 #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
2190 pub fn get_font_metrics(&self) -> LayoutFontMetrics {
2191 // Ensure descent is positive (OpenType may have negative descent)
2192 let descent = if self.font_metrics.descent > 0.0 {
2193 self.font_metrics.descent
2194 } else {
2195 -self.font_metrics.descent
2196 };
2197
2198 LayoutFontMetrics {
2199 ascent: self.font_metrics.ascent,
2200 descent,
2201 line_gap: self.font_metrics.line_gap,
2202 units_per_em: self.font_metrics.units_per_em,
2203 x_height: self.font_metrics.x_height,
2204 cap_height: self.font_metrics.cap_height,
2205 }
2206 }
2207
2208 /// Convert the `ParsedFont` back to bytes using `allsorts::whole_font`
2209 /// This reconstructs the entire font from the parsed data
2210 ///
2211 /// Source bytes come from either the explicit
2212 /// [`ParsedFont::with_source_bytes`] handle (PDF-first
2213 /// construction) *or* the `LocaGlyfState::Deferred` slot
2214 /// installed by [`ParsedFont::from_bytes_shared`]. The
2215 /// production lazy path retains bytes for the lazy `LocaGlyf`
2216 /// loader, so PDF subsetting Just Works without an extra
2217 /// `with_source_bytes` call.
2218 ///
2219 /// # Arguments
2220 /// * `tags` - Optional list of specific table tags to include (None = all tables)
2221 /// # Errors
2222 ///
2223 /// Returns an error string if serializing the font fails.
2224 pub fn to_bytes(&self, tags: Option<&[u32]>) -> Result<Vec<u8>, String> {
2225 let source = self.source_bytes_for_subset().ok_or_else(|| {
2226 "ParsedFont::to_bytes requires source bytes; construct via \
2227 ParsedFont::from_bytes_shared (production lazy path) or \
2228 attach via ParsedFont::with_source_bytes"
2229 .to_string()
2230 })?;
2231 let scope = ReadScope::new(source.as_slice());
2232 let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
2233 let provider = font_file
2234 .table_provider(self.original_index)
2235 .map_err(|e| e.to_string())?;
2236
2237 let tags_to_use = tags.unwrap_or(&[
2238 tag::CMAP,
2239 tag::HEAD,
2240 tag::HHEA,
2241 tag::HMTX,
2242 tag::MAXP,
2243 tag::NAME,
2244 tag::OS_2,
2245 tag::POST,
2246 tag::GLYF,
2247 tag::LOCA,
2248 ]);
2249
2250 whole_font(&provider, tags_to_use).map_err(|e| e.to_string())
2251 }
2252
2253 /// Create a subset font containing only the specified glyph IDs
2254 /// Returns the subset font bytes and a mapping from old to new glyph IDs
2255 ///
2256 /// # Arguments
2257 /// * `glyph_ids` - The glyph IDs to include in the subset (glyph 0/.notdef is always
2258 /// included)
2259 /// * `cmap_target` - Target cmap format (Unicode for web, `MacRoman` for compatibility)
2260 ///
2261 /// # Returns
2262 /// A tuple of (`subset_font_bytes`, `glyph_mapping`) where `glyph_mapping` maps
2263 /// `original_glyph_id` -> (`new_glyph_id`, `original_char`)
2264 #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
2265 /// # Errors
2266 ///
2267 /// Returns an error string if subsetting the font fails.
2268 pub fn subset(
2269 &self,
2270 glyph_ids: &[(u16, char)],
2271 cmap_target: CmapTarget,
2272 ) -> Result<(Vec<u8>, BTreeMap<u16, (u16, char)>), String> {
2273 let source = self.source_bytes_for_subset().ok_or_else(|| {
2274 "ParsedFont::subset requires source bytes; construct via \
2275 ParsedFont::from_bytes_shared (production lazy path) or \
2276 attach via ParsedFont::with_source_bytes"
2277 .to_string()
2278 })?;
2279 let scope = ReadScope::new(source.as_slice());
2280 let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
2281 let provider = font_file
2282 .table_provider(self.original_index)
2283 .map_err(|e| e.to_string())?;
2284
2285 // Build glyph mapping: original_id -> (new_id, char)
2286 let glyph_mapping: BTreeMap<u16, (u16, char)> = glyph_ids
2287 .iter()
2288 .enumerate()
2289 .map(|(new_id, &(original_id, ch))| (original_id, (new_id as u16, ch)))
2290 .collect();
2291
2292 // Extract just the glyph IDs for subsetting
2293 let ids: Vec<u16> = glyph_ids.iter().map(|(id, _)| *id).collect();
2294
2295 // Use PDF profile for embedding fonts in PDFs
2296 let font_bytes = allsorts_subset(&provider, &ids, &SubsetProfile::Pdf, cmap_target)
2297 .map_err(|e| format!("Subset error: {e:?}"))?;
2298
2299 Ok((font_bytes, glyph_mapping))
2300 }
2301
2302 /// Get the width of a glyph in font units (internal, unscaled)
2303 pub fn get_glyph_width_internal(&self, glyph_index: u16) -> Option<usize> {
2304 allsorts::glyph_info::advance(
2305 &self.maxp_table,
2306 &self.hhea_table,
2307 self.hmtx_bytes(),
2308 glyph_index,
2309 )
2310 .ok()
2311 .map(|s| s as usize)
2312 }
2313
2314 /// Get the width of the space character (unscaled font units)
2315 #[inline]
2316 pub const fn get_space_width(&self) -> Option<usize> {
2317 self.space_width
2318 }
2319
2320 /// Add glyph-to-text mapping to reverse cache
2321 /// This should be called during text shaping when we know both the source text and
2322 /// resulting glyphs
2323 pub fn cache_glyph_mapping(&mut self, glyph_id: u16, cluster_text: &str) {
2324 self.reverse_glyph_cache
2325 .insert(glyph_id, cluster_text.to_string());
2326 }
2327
2328 /// Get the cluster text that produced a specific glyph ID
2329 /// Returns the original text that was shaped into this glyph (handles ligatures correctly)
2330 pub fn get_glyph_cluster_text(&self, glyph_id: u16) -> Option<&str> {
2331 self.reverse_glyph_cache.get(&glyph_id).map(String::as_str)
2332 }
2333
2334 /// Get the first character from the cluster text for a glyph ID
2335 /// This is useful for PDF `ToUnicode` `CMap` generation which requires single character
2336 /// mappings
2337 pub fn get_glyph_primary_char(&self, glyph_id: u16) -> Option<char> {
2338 self.reverse_glyph_cache
2339 .get(&glyph_id)
2340 .and_then(|text| text.chars().next())
2341 }
2342
2343 /// Clear the reverse glyph cache (useful for memory management)
2344 pub fn clear_glyph_cache(&mut self) {
2345 self.reverse_glyph_cache.clear();
2346 }
2347
2348 /// Get the bounding box size of a glyph (unscaled units) - for PDF
2349 /// Returns (width, height) in font units
2350 pub fn get_glyph_bbox_size(&self, glyph_index: u16) -> Option<(i32, i32)> {
2351 let g = self.get_or_decode_glyph(glyph_index)?;
2352 let glyph_width = i32::from(g.horz_advance);
2353 let glyph_height = i32::from(g.bounding_box.max_y) - i32::from(g.bounding_box.min_y);
2354 Some((glyph_width, glyph_height))
2355 }
2356 }
2357
2358 /// Compute the bounding box from collected glyph outlines.
2359 fn compute_outline_bbox(outlines: &[GlyphOutline]) -> (i16, i16, i16, i16) {
2360 let mut min_x = i16::MAX;
2361 let mut min_y = i16::MAX;
2362 let mut max_x = i16::MIN;
2363 let mut max_y = i16::MIN;
2364 let mut has_points = false;
2365
2366 for outline in outlines {
2367 for op in outline.operations.as_slice() {
2368 let points: &[(i16, i16)] = match op {
2369 GlyphOutlineOperation::MoveTo(m) => &[(m.x, m.y)],
2370 GlyphOutlineOperation::LineTo(l) => &[(l.x, l.y)],
2371 GlyphOutlineOperation::QuadraticCurveTo(q) => {
2372 // Check both control and end point for bbox
2373 min_x = min_x.min(q.ctrl_1_x).min(q.end_x);
2374 min_y = min_y.min(q.ctrl_1_y).min(q.end_y);
2375 max_x = max_x.max(q.ctrl_1_x).max(q.end_x);
2376 max_y = max_y.max(q.ctrl_1_y).max(q.end_y);
2377 has_points = true;
2378 continue;
2379 }
2380 GlyphOutlineOperation::CubicCurveTo(c) => {
2381 min_x = min_x.min(c.ctrl_1_x).min(c.ctrl_2_x).min(c.end_x);
2382 min_y = min_y.min(c.ctrl_1_y).min(c.ctrl_2_y).min(c.end_y);
2383 max_x = max_x.max(c.ctrl_1_x).max(c.ctrl_2_x).max(c.end_x);
2384 max_y = max_y.max(c.ctrl_1_y).max(c.ctrl_2_y).max(c.end_y);
2385 has_points = true;
2386 continue;
2387 }
2388 GlyphOutlineOperation::ClosePath => continue,
2389 };
2390 for &(x, y) in points {
2391 min_x = min_x.min(x);
2392 min_y = min_y.min(y);
2393 max_x = max_x.max(x);
2394 max_y = max_y.max(y);
2395 has_points = true;
2396 }
2397 }
2398 }
2399
2400 if has_points {
2401 (min_x, min_y, max_x, max_y)
2402 } else {
2403 (0, 0, 0, 0)
2404 }
2405 }
2406
2407 #[derive(Debug, Clone)]
2408 pub struct OwnedGlyph {
2409 pub bounding_box: OwnedGlyphBoundingBox,
2410 pub horz_advance: u16,
2411 pub outline: Vec<GlyphOutline>,
2412 pub phantom_points: Option<[Point; 4]>,
2413 /// Raw TrueType points in font units (for hinting). None for composite/CFF glyphs.
2414 pub raw_points: Option<Vec<(i16, i16)>>,
2415 /// On-curve flags for each raw point.
2416 pub raw_on_curve: Option<Vec<bool>>,
2417 /// Contour end-point indices (TrueType).
2418 pub raw_contour_ends: Option<Vec<u16>>,
2419 /// Per-glyph TrueType hinting instructions.
2420 pub instructions: Option<Vec<u8>>,
2421 }
2422
2423 // --- ParsedFontTrait Implementation for ParsedFont ---
2424
2425 impl crate::text3::cache::ShallowClone for ParsedFont {
2426 fn shallow_clone(&self) -> Self {
2427 self.clone() // ParsedFont::clone uses Arc internally, so it's shallow
2428 }
2429 }
2430
2431 impl crate::text3::cache::ParsedFontTrait for ParsedFont {
2432 fn shape_text(
2433 &self,
2434 text: &str,
2435 script: crate::font_traits::Script,
2436 language: crate::font_traits::Language,
2437 direction: crate::font_traits::BidiDirection,
2438 style: &crate::font_traits::StyleProperties,
2439 ) -> Result<Vec<crate::font_traits::Glyph>, crate::font_traits::LayoutError> {
2440 // Call the existing shape_text_for_parsed_font method (defined in default.rs)
2441 crate::text3::default::shape_text_for_parsed_font(
2442 self, text, script, language, direction, style,
2443 )
2444 }
2445
2446 fn get_hash(&self) -> u64 {
2447 self.hash
2448 }
2449
2450 fn get_glyph_size(
2451 &self,
2452 glyph_id: u16,
2453 font_size_px: f32,
2454 ) -> Option<azul_core::geom::LogicalSize> {
2455 self.get_or_decode_glyph(glyph_id).map(|record| {
2456 let units_per_em = f32::from(self.font_metrics.units_per_em);
2457 let scale_factor = if units_per_em > 0.0 {
2458 font_size_px / units_per_em
2459 } else {
2460 0.01
2461 };
2462 let bbox = &record.bounding_box;
2463 azul_core::geom::LogicalSize {
2464 width: f32::from(bbox.max_x - bbox.min_x) * scale_factor,
2465 height: f32::from(bbox.max_y - bbox.min_y) * scale_factor,
2466 }
2467 })
2468 }
2469
2470 fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
2471 let glyph_id = self.lookup_glyph_index('-' as u32)?;
2472 let advance_units = self.get_horizontal_advance(glyph_id);
2473 let scale_factor = if self.font_metrics.units_per_em > 0 {
2474 font_size / f32::from(self.font_metrics.units_per_em)
2475 } else {
2476 return None;
2477 };
2478 let scaled_advance = f32::from(advance_units) * scale_factor;
2479 Some((glyph_id, scaled_advance))
2480 }
2481
2482 fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
2483 let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
2484 let advance_units = self.get_horizontal_advance(glyph_id);
2485 let scale_factor = if self.font_metrics.units_per_em > 0 {
2486 font_size / f32::from(self.font_metrics.units_per_em)
2487 } else {
2488 return None;
2489 };
2490 let scaled_advance = f32::from(advance_units) * scale_factor;
2491 Some((glyph_id, scaled_advance))
2492 }
2493
2494 fn has_glyph(&self, codepoint: u32) -> bool {
2495 self.lookup_glyph_index(codepoint).is_some()
2496 }
2497
2498 fn get_vertical_metrics(
2499 &self,
2500 glyph_id: u16,
2501 ) -> Option<crate::text3::cache::VerticalMetrics> {
2502 self.get_vertical_metrics(glyph_id)
2503 }
2504
2505 fn get_font_metrics(&self) -> LayoutFontMetrics {
2506 self.font_metrics
2507 }
2508
2509 fn num_glyphs(&self) -> u16 {
2510 self.num_glyphs
2511 }
2512
2513 fn get_space_width(&self) -> Option<usize> {
2514 self.space_width
2515 }
2516 }
2517
2518 /// Build an agg-rust `PathStorage` from an `OwnedGlyph` outline (in font units, Y-up → Y-down).
2519 ///
2520 /// Returns `None` if the glyph has no outline operations (e.g. space).
2521 /// The caller is responsible for applying scale and translation transforms.
2522 #[cfg(feature = "cpurender")]
2523 #[must_use] pub fn build_glyph_path(glyph: &OwnedGlyph) -> Option<agg_rust::path_storage::PathStorage> {
2524 use agg_rust::{basics::PATH_FLAGS_NONE, path_storage::PathStorage};
2525
2526 let mut path = PathStorage::new();
2527 let mut has_ops = false;
2528 for outline in &glyph.outline {
2529 for op in outline.operations.as_slice() {
2530 has_ops = true;
2531 match op {
2532 GlyphOutlineOperation::MoveTo(OutlineMoveTo { x, y }) => {
2533 path.move_to(f64::from(*x), -f64::from(*y));
2534 }
2535 GlyphOutlineOperation::LineTo(OutlineLineTo { x, y }) => {
2536 path.line_to(f64::from(*x), -f64::from(*y));
2537 }
2538 GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
2539 ctrl_1_x, ctrl_1_y, end_x, end_y,
2540 }) => {
2541 path.curve3(
2542 f64::from(*ctrl_1_x), -f64::from(*ctrl_1_y),
2543 f64::from(*end_x), -f64::from(*end_y),
2544 );
2545 }
2546 GlyphOutlineOperation::CubicCurveTo(OutlineCubicTo {
2547 ctrl_1_x, ctrl_1_y, ctrl_2_x, ctrl_2_y, end_x, end_y,
2548 }) => {
2549 path.curve4(
2550 f64::from(*ctrl_1_x), -f64::from(*ctrl_1_y),
2551 f64::from(*ctrl_2_x), -f64::from(*ctrl_2_y),
2552 f64::from(*end_x), -f64::from(*end_y),
2553 );
2554 }
2555 GlyphOutlineOperation::ClosePath => {
2556 path.close_polygon(PATH_FLAGS_NONE);
2557 }
2558 }
2559 }
2560 }
2561 if !has_ops {
2562 return None;
2563 }
2564 Some(path)
2565 }
2566
2567 #[cfg(test)]
2568 mod autotest_generated {
2569 //! Adversarial unit tests generated by the autotest fleet.
2570 //!
2571 //! Lives inside `mod parsed` (not at file scope) so it can reach the
2572 //! private sfnt scanner (`manual_be16` / `manual_be32` /
2573 //! `ManualTableProvider`), the outline collector, and the private
2574 //! `ParsedFont` getters.
2575
2576 use allsorts::tables::SfntVersion;
2577
2578 use super::*;
2579
2580 /// Positive control: the built-in `Azul Mock Mono` TrueType font.
2581 /// 13 tables, `glyf` outlines (no CFF), no `vhea`/`vmtx`, upem 1000,
2582 /// 96 glyphs, GSUB + GPOS + GDEF present (empty lists, presence-only).
2583 const MOCK_MONO: &[u8] = crate::text3::mock_fonts::MOCK_MONO_TTF;
2584
2585 fn parse_mock() -> ParsedFont {
2586 let mut warnings = Vec::new();
2587 ParsedFont::from_bytes(MOCK_MONO, 0, &mut warnings)
2588 .expect("Azul Mock Mono must parse (positive control)")
2589 }
2590
2591 fn mock_shared() -> ParsedFont {
2592 let bytes = Arc::new(rust_fontconfig::FontBytes::Owned(Arc::from(MOCK_MONO.to_vec())));
2593 let mut warnings = Vec::new();
2594 ParsedFont::from_bytes_shared(bytes, 0, &mut warnings)
2595 .expect("from_bytes_shared must parse the positive control")
2596 }
2597
2598 fn plain_metrics() -> LayoutFontMetrics {
2599 LayoutFontMetrics {
2600 ascent: 800.0,
2601 descent: -200.0,
2602 line_gap: 0.0,
2603 units_per_em: 1000,
2604 x_height: None,
2605 cap_height: None,
2606 }
2607 }
2608
2609 fn plain_hhea() -> HheaTable {
2610 HheaTable {
2611 ascender: 800,
2612 descender: -200,
2613 line_gap: 0,
2614 advance_width_max: 1000,
2615 min_left_side_bearing: 0,
2616 min_right_side_bearing: 0,
2617 x_max_extent: 0,
2618 caret_slope_rise: 1,
2619 caret_slope_run: 0,
2620 caret_offset: 0,
2621 num_h_metrics: 0,
2622 }
2623 }
2624
2625 /// A hand-built `ParsedFont` with no tables, no cmap and no source
2626 /// bytes — the "default / empty / extreme instance" every getter has
2627 /// to survive.
2628 fn synthetic_font(num_glyphs: u16, mock: Option<Box<MockFont>>) -> ParsedFont {
2629 ParsedFont {
2630 hash: 0,
2631 font_metrics: plain_metrics(),
2632 pdf_font_metrics: PdfFontMetrics::zero(),
2633 num_glyphs,
2634 hhea_table: plain_hhea(),
2635 hmtx_range: (0, 0),
2636 vmtx_range: (0, 0),
2637 vhea_table: None,
2638 maxp_table: MaxpTable {
2639 num_glyphs,
2640 version1_sub_table: None,
2641 },
2642 gsub_bytes: None,
2643 gsub_cache_lazy: std::sync::OnceLock::new(),
2644 gpos_bytes: None,
2645 gpos_cache_lazy: std::sync::OnceLock::new(),
2646 opt_gdef_table: None,
2647 opt_kern_table: None,
2648 last_used: Arc::new(std::sync::atomic::AtomicU64::new(0)),
2649 is_variable_font: false,
2650 glyph_cache: Arc::new(rust_fontconfig::StLock::new(BTreeMap::new())),
2651 loca_glyf: LocaGlyfState::Loaded(None),
2652 space_width: None,
2653 cmap_subtable: None,
2654 mock,
2655 reverse_glyph_cache: BTreeMap::new(),
2656 original_bytes: None,
2657 original_index: 0,
2658 index_to_cid: BTreeMap::new(),
2659 font_type: FontType::TrueType,
2660 font_name: None,
2661 hint_instance: None,
2662 }
2663 }
2664
2665 // ---------------------------------------------------------------
2666 // manual_be16 / manual_be32 (numeric)
2667 // ---------------------------------------------------------------
2668
2669 #[test]
2670 fn manual_be16_zero_max_and_offset() {
2671 assert_eq!(manual_be16(&[0x00, 0x00], 0), 0);
2672 assert_eq!(manual_be16(&[0xFF, 0xFF], 0), 0xFFFF);
2673 assert_eq!(manual_be16(&[0x12, 0x34], 0), 0x1234);
2674 // the widest 16-bit value still fits the u32 return: no truncation
2675 assert_eq!(manual_be16(&[0xAA, 0xBB, 0xFF, 0xFF], 2), u32::from(u16::MAX));
2676 // offsets address the right bytes, not the first ones
2677 assert_eq!(manual_be16(&[0xAA, 0xBB, 0x00, 0x01], 2), 1);
2678 }
2679
2680 #[test]
2681 fn manual_be32_zero_max_and_known_tags() {
2682 assert_eq!(manual_be32(&[0, 0, 0, 0], 0), 0);
2683 // saturating the whole word must not overflow the u32 accumulator
2684 assert_eq!(manual_be32(&[0xFF, 0xFF, 0xFF, 0xFF], 0), u32::MAX);
2685 assert_eq!(manual_be32(b"ttcf", 0), 0x7474_6366);
2686 assert_eq!(manual_be32(&[0x00, 0x01, 0x00, 0x00], 0), 0x0001_0000);
2687 assert_eq!(manual_be32(&[0xEE, 0x00, 0x01, 0x00, 0x00], 1), 0x0001_0000);
2688 // high bit set: the shift must stay unsigned (no sign extension)
2689 assert_eq!(manual_be32(&[0x80, 0x00, 0x00, 0x00], 0), 0x8000_0000);
2690 }
2691
2692 // ---------------------------------------------------------------
2693 // ManualTableProvider (parser)
2694 // ---------------------------------------------------------------
2695
2696 #[test]
2697 fn manual_table_provider_rejects_short_input() {
2698 assert!(ManualTableProvider::new(&[], 0).is_none()); // empty
2699 assert!(ManualTableProvider::new(b" ", 0).is_none()); // whitespace-only
2700 assert!(ManualTableProvider::new(b" \t\n", 0).is_none());
2701 assert!(ManualTableProvider::new(&[0xFF, 0xFE, 0x00], 0).is_none()); // invalid utf8
2702 assert!(ManualTableProvider::new(&[0u8; 11], 0).is_none()); // one byte short
2703 assert!(ManualTableProvider::new(&[0u8; 12], 0).is_some()); // exact minimum
2704 }
2705
2706 #[test]
2707 fn manual_table_provider_garbage_header_terminates() {
2708 // numTables reads back 0xFFFF but every record is past the end of the
2709 // buffer: the scan must break immediately instead of walking off it.
2710 let data = [0xFFu8; 12];
2711 let p = ManualTableProvider::new(&data, 0).expect("12 bytes form an offset table");
2712 assert_eq!(p.num, 0xFFFF);
2713 assert!(p.table_data(tag::HEAD).unwrap().is_none());
2714 assert!(!p.has_table(tag::GLYF));
2715 // table_tags always leads with the 0xFADE sentinel, then stops at the end
2716 assert_eq!(p.table_tags().unwrap(), vec![0x0000_FADE]);
2717 }
2718
2719 #[test]
2720 fn manual_table_provider_ttc_index_out_of_range_is_none() {
2721 let mut data = b"ttcf".to_vec();
2722 data.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]); // version 1.0
2723 data.extend_from_slice(&1u32.to_be_bytes()); // numFonts = 1
2724 data.extend_from_slice(&0u32.to_be_bytes()); // offset[0]
2725
2726 assert!(ManualTableProvider::new(&data, 0).is_some());
2727 // index >= numFonts short-circuits *before* the `12 + font_index * 4`
2728 // arithmetic, so even usize::MAX cannot overflow it
2729 assert!(ManualTableProvider::new(&data, 1).is_none());
2730 assert!(ManualTableProvider::new(&data, 999_999).is_none());
2731 assert!(ManualTableProvider::new(&data, usize::MAX).is_none());
2732 }
2733
2734 #[test]
2735 fn manual_table_provider_ttc_offset_past_end_is_none() {
2736 let mut data = b"ttcf".to_vec();
2737 data.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]);
2738 data.extend_from_slice(&1u32.to_be_bytes());
2739 data.extend_from_slice(&u32::MAX.to_be_bytes()); // offset[0] = 4 GiB
2740 assert!(ManualTableProvider::new(&data, 0).is_none());
2741 }
2742
2743 #[test]
2744 fn manual_table_provider_table_record_past_end_yields_none() {
2745 // One record whose (offset, length) points past the buffer: the
2746 // checked_add + bounds filter must turn it into None, not an OOB slice.
2747 let mut data = vec![0u8; 12 + 16];
2748 data[0..4].copy_from_slice(&0x0001_0000u32.to_be_bytes()); // sfnt version
2749 data[4..6].copy_from_slice(&1u16.to_be_bytes()); // numTables = 1
2750 data[12..16].copy_from_slice(&tag::HEAD.to_be_bytes()); // tag
2751 data[20..24].copy_from_slice(&u32::MAX.to_be_bytes()); // offset = 4 GiB
2752 data[24..28].copy_from_slice(&u32::MAX.to_be_bytes()); // length = 4 GiB
2753 let p = ManualTableProvider::new(&data, 0).unwrap();
2754 assert!(p.table_data(tag::HEAD).unwrap().is_none());
2755 assert!(!p.has_table(tag::HEAD));
2756 }
2757
2758 #[test]
2759 fn manual_table_provider_one_megabyte_of_zeros_terminates() {
2760 let data = vec![0u8; 1_000_000];
2761 let p = ManualTableProvider::new(&data, 0).expect("zeros form a degenerate header");
2762 assert_eq!(p.num, 0); // numTables = 0: nothing to scan
2763 assert!(p.table_data(tag::HEAD).unwrap().is_none());
2764 assert_eq!(p.sfnt_version(), 0);
2765 }
2766
2767 #[test]
2768 fn manual_table_provider_reads_a_real_font() {
2769 let p = ManualTableProvider::new(MOCK_MONO, 0).expect("a real TTF must produce a provider");
2770 assert_eq!(p.sfnt_version(), 0x0001_0000);
2771 assert_eq!(p.num, 13);
2772 assert!(p.has_table(tag::HEAD));
2773 assert!(p.has_table(tag::GLYF) && p.has_table(tag::LOCA));
2774 assert!(!p.has_table(tag::CFF)); // Azul Mock Mono is TrueType, not OpenType-PostScript
2775
2776 let head = p.table_data(tag::HEAD).unwrap().expect("head must be present");
2777 // head.magicNumber sits at offset 12 and is fixed by the spec
2778 assert_eq!(manual_be32(head.as_ref(), 12), 0x5F0F_3CF5);
2779
2780 let tags = p.table_tags().unwrap();
2781 assert_eq!(tags[0], 0x0000_FADE); // diagnostic sentinel
2782 assert_eq!(tags.len(), 14); // sentinel + 13 records
2783 assert!(tags.contains(&tag::GLYF));
2784 }
2785
2786 // ---------------------------------------------------------------
2787 // monotonic_now_nanos
2788 // ---------------------------------------------------------------
2789
2790 #[test]
2791 fn monotonic_now_nanos_never_goes_backwards() {
2792 let a = monotonic_now_nanos();
2793 let b = monotonic_now_nanos();
2794 assert!(b >= a, "clock went backwards: {a} -> {b}");
2795 // the `as u64` truncation cannot wrap in any realistic process lifetime
2796 assert!(b < 60 * 60 * 1_000_000_000);
2797 }
2798
2799 // ---------------------------------------------------------------
2800 // GlyphOutlineCollector
2801 // ---------------------------------------------------------------
2802
2803 #[test]
2804 fn glyph_outline_collector_new_is_empty() {
2805 assert!(GlyphOutlineCollector::new().into_outlines().is_empty());
2806 }
2807
2808 #[test]
2809 fn glyph_outline_collector_flushes_an_unclosed_contour() {
2810 let mut c = GlyphOutlineCollector::new();
2811 c.move_to(Vector2F::new(1.0, 2.0));
2812 c.line_to(Vector2F::new(3.0, 4.0));
2813 // no close(): into_outlines must still flush the pending contour
2814 let outlines = c.into_outlines();
2815 assert_eq!(outlines.len(), 1);
2816 assert_eq!(outlines[0].operations.as_slice().len(), 2);
2817 }
2818
2819 #[test]
2820 fn glyph_outline_collector_splits_contours_on_move_to() {
2821 let mut c = GlyphOutlineCollector::new();
2822 c.move_to(Vector2F::new(0.0, 0.0));
2823 c.line_to(Vector2F::new(10.0, 0.0));
2824 c.close();
2825 // close() already flushed, so this move_to must not emit an empty contour
2826 c.move_to(Vector2F::new(0.0, 0.0));
2827 c.quadratic_curve_to(Vector2F::new(1.0, 1.0), Vector2F::new(2.0, 2.0));
2828 c.cubic_curve_to(
2829 LineSegment2F::new(Vector2F::new(1.0, 1.0), Vector2F::new(2.0, 2.0)),
2830 Vector2F::new(3.0, 3.0),
2831 );
2832 let outlines = c.into_outlines();
2833 assert_eq!(outlines.len(), 2);
2834 assert_eq!(outlines[0].operations.as_slice().len(), 3); // move, line, close
2835 assert_eq!(outlines[1].operations.as_slice().len(), 3); // move, quad, cubic
2836 }
2837
2838 #[test]
2839 fn glyph_outline_collector_saturates_nan_and_infinite_coordinates() {
2840 // allsorts hands us f32s; the `as i16` casts in the sink saturate
2841 // (NaN -> 0, ±inf -> i16::MAX/MIN) rather than wrapping or trapping.
2842 let mut c = GlyphOutlineCollector::new();
2843 c.move_to(Vector2F::new(f32::NAN, f32::INFINITY));
2844 c.line_to(Vector2F::new(f32::NEG_INFINITY, 1e30));
2845 let outlines = c.into_outlines();
2846 let ops = outlines[0].operations.as_slice();
2847 if let GlyphOutlineOperation::MoveTo(m) = &ops[0] {
2848 assert_eq!(m.x, 0); // NaN -> 0
2849 assert_eq!(m.y, i16::MAX); // +inf saturates
2850 } else {
2851 panic!("first op must be a MoveTo");
2852 }
2853 if let GlyphOutlineOperation::LineTo(l) = &ops[1] {
2854 assert_eq!(l.x, i16::MIN); // -inf saturates
2855 assert_eq!(l.y, i16::MAX); // 1e30 saturates
2856 } else {
2857 panic!("second op must be a LineTo");
2858 }
2859 }
2860
2861 // ---------------------------------------------------------------
2862 // compute_outline_bbox
2863 // ---------------------------------------------------------------
2864
2865 #[test]
2866 fn compute_outline_bbox_without_points_is_zero_not_the_sentinel_seed() {
2867 assert_eq!(compute_outline_bbox(&[]), (0, 0, 0, 0));
2868 let only_close = GlyphOutline {
2869 operations: vec![GlyphOutlineOperation::ClosePath].into(),
2870 };
2871 // ClosePath contributes no points: must not leak the
2872 // (i16::MAX, i16::MAX, i16::MIN, i16::MIN) accumulator seed
2873 assert_eq!(
2874 compute_outline_bbox(std::slice::from_ref(&only_close)),
2875 (0, 0, 0, 0)
2876 );
2877 }
2878
2879 #[test]
2880 fn compute_outline_bbox_covers_control_points_and_i16_extremes() {
2881 let outline = GlyphOutline {
2882 operations: vec![
2883 GlyphOutlineOperation::MoveTo(OutlineMoveTo {
2884 x: i16::MIN,
2885 y: i16::MAX,
2886 }),
2887 GlyphOutlineOperation::LineTo(OutlineLineTo { x: 0, y: 0 }),
2888 GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
2889 ctrl_1_x: 5,
2890 ctrl_1_y: -5,
2891 end_x: 6,
2892 end_y: -6,
2893 }),
2894 GlyphOutlineOperation::CubicCurveTo(OutlineCubicTo {
2895 ctrl_1_x: i16::MAX,
2896 ctrl_1_y: i16::MIN,
2897 ctrl_2_x: 0,
2898 ctrl_2_y: 0,
2899 end_x: 1,
2900 end_y: 1,
2901 }),
2902 GlyphOutlineOperation::ClosePath,
2903 ]
2904 .into(),
2905 };
2906 // control points participate in the bbox, and the i16 extremes must not wrap
2907 assert_eq!(
2908 compute_outline_bbox(std::slice::from_ref(&outline)),
2909 (i16::MIN, i16::MIN, i16::MAX, i16::MAX)
2910 );
2911 }
2912
2913 #[test]
2914 fn compute_outline_bbox_spans_every_contour() {
2915 let a = GlyphOutline {
2916 operations: vec![GlyphOutlineOperation::MoveTo(OutlineMoveTo { x: -10, y: -10 })]
2917 .into(),
2918 };
2919 let b = GlyphOutline {
2920 operations: vec![GlyphOutlineOperation::LineTo(OutlineLineTo { x: 20, y: 30 })]
2921 .into(),
2922 };
2923 assert_eq!(compute_outline_bbox(&[a, b]), (-10, -10, 20, 30));
2924 }
2925
2926 // ---------------------------------------------------------------
2927 // PdfFontMetrics
2928 // ---------------------------------------------------------------
2929
2930 #[test]
2931 fn pdf_font_metrics_zero_never_divides_by_zero() {
2932 let z = PdfFontMetrics::zero();
2933 assert_eq!(z.units_per_em, 1000); // callers divide by this: never 0
2934 assert_eq!(z.ascender, 0);
2935 assert_eq!(z.descender, 0);
2936 assert_eq!(z.line_gap, 0);
2937 assert_eq!(z.advance_width_max, 0);
2938 assert_eq!(z.us_weight_class, 0);
2939 assert_eq!(z.y_strikeout_position, 0);
2940 assert_eq!(PdfFontMetrics::default(), z); // Default is the neutral element
2941 let copied = z; // Copy: the zero value is trivially duplicable
2942 assert_eq!(copied, z);
2943 }
2944
2945 // ---------------------------------------------------------------
2946 // SubsetFont::subset_text
2947 // ---------------------------------------------------------------
2948
2949 #[test]
2950 fn subset_text_with_an_empty_mapping_drops_everything() {
2951 let f = SubsetFont {
2952 bytes: Vec::new(),
2953 glyph_mapping: BTreeMap::new(),
2954 };
2955 assert_eq!(f.subset_text(""), "");
2956 assert_eq!(f.subset_text("hello"), "");
2957 // emoji + combining mark: multibyte input must not panic or slice mid-char
2958 assert_eq!(f.subset_text("\u{1F600}e\u{0301}"), "");
2959 }
2960
2961 #[test]
2962 fn subset_text_remaps_chars_to_their_new_gids() {
2963 let mut m = BTreeMap::new();
2964 m.insert(40u16, (65u16, 'A')); // old gid 40 -> new gid 65 -> U+0041
2965 m.insert(41u16, (66u16, 'B'));
2966 let f = SubsetFont {
2967 bytes: Vec::new(),
2968 glyph_mapping: m,
2969 };
2970 assert_eq!(f.subset_text("AB"), "AB");
2971 assert_eq!(f.subset_text("BA"), "BA");
2972 assert_eq!(f.subset_text("A?B"), "AB"); // unmapped chars are dropped
2973 }
2974
2975 #[test]
2976 fn subset_text_gid_boundaries() {
2977 let mut m = BTreeMap::new();
2978 m.insert(1u16, (0u16, 'x')); // new gid 0 -> U+0000
2979 m.insert(2u16, (0xD800u16, 'y')); // surrogate: char::from_u32 -> None
2980 m.insert(3u16, (u16::MAX, 'z')); // U+FFFF is a valid scalar value
2981 let f = SubsetFont {
2982 bytes: Vec::new(),
2983 glyph_mapping: m,
2984 };
2985 assert_eq!(f.subset_text("x"), "\u{0}");
2986 assert_eq!(f.subset_text("y"), ""); // a surrogate gid is silently dropped
2987 assert_eq!(f.subset_text("z"), "\u{FFFF}");
2988 assert_eq!(f.subset_text("xyz"), "\u{0}\u{FFFF}");
2989 }
2990
2991 #[test]
2992 fn subset_text_long_input_terminates() {
2993 let mut m = BTreeMap::new();
2994 m.insert(7u16, (97u16, 'a'));
2995 let f = SubsetFont {
2996 bytes: Vec::new(),
2997 glyph_mapping: m,
2998 };
2999 assert_eq!(f.subset_text(&"a".repeat(100_000)).len(), 100_000);
3000 }
3001
3002 // ---------------------------------------------------------------
3003 // FontParseWarning
3004 // ---------------------------------------------------------------
3005
3006 #[test]
3007 fn font_parse_warning_constructors_preserve_severity_and_message() {
3008 let empty = FontParseWarning::info(String::new());
3009 assert_eq!(empty.severity, FontParseWarningSeverity::Info);
3010 assert!(empty.message.is_empty());
3011
3012 let unicode = FontParseWarning::warning("\u{1F4A5} e\u{0301}\u{202E}".to_string());
3013 assert_eq!(unicode.severity, FontParseWarningSeverity::Warning);
3014 assert_eq!(unicode.message, "\u{1F4A5} e\u{0301}\u{202E}");
3015
3016 let huge = FontParseWarning::error("x".repeat(1_000_000));
3017 assert_eq!(huge.severity, FontParseWarningSeverity::Error);
3018 assert_eq!(huge.message.len(), 1_000_000);
3019
3020 // severity is part of the identity: same message, different level
3021 assert_ne!(
3022 FontParseWarning::error("boom".to_string()),
3023 FontParseWarning::warning("boom".to_string())
3024 );
3025 }
3026
3027 // ---------------------------------------------------------------
3028 // MockFont (constructors)
3029 // ---------------------------------------------------------------
3030
3031 #[test]
3032 fn mock_font_new_defaults_and_extreme_metrics() {
3033 let m = MockFont::new(plain_metrics());
3034 assert_eq!(m.space_width, Some(10)); // documented default
3035 assert!(m.glyph_advances.is_empty());
3036 assert!(m.glyph_sizes.is_empty());
3037 assert!(m.glyph_indices.is_empty());
3038
3039 // metrics are stored verbatim: no normalisation, no panic on NaN/inf/0-upem
3040 let extreme = MockFont::new(LayoutFontMetrics {
3041 ascent: f32::INFINITY,
3042 descent: f32::NAN,
3043 line_gap: f32::MIN,
3044 units_per_em: 0,
3045 x_height: Some(f32::MAX),
3046 cap_height: None,
3047 });
3048 assert!(extreme.font_metrics.ascent.is_infinite());
3049 assert!(extreme.font_metrics.descent.is_nan());
3050 assert_eq!(extreme.font_metrics.units_per_em, 0);
3051 assert_eq!(extreme.space_width, Some(10));
3052 }
3053
3054 #[test]
3055 fn mock_font_builders_store_boundary_values() {
3056 let m = MockFont::new(plain_metrics())
3057 .with_space_width(usize::MAX)
3058 .with_glyph_advance(0, 0)
3059 .with_glyph_advance(u16::MAX, u16::MAX)
3060 .with_glyph_size(u16::MAX, (i32::MIN, i32::MAX))
3061 .with_glyph_index(0, 0)
3062 .with_glyph_index(u32::MAX, u16::MAX); // not a scalar value: stored anyway
3063
3064 assert_eq!(m.space_width, Some(usize::MAX));
3065 assert_eq!(m.glyph_advances.len(), 2);
3066 assert_eq!(m.glyph_advances.get(&u16::MAX), Some(&u16::MAX));
3067 assert_eq!(m.glyph_sizes.get(&u16::MAX), Some(&(i32::MIN, i32::MAX)));
3068 assert_eq!(m.glyph_indices.len(), 2);
3069 assert_eq!(m.glyph_indices.get(&u32::MAX), Some(&u16::MAX));
3070
3071 // last write wins, and an overwrite must not grow the map
3072 let m = m.with_glyph_advance(u16::MAX, 7).with_space_width(0);
3073 assert_eq!(m.glyph_advances.get(&u16::MAX), Some(&7));
3074 assert_eq!(m.glyph_advances.len(), 2);
3075 assert_eq!(m.space_width, Some(0));
3076 }
3077
3078 // ---------------------------------------------------------------
3079 // ParsedFont::from_bytes (parser)
3080 // ---------------------------------------------------------------
3081
3082 #[test]
3083 fn from_bytes_rejects_malformed_input() {
3084 let cases: Vec<(&str, Vec<u8>)> = vec![
3085 ("empty", Vec::new()),
3086 ("whitespace_only", b" \t\n".to_vec()),
3087 ("garbage", (0u8..=255).cycle().take(4096).collect()),
3088 ("invalid_utf8", vec![0xFF, 0xFE, 0x00]),
3089 ("header_only", MOCK_MONO[..12].to_vec()),
3090 ("truncated_font", MOCK_MONO[..64].to_vec()),
3091 ("half_a_font", MOCK_MONO[..MOCK_MONO.len() / 2].to_vec()),
3092 ("one_megabyte_of_nuls", vec![0u8; 1_000_000]),
3093 ("ttcf_junk", {
3094 let mut v = b"ttcf".to_vec();
3095 v.extend_from_slice(&[0xABu8; 1024].repeat(1000));
3096 v
3097 }),
3098 ];
3099 for (name, bytes) in cases {
3100 let mut warnings = Vec::new();
3101 assert!(
3102 ParsedFont::from_bytes(&bytes, 0, &mut warnings).is_none(),
3103 "{name} must not parse into a font"
3104 );
3105 }
3106 }
3107
3108 #[test]
3109 fn from_bytes_appends_a_warning_on_failure() {
3110 let mut warnings = Vec::new();
3111 assert!(ParsedFont::from_bytes(&[], 0, &mut warnings).is_none());
3112 assert!(!warnings.is_empty(), "a failed parse must explain itself");
3113 assert!(warnings
3114 .iter()
3115 .any(|w| w.severity == FontParseWarningSeverity::Error));
3116
3117 // the caller's vec is appended to, never cleared
3118 let before = warnings.len();
3119 assert!(ParsedFont::from_bytes(&[0xFF; 3], 0, &mut warnings).is_none());
3120 assert!(warnings.len() > before);
3121 }
3122
3123 #[test]
3124 fn from_bytes_parses_the_positive_control() {
3125 let font = parse_mock();
3126 assert_eq!(font.num_glyphs(), 96);
3127 assert_eq!(font.num_glyphs(), font.maxp_table.num_glyphs);
3128 assert_eq!(font.font_metrics.units_per_em, 1000);
3129 assert!(font.font_metrics.ascent > 0.0);
3130 assert_eq!(font.font_type, FontType::TrueType);
3131 assert_eq!(font.original_index, 0);
3132 assert_eq!(font.pdf_font_metrics.units_per_em, 1000);
3133 assert!(font.pdf_font_metrics.x_max > font.pdf_font_metrics.x_min);
3134 assert!(font.cmap_subtable.is_some());
3135 assert!(font.gsub_bytes.is_some() && font.gpos_bytes.is_some());
3136 assert!(font.hmtx_range.1 > 0, "hmtx must be located in the source");
3137 assert_eq!(font.vmtx_range, (0, 0), "Azul Mock Mono has no vertical metrics");
3138 assert!(!font.is_variable_font);
3139 }
3140
3141 #[test]
3142 fn from_bytes_with_an_out_of_range_font_index_is_deterministic() {
3143 let baseline = parse_mock();
3144 let mut warnings = Vec::new();
3145 // Azul Mock Mono is a single face, not a .ttc: the manual provider ignores the index
3146 // rather than rejecting it. What must NOT happen is a panic or a
3147 // `12 + font_index * 4` overflow.
3148 if let Some(font) = ParsedFont::from_bytes(MOCK_MONO, usize::MAX, &mut warnings) {
3149 assert_eq!(font.num_glyphs(), baseline.num_glyphs());
3150 assert_eq!(font.original_index, usize::MAX);
3151 assert_ne!(font.hash, baseline.hash, "font_index feeds the identity hash");
3152 }
3153 }
3154
3155 #[test]
3156 fn from_bytes_internal_deferred_flag_skips_the_eager_loca_glyf_load() {
3157 let mut warnings = Vec::new();
3158 let eager = ParsedFont::from_bytes_internal(MOCK_MONO, 0, &mut warnings, false)
3159 .expect("eager parse");
3160 assert!(matches!(eager.loca_glyf, LocaGlyfState::Loaded(Some(_))));
3161
3162 let deferred = ParsedFont::from_bytes_internal(MOCK_MONO, 0, &mut warnings, true)
3163 .expect("deferred parse");
3164 // deferring leaves the slot empty for from_bytes_shared to overwrite
3165 assert!(matches!(deferred.loca_glyf, LocaGlyfState::Loaded(None)));
3166 assert_eq!(eager.num_glyphs(), deferred.num_glyphs());
3167 assert_eq!(eager.hash, deferred.hash);
3168 }
3169
3170 // ---------------------------------------------------------------
3171 // cmap lookups (numeric / predicate)
3172 // ---------------------------------------------------------------
3173
3174 #[test]
3175 fn lookup_glyph_index_handles_codepoint_extremes() {
3176 let font = parse_mock();
3177 assert!(font.lookup_glyph_index('A' as u32).is_some());
3178
3179 // 0, the last valid scalar value, and values beyond the Unicode range must
3180 // all resolve deterministically without panicking
3181 for cp in [0u32, 0x0010_FFFF, 0x0011_0000, u32::MAX / 2, u32::MAX] {
3182 let first = font.lookup_glyph_index(cp);
3183 assert_eq!(first, font.lookup_glyph_index(cp), "cp {cp} must be stable");
3184 // has_glyph is defined as lookup_glyph_index().is_some()
3185 assert_eq!(first.is_some(), font.has_glyph(cp));
3186 }
3187 assert!(!font.has_glyph(0x0010_FFFF));
3188 assert!(!font.has_glyph(u32::MAX));
3189 }
3190
3191 // ---------------------------------------------------------------
3192 // advances (numeric)
3193 // ---------------------------------------------------------------
3194
3195 #[test]
3196 fn get_horizontal_advance_saturates_out_of_range_gids() {
3197 let font = parse_mock();
3198 let gid_a = font.lookup_glyph_index('A' as u32).expect("'A' is in Azul Mock Mono");
3199 assert!(font.get_horizontal_advance(gid_a) > 0);
3200
3201 // out-of-range gid: allsorts short-circuits to 0 rather than erroring
3202 assert!(font.num_glyphs() < u16::MAX);
3203 assert_eq!(font.get_horizontal_advance(font.num_glyphs()), 0);
3204 assert_eq!(font.get_horizontal_advance(u16::MAX), 0);
3205 }
3206
3207 #[test]
3208 fn get_glyph_width_internal_matches_hmtx_and_never_panics() {
3209 let font = parse_mock();
3210 let gid_a = font.lookup_glyph_index('A' as u32).unwrap();
3211 assert_eq!(
3212 font.get_glyph_width_internal(gid_a),
3213 Some(usize::from(font.get_horizontal_advance(gid_a)))
3214 );
3215 // an out-of-range gid yields Some(0) (allsorts returns Ok(0)), never None/panic
3216 assert_eq!(font.get_glyph_width_internal(u16::MAX), Some(0));
3217 assert_eq!(font.get_glyph_width_internal(font.num_glyphs()), Some(0));
3218 }
3219
3220 #[test]
3221 fn space_width_is_cached_at_parse_time() {
3222 let font = parse_mock();
3223 let space_gid = font
3224 .lookup_glyph_index(' ' as u32)
3225 .expect("Azul Mock Mono has a space glyph");
3226 let live = usize::from(font.get_horizontal_advance(space_gid));
3227 assert!(live > 0, "the live hmtx advance for space must be non-zero");
3228
3229 let cached = font
3230 .get_space_width()
3231 .expect("space_width is populated at parse time");
3232 // NOTE: `space_width` is computed inside `from_bytes_internal`, *before* the
3233 // source bytes are attached, so hmtx is unreadable at that point and the
3234 // cached value can legitimately read back 0 (see the comment there).
3235 assert!(
3236 cached == 0 || cached == live,
3237 "space_width must be 0 (stale) or the hmtx advance; got {cached} vs {live}"
3238 );
3239 assert_eq!(font.get_space_width(), font.space_width);
3240 }
3241
3242 #[test]
3243 fn get_hinted_advance_px_rejects_degenerate_inputs() {
3244 let font = parse_mock();
3245 let gid = font.lookup_glyph_index('A' as u32).unwrap();
3246
3247 // ppem 0 would divide by zero when computing the scale
3248 assert!(font.get_hinted_advance_px(gid, 0).is_none());
3249 // out-of-range gid is gated by get_or_decode_glyph
3250 assert!(font.get_hinted_advance_px(u16::MAX, 16).is_none());
3251 assert!(font.get_hinted_advance_px(font.num_glyphs(), 16).is_none());
3252
3253 // extreme ppem must not overflow the F26Dot6 fixed-point math; whatever
3254 // comes back is a finite, non-negative, whole-pixel value
3255 for ppem in [1u16, 16, 255, 4096, u16::MAX] {
3256 if let Some(px) = font.get_hinted_advance_px(gid, ppem) {
3257 assert!(px.is_finite(), "ppem {ppem} produced {px}");
3258 assert!(px >= 0.0, "ppem {ppem} produced {px}");
3259 assert!(
3260 (px - px.trunc()).abs() < f32::EPSILON,
3261 "the advance is rounded to the whole-pixel grid, got {px}"
3262 );
3263 }
3264 }
3265 }
3266
3267 // ---------------------------------------------------------------
3268 // glyph decode + lazy cache
3269 // ---------------------------------------------------------------
3270
3271 #[test]
3272 fn get_or_decode_glyph_bounds_and_cache_identity() {
3273 let font = parse_mock();
3274 assert!(font.num_glyphs() > 1);
3275
3276 // gid >= num_glyphs is refused before any decode is attempted
3277 assert!(font.get_or_decode_glyph(font.num_glyphs()).is_none());
3278 assert!(font.get_or_decode_glyph(u16::MAX).is_none());
3279
3280 let a = font.get_or_decode_glyph(0).expect(".notdef must decode");
3281 let b = font.get_or_decode_glyph(0).expect("the cache must hand it back");
3282 assert!(Arc::ptr_eq(&a, &b), "a second call must hit the cache");
3283
3284 let snap = font.glyph_cache_snapshot();
3285 assert!(Arc::ptr_eq(snap.get(&0).expect("gid 0 is cached"), &a));
3286 assert!(!snap.contains_key(&font.num_glyphs()));
3287 }
3288
3289 #[test]
3290 fn get_or_decode_glyph_stamps_the_lru_clock() {
3291 let font = parse_mock();
3292 assert_eq!(font.last_used_nanos(), 0, "an untouched face reports 0");
3293 let _ = font.get_or_decode_glyph(0);
3294 let t = font.last_used_nanos();
3295 assert!(t > 0, "decoding must stamp last_used");
3296 let _ = font.get_or_decode_glyph(1);
3297 assert!(font.last_used_nanos() >= t, "the stamp must not go backwards");
3298 }
3299
3300 #[test]
3301 fn prime_glyph_cache_decodes_every_glyph_and_is_idempotent() {
3302 let mut font = parse_mock();
3303 // the lazy cache starts empty (the space stub is skipped while the source
3304 // bytes are still unattached)
3305 assert!(font.glyph_cache_snapshot().is_empty());
3306
3307 font.prime_glyph_cache();
3308 let snap = font.glyph_cache_snapshot();
3309 assert_eq!(snap.len(), usize::from(font.num_glyphs()));
3310 assert!(snap.contains_key(&0));
3311 assert!(!snap.contains_key(&font.num_glyphs()), "never decodes past the end");
3312
3313 let mut seen = 0usize;
3314 let mut max_gid = 0u16;
3315 font.for_each_decoded_glyph(|gid, _| {
3316 seen += 1;
3317 max_gid = max_gid.max(gid);
3318 });
3319 assert_eq!(seen, snap.len());
3320 assert_eq!(max_gid, font.num_glyphs() - 1);
3321
3322 font.prime_glyph_cache(); // priming twice must not duplicate or grow
3323 assert_eq!(font.glyph_cache_snapshot().len(), snap.len());
3324 }
3325
3326 #[test]
3327 fn decode_glyph_inner_seeds_the_bbox_from_the_advance() {
3328 let font = parse_mock();
3329 let gid_a = font.lookup_glyph_index('A' as u32).unwrap();
3330 let g = font.decode_glyph_inner(gid_a);
3331 assert_eq!(g.horz_advance, font.get_horizontal_advance(gid_a));
3332 assert!(!g.outline.is_empty(), "'A' has a glyf outline");
3333 assert!(g.bounding_box.max_x > g.bounding_box.min_x);
3334 assert!(g.bounding_box.max_y > g.bounding_box.min_y);
3335 assert!(g.raw_points.is_some() && g.raw_on_curve.is_some());
3336 assert_eq!(
3337 g.raw_points.as_ref().map(Vec::len),
3338 g.raw_on_curve.as_ref().map(Vec::len),
3339 "one on-curve flag per raw point"
3340 );
3341
3342 // gid 0 (.notdef) decodes too, and the bbox stays inside i16
3343 let notdef = font.decode_glyph_inner(0);
3344 assert!(notdef.bounding_box.max_x >= notdef.bounding_box.min_x);
3345 }
3346
3347 #[test]
3348 fn get_glyph_bbox_size_bounds() {
3349 let font = parse_mock();
3350 let gid_a = font.lookup_glyph_index('A' as u32).unwrap();
3351 let (w, h) = font.get_glyph_bbox_size(gid_a).expect("'A' has a bbox");
3352 assert!(w > 0 && h > 0);
3353 assert_eq!(w, i32::from(font.get_horizontal_advance(gid_a)));
3354 // out-of-range gid -> None (not a zero-sized box)
3355 assert!(font.get_glyph_bbox_size(u16::MAX).is_none());
3356 assert!(font.get_glyph_bbox_size(font.num_glyphs()).is_none());
3357 }
3358
3359 #[test]
3360 fn get_vertical_metrics_without_vmtx_is_none() {
3361 let font = parse_mock();
3362 // Azul Mock Mono has neither vhea nor vmtx
3363 assert!(font.get_vertical_metrics(0).is_none());
3364 assert!(font.get_vertical_metrics(u16::MAX).is_none());
3365
3366 // vhea present but a zero-length vmtx range must still bail out
3367 let mut synthetic = synthetic_font(4, None);
3368 synthetic.vhea_table = Some(plain_hhea());
3369 assert_eq!(synthetic.vmtx_range.1, 0);
3370 assert!(synthetic.get_vertical_metrics(0).is_none());
3371 }
3372
3373 // ---------------------------------------------------------------
3374 // lazy loca/glyf: from_bytes_shared, eviction
3375 // ---------------------------------------------------------------
3376
3377 #[test]
3378 fn from_bytes_shared_defers_loca_glyf_and_can_evict() {
3379 let font = mock_shared();
3380 assert!(matches!(font.loca_glyf, LocaGlyfState::Deferred { .. }));
3381 assert!(font.source_bytes_for_subset().is_some());
3382
3383 // nothing is loaded until the first decode, so there is nothing to evict yet
3384 assert!(!font.evict_loca_glyf());
3385
3386 let g1 = font.get_or_decode_glyph(1).expect("gid 1 must decode");
3387 assert!(font.evict_loca_glyf(), "a loaded Deferred slot is evictable");
3388 assert!(!font.evict_loca_glyf(), "evicting twice is a no-op");
3389
3390 // after eviction the face still decodes: it re-parses from the retained bytes
3391 let g2 = font.get_or_decode_glyph(2).expect("gid 2 decodes after eviction");
3392 assert_eq!(g2.horz_advance, font.get_horizontal_advance(2));
3393 // and already-decoded glyphs still come from the glyph cache
3394 assert!(Arc::ptr_eq(&g1, &font.get_or_decode_glyph(1).unwrap()));
3395 }
3396
3397 #[test]
3398 fn eager_faces_are_not_evictable() {
3399 let font = parse_mock();
3400 assert!(matches!(font.loca_glyf, LocaGlyfState::Loaded(Some(_))));
3401 let _ = font.get_or_decode_glyph(0);
3402 // Loaded faces have no retained source bytes to re-parse from
3403 assert!(!font.evict_loca_glyf());
3404 assert!(font.resolve_loca_glyf().is_some());
3405 }
3406
3407 #[test]
3408 fn eager_and_deferred_paths_decode_identical_glyphs() {
3409 let eager = parse_mock();
3410 let lazy = mock_shared();
3411 assert_eq!(eager.num_glyphs(), lazy.num_glyphs());
3412 assert_eq!(eager.hash, lazy.hash); // same bytes + index -> same identity
3413 assert_eq!(eager, lazy); // PartialEq is hash-based
3414
3415 let gid = eager.lookup_glyph_index('A' as u32).unwrap();
3416 assert_eq!(lazy.lookup_glyph_index('A' as u32), Some(gid));
3417 let a = eager.get_or_decode_glyph(gid).unwrap();
3418 let b = lazy.get_or_decode_glyph(gid).unwrap();
3419 assert_eq!(a.horz_advance, b.horz_advance);
3420 assert_eq!(a.outline.len(), b.outline.len());
3421 assert_eq!(a.bounding_box.min_x, b.bounding_box.min_x);
3422 assert_eq!(a.bounding_box.min_y, b.bounding_box.min_y);
3423 assert_eq!(a.bounding_box.max_x, b.bounding_box.max_x);
3424 assert_eq!(a.bounding_box.max_y, b.bounding_box.max_y);
3425 }
3426
3427 #[test]
3428 fn with_source_bytes_shares_the_arc() {
3429 let font = parse_mock();
3430 // from_bytes retains an owned copy, so subsetting works without an
3431 // explicit with_source_bytes call
3432 let auto = font
3433 .source_bytes_for_subset()
3434 .expect("from_bytes retains the source bytes");
3435 assert_eq!(auto.as_slice(), MOCK_MONO);
3436
3437 let arc = Arc::new(rust_fontconfig::FontBytes::Owned(Arc::from(MOCK_MONO.to_vec())));
3438 let font = font.with_source_bytes(Arc::clone(&arc));
3439 let got = font.source_bytes_for_subset().unwrap();
3440 assert!(Arc::ptr_eq(&got, &arc), "attached bytes are shared, not copied");
3441 }
3442
3443 #[test]
3444 fn clone_shares_the_glyph_cache_and_drops_hinting() {
3445 let font = parse_mock();
3446 let g = font.get_or_decode_glyph(0).unwrap();
3447 let clone = font.clone();
3448 assert_eq!(clone, font);
3449
3450 // the decode cache is Arc-shared, so the clone sees the decoded glyph...
3451 assert!(Arc::ptr_eq(&clone.get_or_decode_glyph(0).unwrap(), &g));
3452 // ...and decodes through the clone are visible from the original
3453 let _ = clone.get_or_decode_glyph(1);
3454 assert!(font.glyph_cache_snapshot().contains_key(&1));
3455 // HintInstance is not Clone: it is deliberately dropped
3456 assert!(clone.hint_instance.is_none());
3457 }
3458
3459 #[test]
3460 fn gsub_and_gpos_are_memoised() {
3461 let font = parse_mock();
3462 assert!(font.gsub_bytes.is_some(), "Azul Mock Mono ships a GSUB table");
3463 assert!(font.gpos_bytes.is_some(), "Azul Mock Mono ships a GPOS table");
3464
3465 match (font.gsub(), font.gsub()) {
3466 (Some(a), Some(b)) => assert!(Arc::ptr_eq(a, b), "gsub() must be memoised"),
3467 (None, None) => {}
3468 _ => panic!("gsub() must be deterministic across calls"),
3469 }
3470 match (font.gpos(), font.gpos()) {
3471 (Some(a), Some(b)) => assert!(Arc::ptr_eq(a, b), "gpos() must be memoised"),
3472 (None, None) => {}
3473 _ => panic!("gpos() must be deterministic across calls"),
3474 }
3475 }
3476
3477 // ---------------------------------------------------------------
3478 // to_bytes / subset (round-trip)
3479 // ---------------------------------------------------------------
3480
3481 #[test]
3482 fn to_bytes_round_trips_through_from_bytes() {
3483 let font = parse_mock();
3484 let rebuilt = font
3485 .to_bytes(None)
3486 .expect("from_bytes retains source bytes, so to_bytes must succeed");
3487 assert!(rebuilt.len() > 12);
3488
3489 let mut warnings = Vec::new();
3490 let re = ParsedFont::from_bytes(&rebuilt, 0, &mut warnings)
3491 .expect("a font emitted by to_bytes must parse back");
3492 assert_eq!(re.num_glyphs(), font.num_glyphs());
3493 assert_eq!(re.font_metrics.units_per_em, font.font_metrics.units_per_em);
3494 assert_eq!(re.pdf_font_metrics.x_min, font.pdf_font_metrics.x_min);
3495 assert_eq!(re.pdf_font_metrics.y_max, font.pdf_font_metrics.y_max);
3496
3497 let gid = font.lookup_glyph_index('A' as u32).unwrap();
3498 assert_eq!(
3499 re.lookup_glyph_index('A' as u32),
3500 Some(gid),
3501 "the cmap must survive the round-trip"
3502 );
3503 assert_eq!(
3504 re.get_horizontal_advance(gid),
3505 font.get_horizontal_advance(gid),
3506 "hmtx must survive the round-trip"
3507 );
3508 }
3509
3510 #[test]
3511 fn to_bytes_with_an_absent_tag_errors_instead_of_panicking() {
3512 let font = parse_mock();
3513 // Azul Mock Mono has no CFF table: asking for it must surface an Err string
3514 assert!(font.to_bytes(Some(&[tag::CFF])).is_err());
3515 // an empty tag list still reads head+maxp internally: it must return, not panic
3516 drop(font.to_bytes(Some(&[])));
3517 // duplicate tags must not double-insert into the builder and blow up
3518 drop(font.to_bytes(Some(&[tag::HEAD, tag::HEAD, tag::MAXP])));
3519 }
3520
3521 #[test]
3522 fn subset_maps_glyph_ids_and_produces_a_parseable_font() {
3523 let font = parse_mock();
3524 let gid_a = font.lookup_glyph_index('A' as u32).unwrap();
3525 let gid_b = font.lookup_glyph_index('B' as u32).unwrap();
3526 let (bytes, mapping) = font
3527 .subset(&[(0, '\0'), (gid_a, 'A'), (gid_b, 'B')], CmapTarget::Unrestricted)
3528 .expect("subsetting a TrueType font must succeed");
3529
3530 // the mapping is positional: original gid -> (index in the request, char)
3531 assert_eq!(mapping.len(), 3);
3532 assert_eq!(mapping.get(&0), Some(&(0, '\0')));
3533 assert_eq!(mapping.get(&gid_a), Some(&(1, 'A')));
3534 assert_eq!(mapping.get(&gid_b), Some(&(2, 'B')));
3535
3536 let mut warnings = Vec::new();
3537 let sub =
3538 ParsedFont::from_bytes(&bytes, 0, &mut warnings).expect("the subset must re-parse");
3539 assert!(sub.num_glyphs() >= 3);
3540 assert!(sub.num_glyphs() <= font.num_glyphs());
3541 assert_eq!(
3542 sub.get_horizontal_advance(1),
3543 font.get_horizontal_advance(gid_a),
3544 "the remapped 'A' keeps its advance"
3545 );
3546 }
3547
3548 #[test]
3549 fn subset_edge_inputs_do_not_panic() {
3550 let font = parse_mock();
3551 // an empty glyph list may be accepted or rejected; it must not panic
3552 if let Ok((_, mapping)) = font.subset(&[], CmapTarget::Unrestricted) {
3553 assert!(mapping.is_empty());
3554 }
3555 // an out-of-range gid must come back as a Result, never an OOB read
3556 drop(font.subset(&[(0, '\0'), (u16::MAX, 'x')], CmapTarget::Unrestricted));
3557
3558 // duplicate gids collapse in the returned map: the later entry wins
3559 let gid_a = font.lookup_glyph_index('A' as u32).unwrap();
3560 if let Ok((_, mapping)) = font.subset(
3561 &[(0, '\0'), (gid_a, 'a'), (gid_a, 'b')],
3562 CmapTarget::Unrestricted,
3563 ) {
3564 assert_eq!(mapping.len(), 2);
3565 assert_eq!(mapping.get(&gid_a), Some(&(2, 'b')));
3566 }
3567 }
3568
3569 // ---------------------------------------------------------------
3570 // empty / extreme instances (getters, predicates)
3571 // ---------------------------------------------------------------
3572
3573 #[test]
3574 fn every_getter_survives_an_empty_font() {
3575 let font = synthetic_font(0, None);
3576 assert_eq!(font.num_glyphs(), 0);
3577 assert!(font.get_or_decode_glyph(0).is_none()); // 0 >= num_glyphs
3578 assert!(font.get_or_decode_glyph(u16::MAX).is_none());
3579 assert!(font.get_glyph_bbox_size(0).is_none());
3580 assert!(font.lookup_glyph_index('A' as u32).is_none()); // no cmap
3581 assert!(!font.has_glyph('A' as u32));
3582 assert!(!font.has_glyph(0));
3583 assert_eq!(font.get_horizontal_advance(0), 0);
3584 assert_eq!(font.get_horizontal_advance(u16::MAX), 0);
3585 assert_eq!(font.get_glyph_width_internal(0), Some(0));
3586 assert!(font.get_vertical_metrics(0).is_none());
3587 assert!(font.get_space_width().is_none());
3588 assert!(font.get_space_width_internal().is_none());
3589 assert!(font.glyph_cache_snapshot().is_empty());
3590 assert!(font.gsub().is_none() && font.gpos().is_none());
3591 assert!(font.resolve_loca_glyf().is_none());
3592 assert!(font.source_bytes_for_subset().is_none());
3593 assert_eq!(font.last_used_nanos(), 0);
3594 assert!(!font.evict_loca_glyf());
3595 assert!(font.hmtx_bytes().is_empty());
3596 assert!(font.vmtx_bytes().is_empty());
3597 assert!(font.get_hinted_advance_px(0, 16).is_none());
3598 font.for_each_decoded_glyph(|_, _| panic!("nothing has been decoded"));
3599
3600 // without source bytes, the PDF paths must report an error, not panic
3601 assert!(font.to_bytes(None).is_err());
3602 assert!(font.subset(&[], CmapTarget::Unrestricted).is_err());
3603 }
3604
3605 #[test]
3606 fn hmtx_bytes_without_source_bytes_is_empty_not_a_panic() {
3607 let mut font = synthetic_font(4, None);
3608 // ranges pointing into bytes we never retained must degrade to an empty
3609 // slice rather than unwrapping a None
3610 font.hmtx_range = (16, 32);
3611 font.vmtx_range = (48, 64);
3612 assert!(font.hmtx_bytes().is_empty());
3613 assert!(font.vmtx_bytes().is_empty());
3614 assert_eq!(font.get_horizontal_advance(0), 0);
3615 }
3616
3617 #[test]
3618 fn hmtx_bytes_slices_the_retained_source() {
3619 let mut font = synthetic_font(2, None);
3620 let raw: Vec<u8> = (0u8..100).collect();
3621 font.original_bytes = Some(Arc::new(rust_fontconfig::FontBytes::Owned(Arc::from(raw))));
3622 font.hmtx_range = (10, 4);
3623 font.vmtx_range = (0, 0);
3624 assert_eq!(font.hmtx_bytes(), [10u8, 11, 12, 13].as_slice());
3625 assert!(font.vmtx_bytes().is_empty()); // a zero-length range short-circuits
3626 }
3627
3628 #[test]
3629 fn mock_backed_font_overrides_advances_and_space_width() {
3630 let mock = MockFont::new(plain_metrics())
3631 .with_space_width(42)
3632 .with_glyph_advance(1, 500);
3633 let font = synthetic_font(4, Some(Box::new(mock)));
3634
3635 assert_eq!(font.get_horizontal_advance(1), 500); // the mock wins over hmtx
3636 assert_eq!(font.get_horizontal_advance(3), 0); // unmapped gid -> 0, no panic
3637 assert_eq!(font.get_horizontal_advance(u16::MAX), 0);
3638 assert_eq!(font.get_space_width_internal(), Some(42));
3639
3640 // the decoded record inherits the mock advance and, with no outline,
3641 // seeds its bbox from it
3642 let g = font.get_or_decode_glyph(1).expect("gid 1 < num_glyphs");
3643 assert_eq!(g.horz_advance, 500);
3644 assert_eq!(g.bounding_box.max_x, 500);
3645 assert!(g.outline.is_empty());
3646 assert_eq!(font.get_glyph_bbox_size(1), Some((500, 0)));
3647 }
3648
3649 #[test]
3650 fn get_font_metrics_normalises_the_descent_sign() {
3651 let mut font = synthetic_font(1, None);
3652
3653 font.font_metrics.descent = -200.0;
3654 assert_eq!(font.get_font_metrics().descent, 200.0);
3655 font.font_metrics.descent = 200.0;
3656 assert_eq!(font.get_font_metrics().descent, 200.0);
3657
3658 // -0.0 is not > 0.0, so it takes the negation branch and comes out +0.0
3659 font.font_metrics.descent = -0.0;
3660 assert!(font.get_font_metrics().descent.is_sign_positive());
3661
3662 font.font_metrics.descent = f32::NEG_INFINITY;
3663 assert_eq!(font.get_font_metrics().descent, f32::INFINITY);
3664
3665 // NaN compares false against 0.0, so it is negated — and stays NaN
3666 font.font_metrics.descent = f32::NAN;
3667 assert!(font.get_font_metrics().descent.is_nan());
3668
3669 // every other field passes through untouched
3670 font.font_metrics.descent = -10.0;
3671 font.font_metrics.ascent = f32::MAX;
3672 font.font_metrics.line_gap = -1.0;
3673 let m = font.get_font_metrics();
3674 assert_eq!(m.ascent, f32::MAX);
3675 assert_eq!(m.line_gap, -1.0);
3676 assert_eq!(m.units_per_em, font.font_metrics.units_per_em);
3677 }
3678
3679 #[test]
3680 fn real_font_metrics_have_a_non_negative_descent() {
3681 let font = parse_mock();
3682 let m = font.get_font_metrics();
3683 assert!(m.descent >= 0.0, "get_font_metrics flips the descent sign");
3684 assert_eq!(m.descent, font.font_metrics.descent.abs());
3685 assert_eq!(m.ascent, font.font_metrics.ascent);
3686 assert_eq!(m.units_per_em, 1000);
3687 }
3688
3689 // ---------------------------------------------------------------
3690 // reverse glyph cache
3691 // ---------------------------------------------------------------
3692
3693 #[test]
3694 fn reverse_glyph_cache_round_trip_and_boundaries() {
3695 let mut font = synthetic_font(2, None);
3696 assert!(font.get_glyph_cluster_text(0).is_none());
3697 assert!(font.get_glyph_primary_char(0).is_none());
3698
3699 font.cache_glyph_mapping(0, ""); // empty cluster
3700 font.cache_glyph_mapping(u16::MAX, "fi"); // ligature at the gid boundary
3701 font.cache_glyph_mapping(7, "\u{1F468}\u{200D}\u{1F469}"); // ZWJ cluster
3702
3703 assert_eq!(font.get_glyph_cluster_text(0), Some(""));
3704 assert_eq!(font.get_glyph_primary_char(0), None); // no first char in ""
3705 assert_eq!(font.get_glyph_cluster_text(u16::MAX), Some("fi"));
3706 assert_eq!(font.get_glyph_primary_char(u16::MAX), Some('f'));
3707 assert_eq!(font.get_glyph_primary_char(7), Some('\u{1F468}'));
3708 assert!(font.get_glyph_cluster_text(1).is_none()); // never written
3709
3710 font.cache_glyph_mapping(u16::MAX, "ffi"); // last write wins
3711 assert_eq!(font.get_glyph_cluster_text(u16::MAX), Some("ffi"));
3712
3713 // clear_glyph_cache drops ONLY the reverse map, not the decoded outlines
3714 let decoded = font.get_or_decode_glyph(0).expect("gid 0 decodes");
3715 font.clear_glyph_cache();
3716 assert!(font.get_glyph_cluster_text(0).is_none());
3717 assert!(font.get_glyph_cluster_text(u16::MAX).is_none());
3718 assert!(font.get_glyph_primary_char(7).is_none());
3719 assert!(Arc::ptr_eq(
3720 font.glyph_cache_snapshot().get(&0).expect("outline cache is untouched"),
3721 &decoded
3722 ));
3723 font.clear_glyph_cache(); // idempotent
3724 }
3725
3726 // ---------------------------------------------------------------
3727 // loading / cpurender
3728 // ---------------------------------------------------------------
3729
3730 #[test]
3731 fn build_font_cache_does_not_panic() {
3732 // Smoke: the system font scan must complete. An empty cache is legitimate
3733 // (a headless image may ship no fonts), so only the call itself is asserted.
3734 let _cache = crate::font::loading::build_font_cache();
3735 }
3736
3737 #[cfg(feature = "cpurender")]
3738 #[test]
3739 fn build_glyph_path_needs_at_least_one_operation() {
3740 let empty = OwnedGlyph {
3741 bounding_box: OwnedGlyphBoundingBox {
3742 max_x: 0,
3743 max_y: 0,
3744 min_x: 0,
3745 min_y: 0,
3746 },
3747 horz_advance: 500,
3748 outline: Vec::new(),
3749 phantom_points: None,
3750 raw_points: None,
3751 raw_on_curve: None,
3752 raw_contour_ends: None,
3753 instructions: None,
3754 };
3755 // a space-like glyph has no ops -> None (the caller must skip it)
3756 assert!(build_glyph_path(&empty).is_none());
3757 // an outline whose only contour is empty is still "no ops"
3758 let hollow = OwnedGlyph {
3759 outline: vec![GlyphOutline {
3760 operations: Vec::<GlyphOutlineOperation>::new().into(),
3761 }],
3762 ..empty.clone()
3763 };
3764 assert!(build_glyph_path(&hollow).is_none());
3765
3766 // every op kind, at the i16 extremes, must survive the f64 conversion
3767 let full = OwnedGlyph {
3768 outline: vec![GlyphOutline {
3769 operations: vec![
3770 GlyphOutlineOperation::MoveTo(OutlineMoveTo {
3771 x: i16::MIN,
3772 y: i16::MAX,
3773 }),
3774 GlyphOutlineOperation::LineTo(OutlineLineTo {
3775 x: i16::MAX,
3776 y: i16::MIN,
3777 }),
3778 GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
3779 ctrl_1_x: 0,
3780 ctrl_1_y: 0,
3781 end_x: 1,
3782 end_y: 1,
3783 }),
3784 GlyphOutlineOperation::CubicCurveTo(OutlineCubicTo {
3785 ctrl_1_x: 0,
3786 ctrl_1_y: 0,
3787 ctrl_2_x: 0,
3788 ctrl_2_y: 0,
3789 end_x: 2,
3790 end_y: 2,
3791 }),
3792 GlyphOutlineOperation::ClosePath,
3793 ]
3794 .into(),
3795 }],
3796 ..empty
3797 };
3798 assert!(build_glyph_path(&full).is_some());
3799 }
3800 }
3801}