Skip to main content

azul_layout/
font.rs

1#![cfg(feature = "font_loading")]
2
3use azul_css::{AzString, U8Vec};
4use rust_fontconfig::{FcFontCache, FontSource};
5
6pub mod loading {
7    #![cfg(feature = "std")]
8    #![cfg(feature = "font_loading")]
9    #![cfg_attr(not(feature = "std"), no_std)]
10
11    use std::io::Error as IoError;
12
13    use azul_css::{AzString, StringVec, U8Vec};
14    use rust_fontconfig::FcFontCache;
15
16    #[cfg(not(miri))]
17    pub fn build_font_cache() -> FcFontCache {
18        FcFontCache::build()
19    }
20
21    #[cfg(miri)]
22    pub fn build_font_cache() -> FcFontCache {
23        FcFontCache::default()
24    }
25
26    #[derive(Debug)]
27    pub enum FontReloadError {
28        Io(IoError, AzString),
29        FontNotFound(AzString),
30        FontLoadingNotActive(AzString),
31    }
32
33    impl Clone for FontReloadError {
34        fn clone(&self) -> Self {
35            use self::FontReloadError::*;
36            match self {
37                Io(err, path) => Io(IoError::new(err.kind(), "Io Error"), path.clone()),
38                FontNotFound(id) => FontNotFound(id.clone()),
39                FontLoadingNotActive(id) => FontLoadingNotActive(id.clone()),
40            }
41        }
42    }
43
44    azul_core::impl_display!(FontReloadError, {
45        Io(err, path_buf) => format!("Could not load \"{}\" - IO error: {}", path_buf.as_str(), err),
46        FontNotFound(id) => format!("Could not locate system font: \"{:?}\" found", id),
47        FontLoadingNotActive(id) => format!("Could not load system font: \"{:?}\": crate was not compiled with --features=\"font_loading\"", id)
48    });
49}
50pub mod mock {
51    //! Mock font implementation for testing text layout.
52    //!
53    //! Provides a `MockFont` that simulates font behavior without requiring
54    //! actual font files, useful for unit testing text layout functionality.
55
56    use std::collections::BTreeMap;
57
58    use crate::text3::cache::LayoutFontMetrics;
59
60    /// A mock font implementation for testing text layout without real fonts.
61    ///
62    /// This allows testing text shaping, layout, and rendering code paths
63    /// without needing to load actual TrueType/OpenType font files.
64    #[derive(Debug, Clone)]
65    pub struct MockFont {
66        /// Font metrics (ascent, descent, etc.).
67        pub font_metrics: LayoutFontMetrics,
68        /// Width of the space character in font units.
69        pub space_width: Option<usize>,
70        /// Horizontal advance widths keyed by glyph ID.
71        pub glyph_advances: BTreeMap<u16, u16>,
72        /// Glyph bounding box sizes (width, height) keyed by glyph ID.
73        pub glyph_sizes: BTreeMap<u16, (i32, i32)>,
74        /// Unicode codepoint to glyph ID mapping.
75        pub glyph_indices: BTreeMap<u32, u16>,
76    }
77
78    impl MockFont {
79        /// Creates a new `MockFont` with the given font metrics.
80        pub fn new(font_metrics: LayoutFontMetrics) -> Self {
81            MockFont {
82                font_metrics,
83                space_width: Some(10),
84                glyph_advances: BTreeMap::new(),
85                glyph_sizes: BTreeMap::new(),
86                glyph_indices: BTreeMap::new(),
87            }
88        }
89
90        /// Sets the space character width.
91        pub fn with_space_width(mut self, width: usize) -> Self {
92            self.space_width = Some(width);
93            self
94        }
95
96        /// Adds a horizontal advance value for a glyph.
97        pub fn with_glyph_advance(mut self, glyph_index: u16, advance: u16) -> Self {
98            self.glyph_advances.insert(glyph_index, advance);
99            self
100        }
101
102        /// Adds a bounding box size for a glyph.
103        pub fn with_glyph_size(mut self, glyph_index: u16, size: (i32, i32)) -> Self {
104            self.glyph_sizes.insert(glyph_index, size);
105            self
106        }
107
108        /// Adds a Unicode codepoint to glyph ID mapping.
109        pub fn with_glyph_index(mut self, unicode: u32, index: u16) -> Self {
110            self.glyph_indices.insert(unicode, index);
111            self
112        }
113    }
114}
115
116pub mod parsed {
117    use core::fmt;
118    use std::{collections::BTreeMap, sync::Arc};
119
120    use allsorts::{
121        binary::read::ReadScope,
122        font_data::FontData,
123        layout::{GDEFTable, LayoutCache, LayoutCacheData, GPOS, GSUB},
124        outline::{OutlineBuilder, OutlineSink},
125        pathfinder_geometry::{line_segment::LineSegment2F, vector::Vector2F},
126        subset::{subset as allsorts_subset, whole_font, CmapTarget, SubsetProfile},
127        tables::{
128            cmap::owned::CmapSubtable as OwnedCmapSubtable,
129            glyf::{
130                Glyph, GlyfVisitorContext, LocaGlyf, Point,
131                VariableGlyfContext, VariableGlyfContextStore,
132            },
133            kern::owned::KernTable,
134            FontTableProvider, HheaTable, MaxpTable,
135        },
136        tag,
137    };
138    use azul_core::resources::{
139        GlyphOutline, GlyphOutlineOperation, OutlineCubicTo, OutlineLineTo, OutlineMoveTo,
140        OutlineQuadTo, OwnedGlyphBoundingBox,
141    };
142    use azul_css::props::basic::FontMetrics as CssFontMetrics;
143
144    // Mock font module for testing
145    pub use crate::font::mock::MockFont;
146    use crate::text3::cache::LayoutFontMetrics;
147
148    /// Cached GSUB table for glyph substitution operations.
149    pub type GsubCache = Arc<LayoutCacheData<GSUB>>;
150    /// Cached GPOS table for glyph positioning operations.
151    pub type GposCache = Arc<LayoutCacheData<GPOS>>;
152
153    /// Adapter that collects allsorts outline commands into our `GlyphOutline` format.
154    ///
155    /// Implements `OutlineSink` so it can be passed to `GlyfVisitorContext::visit()`.
156    /// This handles composite glyph resolution, transforms, and variable font
157    /// deltas automatically via allsorts internals.
158    struct GlyphOutlineCollector {
159        contours: Vec<GlyphOutline>,
160        current_contour: Vec<GlyphOutlineOperation>,
161    }
162
163    impl GlyphOutlineCollector {
164        fn new() -> Self {
165            Self {
166                contours: Vec::new(),
167                current_contour: Vec::new(),
168            }
169        }
170
171        fn into_outlines(mut self) -> Vec<GlyphOutline> {
172            if !self.current_contour.is_empty() {
173                self.contours.push(GlyphOutline {
174                    operations: std::mem::take(&mut self.current_contour).into(),
175                });
176            }
177            self.contours
178        }
179    }
180
181    impl OutlineSink for GlyphOutlineCollector {
182        fn move_to(&mut self, to: Vector2F) {
183            if !self.current_contour.is_empty() {
184                self.contours.push(GlyphOutline {
185                    operations: std::mem::take(&mut self.current_contour).into(),
186                });
187            }
188            self.current_contour.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo {
189                x: to.x() as i16,
190                y: to.y() as i16,
191            }));
192        }
193
194        fn line_to(&mut self, to: Vector2F) {
195            self.current_contour.push(GlyphOutlineOperation::LineTo(OutlineLineTo {
196                x: to.x() as i16,
197                y: to.y() as i16,
198            }));
199        }
200
201        fn quadratic_curve_to(&mut self, ctrl: Vector2F, to: Vector2F) {
202            self.current_contour.push(GlyphOutlineOperation::QuadraticCurveTo(
203                OutlineQuadTo {
204                    ctrl_1_x: ctrl.x() as i16,
205                    ctrl_1_y: ctrl.y() as i16,
206                    end_x: to.x() as i16,
207                    end_y: to.y() as i16,
208                },
209            ));
210        }
211
212        fn cubic_curve_to(&mut self, ctrl: LineSegment2F, to: Vector2F) {
213            self.current_contour.push(GlyphOutlineOperation::CubicCurveTo(
214                OutlineCubicTo {
215                    ctrl_1_x: ctrl.from_x() as i16,
216                    ctrl_1_y: ctrl.from_y() as i16,
217                    ctrl_2_x: ctrl.to_x() as i16,
218                    ctrl_2_y: ctrl.to_y() as i16,
219                    end_x: to.x() as i16,
220                    end_y: to.y() as i16,
221                },
222            ));
223        }
224
225        fn close(&mut self) {
226            self.current_contour.push(GlyphOutlineOperation::ClosePath);
227            self.contours.push(GlyphOutline {
228                operations: std::mem::take(&mut self.current_contour).into(),
229            });
230        }
231    }
232
233    /// Parsed font data with all required tables for text layout and PDF generation.
234    ///
235    /// This struct holds the parsed representation of a TrueType/OpenType font,
236    /// including glyph outlines, metrics, and shaping tables. It's used for:
237    /// - Text layout (via GSUB/GPOS tables)
238    /// - Glyph rendering (via glyf/CFF outlines)
239    /// - PDF font embedding (via font metrics and subsetting)
240    #[derive(Clone)]
241    pub struct ParsedFont {
242        /// Hash of the font bytes for caching and equality checks.
243        pub hash: u64,
244        /// Layout-specific font metrics (ascent, descent, line gap).
245        pub font_metrics: LayoutFontMetrics,
246        /// PDF-specific detailed font metrics from HEAD, HHEA, OS/2 tables.
247        pub pdf_font_metrics: PdfFontMetrics,
248        /// Total number of glyphs in the font (from maxp table).
249        pub num_glyphs: u16,
250        /// Horizontal header table (hhea) containing global horizontal metrics.
251        pub hhea_table: HheaTable,
252        /// Raw horizontal metrics data (hmtx table bytes).
253        pub hmtx_data: Vec<u8>,
254        /// Raw vertical metrics data (vmtx table bytes, if present).
255        pub vmtx_data: Vec<u8>,
256        /// Maximum profile table (maxp) containing glyph count and memory hints.
257        pub maxp_table: MaxpTable,
258        /// Cached GSUB table for glyph substitution (ligatures, alternates).
259        pub gsub_cache: Option<GsubCache>,
260        /// Cached GPOS table for glyph positioning (kerning, mark placement).
261        pub gpos_cache: Option<GposCache>,
262        /// Glyph definition table (GDEF) for glyph classification.
263        pub opt_gdef_table: Option<Arc<GDEFTable>>,
264        /// Legacy kerning table (kern) for fonts without GPOS.
265        pub opt_kern_table: Option<Arc<KernTable>>,
266        /// Decoded glyph records with outlines and metrics, keyed by glyph ID.
267        pub glyph_records_decoded: BTreeMap<u16, OwnedGlyph>,
268        /// Cached width of the space character in font units.
269        pub space_width: Option<usize>,
270        /// Character-to-glyph mapping (cmap subtable).
271        pub cmap_subtable: Option<OwnedCmapSubtable>,
272        /// Mock font data for testing (replaces real font behavior).
273        pub mock: Option<Box<MockFont>>,
274        /// Reverse mapping: glyph_id -> cluster text (handles ligatures like "fi").
275        pub reverse_glyph_cache: std::collections::BTreeMap<u16, String>,
276        /// Original font bytes (needed for subsetting and reconstruction).
277        pub original_bytes: Vec<u8>,
278        /// Font index within collection (0 for single-font files).
279        pub original_index: usize,
280        /// GID to CID mapping for CFF fonts (required for PDF embedding).
281        pub index_to_cid: BTreeMap<u16, u16>,
282        /// Font type (TrueType outlines or OpenType CFF).
283        pub font_type: FontType,
284        /// PostScript font name from the NAME table.
285        pub font_name: Option<String>,
286    }
287
288    /// Distinguishes TrueType fonts from OpenType CFF fonts.
289    ///
290    /// This affects how glyph outlines are extracted and how the font
291    /// is embedded in PDF documents.
292    #[derive(Debug, Clone, PartialEq)]
293    pub enum FontType {
294        /// TrueType font with quadratic Bézier outlines in glyf table.
295        TrueType,
296        /// OpenType font with cubic Bézier outlines in CFF table.
297        /// Contains the serialized CFF data for PDF embedding.
298        OpenTypeCFF(Vec<u8>),
299    }
300
301    /// PDF-specific font metrics from HEAD, HHEA, and OS/2 tables.
302    ///
303    /// These metrics are used for PDF font descriptors and accurate
304    /// text positioning in generated PDF documents.
305    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
306    #[repr(C)]
307    pub struct PdfFontMetrics {
308        // -- HEAD table fields --
309        /// Font units per em-square (typically 1000 or 2048).
310        pub units_per_em: u16,
311        /// Font flags (italic, bold, fixed-pitch, etc.).
312        pub font_flags: u16,
313        /// Minimum x-coordinate across all glyphs.
314        pub x_min: i16,
315        /// Minimum y-coordinate across all glyphs.
316        pub y_min: i16,
317        /// Maximum x-coordinate across all glyphs.
318        pub x_max: i16,
319        /// Maximum y-coordinate across all glyphs.
320        pub y_max: i16,
321
322        // -- HHEA table fields --
323        /// Typographic ascender (distance above baseline).
324        pub ascender: i16,
325        /// Typographic descender (distance below baseline, usually negative).
326        pub descender: i16,
327        /// Recommended line gap between lines of text.
328        pub line_gap: i16,
329        /// Maximum horizontal advance width across all glyphs.
330        pub advance_width_max: u16,
331        /// Caret slope rise for italic angle calculation.
332        pub caret_slope_rise: i16,
333        /// Caret slope run for italic angle calculation.
334        pub caret_slope_run: i16,
335
336        // -- OS/2 table fields (0 if table not present) --
337        /// Average width of lowercase letters.
338        pub x_avg_char_width: i16,
339        /// Visual weight class (100-900, 400=normal, 700=bold).
340        pub us_weight_class: u16,
341        /// Visual width class (1-9, 5=normal).
342        pub us_width_class: u16,
343        /// Thickness of strikeout stroke in font units.
344        pub y_strikeout_size: i16,
345        /// Vertical position of strikeout stroke.
346        pub y_strikeout_position: i16,
347    }
348
349    impl Default for PdfFontMetrics {
350        fn default() -> Self {
351            PdfFontMetrics::zero()
352        }
353    }
354
355    impl PdfFontMetrics {
356        pub const fn zero() -> Self {
357            PdfFontMetrics {
358                units_per_em: 1000,
359                font_flags: 0,
360                x_min: 0,
361                y_min: 0,
362                x_max: 0,
363                y_max: 0,
364                ascender: 0,
365                descender: 0,
366                line_gap: 0,
367                advance_width_max: 0,
368                caret_slope_rise: 0,
369                caret_slope_run: 0,
370                x_avg_char_width: 0,
371                us_weight_class: 0,
372                us_width_class: 0,
373                y_strikeout_size: 0,
374                y_strikeout_position: 0,
375            }
376        }
377    }
378
379    /// Result of font subsetting operation.
380    ///
381    /// Contains the subsetted font bytes and a mapping from original
382    /// glyph IDs to new glyph IDs in the subset.
383    #[derive(Debug, Clone)]
384    pub struct SubsetFont {
385        /// The subsetted font file bytes (smaller than original).
386        pub bytes: Vec<u8>,
387        /// Mapping: original glyph ID -> (new subset glyph ID, source character).
388        pub glyph_mapping: BTreeMap<u16, (u16, char)>,
389    }
390
391    impl SubsetFont {
392        /// Return the changed text so that when rendering with the subset font (instead of the
393        /// original) the renderer will end up at the same glyph IDs as if we used the original text
394        /// on the original font
395        pub fn subset_text(&self, text: &str) -> String {
396            text.chars()
397                .filter_map(|c| {
398                    self.glyph_mapping.values().find_map(|(ngid, ch)| {
399                        if *ch == c {
400                            char::from_u32(*ngid as u32)
401                        } else {
402                            None
403                        }
404                    })
405                })
406                .collect()
407        }
408    }
409
410    impl PartialEq for ParsedFont {
411        fn eq(&self, other: &Self) -> bool {
412            self.hash == other.hash
413        }
414    }
415
416    impl Eq for ParsedFont {}
417
418    const FONT_B64_START: &str = "data:font/ttf;base64,";
419
420    impl serde::Serialize for ParsedFont {
421        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
422            use base64::Engine;
423            let s = format!(
424                "{FONT_B64_START}{}",
425                base64::prelude::BASE64_STANDARD.encode(&self.to_bytes(None).unwrap_or_default())
426            );
427            s.serialize(serializer)
428        }
429    }
430
431    impl<'de> serde::Deserialize<'de> for ParsedFont {
432        fn deserialize<D: serde::Deserializer<'de>>(
433            deserializer: D,
434        ) -> Result<ParsedFont, D::Error> {
435            use base64::Engine;
436            let s = String::deserialize(deserializer)?;
437            let b64 = if s.starts_with(FONT_B64_START) {
438                let b = &s[FONT_B64_START.len()..];
439                base64::prelude::BASE64_STANDARD.decode(&b).ok()
440            } else {
441                None
442            };
443
444            let mut warnings = Vec::new();
445            ParsedFont::from_bytes(&b64.unwrap_or_default(), 0, &mut warnings).ok_or_else(|| {
446                serde::de::Error::custom(format!("Font deserialization error: {warnings:?}"))
447            })
448        }
449    }
450
451    impl fmt::Debug for ParsedFont {
452        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453            f.debug_struct("ParsedFont")
454                .field("hash", &self.hash)
455                .field("font_metrics", &self.font_metrics)
456                .field("num_glyphs", &self.num_glyphs)
457                .field("hhea_table", &self.hhea_table)
458                .field(
459                    "hmtx_data",
460                    &format_args!("<{} bytes>", self.hmtx_data.len()),
461                )
462                .field("maxp_table", &self.maxp_table)
463                .field(
464                    "glyph_records_decoded",
465                    &format_args!("{} entries", self.glyph_records_decoded.len()),
466                )
467                .field("space_width", &self.space_width)
468                .field("cmap_subtable", &self.cmap_subtable)
469                .finish()
470        }
471    }
472
473    /// Warning or error message generated during font parsing.
474    #[derive(Debug, Clone, PartialEq, Eq)]
475    pub struct FontParseWarning {
476        /// Severity level of this warning.
477        pub severity: FontParseWarningSeverity,
478        /// Human-readable description of the issue.
479        pub message: String,
480    }
481
482    /// Severity level for font parsing warnings.
483    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
484    pub enum FontParseWarningSeverity {
485        /// Informational message (not an error).
486        Info,
487        /// Warning that may affect font rendering.
488        Warning,
489        /// Error that prevents proper font usage.
490        Error,
491    }
492
493    impl FontParseWarning {
494        /// Creates an info-level message.
495        pub fn info(message: String) -> Self {
496            Self {
497                severity: FontParseWarningSeverity::Info,
498                message,
499            }
500        }
501
502        /// Creates a warning-level message.
503        pub fn warning(message: String) -> Self {
504            Self {
505                severity: FontParseWarningSeverity::Warning,
506                message,
507            }
508        }
509
510        /// Creates an error-level message.
511        pub fn error(message: String) -> Self {
512            Self {
513                severity: FontParseWarningSeverity::Error,
514                message,
515            }
516        }
517    }
518
519    impl ParsedFont {
520        /// Parse a font from bytes using allsorts
521        ///
522        /// # Arguments
523        /// * `font_bytes` - The font file data
524        /// * `font_index` - Index of the font in a font collection (0 for single fonts)
525        /// * `warnings` - Optional vector to collect parsing warnings
526        ///
527        /// # Returns
528        /// `Some(ParsedFont)` if parsing succeeds, `None` otherwise
529        ///
530        /// Note: Outlines are always parsed (parse_outlines = true)
531        pub fn from_bytes(
532            font_bytes: &[u8],
533            font_index: usize,
534            warnings: &mut Vec<FontParseWarning>,
535        ) -> Option<Self> {
536            use std::{
537                collections::hash_map::DefaultHasher,
538                hash::{Hash, Hasher},
539            };
540
541            use allsorts::{
542                binary::read::ReadScope,
543                font_data::FontData,
544                tables::{
545                    cmap::{owned::CmapSubtable as OwnedCmapSubtable, CmapSubtable},
546                    FontTableProvider, HeadTable, HheaTable, MaxpTable,
547                },
548                tag,
549            };
550
551            let scope = ReadScope::new(font_bytes);
552            let font_file = match scope.read::<FontData<'_>>() {
553                Ok(ff) => {
554                    warnings.push(FontParseWarning::info(
555                        "Successfully read font data".to_string(),
556                    ));
557                    ff
558                }
559                Err(e) => {
560                    warnings.push(FontParseWarning::error(format!(
561                        "Failed to read font data: {}",
562                        e
563                    )));
564                    return None;
565                }
566            };
567            let provider = match font_file.table_provider(font_index) {
568                Ok(p) => {
569                    warnings.push(FontParseWarning::info(format!(
570                        "Successfully loaded font at index {}",
571                        font_index
572                    )));
573                    p
574                }
575                Err(e) => {
576                    warnings.push(FontParseWarning::error(format!(
577                        "Failed to get table provider for font index {}: {}",
578                        font_index, e
579                    )));
580                    return None;
581                }
582            };
583
584            // Extract font name from NAME table early (before provider is moved)
585            let font_name = provider.table_data(tag::NAME).ok().and_then(|name_data| {
586                ReadScope::new(&name_data?)
587                    .read::<allsorts::tables::NameTable>()
588                    .ok()
589                    .and_then(|name_table| {
590                        name_table.string_for_id(allsorts::tables::NameTable::POSTSCRIPT_NAME)
591                    })
592            });
593
594            let head_table = provider
595                .table_data(tag::HEAD)
596                .ok()
597                .and_then(|head_data| ReadScope::new(&head_data?).read::<HeadTable>().ok())?;
598
599            let maxp_table = provider
600                .table_data(tag::MAXP)
601                .ok()
602                .and_then(|maxp_data| ReadScope::new(&maxp_data?).read::<MaxpTable>().ok())
603                .unwrap_or(MaxpTable {
604                    num_glyphs: 0,
605                    version1_sub_table: None,
606                });
607
608            let num_glyphs = maxp_table.num_glyphs as usize;
609
610            let hmtx_data = provider
611                .table_data(tag::HMTX)
612                .ok()
613                .and_then(|s| Some(s?.to_vec()))
614                .unwrap_or_default();
615
616            let vmtx_data = provider
617                .table_data(tag::VMTX)
618                .ok()
619                .and_then(|s| Some(s?.to_vec()))
620                .unwrap_or_default();
621
622            let hhea_table = provider
623                .table_data(tag::HHEA)
624                .ok()
625                .and_then(|hhea_data| ReadScope::new(&hhea_data?).read::<HheaTable>().ok())
626                .unwrap_or(unsafe { std::mem::zeroed() });
627
628            // Build layout-specific font metrics
629            let font_metrics = LayoutFontMetrics {
630                units_per_em: if head_table.units_per_em == 0 {
631                    1000
632                } else {
633                    head_table.units_per_em
634                },
635                ascent: hhea_table.ascender as f32,
636                descent: hhea_table.descender as f32,
637                line_gap: hhea_table.line_gap as f32,
638            };
639
640            // Build PDF-specific font metrics
641            let pdf_font_metrics =
642                Self::parse_pdf_font_metrics(font_bytes, font_index, &head_table, &hhea_table);
643
644            // Use allsorts LocaGlyf + GlyfVisitorContext for outline extraction.
645            // This correctly handles composite glyphs (recursive resolution, transforms)
646            // and variable font deltas (gvar) automatically via allsorts internals.
647            let has_glyf = provider.has_table(tag::GLYF) && provider.has_table(tag::LOCA);
648
649            let glyph_records_decoded: BTreeMap<u16, OwnedGlyph> = if has_glyf {
650                warnings.push(FontParseWarning::info(
651                    "Parsing glyph outlines via allsorts OutlineBuilder (composite-safe)".to_string(),
652                ));
653
654                // Load LocaGlyf for the visitor
655                match LocaGlyf::load(&provider) {
656                    Ok(mut loca_glyf) => {
657                        // Optionally set up variable font context for gvar deltas
658                        let var_store = VariableGlyfContextStore::read(&provider).ok();
659                        let var_context = var_store.as_ref()
660                            .and_then(|store| VariableGlyfContext::new(store).ok());
661
662                        let mut visitor = GlyfVisitorContext::new(
663                            &mut loca_glyf,
664                            var_context,
665                        );
666
667                        let mut map = BTreeMap::new();
668                        for glyph_index in 0..num_glyphs.min(u16::MAX as usize) {
669                            let gid = glyph_index as u16;
670                            let horz_advance = allsorts::glyph_info::advance(
671                                &maxp_table, &hhea_table, &hmtx_data, gid,
672                            ).unwrap_or_default();
673
674                            // Visit the glyph outline via allsorts (handles composites + transforms)
675                            let mut collector = GlyphOutlineCollector::new();
676                            // Use default variation instance (no user tuple)
677                            let visit_result = visitor.visit(gid, None, &mut collector);
678
679                            let outlines = match visit_result {
680                                Ok(()) => collector.into_outlines(),
681                                Err(_) => Vec::new(),
682                            };
683
684                            // Get bounding box from the collected outlines
685                            let (min_x, min_y, max_x, max_y) = compute_outline_bbox(&outlines);
686
687                            map.insert(gid, OwnedGlyph {
688                                horz_advance,
689                                bounding_box: OwnedGlyphBoundingBox {
690                                    min_x, min_y, max_x, max_y,
691                                },
692                                outline: outlines,
693                                phantom_points: None,
694                            });
695                        }
696                        map
697                    }
698                    Err(e) => {
699                        warnings.push(FontParseWarning::warning(format!(
700                            "Failed to load LocaGlyf: {} — falling back to hmtx-only", e
701                        )));
702                        // Fall back to hmtx-only metrics
703                        (0..num_glyphs.min(u16::MAX as usize))
704                            .map(|glyph_index| {
705                                let gid = glyph_index as u16;
706                                let horz_advance = allsorts::glyph_info::advance(
707                                    &maxp_table, &hhea_table, &hmtx_data, gid,
708                                ).unwrap_or_default();
709                                (gid, OwnedGlyph {
710                                    horz_advance,
711                                    bounding_box: OwnedGlyphBoundingBox {
712                                        min_x: 0, min_y: 0,
713                                        max_x: horz_advance as i16, max_y: 0,
714                                    },
715                                    outline: Vec::new(),
716                                    phantom_points: None,
717                                })
718                            })
719                            .collect()
720                    }
721                }
722            } else {
723                // CFF fonts or fonts without glyf table: Parse metrics only from hmtx
724                warnings.push(FontParseWarning::info(format!(
725                    "Using hmtx-only fallback for {} glyphs (CFF font or no glyf table)",
726                    num_glyphs
727                )));
728                (0..num_glyphs.min(u16::MAX as usize))
729                    .map(|glyph_index| {
730                        let gid = glyph_index as u16;
731                        let horz_advance = allsorts::glyph_info::advance(
732                            &maxp_table, &hhea_table, &hmtx_data, gid,
733                        ).unwrap_or_default();
734
735                        (gid, OwnedGlyph {
736                            horz_advance,
737                            bounding_box: OwnedGlyphBoundingBox {
738                                min_x: 0, min_y: 0,
739                                max_x: horz_advance as i16, max_y: 0,
740                            },
741                            outline: Vec::new(),
742                            phantom_points: None,
743                        })
744                    })
745                    .collect::<BTreeMap<_, _>>()
746            };
747
748            let mut font_data_impl = allsorts::font::Font::new(provider).ok()?;
749
750            // Required for font layout: gsub_cache, gpos_cache and gdef_table
751            let gsub_cache = font_data_impl.gsub_cache().ok().and_then(|s| s);
752            let gpos_cache = font_data_impl.gpos_cache().ok().and_then(|s| s);
753            let opt_gdef_table = font_data_impl.gdef_table().ok().and_then(|o| o);
754            let num_glyphs = font_data_impl.num_glyphs();
755
756            let opt_kern_table = font_data_impl
757                .kern_table()
758                .ok()
759                .and_then(|s| Some(s?.to_owned()));
760
761            let cmap_data = font_data_impl.cmap_subtable_data();
762            let cmap_subtable = ReadScope::new(cmap_data);
763            let cmap_subtable = cmap_subtable
764                .read::<CmapSubtable<'_>>()
765                .ok()
766                .and_then(|s| s.to_owned());
767
768            // Calculate hash of font data
769            let mut hasher = DefaultHasher::new();
770            font_bytes.hash(&mut hasher);
771            font_index.hash(&mut hasher);
772            let hash = hasher.finish();
773
774            let mut font = ParsedFont {
775                hash,
776                font_metrics,
777                pdf_font_metrics,
778                num_glyphs,
779                hhea_table,
780                hmtx_data,
781                vmtx_data,
782                maxp_table,
783                gsub_cache,
784                gpos_cache,
785                opt_gdef_table,
786                opt_kern_table,
787                cmap_subtable,
788                glyph_records_decoded,
789                space_width: None,
790                mock: None,
791                reverse_glyph_cache: BTreeMap::new(),
792                original_bytes: font_bytes.to_vec(),
793                original_index: font_index,
794                index_to_cid: BTreeMap::new(), // Will be filled for CFF fonts
795                font_type: FontType::TrueType, // Default, will be updated if CFF
796                font_name,
797            };
798
799            // Calculate space width
800            let space_width = font.get_space_width_internal();
801
802            // Ensure space glyph is in glyph_records_decoded
803            // Space glyphs often don't have outlines, so they may not be loaded by default
804            let _ = (|| {
805                let space_gid = font.lookup_glyph_index(' ' as u32)?;
806                if font.glyph_records_decoded.contains_key(&space_gid) {
807                    return None; // Already exists
808                }
809                let space_width_val = space_width?;
810                let space_record = OwnedGlyph {
811                    bounding_box: OwnedGlyphBoundingBox {
812                        max_x: 0,
813                        max_y: 0,
814                        min_x: 0,
815                        min_y: 0,
816                    },
817                    horz_advance: space_width_val as u16,
818                    outline: Vec::new(),
819                    phantom_points: None,
820                };
821                font.glyph_records_decoded.insert(space_gid, space_record);
822                Some(())
823            })();
824
825            font.space_width = space_width;
826
827            Some(font)
828        }
829
830        /// Parse PDF-specific font metrics from HEAD, HHEA, and OS/2 tables
831        fn parse_pdf_font_metrics(
832            font_bytes: &[u8],
833            font_index: usize,
834            head_table: &allsorts::tables::HeadTable,
835            hhea_table: &allsorts::tables::HheaTable,
836        ) -> PdfFontMetrics {
837            use allsorts::{
838                binary::read::ReadScope,
839                font_data::FontData,
840                tables::{os2::Os2, FontTableProvider},
841                tag,
842            };
843
844            let scope = ReadScope::new(font_bytes);
845            let font_file = scope.read::<FontData<'_>>().ok();
846            let provider = font_file
847                .as_ref()
848                .and_then(|ff| ff.table_provider(font_index).ok());
849
850            let os2_table = provider
851                .as_ref()
852                .and_then(|p| p.table_data(tag::OS_2).ok())
853                .and_then(|os2_data| {
854                    let data = os2_data?;
855                    let scope = ReadScope::new(&data);
856                    scope.read_dep::<Os2>(data.len()).ok()
857                });
858
859            // Base metrics from HEAD and HHEA (always present)
860            let base = PdfFontMetrics {
861                units_per_em: head_table.units_per_em,
862                font_flags: head_table.flags,
863                x_min: head_table.x_min,
864                y_min: head_table.y_min,
865                x_max: head_table.x_max,
866                y_max: head_table.y_max,
867                ascender: hhea_table.ascender,
868                descender: hhea_table.descender,
869                line_gap: hhea_table.line_gap,
870                advance_width_max: hhea_table.advance_width_max,
871                caret_slope_rise: hhea_table.caret_slope_rise,
872                caret_slope_run: hhea_table.caret_slope_run,
873                ..PdfFontMetrics::zero()
874            };
875
876            // Add OS/2 metrics if available
877            os2_table
878                .map(|os2| PdfFontMetrics {
879                    x_avg_char_width: os2.x_avg_char_width,
880                    us_weight_class: os2.us_weight_class,
881                    us_width_class: os2.us_width_class,
882                    y_strikeout_size: os2.y_strikeout_size,
883                    y_strikeout_position: os2.y_strikeout_position,
884                    ..base
885                })
886                .unwrap_or(base)
887        }
888
889        /// Returns the width of the space character in font units.
890        ///
891        /// This is used internally for text layout calculations.
892        /// Returns `None` if the font has no space glyph or its width cannot be determined.
893        fn get_space_width_internal(&self) -> Option<usize> {
894            if let Some(mock) = self.mock.as_ref() {
895                return mock.space_width;
896            }
897            let glyph_index = self.lookup_glyph_index(' ' as u32)?;
898
899            allsorts::glyph_info::advance(
900                &self.maxp_table,
901                &self.hhea_table,
902                &self.hmtx_data,
903                glyph_index,
904            )
905            .ok()
906            .map(|s| s as usize)
907        }
908
909        /// Look up the glyph index for a Unicode codepoint
910        pub fn lookup_glyph_index(&self, codepoint: u32) -> Option<u16> {
911            let cmap = self.cmap_subtable.as_ref()?;
912            cmap.map_glyph(codepoint).ok().flatten()
913        }
914
915        /// Get the horizontal advance width for a glyph in font units
916        pub fn get_horizontal_advance(&self, glyph_index: u16) -> u16 {
917            if let Some(mock) = self.mock.as_ref() {
918                return mock.glyph_advances.get(&glyph_index).copied().unwrap_or(0);
919            }
920            self.glyph_records_decoded
921                .get(&glyph_index)
922                .map(|gi| gi.horz_advance)
923                .unwrap_or_default()
924        }
925
926        /// Get the number of glyphs in this font
927        pub fn num_glyphs(&self) -> u16 {
928            self.num_glyphs
929        }
930
931        /// Check if this font has a glyph for the given codepoint
932        pub fn has_glyph(&self, codepoint: u32) -> bool {
933            self.lookup_glyph_index(codepoint).is_some()
934        }
935
936        /// Get vertical metrics for a glyph (for vertical text layout).
937        ///
938        /// Currently always returns `None` because vertical layout tables
939        /// (vhea, vmtx) are not parsed. Vertical text layout is not yet supported.
940        pub fn get_vertical_metrics(
941            &self,
942            _glyph_id: u16,
943        ) -> Option<crate::text3::cache::VerticalMetrics> {
944            // Vertical text layout requires parsing vhea and vmtx tables
945            None
946        }
947
948        /// Get layout-specific font metrics
949        pub fn get_font_metrics(&self) -> crate::text3::cache::LayoutFontMetrics {
950            // Ensure descent is positive (OpenType may have negative descent)
951            let descent = if self.font_metrics.descent > 0.0 {
952                self.font_metrics.descent
953            } else {
954                -self.font_metrics.descent
955            };
956
957            crate::text3::cache::LayoutFontMetrics {
958                ascent: self.font_metrics.ascent,
959                descent,
960                line_gap: self.font_metrics.line_gap,
961                units_per_em: self.font_metrics.units_per_em,
962            }
963        }
964
965        /// Convert the ParsedFont back to bytes using allsorts::whole_font
966        /// This reconstructs the entire font from the parsed data
967        ///
968        /// # Arguments
969        /// * `tags` - Optional list of specific table tags to include (None = all tables)
970        pub fn to_bytes(&self, tags: Option<&[u32]>) -> Result<Vec<u8>, String> {
971            let scope = ReadScope::new(&self.original_bytes);
972            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
973            let provider = font_file
974                .table_provider(self.original_index)
975                .map_err(|e| e.to_string())?;
976
977            let tags_to_use = tags.unwrap_or(&[
978                tag::CMAP,
979                tag::HEAD,
980                tag::HHEA,
981                tag::HMTX,
982                tag::MAXP,
983                tag::NAME,
984                tag::OS_2,
985                tag::POST,
986                tag::GLYF,
987                tag::LOCA,
988            ]);
989
990            whole_font(&provider, tags_to_use).map_err(|e| e.to_string())
991        }
992
993        /// Create a subset font containing only the specified glyph IDs
994        /// Returns the subset font bytes and a mapping from old to new glyph IDs
995        ///
996        /// # Arguments
997        /// * `glyph_ids` - The glyph IDs to include in the subset (glyph 0/.notdef is always
998        ///   included)
999        /// * `cmap_target` - Target cmap format (Unicode for web, MacRoman for compatibility)
1000        ///
1001        /// # Returns
1002        /// A tuple of (subset_font_bytes, glyph_mapping) where glyph_mapping maps
1003        /// original_glyph_id -> (new_glyph_id, original_char)
1004        pub fn subset(
1005            &self,
1006            glyph_ids: &[(u16, char)],
1007            cmap_target: CmapTarget,
1008        ) -> Result<(Vec<u8>, BTreeMap<u16, (u16, char)>), String> {
1009            let scope = ReadScope::new(&self.original_bytes);
1010            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
1011            let provider = font_file
1012                .table_provider(self.original_index)
1013                .map_err(|e| e.to_string())?;
1014
1015            // Build glyph mapping: original_id -> (new_id, char)
1016            let glyph_mapping: BTreeMap<u16, (u16, char)> = glyph_ids
1017                .iter()
1018                .enumerate()
1019                .map(|(new_id, &(original_id, ch))| (original_id, (new_id as u16, ch)))
1020                .collect();
1021
1022            // Extract just the glyph IDs for subsetting
1023            let ids: Vec<u16> = glyph_ids.iter().map(|(id, _)| *id).collect();
1024
1025            // Use PDF profile for embedding fonts in PDFs
1026            let font_bytes = allsorts_subset(&provider, &ids, &SubsetProfile::Pdf, cmap_target)
1027                .map_err(|e| format!("Subset error: {:?}", e))?;
1028
1029            Ok((font_bytes, glyph_mapping))
1030        }
1031
1032        /// Get the width of a glyph in font units (internal, unscaled)
1033        pub fn get_glyph_width_internal(&self, glyph_index: u16) -> Option<usize> {
1034            allsorts::glyph_info::advance(
1035                &self.maxp_table,
1036                &self.hhea_table,
1037                &self.hmtx_data,
1038                glyph_index,
1039            )
1040            .ok()
1041            .map(|s| s as usize)
1042        }
1043
1044        /// Get the width of the space character (unscaled font units)
1045        #[inline]
1046        pub const fn get_space_width(&self) -> Option<usize> {
1047            self.space_width
1048        }
1049
1050        /// Add glyph-to-text mapping to reverse cache
1051        /// This should be called during text shaping when we know both the source text and
1052        /// resulting glyphs
1053        pub fn cache_glyph_mapping(&mut self, glyph_id: u16, cluster_text: &str) {
1054            self.reverse_glyph_cache
1055                .insert(glyph_id, cluster_text.to_string());
1056        }
1057
1058        /// Get the cluster text that produced a specific glyph ID
1059        /// Returns the original text that was shaped into this glyph (handles ligatures correctly)
1060        pub fn get_glyph_cluster_text(&self, glyph_id: u16) -> Option<&str> {
1061            self.reverse_glyph_cache.get(&glyph_id).map(|s| s.as_str())
1062        }
1063
1064        /// Get the first character from the cluster text for a glyph ID
1065        /// This is useful for PDF ToUnicode CMap generation which requires single character
1066        /// mappings
1067        pub fn get_glyph_primary_char(&self, glyph_id: u16) -> Option<char> {
1068            self.reverse_glyph_cache
1069                .get(&glyph_id)
1070                .and_then(|text| text.chars().next())
1071        }
1072
1073        /// Clear the reverse glyph cache (useful for memory management)
1074        pub fn clear_glyph_cache(&mut self) {
1075            self.reverse_glyph_cache.clear();
1076        }
1077
1078        /// Get the bounding box size of a glyph (unscaled units) - for PDF
1079        /// Returns (width, height) in font units
1080        pub fn get_glyph_bbox_size(&self, glyph_index: u16) -> Option<(i32, i32)> {
1081            let g = self.glyph_records_decoded.get(&glyph_index)?;
1082            let glyph_width = g.horz_advance as i32;
1083            let glyph_height = g.bounding_box.max_y as i32 - g.bounding_box.min_y as i32;
1084            Some((glyph_width, glyph_height))
1085        }
1086    }
1087
1088    /// Compute the bounding box from collected glyph outlines.
1089    fn compute_outline_bbox(outlines: &[GlyphOutline]) -> (i16, i16, i16, i16) {
1090        let mut min_x = i16::MAX;
1091        let mut min_y = i16::MAX;
1092        let mut max_x = i16::MIN;
1093        let mut max_y = i16::MIN;
1094        let mut has_points = false;
1095
1096        for outline in outlines {
1097            for op in outline.operations.as_slice() {
1098                let points: &[(i16, i16)] = match op {
1099                    GlyphOutlineOperation::MoveTo(m) => &[(m.x, m.y)],
1100                    GlyphOutlineOperation::LineTo(l) => &[(l.x, l.y)],
1101                    GlyphOutlineOperation::QuadraticCurveTo(q) => {
1102                        // Check both control and end point for bbox
1103                        min_x = min_x.min(q.ctrl_1_x).min(q.end_x);
1104                        min_y = min_y.min(q.ctrl_1_y).min(q.end_y);
1105                        max_x = max_x.max(q.ctrl_1_x).max(q.end_x);
1106                        max_y = max_y.max(q.ctrl_1_y).max(q.end_y);
1107                        has_points = true;
1108                        continue;
1109                    }
1110                    GlyphOutlineOperation::CubicCurveTo(c) => {
1111                        min_x = min_x.min(c.ctrl_1_x).min(c.ctrl_2_x).min(c.end_x);
1112                        min_y = min_y.min(c.ctrl_1_y).min(c.ctrl_2_y).min(c.end_y);
1113                        max_x = max_x.max(c.ctrl_1_x).max(c.ctrl_2_x).max(c.end_x);
1114                        max_y = max_y.max(c.ctrl_1_y).max(c.ctrl_2_y).max(c.end_y);
1115                        has_points = true;
1116                        continue;
1117                    }
1118                    GlyphOutlineOperation::ClosePath => continue,
1119                };
1120                for &(x, y) in points {
1121                    min_x = min_x.min(x);
1122                    min_y = min_y.min(y);
1123                    max_x = max_x.max(x);
1124                    max_y = max_y.max(y);
1125                    has_points = true;
1126                }
1127            }
1128        }
1129
1130        if has_points {
1131            (min_x, min_y, max_x, max_y)
1132        } else {
1133            (0, 0, 0, 0)
1134        }
1135    }
1136
1137    #[derive(Debug, Clone)]
1138    pub struct OwnedGlyph {
1139        pub bounding_box: OwnedGlyphBoundingBox,
1140        pub horz_advance: u16,
1141        pub outline: Vec<GlyphOutline>,
1142        pub phantom_points: Option<[Point; 4]>,
1143    }
1144
1145    // --- ParsedFontTrait Implementation for ParsedFont ---
1146
1147    impl crate::text3::cache::ShallowClone for ParsedFont {
1148        fn shallow_clone(&self) -> Self {
1149            self.clone() // ParsedFont::clone uses Arc internally, so it's shallow
1150        }
1151    }
1152
1153    impl crate::text3::cache::ParsedFontTrait for ParsedFont {
1154        fn shape_text(
1155            &self,
1156            text: &str,
1157            script: crate::font_traits::Script,
1158            language: crate::font_traits::Language,
1159            direction: crate::font_traits::BidiDirection,
1160            style: &crate::font_traits::StyleProperties,
1161        ) -> Result<Vec<crate::font_traits::Glyph>, crate::font_traits::LayoutError> {
1162            // Call the existing shape_text_for_parsed_font method (defined in default.rs)
1163            crate::text3::default::shape_text_for_parsed_font(
1164                self, text, script, language, direction, style,
1165            )
1166        }
1167
1168        fn get_hash(&self) -> u64 {
1169            self.hash
1170        }
1171
1172        fn get_glyph_size(
1173            &self,
1174            glyph_id: u16,
1175            font_size_px: f32,
1176        ) -> Option<azul_core::geom::LogicalSize> {
1177            self.glyph_records_decoded.get(&glyph_id).map(|record| {
1178                let units_per_em = self.font_metrics.units_per_em as f32;
1179                let scale_factor = if units_per_em > 0.0 {
1180                    font_size_px / units_per_em
1181                } else {
1182                    0.01
1183                };
1184                let bbox = &record.bounding_box;
1185                azul_core::geom::LogicalSize {
1186                    width: (bbox.max_x - bbox.min_x) as f32 * scale_factor,
1187                    height: (bbox.max_y - bbox.min_y) as f32 * scale_factor,
1188                }
1189            })
1190        }
1191
1192        fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
1193            let glyph_id = self.lookup_glyph_index('-' as u32)?;
1194            let advance_units = self.get_horizontal_advance(glyph_id);
1195            let scale_factor = if self.font_metrics.units_per_em > 0 {
1196                font_size / (self.font_metrics.units_per_em as f32)
1197            } else {
1198                return None;
1199            };
1200            let scaled_advance = advance_units as f32 * scale_factor;
1201            Some((glyph_id, scaled_advance))
1202        }
1203
1204        fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
1205            let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
1206            let advance_units = self.get_horizontal_advance(glyph_id);
1207            let scale_factor = if self.font_metrics.units_per_em > 0 {
1208                font_size / (self.font_metrics.units_per_em as f32)
1209            } else {
1210                return None;
1211            };
1212            let scaled_advance = advance_units as f32 * scale_factor;
1213            Some((glyph_id, scaled_advance))
1214        }
1215
1216        fn has_glyph(&self, codepoint: u32) -> bool {
1217            self.lookup_glyph_index(codepoint).is_some()
1218        }
1219
1220        fn get_vertical_metrics(
1221            &self,
1222            glyph_id: u16,
1223        ) -> Option<crate::text3::cache::VerticalMetrics> {
1224            // Default implementation - can be enhanced later
1225            None
1226        }
1227
1228        fn get_font_metrics(&self) -> crate::text3::cache::LayoutFontMetrics {
1229            self.font_metrics.clone()
1230        }
1231
1232        fn num_glyphs(&self) -> u16 {
1233            self.num_glyphs
1234        }
1235    }
1236}