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        subset::{subset as allsorts_subset, whole_font, CmapTarget, SubsetProfile},
125        tables::{
126            cmap::owned::CmapSubtable as OwnedCmapSubtable,
127            glyf::{
128                ComponentOffsets, CompositeGlyph, CompositeGlyphArgument, CompositeGlyphComponent,
129                CompositeGlyphScale, EmptyGlyph, Glyph, Point, SimpleGlyph,
130            },
131            kern::owned::KernTable,
132            FontTableProvider, HheaTable, MaxpTable,
133        },
134        tag,
135    };
136    use azul_core::resources::{
137        GlyphOutline, GlyphOutlineOperation, OutlineCubicTo, OutlineLineTo, OutlineMoveTo,
138        OutlineQuadTo, OwnedGlyphBoundingBox,
139    };
140    use azul_css::props::basic::FontMetrics as CssFontMetrics;
141
142    // Mock font module for testing
143    pub use crate::font::mock::MockFont;
144    use crate::text3::cache::LayoutFontMetrics;
145
146    /// Cached GSUB table for glyph substitution operations.
147    pub type GsubCache = Arc<LayoutCacheData<GSUB>>;
148    /// Cached GPOS table for glyph positioning operations.
149    pub type GposCache = Arc<LayoutCacheData<GPOS>>;
150    /// Glyph outline contours: outer Vec = contours, inner Vec = operations per contour.
151    pub type GlyphOutlineContours = Vec<Vec<GlyphOutlineOperation>>;
152
153    /// Parsed font data with all required tables for text layout and PDF generation.
154    ///
155    /// This struct holds the parsed representation of a TrueType/OpenType font,
156    /// including glyph outlines, metrics, and shaping tables. It's used for:
157    /// - Text layout (via GSUB/GPOS tables)
158    /// - Glyph rendering (via glyf/CFF outlines)
159    /// - PDF font embedding (via font metrics and subsetting)
160    #[derive(Clone)]
161    pub struct ParsedFont {
162        /// Hash of the font bytes for caching and equality checks.
163        pub hash: u64,
164        /// Layout-specific font metrics (ascent, descent, line gap).
165        pub font_metrics: LayoutFontMetrics,
166        /// PDF-specific detailed font metrics from HEAD, HHEA, OS/2 tables.
167        pub pdf_font_metrics: PdfFontMetrics,
168        /// Total number of glyphs in the font (from maxp table).
169        pub num_glyphs: u16,
170        /// Horizontal header table (hhea) containing global horizontal metrics.
171        pub hhea_table: HheaTable,
172        /// Raw horizontal metrics data (hmtx table bytes).
173        pub hmtx_data: Vec<u8>,
174        /// Raw vertical metrics data (vmtx table bytes, if present).
175        pub vmtx_data: Vec<u8>,
176        /// Maximum profile table (maxp) containing glyph count and memory hints.
177        pub maxp_table: MaxpTable,
178        /// Cached GSUB table for glyph substitution (ligatures, alternates).
179        pub gsub_cache: Option<GsubCache>,
180        /// Cached GPOS table for glyph positioning (kerning, mark placement).
181        pub gpos_cache: Option<GposCache>,
182        /// Glyph definition table (GDEF) for glyph classification.
183        pub opt_gdef_table: Option<Arc<GDEFTable>>,
184        /// Legacy kerning table (kern) for fonts without GPOS.
185        pub opt_kern_table: Option<Arc<KernTable>>,
186        /// Decoded glyph records with outlines and metrics, keyed by glyph ID.
187        pub glyph_records_decoded: BTreeMap<u16, OwnedGlyph>,
188        /// Cached width of the space character in font units.
189        pub space_width: Option<usize>,
190        /// Character-to-glyph mapping (cmap subtable).
191        pub cmap_subtable: Option<OwnedCmapSubtable>,
192        /// Mock font data for testing (replaces real font behavior).
193        pub mock: Option<Box<MockFont>>,
194        /// Reverse mapping: glyph_id -> cluster text (handles ligatures like "fi").
195        pub reverse_glyph_cache: std::collections::BTreeMap<u16, String>,
196        /// Original font bytes (needed for subsetting and reconstruction).
197        pub original_bytes: Vec<u8>,
198        /// Font index within collection (0 for single-font files).
199        pub original_index: usize,
200        /// GID to CID mapping for CFF fonts (required for PDF embedding).
201        pub index_to_cid: BTreeMap<u16, u16>,
202        /// Font type (TrueType outlines or OpenType CFF).
203        pub font_type: FontType,
204        /// PostScript font name from the NAME table.
205        pub font_name: Option<String>,
206    }
207
208    /// Distinguishes TrueType fonts from OpenType CFF fonts.
209    ///
210    /// This affects how glyph outlines are extracted and how the font
211    /// is embedded in PDF documents.
212    #[derive(Debug, Clone, PartialEq)]
213    pub enum FontType {
214        /// TrueType font with quadratic Bézier outlines in glyf table.
215        TrueType,
216        /// OpenType font with cubic Bézier outlines in CFF table.
217        /// Contains the serialized CFF data for PDF embedding.
218        OpenTypeCFF(Vec<u8>),
219    }
220
221    /// PDF-specific font metrics from HEAD, HHEA, and OS/2 tables.
222    ///
223    /// These metrics are used for PDF font descriptors and accurate
224    /// text positioning in generated PDF documents.
225    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
226    #[repr(C)]
227    pub struct PdfFontMetrics {
228        // -- HEAD table fields --
229        /// Font units per em-square (typically 1000 or 2048).
230        pub units_per_em: u16,
231        /// Font flags (italic, bold, fixed-pitch, etc.).
232        pub font_flags: u16,
233        /// Minimum x-coordinate across all glyphs.
234        pub x_min: i16,
235        /// Minimum y-coordinate across all glyphs.
236        pub y_min: i16,
237        /// Maximum x-coordinate across all glyphs.
238        pub x_max: i16,
239        /// Maximum y-coordinate across all glyphs.
240        pub y_max: i16,
241
242        // -- HHEA table fields --
243        /// Typographic ascender (distance above baseline).
244        pub ascender: i16,
245        /// Typographic descender (distance below baseline, usually negative).
246        pub descender: i16,
247        /// Recommended line gap between lines of text.
248        pub line_gap: i16,
249        /// Maximum horizontal advance width across all glyphs.
250        pub advance_width_max: u16,
251        /// Caret slope rise for italic angle calculation.
252        pub caret_slope_rise: i16,
253        /// Caret slope run for italic angle calculation.
254        pub caret_slope_run: i16,
255
256        // -- OS/2 table fields (0 if table not present) --
257        /// Average width of lowercase letters.
258        pub x_avg_char_width: i16,
259        /// Visual weight class (100-900, 400=normal, 700=bold).
260        pub us_weight_class: u16,
261        /// Visual width class (1-9, 5=normal).
262        pub us_width_class: u16,
263        /// Thickness of strikeout stroke in font units.
264        pub y_strikeout_size: i16,
265        /// Vertical position of strikeout stroke.
266        pub y_strikeout_position: i16,
267    }
268
269    impl Default for PdfFontMetrics {
270        fn default() -> Self {
271            PdfFontMetrics::zero()
272        }
273    }
274
275    impl PdfFontMetrics {
276        pub const fn zero() -> Self {
277            PdfFontMetrics {
278                units_per_em: 1000,
279                font_flags: 0,
280                x_min: 0,
281                y_min: 0,
282                x_max: 0,
283                y_max: 0,
284                ascender: 0,
285                descender: 0,
286                line_gap: 0,
287                advance_width_max: 0,
288                caret_slope_rise: 0,
289                caret_slope_run: 0,
290                x_avg_char_width: 0,
291                us_weight_class: 0,
292                us_width_class: 0,
293                y_strikeout_size: 0,
294                y_strikeout_position: 0,
295            }
296        }
297    }
298
299    /// Result of font subsetting operation.
300    ///
301    /// Contains the subsetted font bytes and a mapping from original
302    /// glyph IDs to new glyph IDs in the subset.
303    #[derive(Debug, Clone)]
304    pub struct SubsetFont {
305        /// The subsetted font file bytes (smaller than original).
306        pub bytes: Vec<u8>,
307        /// Mapping: original glyph ID -> (new subset glyph ID, source character).
308        pub glyph_mapping: BTreeMap<u16, (u16, char)>,
309    }
310
311    impl SubsetFont {
312        /// Return the changed text so that when rendering with the subset font (instead of the
313        /// original) the renderer will end up at the same glyph IDs as if we used the original text
314        /// on the original font
315        pub fn subset_text(&self, text: &str) -> String {
316            text.chars()
317                .filter_map(|c| {
318                    self.glyph_mapping.values().find_map(|(ngid, ch)| {
319                        if *ch == c {
320                            char::from_u32(*ngid as u32)
321                        } else {
322                            None
323                        }
324                    })
325                })
326                .collect()
327        }
328    }
329
330    impl PartialEq for ParsedFont {
331        fn eq(&self, other: &Self) -> bool {
332            self.hash == other.hash
333        }
334    }
335
336    impl Eq for ParsedFont {}
337
338    const FONT_B64_START: &str = "data:font/ttf;base64,";
339
340    impl serde::Serialize for ParsedFont {
341        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
342            use base64::Engine;
343            let s = format!(
344                "{FONT_B64_START}{}",
345                base64::prelude::BASE64_STANDARD.encode(&self.to_bytes(None).unwrap_or_default())
346            );
347            s.serialize(serializer)
348        }
349    }
350
351    impl<'de> serde::Deserialize<'de> for ParsedFont {
352        fn deserialize<D: serde::Deserializer<'de>>(
353            deserializer: D,
354        ) -> Result<ParsedFont, D::Error> {
355            use base64::Engine;
356            let s = String::deserialize(deserializer)?;
357            let b64 = if s.starts_with(FONT_B64_START) {
358                let b = &s[FONT_B64_START.len()..];
359                base64::prelude::BASE64_STANDARD.decode(&b).ok()
360            } else {
361                None
362            };
363
364            let mut warnings = Vec::new();
365            ParsedFont::from_bytes(&b64.unwrap_or_default(), 0, &mut warnings).ok_or_else(|| {
366                serde::de::Error::custom(format!("Font deserialization error: {warnings:?}"))
367            })
368        }
369    }
370
371    impl fmt::Debug for ParsedFont {
372        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373            f.debug_struct("ParsedFont")
374                .field("hash", &self.hash)
375                .field("font_metrics", &self.font_metrics)
376                .field("num_glyphs", &self.num_glyphs)
377                .field("hhea_table", &self.hhea_table)
378                .field(
379                    "hmtx_data",
380                    &format_args!("<{} bytes>", self.hmtx_data.len()),
381                )
382                .field("maxp_table", &self.maxp_table)
383                .field(
384                    "glyph_records_decoded",
385                    &format_args!("{} entries", self.glyph_records_decoded.len()),
386                )
387                .field("space_width", &self.space_width)
388                .field("cmap_subtable", &self.cmap_subtable)
389                .finish()
390        }
391    }
392
393    /// Warning or error message generated during font parsing.
394    #[derive(Debug, Clone, PartialEq, Eq)]
395    pub struct FontParseWarning {
396        /// Severity level of this warning.
397        pub severity: FontParseWarningSeverity,
398        /// Human-readable description of the issue.
399        pub message: String,
400    }
401
402    /// Severity level for font parsing warnings.
403    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
404    pub enum FontParseWarningSeverity {
405        /// Informational message (not an error).
406        Info,
407        /// Warning that may affect font rendering.
408        Warning,
409        /// Error that prevents proper font usage.
410        Error,
411    }
412
413    impl FontParseWarning {
414        /// Creates an info-level message.
415        pub fn info(message: String) -> Self {
416            Self {
417                severity: FontParseWarningSeverity::Info,
418                message,
419            }
420        }
421
422        /// Creates a warning-level message.
423        pub fn warning(message: String) -> Self {
424            Self {
425                severity: FontParseWarningSeverity::Warning,
426                message,
427            }
428        }
429
430        /// Creates an error-level message.
431        pub fn error(message: String) -> Self {
432            Self {
433                severity: FontParseWarningSeverity::Error,
434                message,
435            }
436        }
437    }
438
439    impl ParsedFont {
440        /// Parse a font from bytes using allsorts
441        ///
442        /// # Arguments
443        /// * `font_bytes` - The font file data
444        /// * `font_index` - Index of the font in a font collection (0 for single fonts)
445        /// * `warnings` - Optional vector to collect parsing warnings
446        ///
447        /// # Returns
448        /// `Some(ParsedFont)` if parsing succeeds, `None` otherwise
449        ///
450        /// Note: Outlines are always parsed (parse_outlines = true)
451        pub fn from_bytes(
452            font_bytes: &[u8],
453            font_index: usize,
454            warnings: &mut Vec<FontParseWarning>,
455        ) -> Option<Self> {
456            use std::{
457                collections::hash_map::DefaultHasher,
458                hash::{Hash, Hasher},
459            };
460
461            use allsorts::{
462                binary::read::ReadScope,
463                font_data::FontData,
464                tables::{
465                    cmap::{owned::CmapSubtable as OwnedCmapSubtable, CmapSubtable},
466                    glyf::{GlyfRecord, GlyfTable},
467                    loca::{LocaOffsets, LocaTable},
468                    FontTableProvider, HeadTable, HheaTable, MaxpTable,
469                },
470                tag,
471            };
472
473            let scope = ReadScope::new(font_bytes);
474            let font_file = match scope.read::<FontData<'_>>() {
475                Ok(ff) => {
476                    warnings.push(FontParseWarning::info(
477                        "Successfully read font data".to_string(),
478                    ));
479                    ff
480                }
481                Err(e) => {
482                    warnings.push(FontParseWarning::error(format!(
483                        "Failed to read font data: {}",
484                        e
485                    )));
486                    return None;
487                }
488            };
489            let provider = match font_file.table_provider(font_index) {
490                Ok(p) => {
491                    warnings.push(FontParseWarning::info(format!(
492                        "Successfully loaded font at index {}",
493                        font_index
494                    )));
495                    p
496                }
497                Err(e) => {
498                    warnings.push(FontParseWarning::error(format!(
499                        "Failed to get table provider for font index {}: {}",
500                        font_index, e
501                    )));
502                    return None;
503                }
504            };
505
506            // Extract font name from NAME table early (before provider is moved)
507            let font_name = provider.table_data(tag::NAME).ok().and_then(|name_data| {
508                ReadScope::new(&name_data?)
509                    .read::<allsorts::tables::NameTable>()
510                    .ok()
511                    .and_then(|name_table| {
512                        name_table.string_for_id(allsorts::tables::NameTable::POSTSCRIPT_NAME)
513                    })
514            });
515
516            let head_table = provider
517                .table_data(tag::HEAD)
518                .ok()
519                .and_then(|head_data| ReadScope::new(&head_data?).read::<HeadTable>().ok())?;
520
521            let maxp_table = provider
522                .table_data(tag::MAXP)
523                .ok()
524                .and_then(|maxp_data| ReadScope::new(&maxp_data?).read::<MaxpTable>().ok())
525                .unwrap_or(MaxpTable {
526                    num_glyphs: 0,
527                    version1_sub_table: None,
528                });
529
530            let index_to_loc = head_table.index_to_loc_format;
531            let num_glyphs = maxp_table.num_glyphs as usize;
532
533            let loca_table = provider.table_data(tag::LOCA).ok();
534            let loca_table = loca_table
535                .as_ref()
536                .and_then(|loca_data| {
537                    ReadScope::new(&loca_data.as_ref()?)
538                        .read_dep::<LocaTable<'_>>((
539                            num_glyphs.min(u16::MAX as usize) as u16,
540                            index_to_loc,
541                        ))
542                        .ok()
543                })
544                .unwrap_or(LocaTable {
545                    offsets: LocaOffsets::Long(allsorts::binary::read::ReadArray::empty()),
546                });
547
548            let glyf_table = provider.table_data(tag::GLYF).ok();
549            let mut glyf_table = glyf_table
550                .as_ref()
551                .and_then(|glyf_data| {
552                    ReadScope::new(&glyf_data.as_ref()?)
553                        .read_dep::<GlyfTable<'_>>(&loca_table)
554                        .ok()
555                })
556                .unwrap_or(GlyfTable::new(Vec::new()).unwrap());
557
558            let hmtx_data = provider
559                .table_data(tag::HMTX)
560                .ok()
561                .and_then(|s| Some(s?.to_vec()))
562                .unwrap_or_default();
563
564            let vmtx_data = provider
565                .table_data(tag::VMTX)
566                .ok()
567                .and_then(|s| Some(s?.to_vec()))
568                .unwrap_or_default();
569
570            let hhea_table = provider
571                .table_data(tag::HHEA)
572                .ok()
573                .and_then(|hhea_data| ReadScope::new(&hhea_data?).read::<HheaTable>().ok())
574                .unwrap_or(unsafe { std::mem::zeroed() });
575
576            // Build layout-specific font metrics
577            let font_metrics = LayoutFontMetrics {
578                units_per_em: if head_table.units_per_em == 0 {
579                    1000
580                } else {
581                    head_table.units_per_em
582                },
583                ascent: hhea_table.ascender as f32,
584                descent: hhea_table.descender as f32,
585                line_gap: hhea_table.line_gap as f32,
586            };
587
588            // Build PDF-specific font metrics
589            let pdf_font_metrics =
590                Self::parse_pdf_font_metrics(font_bytes, font_index, &head_table, &hhea_table);
591
592            // Parse glyph outlines and metrics (always enabled for PDF generation)
593            // For CFF fonts (no glyf table), we fall back to hmtx-only metrics
594            let glyf_records_count = glyf_table.records().len();
595            let use_glyf_parsing = glyf_records_count > 0;
596
597            warnings.push(FontParseWarning::info(format!(
598                "Font has {} glyf records, {} total glyphs, use_glyf_parsing={}",
599                glyf_records_count, num_glyphs, use_glyf_parsing
600            )));
601
602            let glyph_records_decoded = if use_glyf_parsing {
603                warnings.push(FontParseWarning::info(
604                    "Parsing glyph outlines from glyf table".to_string(),
605                ));
606                // Full parsing: outlines + metrics from TrueType glyf table
607                // CRITICAL: Always call .parse() first to convert Present -> Parsed!
608                glyf_table
609                    .records_mut()
610                    .into_iter()
611                    .enumerate()
612                    .filter_map(|(glyph_index, glyph_record)| {
613                        if glyph_index > (u16::MAX as usize) {
614                            return None;
615                        }
616
617                        // ALWAYS parse the glyph record first!
618                        if let Err(_e) = glyph_record.parse() {
619                            // If parsing fails, we can still try to get the advance width
620                            let glyph_index = glyph_index as u16;
621                            let horz_advance = allsorts::glyph_info::advance(
622                                &maxp_table,
623                                &hhea_table,
624                                &hmtx_data,
625                                glyph_index,
626                            )
627                            .unwrap_or_default();
628
629                            // Return minimal glyph with just advance
630                            return Some((
631                                glyph_index,
632                                OwnedGlyph {
633                                    horz_advance,
634                                    bounding_box: OwnedGlyphBoundingBox {
635                                        min_x: 0,
636                                        min_y: 0,
637                                        max_x: horz_advance as i16,
638                                        max_y: 0,
639                                    },
640                                    outline: Vec::new(),
641                                    unresolved_composite: Vec::new(),
642                                    phantom_points: None,
643                                },
644                            ));
645                        }
646
647                        let glyph_index = glyph_index as u16;
648                        let horz_advance = allsorts::glyph_info::advance(
649                            &maxp_table,
650                            &hhea_table,
651                            &hmtx_data,
652                            glyph_index,
653                        )
654                        .unwrap_or_default();
655
656                        // After parse(), record should be Parsed, not Present
657                        match glyph_record {
658                            GlyfRecord::Present { .. } => {
659                                // This shouldn't happen after parse(), but handle it anyway
660                                Some((
661                                    glyph_index,
662                                    OwnedGlyph {
663                                        horz_advance,
664                                        bounding_box: OwnedGlyphBoundingBox {
665                                            min_x: 0,
666                                            min_y: 0,
667                                            max_x: horz_advance as i16,
668                                            max_y: 0,
669                                        },
670                                        outline: Vec::new(),
671                                        unresolved_composite: Vec::new(),
672                                        phantom_points: None,
673                                    },
674                                ))
675                            }
676                            GlyfRecord::Parsed(g) => OwnedGlyph::from_glyph_data(g, horz_advance)
677                                .map(|g| (glyph_index, g)),
678                        }
679                    })
680                    .collect::<Vec<_>>()
681                    .into_iter()
682                    .collect::<BTreeMap<_, _>>()
683            } else {
684                // CFF fonts or fonts without glyf table: Parse metrics only from hmtx
685                // This creates OwnedGlyph records with advance width but no outlines
686                warnings.push(FontParseWarning::info(format!(
687                    "Using hmtx-only fallback for {} glyphs (CFF font or no glyf table)",
688                    num_glyphs
689                )));
690                (0..num_glyphs as usize)
691                    .filter_map(|glyph_index| {
692                        if glyph_index > u16::MAX as usize {
693                            return None;
694                        }
695                        let glyph_index_u16 = glyph_index as u16;
696                        let horz_advance = allsorts::glyph_info::advance(
697                            &maxp_table,
698                            &hhea_table,
699                            &hmtx_data,
700                            glyph_index_u16,
701                        )
702                        .unwrap_or_default();
703
704                        Some((
705                            glyph_index_u16,
706                            OwnedGlyph {
707                                horz_advance,
708                                bounding_box: OwnedGlyphBoundingBox {
709                                    min_x: 0,
710                                    min_y: 0,
711                                    max_x: horz_advance as i16,
712                                    max_y: 0,
713                                },
714                                outline: Vec::new(), // No outline data
715                                unresolved_composite: Vec::new(),
716                                phantom_points: None,
717                            },
718                        ))
719                    })
720                    .collect::<BTreeMap<_, _>>()
721            };
722
723            // Resolve composite glyphs in multiple passes
724            let mut glyph_records_decoded = glyph_records_decoded;
725            for _ in 0..6 {
726                let composite_glyphs_to_resolve = glyph_records_decoded
727                    .iter()
728                    .filter(|s| !s.1.unresolved_composite.is_empty())
729                    .map(|(k, v)| (*k, v.clone()))
730                    .collect::<Vec<_>>();
731
732                if composite_glyphs_to_resolve.is_empty() {
733                    break;
734                }
735
736                for (k, mut v) in composite_glyphs_to_resolve {
737                    resolved_glyph_components(&mut v, &glyph_records_decoded);
738                    glyph_records_decoded.insert(k, v);
739                }
740            }
741
742            let mut font_data_impl = allsorts::font::Font::new(provider).ok()?;
743
744            // Required for font layout: gsub_cache, gpos_cache and gdef_table
745            let gsub_cache = font_data_impl.gsub_cache().ok().and_then(|s| s);
746            let gpos_cache = font_data_impl.gpos_cache().ok().and_then(|s| s);
747            let opt_gdef_table = font_data_impl.gdef_table().ok().and_then(|o| o);
748            let num_glyphs = font_data_impl.num_glyphs();
749
750            let opt_kern_table = font_data_impl
751                .kern_table()
752                .ok()
753                .and_then(|s| Some(s?.to_owned()));
754
755            let cmap_data = font_data_impl.cmap_subtable_data();
756            let cmap_subtable = ReadScope::new(cmap_data);
757            let cmap_subtable = cmap_subtable
758                .read::<CmapSubtable<'_>>()
759                .ok()
760                .and_then(|s| s.to_owned());
761
762            // Calculate hash of font data
763            let mut hasher = DefaultHasher::new();
764            font_bytes.hash(&mut hasher);
765            font_index.hash(&mut hasher);
766            let hash = hasher.finish();
767
768            let mut font = ParsedFont {
769                hash,
770                font_metrics,
771                pdf_font_metrics,
772                num_glyphs,
773                hhea_table,
774                hmtx_data,
775                vmtx_data,
776                maxp_table,
777                gsub_cache,
778                gpos_cache,
779                opt_gdef_table,
780                opt_kern_table,
781                cmap_subtable,
782                glyph_records_decoded,
783                space_width: None,
784                mock: None,
785                reverse_glyph_cache: BTreeMap::new(),
786                original_bytes: font_bytes.to_vec(),
787                original_index: font_index,
788                index_to_cid: BTreeMap::new(), // Will be filled for CFF fonts
789                font_type: FontType::TrueType, // Default, will be updated if CFF
790                font_name,
791            };
792
793            // Calculate space width
794            let space_width = font.get_space_width_internal();
795
796            // Ensure space glyph is in glyph_records_decoded
797            // Space glyphs often don't have outlines, so they may not be loaded by default
798            let _ = (|| {
799                let space_gid = font.lookup_glyph_index(' ' as u32)?;
800                if font.glyph_records_decoded.contains_key(&space_gid) {
801                    return None; // Already exists
802                }
803                let space_width_val = space_width?;
804                let space_record = OwnedGlyph {
805                    bounding_box: OwnedGlyphBoundingBox {
806                        max_x: 0,
807                        max_y: 0,
808                        min_x: 0,
809                        min_y: 0,
810                    },
811                    horz_advance: space_width_val as u16,
812                    outline: Vec::new(),
813                    unresolved_composite: Vec::new(),
814                    phantom_points: None,
815                };
816                font.glyph_records_decoded.insert(space_gid, space_record);
817                Some(())
818            })();
819
820            font.space_width = space_width;
821
822            Some(font)
823        }
824
825        /// Parse PDF-specific font metrics from HEAD, HHEA, and OS/2 tables
826        fn parse_pdf_font_metrics(
827            font_bytes: &[u8],
828            font_index: usize,
829            head_table: &allsorts::tables::HeadTable,
830            hhea_table: &allsorts::tables::HheaTable,
831        ) -> PdfFontMetrics {
832            use allsorts::{
833                binary::read::ReadScope,
834                font_data::FontData,
835                tables::{os2::Os2, FontTableProvider},
836                tag,
837            };
838
839            let scope = ReadScope::new(font_bytes);
840            let font_file = scope.read::<FontData<'_>>().ok();
841            let provider = font_file
842                .as_ref()
843                .and_then(|ff| ff.table_provider(font_index).ok());
844
845            let os2_table = provider
846                .as_ref()
847                .and_then(|p| p.table_data(tag::OS_2).ok())
848                .and_then(|os2_data| {
849                    let data = os2_data?;
850                    let scope = ReadScope::new(&data);
851                    scope.read_dep::<Os2>(data.len()).ok()
852                });
853
854            // Base metrics from HEAD and HHEA (always present)
855            let base = PdfFontMetrics {
856                units_per_em: head_table.units_per_em,
857                font_flags: head_table.flags,
858                x_min: head_table.x_min,
859                y_min: head_table.y_min,
860                x_max: head_table.x_max,
861                y_max: head_table.y_max,
862                ascender: hhea_table.ascender,
863                descender: hhea_table.descender,
864                line_gap: hhea_table.line_gap,
865                advance_width_max: hhea_table.advance_width_max,
866                caret_slope_rise: hhea_table.caret_slope_rise,
867                caret_slope_run: hhea_table.caret_slope_run,
868                ..PdfFontMetrics::zero()
869            };
870
871            // Add OS/2 metrics if available
872            os2_table
873                .map(|os2| PdfFontMetrics {
874                    x_avg_char_width: os2.x_avg_char_width,
875                    us_weight_class: os2.us_weight_class,
876                    us_width_class: os2.us_width_class,
877                    y_strikeout_size: os2.y_strikeout_size,
878                    y_strikeout_position: os2.y_strikeout_position,
879                    ..base
880                })
881                .unwrap_or(base)
882        }
883
884        /// Returns the width of the space character in font units.
885        ///
886        /// This is used internally for text layout calculations.
887        /// Returns `None` if the font has no space glyph or its width cannot be determined.
888        fn get_space_width_internal(&self) -> Option<usize> {
889            if let Some(mock) = self.mock.as_ref() {
890                return mock.space_width;
891            }
892            let glyph_index = self.lookup_glyph_index(' ' as u32)?;
893
894            allsorts::glyph_info::advance(
895                &self.maxp_table,
896                &self.hhea_table,
897                &self.hmtx_data,
898                glyph_index,
899            )
900            .ok()
901            .map(|s| s as usize)
902        }
903
904        /// Look up the glyph index for a Unicode codepoint
905        pub fn lookup_glyph_index(&self, codepoint: u32) -> Option<u16> {
906            let cmap = self.cmap_subtable.as_ref()?;
907            cmap.map_glyph(codepoint).ok().flatten()
908        }
909
910        /// Get the horizontal advance width for a glyph in font units
911        pub fn get_horizontal_advance(&self, glyph_index: u16) -> u16 {
912            if let Some(mock) = self.mock.as_ref() {
913                return mock.glyph_advances.get(&glyph_index).copied().unwrap_or(0);
914            }
915            self.glyph_records_decoded
916                .get(&glyph_index)
917                .map(|gi| gi.horz_advance)
918                .unwrap_or_default()
919        }
920
921        /// Get the number of glyphs in this font
922        pub fn num_glyphs(&self) -> u16 {
923            self.num_glyphs
924        }
925
926        /// Check if this font has a glyph for the given codepoint
927        pub fn has_glyph(&self, codepoint: u32) -> bool {
928            self.lookup_glyph_index(codepoint).is_some()
929        }
930
931        /// Get vertical metrics for a glyph (for vertical text layout).
932        ///
933        /// Currently always returns `None` because vertical layout tables
934        /// (vhea, vmtx) are not parsed. Vertical text layout is not yet supported.
935        pub fn get_vertical_metrics(
936            &self,
937            _glyph_id: u16,
938        ) -> Option<crate::text3::cache::VerticalMetrics> {
939            // Vertical text layout requires parsing vhea and vmtx tables
940            None
941        }
942
943        /// Get layout-specific font metrics
944        pub fn get_font_metrics(&self) -> crate::text3::cache::LayoutFontMetrics {
945            // Ensure descent is positive (OpenType may have negative descent)
946            let descent = if self.font_metrics.descent > 0.0 {
947                self.font_metrics.descent
948            } else {
949                -self.font_metrics.descent
950            };
951
952            crate::text3::cache::LayoutFontMetrics {
953                ascent: self.font_metrics.ascent,
954                descent,
955                line_gap: self.font_metrics.line_gap,
956                units_per_em: self.font_metrics.units_per_em,
957            }
958        }
959
960        /// Convert the ParsedFont back to bytes using allsorts::whole_font
961        /// This reconstructs the entire font from the parsed data
962        ///
963        /// # Arguments
964        /// * `tags` - Optional list of specific table tags to include (None = all tables)
965        pub fn to_bytes(&self, tags: Option<&[u32]>) -> Result<Vec<u8>, String> {
966            let scope = ReadScope::new(&self.original_bytes);
967            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
968            let provider = font_file
969                .table_provider(self.original_index)
970                .map_err(|e| e.to_string())?;
971
972            let tags_to_use = tags.unwrap_or(&[
973                tag::CMAP,
974                tag::HEAD,
975                tag::HHEA,
976                tag::HMTX,
977                tag::MAXP,
978                tag::NAME,
979                tag::OS_2,
980                tag::POST,
981                tag::GLYF,
982                tag::LOCA,
983            ]);
984
985            whole_font(&provider, tags_to_use).map_err(|e| e.to_string())
986        }
987
988        /// Create a subset font containing only the specified glyph IDs
989        /// Returns the subset font bytes and a mapping from old to new glyph IDs
990        ///
991        /// # Arguments
992        /// * `glyph_ids` - The glyph IDs to include in the subset (glyph 0/.notdef is always
993        ///   included)
994        /// * `cmap_target` - Target cmap format (Unicode for web, MacRoman for compatibility)
995        ///
996        /// # Returns
997        /// A tuple of (subset_font_bytes, glyph_mapping) where glyph_mapping maps
998        /// original_glyph_id -> (new_glyph_id, original_char)
999        pub fn subset(
1000            &self,
1001            glyph_ids: &[(u16, char)],
1002            cmap_target: CmapTarget,
1003        ) -> Result<(Vec<u8>, BTreeMap<u16, (u16, char)>), String> {
1004            let scope = ReadScope::new(&self.original_bytes);
1005            let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
1006            let provider = font_file
1007                .table_provider(self.original_index)
1008                .map_err(|e| e.to_string())?;
1009
1010            // Build glyph mapping: original_id -> (new_id, char)
1011            let glyph_mapping: BTreeMap<u16, (u16, char)> = glyph_ids
1012                .iter()
1013                .enumerate()
1014                .map(|(new_id, &(original_id, ch))| (original_id, (new_id as u16, ch)))
1015                .collect();
1016
1017            // Extract just the glyph IDs for subsetting
1018            let ids: Vec<u16> = glyph_ids.iter().map(|(id, _)| *id).collect();
1019
1020            // Use PDF profile for embedding fonts in PDFs
1021            let font_bytes = allsorts_subset(&provider, &ids, &SubsetProfile::Pdf, cmap_target)
1022                .map_err(|e| format!("Subset error: {:?}", e))?;
1023
1024            Ok((font_bytes, glyph_mapping))
1025        }
1026
1027        /// Get the width of a glyph in font units (internal, unscaled)
1028        pub fn get_glyph_width_internal(&self, glyph_index: u16) -> Option<usize> {
1029            allsorts::glyph_info::advance(
1030                &self.maxp_table,
1031                &self.hhea_table,
1032                &self.hmtx_data,
1033                glyph_index,
1034            )
1035            .ok()
1036            .map(|s| s as usize)
1037        }
1038
1039        /// Get the width of the space character (unscaled font units)
1040        #[inline]
1041        pub const fn get_space_width(&self) -> Option<usize> {
1042            self.space_width
1043        }
1044
1045        /// Add glyph-to-text mapping to reverse cache
1046        /// This should be called during text shaping when we know both the source text and
1047        /// resulting glyphs
1048        pub fn cache_glyph_mapping(&mut self, glyph_id: u16, cluster_text: &str) {
1049            self.reverse_glyph_cache
1050                .insert(glyph_id, cluster_text.to_string());
1051        }
1052
1053        /// Get the cluster text that produced a specific glyph ID
1054        /// Returns the original text that was shaped into this glyph (handles ligatures correctly)
1055        pub fn get_glyph_cluster_text(&self, glyph_id: u16) -> Option<&str> {
1056            self.reverse_glyph_cache.get(&glyph_id).map(|s| s.as_str())
1057        }
1058
1059        /// Get the first character from the cluster text for a glyph ID
1060        /// This is useful for PDF ToUnicode CMap generation which requires single character
1061        /// mappings
1062        pub fn get_glyph_primary_char(&self, glyph_id: u16) -> Option<char> {
1063            self.reverse_glyph_cache
1064                .get(&glyph_id)
1065                .and_then(|text| text.chars().next())
1066        }
1067
1068        /// Clear the reverse glyph cache (useful for memory management)
1069        pub fn clear_glyph_cache(&mut self) {
1070            self.reverse_glyph_cache.clear();
1071        }
1072
1073        /// Get the bounding box size of a glyph (unscaled units) - for PDF
1074        /// Returns (width, height) in font units
1075        pub fn get_glyph_bbox_size(&self, glyph_index: u16) -> Option<(i32, i32)> {
1076            let g = self.glyph_records_decoded.get(&glyph_index)?;
1077            let glyph_width = g.horz_advance as i32;
1078            let glyph_height = g.bounding_box.max_y as i32 - g.bounding_box.min_y as i32;
1079            Some((glyph_width, glyph_height))
1080        }
1081    }
1082
1083    #[derive(Debug, Clone, PartialEq, PartialOrd)]
1084    struct GlyphOutlineBuilder {
1085        operations: Vec<GlyphOutlineOperation>,
1086    }
1087
1088    impl Default for GlyphOutlineBuilder {
1089        fn default() -> Self {
1090            GlyphOutlineBuilder {
1091                operations: Vec::new(),
1092            }
1093        }
1094    }
1095
1096    #[derive(Debug, Clone)]
1097    pub struct OwnedGlyph {
1098        pub bounding_box: OwnedGlyphBoundingBox,
1099        pub horz_advance: u16,
1100        pub outline: Vec<GlyphOutline>,
1101        // unresolved outlines, later to be added
1102        pub unresolved_composite: Vec<CompositeGlyphComponent>,
1103        pub phantom_points: Option<[Point; 4]>,
1104    }
1105
1106    impl OwnedGlyph {
1107        pub fn from_glyph_data(glyph: &Glyph, horz_advance: u16) -> Option<Self> {
1108            let bbox = glyph.bounding_box()?;
1109            Some(Self {
1110                bounding_box: OwnedGlyphBoundingBox {
1111                    max_x: bbox.x_max,
1112                    max_y: bbox.y_max,
1113                    min_x: bbox.x_min,
1114                    min_y: bbox.y_min,
1115                },
1116                horz_advance,
1117                phantom_points: glyph.phantom_points(),
1118                unresolved_composite: match glyph {
1119                    Glyph::Empty(_) => Vec::new(),
1120                    Glyph::Composite(c) => c.glyphs.clone(),
1121                    Glyph::Simple(s) => Vec::new(),
1122                },
1123                outline: translate_glyph_outline(glyph)
1124                    .unwrap_or_default()
1125                    .into_iter()
1126                    .map(|ol| GlyphOutline {
1127                        operations: ol.into(),
1128                    })
1129                    .collect(),
1130            })
1131        }
1132    }
1133
1134    /// Converts a glyph to its outline contours.
1135    fn translate_glyph_outline(glyph: &Glyph) -> Option<GlyphOutlineContours> {
1136        match glyph {
1137            Glyph::Empty(e) => translate_empty_glyph(e),
1138            Glyph::Simple(sg) => translate_simple_glyph(sg),
1139            Glyph::Composite(cg) => translate_composite_glyph(cg),
1140        }
1141    }
1142
1143    /// Translates an empty glyph (uses phantom points for bounds).
1144    fn translate_empty_glyph(glyph: &EmptyGlyph) -> Option<GlyphOutlineContours> {
1145        let f = glyph.phantom_points?;
1146        Some(vec![vec![
1147            GlyphOutlineOperation::MoveTo(OutlineMoveTo {
1148                x: f[0].0,
1149                y: f[0].1,
1150            }),
1151            GlyphOutlineOperation::LineTo(OutlineLineTo {
1152                x: f[1].0,
1153                y: f[1].1,
1154            }),
1155            GlyphOutlineOperation::LineTo(OutlineLineTo {
1156                x: f[2].0,
1157                y: f[2].1,
1158            }),
1159            GlyphOutlineOperation::LineTo(OutlineLineTo {
1160                x: f[3].0,
1161                y: f[3].1,
1162            }),
1163            GlyphOutlineOperation::ClosePath,
1164        ]])
1165    }
1166
1167    /// Translates a simple glyph (TrueType outlines with quadratic curves).
1168    fn translate_simple_glyph(glyph: &SimpleGlyph) -> Option<GlyphOutlineContours> {
1169        let mut outlines = Vec::new();
1170
1171        // Process each contour
1172        for contour in glyph.contours() {
1173            let mut operations = Vec::new();
1174            let contour_len = contour.len();
1175
1176            if contour_len == 0 {
1177                continue;
1178            }
1179
1180            // Find first on-curve point (or use first point if none exist)
1181            let first_on_curve_idx = contour
1182                .iter()
1183                .position(|(flag, _)| flag.is_on_curve())
1184                .unwrap_or(0);
1185
1186            let (first_flag, first_point) = contour[first_on_curve_idx];
1187
1188            // Handle special case: all points are off-curve
1189            if !first_flag.is_on_curve() {
1190                // Create an implicit on-curve point between last and first
1191                let last_idx = contour_len - 1;
1192                let (_, last_point) = contour[last_idx];
1193                let implicit_x = (last_point.0 + first_point.0) / 2;
1194                let implicit_y = (last_point.1 + first_point.1) / 2;
1195                operations.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo {
1196                    x: implicit_x,
1197                    y: implicit_y,
1198                }));
1199            } else {
1200                operations.push(GlyphOutlineOperation::MoveTo(OutlineMoveTo {
1201                    x: first_point.0,
1202                    y: first_point.1,
1203                }));
1204            }
1205
1206            // Process remaining points
1207            let mut i = 0;
1208            while i < contour_len {
1209                let curr_idx = (first_on_curve_idx + 1 + i) % contour_len;
1210                let (curr_flag, curr_point) = contour[curr_idx];
1211                let next_idx = (curr_idx + 1) % contour_len;
1212                let (next_flag, next_point) = contour[next_idx];
1213
1214                if curr_flag.is_on_curve() {
1215                    // Current point is on-curve, add LineTo
1216                    operations.push(GlyphOutlineOperation::LineTo(OutlineLineTo {
1217                        x: curr_point.0,
1218                        y: curr_point.1,
1219                    }));
1220                    i += 1;
1221                } else if next_flag.is_on_curve() {
1222                    // Current off-curve, next on-curve: QuadraticCurveTo
1223                    operations.push(GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
1224                        ctrl_1_x: curr_point.0,
1225                        ctrl_1_y: curr_point.1,
1226                        end_x: next_point.0,
1227                        end_y: next_point.1,
1228                    }));
1229                    i += 2; // Skip both points
1230                } else {
1231                    // Both off-curve, create implicit on-curve point
1232                    let implicit_x = (curr_point.0 + next_point.0) / 2;
1233                    let implicit_y = (curr_point.1 + next_point.1) / 2;
1234
1235                    operations.push(GlyphOutlineOperation::QuadraticCurveTo(OutlineQuadTo {
1236                        ctrl_1_x: curr_point.0,
1237                        ctrl_1_y: curr_point.1,
1238                        end_x: implicit_x,
1239                        end_y: implicit_y,
1240                    }));
1241                    i += 1; // Only advance by one point
1242                }
1243            }
1244
1245            // Close the path
1246            operations.push(GlyphOutlineOperation::ClosePath);
1247            outlines.push(operations);
1248        }
1249
1250        Some(outlines)
1251    }
1252
1253    /// Translates a composite glyph (placeholder, resolved in second pass).
1254    fn translate_composite_glyph(glyph: &CompositeGlyph) -> Option<GlyphOutlineContours> {
1255        // Composite glyphs will be resolved in a second pass
1256        // Return a placeholder based on bounding box for now
1257        let bbox = glyph.bounding_box;
1258        Some(vec![vec![
1259            GlyphOutlineOperation::MoveTo(OutlineMoveTo {
1260                x: bbox.x_min,
1261                y: bbox.y_min,
1262            }),
1263            GlyphOutlineOperation::LineTo(OutlineLineTo {
1264                x: bbox.x_max,
1265                y: bbox.y_min,
1266            }),
1267            GlyphOutlineOperation::LineTo(OutlineLineTo {
1268                x: bbox.x_max,
1269                y: bbox.y_max,
1270            }),
1271            GlyphOutlineOperation::LineTo(OutlineLineTo {
1272                x: bbox.x_min,
1273                y: bbox.y_max,
1274            }),
1275            GlyphOutlineOperation::ClosePath,
1276        ]])
1277    }
1278
1279    // Additional function to resolve composite glyphs in a second pass
1280    pub fn resolved_glyph_components(og: &mut OwnedGlyph, all_glyphs: &BTreeMap<u16, OwnedGlyph>) {
1281        // TODO: does not respect attachment points or anything like this
1282        // only checks whether we can resolve the glyph from the map
1283        let mut unresolved_composites = Vec::new();
1284        for i in og.unresolved_composite.iter() {
1285            let owned_glyph = match all_glyphs.get(&i.glyph_index) {
1286                Some(s) => s,
1287                None => {
1288                    unresolved_composites.push(i.clone());
1289                    continue;
1290                }
1291            };
1292            og.outline.extend_from_slice(&owned_glyph.outline);
1293        }
1294
1295        og.unresolved_composite = unresolved_composites;
1296    }
1297
1298    fn transform_component_outlines(
1299        outlines: &mut Vec<Vec<GlyphOutlineOperation>>,
1300        scale: Option<CompositeGlyphScale>,
1301        arg1: CompositeGlyphArgument,
1302        arg2: CompositeGlyphArgument,
1303        offset_type: ComponentOffsets,
1304    ) {
1305        // Extract offset values
1306        let (offset_x, offset_y) = match (arg1, arg2) {
1307            (CompositeGlyphArgument::I16(x), CompositeGlyphArgument::I16(y)) => (x, y),
1308            (CompositeGlyphArgument::U16(x), CompositeGlyphArgument::U16(y)) => {
1309                (x as i16, y as i16)
1310            }
1311            (CompositeGlyphArgument::I8(x), CompositeGlyphArgument::I8(y)) => {
1312                (i16::from(x), i16::from(y))
1313            }
1314            (CompositeGlyphArgument::U8(x), CompositeGlyphArgument::U8(y)) => {
1315                (i16::from(x), i16::from(y))
1316            }
1317            _ => (0, 0), // Mismatched types, use default
1318        };
1319
1320        // Apply transformation to each outline
1321        for outline in outlines {
1322            for op in outline.as_mut_slice() {
1323                match op {
1324                    GlyphOutlineOperation::MoveTo(point) => {
1325                        transform_point(point, offset_x, offset_y, scale, offset_type);
1326                    }
1327                    GlyphOutlineOperation::LineTo(point) => {
1328                        transform_point_lineto(point, offset_x, offset_y, scale, offset_type);
1329                    }
1330                    GlyphOutlineOperation::QuadraticCurveTo(curve) => {
1331                        transform_quad_point(curve, offset_x, offset_y, scale, offset_type);
1332                    }
1333                    GlyphOutlineOperation::CubicCurveTo(curve) => {
1334                        transform_cubic_point(curve, offset_x, offset_y, scale, offset_type);
1335                    }
1336                    GlyphOutlineOperation::ClosePath => {}
1337                }
1338            }
1339        }
1340    }
1341
1342    fn transform_point(
1343        point: &mut OutlineMoveTo,
1344        offset_x: i16,
1345        offset_y: i16,
1346        scale: Option<CompositeGlyphScale>,
1347        offset_type: ComponentOffsets,
1348    ) {
1349        // Apply scale if present
1350        if let Some(scale_factor) = scale {
1351            match scale_factor {
1352                CompositeGlyphScale::Scale(s) => {
1353                    let scale = f32::from(s);
1354                    point.x = (point.x as f32 * scale) as i16;
1355                    point.y = (point.y as f32 * scale) as i16;
1356                }
1357                CompositeGlyphScale::XY { x_scale, y_scale } => {
1358                    point.x = (point.x as f32 * f32::from(x_scale)) as i16;
1359                    point.y = (point.y as f32 * f32::from(y_scale)) as i16;
1360                }
1361                CompositeGlyphScale::Matrix(matrix) => {
1362                    let new_x = (point.x as f32 * f32::from(matrix[0][0])
1363                        + point.y as f32 * f32::from(matrix[0][1]))
1364                        as i16;
1365                    let new_y = (point.x as f32 * f32::from(matrix[1][0])
1366                        + point.y as f32 * f32::from(matrix[1][1]))
1367                        as i16;
1368                    point.x = new_x;
1369                    point.y = new_y;
1370                }
1371            }
1372        }
1373
1374        // Apply offset based on offset type
1375        match offset_type {
1376            ComponentOffsets::Scaled => {
1377                // Offset is already scaled by the transform
1378                point.x += offset_x;
1379                point.y += offset_y;
1380            }
1381            ComponentOffsets::Unscaled => {
1382                // Offset should be applied after scaling
1383                point.x += offset_x;
1384                point.y += offset_y;
1385            }
1386        }
1387    }
1388
1389    // Implement the same transform_point function for LineTo
1390    fn transform_point_lineto(
1391        point: &mut OutlineLineTo,
1392        offset_x: i16,
1393        offset_y: i16,
1394        scale: Option<CompositeGlyphScale>,
1395        offset_type: ComponentOffsets,
1396    ) {
1397        // Same implementation as above, just with OutlineLineTo
1398        // Apply scale if present
1399        if let Some(scale_factor) = scale {
1400            match scale_factor {
1401                CompositeGlyphScale::Scale(s) => {
1402                    let scale = f32::from(s);
1403                    point.x = (point.x as f32 * scale) as i16;
1404                    point.y = (point.y as f32 * scale) as i16;
1405                }
1406                CompositeGlyphScale::XY { x_scale, y_scale } => {
1407                    point.x = (point.x as f32 * f32::from(x_scale)) as i16;
1408                    point.y = (point.y as f32 * f32::from(y_scale)) as i16;
1409                }
1410                CompositeGlyphScale::Matrix(matrix) => {
1411                    let new_x = (point.x as f32 * f32::from(matrix[0][0])
1412                        + point.y as f32 * f32::from(matrix[0][1]))
1413                        as i16;
1414                    let new_y = (point.x as f32 * f32::from(matrix[1][0])
1415                        + point.y as f32 * f32::from(matrix[1][1]))
1416                        as i16;
1417                    point.x = new_x;
1418                    point.y = new_y;
1419                }
1420            }
1421        }
1422
1423        // Apply offset based on offset type
1424        match offset_type {
1425            ComponentOffsets::Scaled => {
1426                // Offset is already scaled by the transform
1427                point.x += offset_x;
1428                point.y += offset_y;
1429            }
1430            ComponentOffsets::Unscaled => {
1431                // Offset should be applied after scaling
1432                point.x += offset_x;
1433                point.y += offset_y;
1434            }
1435        }
1436    }
1437
1438    fn transform_quad_point(
1439        point: &mut OutlineQuadTo,
1440        offset_x: i16,
1441        offset_y: i16,
1442        scale: Option<CompositeGlyphScale>,
1443        offset_type: ComponentOffsets,
1444    ) {
1445        // Apply scale if present
1446        if let Some(scale_factor) = scale {
1447            match scale_factor {
1448                CompositeGlyphScale::Scale(s) => {
1449                    let scale = f32::from(s);
1450                    point.ctrl_1_x = (point.ctrl_1_x as f32 * scale) as i16;
1451                    point.ctrl_1_y = (point.ctrl_1_y as f32 * scale) as i16;
1452                    point.end_x = (point.end_x as f32 * scale) as i16;
1453                    point.end_y = (point.end_y as f32 * scale) as i16;
1454                }
1455                CompositeGlyphScale::XY { x_scale, y_scale } => {
1456                    point.ctrl_1_x = (point.ctrl_1_x as f32 * f32::from(x_scale)) as i16;
1457                    point.ctrl_1_y = (point.ctrl_1_y as f32 * f32::from(y_scale)) as i16;
1458                    point.end_x = (point.end_x as f32 * f32::from(x_scale)) as i16;
1459                    point.end_y = (point.end_y as f32 * f32::from(y_scale)) as i16;
1460                }
1461                CompositeGlyphScale::Matrix(matrix) => {
1462                    // Transform control point
1463                    let new_ctrl_x = (point.ctrl_1_x as f32 * f32::from(matrix[0][0])
1464                        + point.ctrl_1_y as f32 * f32::from(matrix[0][1]))
1465                        as i16;
1466                    let new_ctrl_y = (point.ctrl_1_x as f32 * f32::from(matrix[1][0])
1467                        + point.ctrl_1_y as f32 * f32::from(matrix[1][1]))
1468                        as i16;
1469
1470                    // Transform end point
1471                    let new_end_x = (point.end_x as f32 * f32::from(matrix[0][0])
1472                        + point.end_y as f32 * f32::from(matrix[0][1]))
1473                        as i16;
1474                    let new_end_y = (point.end_x as f32 * f32::from(matrix[1][0])
1475                        + point.end_y as f32 * f32::from(matrix[1][1]))
1476                        as i16;
1477
1478                    point.ctrl_1_x = new_ctrl_x;
1479                    point.ctrl_1_y = new_ctrl_y;
1480                    point.end_x = new_end_x;
1481                    point.end_y = new_end_y;
1482                }
1483            }
1484        }
1485
1486        // Apply offset based on offset type
1487        match offset_type {
1488            ComponentOffsets::Scaled => {
1489                point.ctrl_1_x += offset_x;
1490                point.ctrl_1_y += offset_y;
1491                point.end_x += offset_x;
1492                point.end_y += offset_y;
1493            }
1494            ComponentOffsets::Unscaled => {
1495                point.ctrl_1_x += offset_x;
1496                point.ctrl_1_y += offset_y;
1497                point.end_x += offset_x;
1498                point.end_y += offset_y;
1499            }
1500        }
1501    }
1502
1503    fn transform_cubic_point(
1504        point: &mut OutlineCubicTo,
1505        offset_x: i16,
1506        offset_y: i16,
1507        scale: Option<CompositeGlyphScale>,
1508        offset_type: ComponentOffsets,
1509    ) {
1510        // Apply scale if present
1511        if let Some(scale_factor) = scale {
1512            match scale_factor {
1513                CompositeGlyphScale::Scale(s) => {
1514                    let scale = f32::from(s);
1515                    point.ctrl_1_x = (point.ctrl_1_x as f32 * scale) as i16;
1516                    point.ctrl_1_y = (point.ctrl_1_y as f32 * scale) as i16;
1517                    point.ctrl_2_x = (point.ctrl_2_x as f32 * scale) as i16;
1518                    point.ctrl_2_y = (point.ctrl_2_y as f32 * scale) as i16;
1519                    point.end_x = (point.end_x as f32 * scale) as i16;
1520                    point.end_y = (point.end_y as f32 * scale) as i16;
1521                }
1522                CompositeGlyphScale::XY { x_scale, y_scale } => {
1523                    point.ctrl_1_x = (point.ctrl_1_x as f32 * f32::from(x_scale)) as i16;
1524                    point.ctrl_1_y = (point.ctrl_1_y as f32 * f32::from(y_scale)) as i16;
1525                    point.ctrl_2_x = (point.ctrl_2_x as f32 * f32::from(x_scale)) as i16;
1526                    point.ctrl_2_y = (point.ctrl_2_y as f32 * f32::from(y_scale)) as i16;
1527                    point.end_x = (point.end_x as f32 * f32::from(x_scale)) as i16;
1528                    point.end_y = (point.end_y as f32 * f32::from(y_scale)) as i16;
1529                }
1530                CompositeGlyphScale::Matrix(matrix) => {
1531                    // Transform first control point
1532                    let new_ctrl1_x = (point.ctrl_1_x as f32 * f32::from(matrix[0][0])
1533                        + point.ctrl_1_y as f32 * f32::from(matrix[0][1]))
1534                        as i16;
1535                    let new_ctrl1_y = (point.ctrl_1_x as f32 * f32::from(matrix[1][0])
1536                        + point.ctrl_1_y as f32 * f32::from(matrix[1][1]))
1537                        as i16;
1538
1539                    // Transform second control point
1540                    let new_ctrl2_x = (point.ctrl_2_x as f32 * f32::from(matrix[0][0])
1541                        + point.ctrl_2_y as f32 * f32::from(matrix[0][1]))
1542                        as i16;
1543                    let new_ctrl2_y = (point.ctrl_2_x as f32 * f32::from(matrix[1][0])
1544                        + point.ctrl_2_y as f32 * f32::from(matrix[1][1]))
1545                        as i16;
1546
1547                    // Transform end point
1548                    let new_end_x = (point.end_x as f32 * f32::from(matrix[0][0])
1549                        + point.end_y as f32 * f32::from(matrix[0][1]))
1550                        as i16;
1551                    let new_end_y = (point.end_x as f32 * f32::from(matrix[1][0])
1552                        + point.end_y as f32 * f32::from(matrix[1][1]))
1553                        as i16;
1554
1555                    point.ctrl_1_x = new_ctrl1_x;
1556                    point.ctrl_1_y = new_ctrl1_y;
1557                    point.ctrl_2_x = new_ctrl2_x;
1558                    point.ctrl_2_y = new_ctrl2_y;
1559                    point.end_x = new_end_x;
1560                    point.end_y = new_end_y;
1561                }
1562            }
1563        }
1564
1565        // Apply offset based on offset type
1566        match offset_type {
1567            ComponentOffsets::Scaled => {
1568                point.ctrl_1_x += offset_x;
1569                point.ctrl_1_y += offset_y;
1570                point.ctrl_2_x += offset_x;
1571                point.ctrl_2_y += offset_y;
1572                point.end_x += offset_x;
1573                point.end_y += offset_y;
1574            }
1575            ComponentOffsets::Unscaled => {
1576                point.ctrl_1_x += offset_x;
1577                point.ctrl_1_y += offset_y;
1578                point.ctrl_2_x += offset_x;
1579                point.ctrl_2_y += offset_y;
1580                point.end_x += offset_x;
1581                point.end_y += offset_y;
1582            }
1583        }
1584    }
1585
1586    // --- ParsedFontTrait Implementation for ParsedFont ---
1587
1588    impl crate::text3::cache::ShallowClone for ParsedFont {
1589        fn shallow_clone(&self) -> Self {
1590            self.clone() // ParsedFont::clone uses Arc internally, so it's shallow
1591        }
1592    }
1593
1594    impl crate::text3::cache::ParsedFontTrait for ParsedFont {
1595        fn shape_text(
1596            &self,
1597            text: &str,
1598            script: crate::font_traits::Script,
1599            language: crate::font_traits::Language,
1600            direction: crate::font_traits::BidiDirection,
1601            style: &crate::font_traits::StyleProperties,
1602        ) -> Result<Vec<crate::font_traits::Glyph>, crate::font_traits::LayoutError> {
1603            // Call the existing shape_text_for_parsed_font method (defined in default.rs)
1604            crate::text3::default::shape_text_for_parsed_font(
1605                self, text, script, language, direction, style,
1606            )
1607        }
1608
1609        fn get_hash(&self) -> u64 {
1610            self.hash
1611        }
1612
1613        fn get_glyph_size(
1614            &self,
1615            glyph_id: u16,
1616            font_size_px: f32,
1617        ) -> Option<azul_core::geom::LogicalSize> {
1618            self.glyph_records_decoded.get(&glyph_id).map(|record| {
1619                let units_per_em = self.font_metrics.units_per_em as f32;
1620                let scale_factor = if units_per_em > 0.0 {
1621                    font_size_px / units_per_em
1622                } else {
1623                    0.01
1624                };
1625                let bbox = &record.bounding_box;
1626                azul_core::geom::LogicalSize {
1627                    width: (bbox.max_x - bbox.min_x) as f32 * scale_factor,
1628                    height: (bbox.max_y - bbox.min_y) as f32 * scale_factor,
1629                }
1630            })
1631        }
1632
1633        fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
1634            let glyph_id = self.lookup_glyph_index('-' as u32)?;
1635            let advance_units = self.get_horizontal_advance(glyph_id);
1636            let scale_factor = if self.font_metrics.units_per_em > 0 {
1637                font_size / (self.font_metrics.units_per_em as f32)
1638            } else {
1639                return None;
1640            };
1641            let scaled_advance = advance_units as f32 * scale_factor;
1642            Some((glyph_id, scaled_advance))
1643        }
1644
1645        fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
1646            let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
1647            let advance_units = self.get_horizontal_advance(glyph_id);
1648            let scale_factor = if self.font_metrics.units_per_em > 0 {
1649                font_size / (self.font_metrics.units_per_em as f32)
1650            } else {
1651                return None;
1652            };
1653            let scaled_advance = advance_units as f32 * scale_factor;
1654            Some((glyph_id, scaled_advance))
1655        }
1656
1657        fn has_glyph(&self, codepoint: u32) -> bool {
1658            self.lookup_glyph_index(codepoint).is_some()
1659        }
1660
1661        fn get_vertical_metrics(
1662            &self,
1663            glyph_id: u16,
1664        ) -> Option<crate::text3::cache::VerticalMetrics> {
1665            // Default implementation - can be enhanced later
1666            None
1667        }
1668
1669        fn get_font_metrics(&self) -> crate::text3::cache::LayoutFontMetrics {
1670            self.font_metrics.clone()
1671        }
1672
1673        fn num_glyphs(&self) -> u16 {
1674            self.num_glyphs
1675        }
1676    }
1677}