Skip to main content

azul_layout/text3/
default.rs

1use std::{path::Path, sync::Arc};
2
3use allsorts::{
4    gpos,
5    gsub::{self, FeatureInfo, FeatureMask, Features},
6};
7use azul_core::{geom::LogicalSize, glyph::Placement};
8use azul_css::props::basic::FontRef;
9use rust_fontconfig::FcFontCache;
10
11use crate::{
12    font::parsed::ParsedFont,
13    text3::{
14        cache::{
15            BidiDirection, BidiLevel, FontManager, FontSelector, FontVariantCaps,
16            FontVariantLigatures, FontVariantNumeric, Glyph, GlyphOrientation, GlyphSource,
17            LayoutError, LayoutFontMetrics, ParsedFontTrait, Point, ShallowClone, StyleProperties,
18            TextCombineUpright, TextDecoration, TextOrientation, VerticalMetrics, WritingMode,
19        },
20        script::Script,
21    },
22};
23
24/// Creates a FontRef from font bytes by parsing them into a ParsedFont.
25///
26/// This is a bridge function that:
27///
28/// 1. Parses the bytes into a ParsedFont
29/// 2. Wraps it in a FontRef with proper reference counting
30///
31/// # Arguments
32///
33/// - `font_bytes` - The raw font file data
34/// - `font_index` - Index of the font in a font collection (0 for single fonts)
35/// - `parse_outlines` - Whether to parse glyph outlines (expensive, usually false for layout)
36pub fn font_ref_from_bytes(
37    font_bytes: &[u8],
38    font_index: usize,
39    parse_outlines: bool,
40) -> Option<FontRef> {
41    // Parse the font bytes into ParsedFont
42    let mut warnings = Vec::new();
43    let parsed_font = ParsedFont::from_bytes(font_bytes, font_index, &mut warnings)?;
44
45    Some(crate::parsed_font_to_font_ref(parsed_font))
46}
47
48/// A FontLoader that parses font data from a byte slice.
49///
50/// It is designed to be used in conjunction with a mechanism that reads font files
51/// from paths into memory. This loader simply handles the parsing aspect.
52#[derive(Debug, Default, Clone)]
53pub struct PathLoader;
54
55impl PathLoader {
56    pub fn new() -> Self {
57        PathLoader
58    }
59
60    /// A helper method to read a font from a path and delegate to the trait's `load_font`.
61    /// Note: This is a convenience and not part of the `FontLoaderTrait`.
62    pub fn load_from_path(&self, path: &Path, font_index: usize) -> Result<FontRef, LayoutError> {
63        let font_bytes = std::fs::read(path).map_err(|e| {
64            LayoutError::FontNotFound(FontSelector {
65                family: path.to_string_lossy().into_owned(),
66                weight: rust_fontconfig::FcWeight::Normal,
67                style: crate::text3::cache::FontStyle::Normal,
68                unicode_ranges: Vec::new(),
69            })
70        })?;
71        self.load_font(&font_bytes, font_index)
72    }
73}
74
75impl FontManager<FontRef> {
76    pub fn new_with_fc_cache(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
77        FontManager::new(fc_cache)
78    }
79}
80
81impl PathLoader {
82    /// Loads a font from a byte slice.
83    ///
84    /// This implementation parses the bytes into a ParsedFont and wraps it in a FontRef.
85    pub fn load_font(&self, font_bytes: &[u8], font_index: usize) -> Result<FontRef, LayoutError> {
86        // Parse the font bytes and wrap in FontRef
87        font_ref_from_bytes(font_bytes, font_index, true).ok_or_else(|| {
88            LayoutError::ShapingError("Failed to parse font with allsorts".to_string())
89        })
90    }
91}
92
93// ParsedFontTrait Implementation for FontRef
94
95// Implement ShallowClone for FontRef
96impl crate::text3::cache::ShallowClone for FontRef {
97    fn shallow_clone(&self) -> Self {
98        // FontRef::clone increments the reference count
99        self.clone()
100    }
101}
102
103// Helper to get the inner ParsedFont from FontRef
104#[inline]
105fn get_parsed_font(font_ref: &FontRef) -> &ParsedFont {
106    unsafe { &*(font_ref.get_parsed() as *const ParsedFont) }
107}
108
109impl ParsedFontTrait for FontRef {
110    fn shape_text(
111        &self,
112        text: &str,
113        script: Script,
114        language: crate::text3::script::Language,
115        direction: BidiDirection,
116        style: &StyleProperties,
117    ) -> Result<Vec<Glyph>, LayoutError> {
118        // Delegate to the inner ParsedFont's shape_text, passing self as font_ref
119        let parsed = get_parsed_font(self);
120        parsed.shape_text_for_font_ref(self, text, script, language, direction, style)
121    }
122
123    fn get_hash(&self) -> u64 {
124        get_parsed_font(self).hash
125    }
126
127    fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
128        get_parsed_font(self).get_glyph_size(glyph_id, font_size)
129    }
130
131    fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
132        get_parsed_font(self).get_hyphen_glyph_and_advance(font_size)
133    }
134
135    fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
136        get_parsed_font(self).get_kashida_glyph_and_advance(font_size)
137    }
138
139    fn has_glyph(&self, codepoint: u32) -> bool {
140        get_parsed_font(self).has_glyph(codepoint)
141    }
142
143    fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
144        get_parsed_font(self).get_vertical_metrics(glyph_id)
145    }
146
147    fn get_font_metrics(&self) -> LayoutFontMetrics {
148        get_parsed_font(self).font_metrics.clone()
149    }
150
151    fn num_glyphs(&self) -> u16 {
152        get_parsed_font(self).num_glyphs
153    }
154}
155
156/// Extension trait for FontRef to provide access to font bytes and metrics
157///
158/// This trait provides methods that require access to the inner ParsedFont data.
159pub trait FontRefExt {
160    /// Get the original font bytes
161    fn get_bytes(&self) -> &[u8];
162    /// Get the full font metrics (PDF-style metrics from HEAD, HHEA, OS/2 tables)
163    fn get_full_font_metrics(&self) -> azul_css::props::basic::FontMetrics;
164}
165
166impl FontRefExt for FontRef {
167    fn get_bytes(&self) -> &[u8] {
168        &get_parsed_font(self).original_bytes
169    }
170
171    fn get_full_font_metrics(&self) -> azul_css::props::basic::FontMetrics {
172        use azul_css::{OptionI16, OptionU16, OptionU32};
173
174        let parsed = get_parsed_font(self);
175        let pdf = &parsed.pdf_font_metrics;
176
177        // PdfFontMetrics only has a subset of fields; fill others with defaults
178        azul_css::props::basic::FontMetrics {
179            // HEAD table fields
180            units_per_em: pdf.units_per_em,
181            font_flags: pdf.font_flags,
182            x_min: pdf.x_min,
183            y_min: pdf.y_min,
184            x_max: pdf.x_max,
185            y_max: pdf.y_max,
186
187            // HHEA table fields
188            ascender: pdf.ascender,
189            descender: pdf.descender,
190            line_gap: pdf.line_gap,
191            advance_width_max: pdf.advance_width_max,
192            min_left_side_bearing: 0,  // Not in PdfFontMetrics
193            min_right_side_bearing: 0, // Not in PdfFontMetrics
194            x_max_extent: 0,           // Not in PdfFontMetrics
195            caret_slope_rise: pdf.caret_slope_rise,
196            caret_slope_run: pdf.caret_slope_run,
197            caret_offset: 0,  // Not in PdfFontMetrics
198            num_h_metrics: 0, // Not in PdfFontMetrics
199
200            // OS/2 table fields
201            x_avg_char_width: pdf.x_avg_char_width,
202            us_weight_class: pdf.us_weight_class,
203            us_width_class: pdf.us_width_class,
204            fs_type: 0,                // Not in PdfFontMetrics
205            y_subscript_x_size: 0,     // Not in PdfFontMetrics
206            y_subscript_y_size: 0,     // Not in PdfFontMetrics
207            y_subscript_x_offset: 0,   // Not in PdfFontMetrics
208            y_subscript_y_offset: 0,   // Not in PdfFontMetrics
209            y_superscript_x_size: 0,   // Not in PdfFontMetrics
210            y_superscript_y_size: 0,   // Not in PdfFontMetrics
211            y_superscript_x_offset: 0, // Not in PdfFontMetrics
212            y_superscript_y_offset: 0, // Not in PdfFontMetrics
213            y_strikeout_size: pdf.y_strikeout_size,
214            y_strikeout_position: pdf.y_strikeout_position,
215            s_family_class: 0, // Not in PdfFontMetrics
216            panose: azul_css::props::basic::Panose::zero(),
217            ul_unicode_range1: 0,   // Not in PdfFontMetrics
218            ul_unicode_range2: 0,   // Not in PdfFontMetrics
219            ul_unicode_range3: 0,   // Not in PdfFontMetrics
220            ul_unicode_range4: 0,   // Not in PdfFontMetrics
221            ach_vend_id: 0,         // Not in PdfFontMetrics
222            fs_selection: 0,        // Not in PdfFontMetrics
223            us_first_char_index: 0, // Not in PdfFontMetrics
224            us_last_char_index: 0,  // Not in PdfFontMetrics
225
226            // OS/2 version 0 fields (optional)
227            s_typo_ascender: OptionI16::None,
228            s_typo_descender: OptionI16::None,
229            s_typo_line_gap: OptionI16::None,
230            us_win_ascent: OptionU16::None,
231            us_win_descent: OptionU16::None,
232
233            // OS/2 version 1 fields (optional)
234            ul_code_page_range1: OptionU32::None,
235            ul_code_page_range2: OptionU32::None,
236
237            // OS/2 version 2 fields (optional)
238            sx_height: OptionI16::None,
239            s_cap_height: OptionI16::None,
240            us_default_char: OptionU16::None,
241            us_break_char: OptionU16::None,
242            us_max_context: OptionU16::None,
243
244            // OS/2 version 3 fields (optional)
245            us_lower_optical_point_size: OptionU16::None,
246            us_upper_optical_point_size: OptionU16::None,
247        }
248    }
249}
250
251// ParsedFont helper method for FontRef
252//
253// This allows ParsedFont to create glyphs that use FontRef
254//
255// FontRef is just a C-style Arc wrapper around ParsedFont, so we delegate to
256// the common shaping implementation and convert the font reference type.
257
258impl ParsedFont {
259    /// Internal helper that shapes text and returns Glyph
260    /// Delegates to shape_text_internal and converts the font reference.
261    fn shape_text_for_font_ref(
262        &self,
263        font_ref: &FontRef,
264        text: &str,
265        script: Script,
266        language: crate::text3::script::Language,
267        direction: BidiDirection,
268        style: &StyleProperties,
269    ) -> Result<Vec<Glyph>, LayoutError> {
270        // Use the common shaping implementation
271        let shaped = shape_text_internal(self, text, script, language, direction, style)?;
272
273        // Convert Glyph - now using font_hash and font_metrics instead of font reference
274        let font_hash = font_ref.get_hash();
275        let font_metrics = LayoutFontMetrics {
276            ascent: self.font_metrics.ascent,
277            descent: self.font_metrics.descent,
278            line_gap: self.font_metrics.line_gap,
279            units_per_em: self.font_metrics.units_per_em,
280        };
281
282        Ok(shaped
283            .into_iter()
284            .map(|g| Glyph {
285                glyph_id: g.glyph_id,
286                codepoint: g.codepoint,
287                font_hash,
288                font_metrics: font_metrics.clone(),
289                style: g.style,
290                source: g.source,
291                logical_byte_index: g.logical_byte_index,
292                logical_byte_len: g.logical_byte_len,
293                content_index: g.content_index,
294                cluster: g.cluster,
295                advance: g.advance,
296                kerning: g.kerning,
297                offset: g.offset,
298                vertical_advance: g.vertical_advance,
299                vertical_origin_y: g.vertical_origin_y,
300                vertical_bearing: g.vertical_bearing,
301                orientation: g.orientation,
302                script: g.script,
303                bidi_level: g.bidi_level,
304            })
305            .collect())
306    }
307
308    fn get_hash(&self) -> u64 {
309        self.hash
310    }
311
312    fn get_glyph_size(&self, glyph_id: u16, font_size_px: f32) -> Option<LogicalSize> {
313        self.glyph_records_decoded.get(&glyph_id).map(|record| {
314            let units_per_em = self.font_metrics.units_per_em as f32;
315            let scale_factor = if units_per_em > 0.0 {
316                font_size_px / units_per_em
317            } else {
318                0.01 // Avoid division by zero
319            };
320
321            // max_x, max_y, min_x, min_y in font units
322            let bbox = &record.bounding_box;
323
324            LogicalSize {
325                width: (bbox.max_x - bbox.min_x) as f32 * scale_factor,
326                height: (bbox.max_y - bbox.min_y) as f32 * scale_factor,
327            }
328        })
329    }
330
331    fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
332        let glyph_id = self.lookup_glyph_index('-' as u32)?;
333        let advance_units = self.get_horizontal_advance(glyph_id);
334        let scale_factor = if self.font_metrics.units_per_em > 0 {
335            font_size / (self.font_metrics.units_per_em as f32)
336        } else {
337            return None;
338        };
339        let scaled_advance = advance_units as f32 * scale_factor;
340        Some((glyph_id, scaled_advance))
341    }
342
343    fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
344        // U+0640 is the Arabic Tatweel character, used for kashida justification.
345        let glyph_id = self.lookup_glyph_index('\u{0640}' as u32)?;
346        let advance_units = self.get_horizontal_advance(glyph_id);
347        let scale_factor = if self.font_metrics.units_per_em > 0 {
348            font_size / (self.font_metrics.units_per_em as f32)
349        } else {
350            return None;
351        };
352        let scaled_advance = advance_units as f32 * scale_factor;
353        Some((glyph_id, scaled_advance))
354    }
355}
356
357// Helper Functions
358
359/// Builds a FeatureMask with the appropriate OpenType features for a given script.
360/// This ensures proper text shaping for complex scripts like Arabic, Devanagari, etc.
361///
362/// The function includes:
363/// - Common features for all scripts (ligatures, contextual alternates, etc.)
364/// - Script-specific features (positional forms for Arabic, conjuncts for Indic, etc.)
365///
366/// This is designed to be stable and explicit - we control exactly which features
367/// are enabled rather than relying on allsorts' defaults which may change.
368fn build_feature_mask_for_script(script: Script) -> FeatureMask {
369    use Script::*;
370
371    // Start with common features that apply to most scripts
372    let mut mask = FeatureMask::default(); // Includes: CALT, CCMP, CLIG, LIGA, LOCL, RLIG
373
374    // Add script-specific features
375    match script {
376        // Arabic and related scripts - require positional forms
377        Arabic => {
378            mask |= FeatureMask::INIT; // Initial forms (at start of word)
379            mask |= FeatureMask::MEDI; // Medial forms (middle of word)
380            mask |= FeatureMask::FINA; // Final forms (end of word)
381            mask |= FeatureMask::ISOL; // Isolated forms (standalone)
382                                       // Note: RLIG (required ligatures) already in default for
383                                       // lam-alef ligatures
384        }
385
386        // Indic scripts - require complex conjunct formation and reordering
387        Devanagari | Bengali | Gujarati | Gurmukhi | Kannada | Malayalam | Oriya | Tamil
388        | Telugu => {
389            mask |= FeatureMask::NUKT; // Nukta forms
390            mask |= FeatureMask::AKHN; // Akhand ligatures
391            mask |= FeatureMask::RPHF; // Reph form
392            mask |= FeatureMask::RKRF; // Rakar form
393            mask |= FeatureMask::PREF; // Pre-base forms
394            mask |= FeatureMask::BLWF; // Below-base forms
395            mask |= FeatureMask::ABVF; // Above-base forms
396            mask |= FeatureMask::HALF; // Half forms
397            mask |= FeatureMask::PSTF; // Post-base forms
398            mask |= FeatureMask::VATU; // Vattu variants
399            mask |= FeatureMask::CJCT; // Conjunct forms
400        }
401
402        // Myanmar (Burmese) - has complex reordering
403        Myanmar => {
404            mask |= FeatureMask::PREF; // Pre-base forms
405            mask |= FeatureMask::BLWF; // Below-base forms
406            mask |= FeatureMask::PSTF; // Post-base forms
407        }
408
409        // Khmer - has complex reordering and stacking
410        Khmer => {
411            mask |= FeatureMask::PREF; // Pre-base forms
412            mask |= FeatureMask::BLWF; // Below-base forms
413            mask |= FeatureMask::ABVF; // Above-base forms
414            mask |= FeatureMask::PSTF; // Post-base forms
415        }
416
417        // Thai - has tone marks and vowel reordering
418        Thai => {
419            // Thai mostly uses default features, but may have some special marks
420            // The default mask is sufficient for most Thai fonts
421        }
422
423        // Hebrew - may have contextual forms but less complex than Arabic
424        Hebrew => {
425            // Hebrew fonts may use contextual alternates already in default
426            // Some fonts have special features but they're rare
427        }
428
429        // Hangul (Korean) - has complex syllable composition
430        Hangul => {
431            // Note: Hangul jamo features (LJMO, VJMO, TJMO) are not available in allsorts'
432            // FeatureMask Most modern Hangul fonts work correctly with the default
433            // features as syllable composition is usually handled at a lower level
434        }
435
436        // Ethiopic - has syllabic script with some ligatures
437        Ethiopic => {
438            // Default features are usually sufficient
439            // LIGA and CLIG already in default mask
440        }
441
442        // Latin, Greek, Cyrillic - standard features are sufficient
443        Latin | Greek | Cyrillic => {
444            // Default mask includes all needed features:
445            // - LIGA: standard ligatures (fi, fl, etc.)
446            // - CLIG: contextual ligatures
447            // - CALT: contextual alternates
448            // - CCMP: mark composition
449        }
450
451        // Georgian - uses standard features
452        Georgian => {
453            // Default features sufficient
454        }
455
456        // CJK scripts (Hiragana, Katakana, Mandarin/Hani)
457        Hiragana | Katakana | Mandarin => {
458            // CJK fonts may use vertical alternates, but those are controlled
459            // by writing-mode, not GSUB features in the horizontal direction.
460            // Default features are sufficient.
461        }
462
463        // Sinhala - Indic-derived but simpler
464        Sinhala => {
465            mask |= FeatureMask::AKHN; // Akhand ligatures
466            mask |= FeatureMask::RPHF; // Reph form
467            mask |= FeatureMask::VATU; // Vattu variants
468        }
469    }
470
471    mask
472}
473
474/// Maps the layout engine's `Script` enum to an OpenType script tag `u32`.
475fn to_opentype_script_tag(script: Script) -> u32 {
476    use Script::*;
477    // Tags from https://docs.microsoft.com/en-us/typography/opentype/spec/scripttags
478    match script {
479        Arabic => u32::from_be_bytes(*b"arab"),
480        Bengali => u32::from_be_bytes(*b"beng"),
481        Cyrillic => u32::from_be_bytes(*b"cyrl"),
482        Devanagari => u32::from_be_bytes(*b"deva"),
483        Ethiopic => u32::from_be_bytes(*b"ethi"),
484        Georgian => u32::from_be_bytes(*b"geor"),
485        Greek => u32::from_be_bytes(*b"grek"),
486        Gujarati => u32::from_be_bytes(*b"gujr"),
487        Gurmukhi => u32::from_be_bytes(*b"guru"),
488        Hangul => u32::from_be_bytes(*b"hang"),
489        Hebrew => u32::from_be_bytes(*b"hebr"),
490        Hiragana => u32::from_be_bytes(*b"kana"),
491        Kannada => u32::from_be_bytes(*b"knda"),
492        Katakana => u32::from_be_bytes(*b"kana"),
493        Khmer => u32::from_be_bytes(*b"khmr"),
494        Latin => u32::from_be_bytes(*b"latn"),
495        Malayalam => u32::from_be_bytes(*b"mlym"),
496        Mandarin => u32::from_be_bytes(*b"hani"),
497        Myanmar => u32::from_be_bytes(*b"mymr"),
498        Oriya => u32::from_be_bytes(*b"orya"),
499        Sinhala => u32::from_be_bytes(*b"sinh"),
500        Tamil => u32::from_be_bytes(*b"taml"),
501        Telugu => u32::from_be_bytes(*b"telu"),
502        Thai => u32::from_be_bytes(*b"thai"),
503    }
504}
505
506/// Parses a CSS-style font-feature-settings string like `"liga"`, `"liga=0"`, or `"ss01"`.
507/// Returns an OpenType tag and a value.
508fn parse_font_feature(feature_str: &str) -> Option<(u32, u32)> {
509    let mut parts = feature_str.split('=');
510    let tag_str = parts.next()?.trim();
511    let value_str = parts.next().unwrap_or("1").trim(); // Default to 1 (on) if no value
512
513    // OpenType feature tags must be 4 characters long.
514    if tag_str.len() > 4 {
515        return None;
516    }
517    // Pad with spaces if necessary
518    let padded_tag_str = format!("{:<4}", tag_str);
519
520    let tag = u32::from_be_bytes(padded_tag_str.as_bytes().try_into().ok()?);
521    let value = value_str.parse::<u32>().ok()?;
522
523    Some((tag, value))
524}
525
526/// A helper to add OpenType features based on CSS `font-variant-*` properties.
527fn add_variant_features(style: &StyleProperties, features: &mut Vec<FeatureInfo>) {
528    // Helper to add a feature that is simply "on".
529    let mut add_on = |tag_str: &[u8; 4]| {
530        features.push(FeatureInfo {
531            feature_tag: u32::from_be_bytes(*tag_str),
532            alternate: None,
533        });
534    };
535
536    // Note on disabling features: The CSS properties `font-variant-ligatures: none` or
537    // `no-common-ligatures` are meant to disable features that may be on by default for a
538    // given script. The `allsorts` API for applying custom features is additive and does not
539    // currently support disabling default features. This implementation only handles enabling
540    // non-default features.
541
542    // Ligatures
543    match style.font_variant_ligatures {
544        FontVariantLigatures::Discretionary => add_on(b"dlig"),
545        FontVariantLigatures::Historical => add_on(b"hlig"),
546        FontVariantLigatures::Contextual => add_on(b"calt"),
547        _ => {} // Other cases are either default-on or require disabling.
548    }
549
550    // Caps
551    match style.font_variant_caps {
552        FontVariantCaps::SmallCaps => add_on(b"smcp"),
553        FontVariantCaps::AllSmallCaps => {
554            add_on(b"c2sc");
555            add_on(b"smcp");
556        }
557        FontVariantCaps::PetiteCaps => add_on(b"pcap"),
558        FontVariantCaps::AllPetiteCaps => {
559            add_on(b"c2pc");
560            add_on(b"pcap");
561        }
562        FontVariantCaps::Unicase => add_on(b"unic"),
563        FontVariantCaps::TitlingCaps => add_on(b"titl"),
564        FontVariantCaps::Normal => {}
565    }
566
567    // Numeric
568    match style.font_variant_numeric {
569        FontVariantNumeric::LiningNums => add_on(b"lnum"),
570        FontVariantNumeric::OldstyleNums => add_on(b"onum"),
571        FontVariantNumeric::ProportionalNums => add_on(b"pnum"),
572        FontVariantNumeric::TabularNums => add_on(b"tnum"),
573        FontVariantNumeric::DiagonalFractions => add_on(b"frac"),
574        FontVariantNumeric::StackedFractions => add_on(b"afrc"),
575        FontVariantNumeric::Ordinal => add_on(b"ordn"),
576        FontVariantNumeric::SlashedZero => add_on(b"zero"),
577        FontVariantNumeric::Normal => {}
578    }
579}
580
581/// Maps the `hyphenation::Language` enum to an OpenType language tag `u32`.
582#[cfg(feature = "text_layout_hyphenation")]
583fn to_opentype_lang_tag(lang: hyphenation::Language) -> u32 {
584    use hyphenation::Language::*;
585    // A complete list of language tags can be found at:
586    // https://docs.microsoft.com/en-us/typography/opentype/spec/languagetags
587    let tag_bytes = match lang {
588        Afrikaans => *b"AFK ",
589        Albanian => *b"SQI ",
590        Armenian => *b"HYE ",
591        Assamese => *b"ASM ",
592        Basque => *b"EUQ ",
593        Belarusian => *b"BEL ",
594        Bengali => *b"BEN ",
595        Bulgarian => *b"BGR ",
596        Catalan => *b"CAT ",
597        Chinese => *b"ZHS ",
598        Coptic => *b"COP ",
599        Croatian => *b"HRV ",
600        Czech => *b"CSY ",
601        Danish => *b"DAN ",
602        Dutch => *b"NLD ",
603        EnglishGB => *b"ENG ",
604        EnglishUS => *b"ENU ",
605        Esperanto => *b"ESP ",
606        Estonian => *b"ETI ",
607        Ethiopic => *b"ETI ",
608        Finnish => *b"FIN ",
609        FinnishScholastic => *b"FIN ",
610        French => *b"FRA ",
611        Friulan => *b"FRL ",
612        Galician => *b"GLC ",
613        Georgian => *b"KAT ",
614        German1901 => *b"DEU ",
615        German1996 => *b"DEU ",
616        GermanSwiss => *b"DES ",
617        GreekAncient => *b"GRC ",
618        GreekMono => *b"ELL ",
619        GreekPoly => *b"ELL ",
620        Gujarati => *b"GUJ ",
621        Hindi => *b"HIN ",
622        Hungarian => *b"HUN ",
623        Icelandic => *b"ISL ",
624        Indonesian => *b"IND ",
625        Interlingua => *b"INA ",
626        Irish => *b"IRI ",
627        Italian => *b"ITA ",
628        Kannada => *b"KAN ",
629        Kurmanji => *b"KUR ",
630        Latin => *b"LAT ",
631        LatinClassic => *b"LAT ",
632        LatinLiturgical => *b"LAT ",
633        Latvian => *b"LVI ",
634        Lithuanian => *b"LTH ",
635        Macedonian => *b"MKD ",
636        Malayalam => *b"MAL ",
637        Marathi => *b"MAR ",
638        Mongolian => *b"MNG ",
639        NorwegianBokmal => *b"NOR ",
640        NorwegianNynorsk => *b"NYN ",
641        Occitan => *b"OCI ",
642        Oriya => *b"ORI ",
643        Pali => *b"PLI ",
644        Panjabi => *b"PAN ",
645        Piedmontese => *b"PMS ",
646        Polish => *b"PLK ",
647        Portuguese => *b"PTG ",
648        Romanian => *b"ROM ",
649        Romansh => *b"RMC ",
650        Russian => *b"RUS ",
651        Sanskrit => *b"SAN ",
652        SerbianCyrillic => *b"SRB ",
653        SerbocroatianCyrillic => *b"SHC ",
654        SerbocroatianLatin => *b"SHL ",
655        SlavonicChurch => *b"CSL ",
656        Slovak => *b"SKY ",
657        Slovenian => *b"SLV ",
658        Spanish => *b"ESP ",
659        Swedish => *b"SVE ",
660        Tamil => *b"TAM ",
661        Telugu => *b"TEL ",
662        Thai => *b"THA ",
663        Turkish => *b"TRK ",
664        Turkmen => *b"TUK ",
665        Ukrainian => *b"UKR ",
666        Uppersorbian => *b"HSB ",
667        Welsh => *b"CYM ",
668    };
669    u32::from_be_bytes(tag_bytes)
670}
671
672/// Internal shaping implementation - the single source of truth for text shaping.
673/// Both FontRef and ParsedFont use this function.
674fn shape_text_internal(
675    parsed_font: &ParsedFont,
676    text: &str,
677    script: Script,
678    language: crate::text3::script::Language,
679    direction: BidiDirection,
680    style: &StyleProperties,
681) -> Result<Vec<Glyph>, LayoutError> {
682    let script_tag = to_opentype_script_tag(script);
683    #[cfg(feature = "text_layout_hyphenation")]
684    let lang_tag = to_opentype_lang_tag(language);
685    #[cfg(not(feature = "text_layout_hyphenation"))]
686    let lang_tag = 0u32;
687
688    let mut user_features: Vec<FeatureInfo> = style
689        .font_features
690        .iter()
691        .filter_map(|s| parse_font_feature(s))
692        .map(|(tag, value)| FeatureInfo {
693            feature_tag: tag,
694            alternate: if value > 1 {
695                Some(value as usize)
696            } else {
697                None
698            },
699        })
700        .collect();
701    add_variant_features(style, &mut user_features);
702
703    let opt_gdef = parsed_font.opt_gdef_table.as_ref().map(|v| &**v);
704
705    let mut raw_glyphs: Vec<allsorts::gsub::RawGlyph<()>> = text
706        .char_indices()
707        .filter_map(|(cluster, ch)| {
708            let glyph_index = parsed_font.lookup_glyph_index(ch as u32).unwrap_or(0);
709            if cluster > u16::MAX as usize {
710                None
711            } else {
712                Some(allsorts::gsub::RawGlyph {
713                    unicodes: tinyvec::tiny_vec![[char; 1] => ch],
714                    glyph_index,
715                    liga_component_pos: cluster as u16,
716                    glyph_origin: allsorts::gsub::GlyphOrigin::Char(ch),
717                    flags: allsorts::gsub::RawGlyphFlags::empty(),
718                    extra_data: (),
719                    variation: None,
720                })
721            }
722        })
723        .collect();
724
725    if let Some(gsub) = parsed_font.gsub_cache.as_ref() {
726        let features = if user_features.is_empty() {
727            Features::Mask(build_feature_mask_for_script(script))
728        } else {
729            Features::Custom(user_features.clone())
730        };
731
732        let dotted_circle_index = parsed_font
733            .lookup_glyph_index(allsorts::DOTTED_CIRCLE as u32)
734            .unwrap_or(0);
735        gsub::apply(
736            dotted_circle_index,
737            gsub,
738            opt_gdef,
739            script_tag,
740            Some(lang_tag),
741            &features,
742            None,
743            parsed_font.num_glyphs(),
744            &mut raw_glyphs,
745        )
746        .map_err(|e| LayoutError::ShapingError(e.to_string()))?;
747    }
748
749    let mut infos = gpos::Info::init_from_glyphs(opt_gdef, raw_glyphs);
750
751    if let Some(gpos) = parsed_font.gpos_cache.as_ref() {
752        let kern_table = parsed_font
753            .opt_kern_table
754            .as_ref()
755            .map(|kt| kt.as_borrowed());
756        let apply_kerning = kern_table.is_some();
757        gpos::apply(
758            gpos,
759            opt_gdef,
760            kern_table,
761            apply_kerning,
762            &Features::Custom(user_features),
763            None,
764            script_tag,
765            Some(lang_tag),
766            &mut infos,
767        )
768        .map_err(|e| LayoutError::ShapingError(e.to_string()))?;
769    }
770
771    let font_size = style.font_size_px;
772    let scale_factor = if parsed_font.font_metrics.units_per_em > 0 {
773        font_size / (parsed_font.font_metrics.units_per_em as f32)
774    } else {
775        0.01
776    };
777
778    let mut shaped_glyphs = Vec::new();
779    for info in infos.iter() {
780        let cluster = info.glyph.liga_component_pos as u32;
781        let source_char = text
782            .get(cluster as usize..)
783            .and_then(|s| s.chars().next())
784            .unwrap_or('\u{FFFD}');
785
786        let base_advance = parsed_font.get_horizontal_advance(info.glyph.glyph_index);
787        let advance = base_advance as f32 * scale_factor;
788        let kerning = info.kerning as f32 * scale_factor;
789
790        let (offset_x_units, offset_y_units) =
791            if let allsorts::gpos::Placement::Distance(x, y) = info.placement {
792                (x, y)
793            } else {
794                (0, 0)
795            };
796        let offset_x = offset_x_units as f32 * scale_factor;
797        let offset_y = offset_y_units as f32 * scale_factor;
798
799        let glyph = Glyph {
800            glyph_id: info.glyph.glyph_index,
801            codepoint: source_char,
802            font_hash: parsed_font.get_hash(),
803            font_metrics: LayoutFontMetrics {
804                ascent: parsed_font.font_metrics.ascent,
805                descent: parsed_font.font_metrics.descent,
806                line_gap: parsed_font.font_metrics.line_gap,
807                units_per_em: parsed_font.font_metrics.units_per_em,
808            },
809            style: Arc::new(style.clone()),
810            source: GlyphSource::Char,
811            logical_byte_index: cluster as usize,
812            logical_byte_len: source_char.len_utf8(),
813            content_index: 0,
814            cluster,
815            advance,
816            kerning,
817            offset: Point {
818                x: offset_x,
819                y: offset_y,
820            },
821            vertical_advance: 0.0,
822            vertical_origin_y: 0.0,
823            vertical_bearing: Point { x: 0.0, y: 0.0 },
824            orientation: GlyphOrientation::Horizontal,
825            script,
826            bidi_level: BidiLevel::new(if direction.is_rtl() { 1 } else { 0 }),
827        };
828        shaped_glyphs.push(glyph);
829    }
830
831    Ok(shaped_glyphs)
832}
833
834/// Public helper function to shape text for ParsedFont, returning Glyph
835/// This is used by the ParsedFontTrait implementation for ParsedFont
836pub fn shape_text_for_parsed_font(
837    parsed_font: &ParsedFont,
838    text: &str,
839    script: Script,
840    language: crate::text3::script::Language,
841    direction: BidiDirection,
842    style: &StyleProperties,
843) -> Result<Vec<Glyph>, LayoutError> {
844    // Delegate to the single internal implementation
845    shape_text_internal(parsed_font, text, script, language, direction, style)
846}