Skip to main content

fonts/
font.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::borrow::ToOwned;
6use std::collections::HashMap;
7use std::hash::Hash;
8use std::ops::Deref;
9use std::sync::{Arc, OnceLock};
10use std::{iter, str};
11
12use app_units::Au;
13use atomic_refcell::AtomicRef;
14use bitflags::bitflags;
15use euclid::default::{Point2D, Rect};
16use euclid::num::Zero;
17use font_types::NameId;
18use fonts_traits::FontDescriptor;
19use icu_locale_core::subtags::Language;
20use icu_properties::props::{EnumeratedProperty, GeneralCategory};
21use log::debug;
22use malloc_size_of_derive::MallocSizeOf;
23use parking_lot::RwLock;
24use read_fonts::collections::int_set::Domain;
25use read_fonts::tables::fvar::Fvar;
26use read_fonts::tables::name::Name as NameTable;
27use read_fonts::tables::os2::{Os2, SelectionFlags};
28use read_fonts::types::Tag;
29use read_fonts::{FontRead, ReadError};
30use rustc_hash::FxHashMap;
31use serde::{Deserialize, Serialize};
32use servo_base::id::PainterId;
33use servo_base::text::{UnicodeBlock, UnicodeBlockMethod};
34use skrifa::string::LocalizedString;
35use smallvec::SmallVec;
36use style::Atom;
37use style::computed_values::font_optical_sizing::T as FontOpticalSizing;
38use style::computed_values::font_variant_caps;
39use style::computed_values::font_variant_position::T as FontVariantPosition;
40use style::properties::style_structs::Font as FontStyleStruct;
41use style::values::computed::font::{
42    FamilyName, FontFamilyNameSyntax, GenericFontFamily, SingleFontFamily,
43};
44use style::values::computed::{
45    FontFeatureSettings, FontStyle, FontSynthesis, FontVariantEastAsian, FontVariantLigatures,
46    FontVariantNumeric, FontWeight, FontWidth,
47};
48use unicode_script::Script;
49use webrender_api::{
50    FontInstanceFlags, FontInstanceKey, FontInstancePlatformOptions, FontVariation,
51};
52
53use crate::font_feature_values::ResolvedFontVariantAlternates;
54use crate::platform::font::{FontTable, PlatformFont};
55use crate::platform::font_list::fallback_font_families;
56use crate::{
57    EmojiPresentationPreference, FallbackFontSelectionOptions, FontContext, FontData,
58    FontDataAndIndex, FontDataError, FontIdentifier, FontTemplateDescriptor, FontTemplateRef,
59    FontTemplateRefMethods, GlyphId, LocalFontIdentifier, ShapedGlyph, ShapedText, Shaper,
60    compute_used_font_features,
61};
62
63pub(crate) const AFRC: Tag = Tag::new(b"afrc");
64pub(crate) const BASE: Tag = Tag::new(b"BASE");
65pub(crate) const CALT: Tag = Tag::new(b"calt");
66pub(crate) const CBDT: Tag = Tag::new(b"CBDT");
67pub(crate) const CLIG: Tag = Tag::new(b"clig");
68pub(crate) const COLR: Tag = Tag::new(b"COLR");
69pub(crate) const CWSH: Tag = Tag::new(b"cwsh");
70pub(crate) const FRAC: Tag = Tag::new(b"frac");
71pub(crate) const DLIG: Tag = Tag::new(b"dlig");
72pub(crate) const FVAR: Tag = Tag::new(b"fvar");
73pub(crate) const FWID: Tag = Tag::new(b"fwid");
74pub(crate) const GPOS: Tag = Tag::new(b"GPOS");
75pub(crate) const GSUB: Tag = Tag::new(b"GSUB");
76pub(crate) const HIST: Tag = Tag::new(b"hist");
77pub(crate) const HLIG: Tag = Tag::new(b"hlig");
78pub(crate) const ITAL: Tag = Tag::new(b"ital");
79pub(crate) const JP04: Tag = Tag::new(b"jp04");
80pub(crate) const JP78: Tag = Tag::new(b"jp78");
81pub(crate) const JP83: Tag = Tag::new(b"jp83");
82pub(crate) const JP90: Tag = Tag::new(b"jp90");
83pub(crate) const KERN: Tag = Tag::new(b"kern");
84pub(crate) const LIGA: Tag = Tag::new(b"liga");
85pub(crate) const LNUM: Tag = Tag::new(b"lnum");
86pub(crate) const NALT: Tag = Tag::new(b"nalt");
87pub(crate) const NAME: Tag = Tag::new(b"name");
88pub(crate) const ONUM: Tag = Tag::new(b"onum");
89pub(crate) const ORNM: Tag = Tag::new(b"ornm");
90pub(crate) const ORDN: Tag = Tag::new(b"ordn");
91pub(crate) const PNUM: Tag = Tag::new(b"pnum");
92pub(crate) const PWID: Tag = Tag::new(b"pwid");
93pub(crate) const RUBY: Tag = Tag::new(b"ruby");
94pub(crate) const SALT: Tag = Tag::new(b"salt");
95pub(crate) const SBIX: Tag = Tag::new(b"sbix");
96pub(crate) const SLNT: Tag = Tag::new(b"slnt");
97pub(crate) const SMPL: Tag = Tag::new(b"smpl");
98pub(crate) const SUBS: Tag = Tag::new(b"subs");
99pub(crate) const SUPS: Tag = Tag::new(b"sups");
100pub(crate) const SWSH: Tag = Tag::new(b"swsh");
101pub(crate) const TNUM: Tag = Tag::new(b"tnum");
102pub(crate) const TRAD: Tag = Tag::new(b"trad");
103pub(crate) const ZERO: Tag = Tag::new(b"zero");
104
105pub const LAST_RESORT_GLYPH_ADVANCE: FractionalPixel = 10.0;
106
107// PlatformFont encapsulates access to the platform's font API,
108// e.g. quartz, FreeType. It provides access to metrics and tables
109// needed by the text shaper as well as access to the underlying font
110// resources needed by the graphics layer to draw glyphs.
111
112pub trait PlatformFontMethods: Sized {
113    #[servo_tracing::instrument(name = "PlatformFontMethods::new_from_template", skip_all)]
114    fn new_from_template(
115        template: FontTemplateRef,
116        pt_size: Option<Au>,
117        data: &Option<FontData>,
118        synthetic_bold: bool,
119    ) -> Result<PlatformFont, &'static str> {
120        let template = template.borrow();
121        let font_identifier = template.identifier.clone();
122
123        match font_identifier {
124            FontIdentifier::Local(font_identifier) => {
125                Self::new_from_local_font_identifier(font_identifier, pt_size, synthetic_bold)
126            },
127            FontIdentifier::Web(_) | FontIdentifier::ArrayBuffer(_) => Self::new_from_data(
128                font_identifier,
129                data.as_ref()
130                    .expect("Should never create a web font without data."),
131                pt_size,
132                synthetic_bold,
133            ),
134        }
135    }
136
137    fn new_from_local_font_identifier(
138        font_identifier: LocalFontIdentifier,
139        pt_size: Option<Au>,
140        synthetic_bold: bool,
141    ) -> Result<PlatformFont, &'static str>;
142
143    fn new_from_data(
144        font_identifier: FontIdentifier,
145        data: &FontData,
146        pt_size: Option<Au>,
147        synthetic_bold: bool,
148    ) -> Result<PlatformFont, &'static str>;
149
150    /// Create a platform font with the given variations from a existing font.
151    ///
152    /// `self` is consumed to work around platform differences. On some platforms, changing the
153    /// variations requires creating an entirely new font face, whereas on others the returned
154    /// font is `self`.
155    fn copy_with_variations(
156        self,
157        _font_identifer: &FontIdentifier,
158        _variations: &[FontVariation],
159    ) -> Result<Self, &'static str>;
160
161    /// Get a [`FontTemplateDescriptor`] from a [`PlatformFont`]. This is used to get
162    /// descriptors for web fonts.
163    fn descriptor(&self) -> FontTemplateDescriptor;
164
165    fn glyph_index(&self, codepoint: char) -> Option<GlyphId>;
166    fn glyph_h_advance(&self, _: GlyphId) -> Option<FractionalPixel>;
167    fn glyph_h_kerning(&self, glyph0: GlyphId, glyph1: GlyphId) -> FractionalPixel;
168
169    fn metrics(&self) -> FontMetrics;
170    fn table_for_tag(&self, _: Tag) -> Option<FontTable>;
171    fn typographic_bounds(&self, _: GlyphId) -> Rect<f32>;
172
173    /// Get the necessary [`FontInstanceFlags`] for this font.
174    fn webrender_font_instance_flags(&self) -> FontInstanceFlags;
175
176    /// Get the necessary [`FontInstancePlatformOptions`] for this font.
177    fn webrender_font_instance_platform_options(&self) -> FontInstancePlatformOptions {
178        Default::default()
179    }
180
181    /// Return all the variation values that the font was instantiated with.
182    fn variations(&self) -> &[FontVariation];
183
184    fn descriptor_from_os2_table(os2: &Os2) -> FontTemplateDescriptor {
185        let mut style = FontStyle::NORMAL;
186        if os2.fs_selection().contains(SelectionFlags::ITALIC) {
187            style = FontStyle::ITALIC;
188        }
189
190        let weight = FontWeight::from_float(os2.us_weight_class() as f32);
191        let width = match os2.us_width_class() {
192            1 => FontWidth::ULTRA_CONDENSED,
193            2 => FontWidth::EXTRA_CONDENSED,
194            3 => FontWidth::CONDENSED,
195            4 => FontWidth::SEMI_CONDENSED,
196            5 => FontWidth::NORMAL,
197            6 => FontWidth::SEMI_EXPANDED,
198            7 => FontWidth::EXPANDED,
199            8 => FontWidth::EXTRA_EXPANDED,
200            9 => FontWidth::ULTRA_EXPANDED,
201            _ => FontWidth::NORMAL,
202        };
203
204        FontTemplateDescriptor::new(weight, width, style)
205    }
206}
207
208// Used to abstract over the shaper's choice of fixed int representation.
209pub(crate) type FractionalPixel = f64;
210
211pub(crate) trait FontTableMethods {
212    fn buffer(&self) -> &[u8];
213    fn parse_as_specific_table<'a, Table>(&'a self) -> Result<Table, ReadError>
214    where
215        Table: FontRead<'a, Args = ()>,
216    {
217        Table::read(read_fonts::FontData::new(self.buffer()))
218    }
219}
220
221#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
222pub struct FontMetrics {
223    pub underline_size: Au,
224    pub underline_offset: Au,
225    pub strikeout_size: Au,
226    pub strikeout_offset: Au,
227    pub leading: Au,
228    pub x_height: Au,
229    pub em_size: Au,
230    pub ascent: Au,
231    pub descent: Au,
232    pub max_advance: Au,
233    pub average_advance: Au,
234    pub line_gap: Au,
235    pub zero_horizontal_advance: Option<Au>,
236    pub ic_horizontal_advance: Option<Au>,
237    /// The advance of the space character (' ') in this font or if there is no space,
238    /// the average char advance.
239    pub space_advance: Au,
240}
241
242impl FontMetrics {
243    /// Create an empty [`FontMetrics`] mainly to be used in situations where
244    /// no font can be found.
245    pub fn empty() -> Arc<Self> {
246        static EMPTY: OnceLock<Arc<FontMetrics>> = OnceLock::new();
247        EMPTY.get_or_init(Default::default).clone()
248    }
249
250    /// Whether or not the block metrics of the two `FontMetrics` instances differ in a way
251    /// that requires the resulting block size of a containing inline box to change.
252    pub fn block_metrics_meaningfully_differ(&self, other: &Self) -> bool {
253        self.ascent != other.ascent ||
254            self.descent != other.descent ||
255            self.line_gap != other.line_gap
256    }
257}
258
259#[derive(Debug, Default)]
260struct CachedShapeData {
261    glyph_advances: FxHashMap<GlyphId, FractionalPixel>,
262    glyph_indices: FxHashMap<char, Option<GlyphId>>,
263    shaped_text: HashMap<ShapeCacheEntry, Arc<ShapedText>>,
264}
265
266impl malloc_size_of::MallocSizeOf for CachedShapeData {
267    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
268        // Estimate the size of the shaped text cache. This will be smaller, because
269        // HashMap has some overhead, but we are mainly interested in the actual data.
270        let shaped_text_size = self
271            .shaped_text
272            .iter()
273            .map(|(key, value)| key.size_of(ops) + (*value).size_of(ops))
274            .sum::<usize>();
275        self.glyph_advances.size_of(ops) + self.glyph_indices.size_of(ops) + shaped_text_size
276    }
277}
278
279pub struct Font {
280    pub(crate) handle: PlatformFont,
281    pub(crate) template: FontTemplateRef,
282    pub metrics: OnceLock<Arc<FontMetrics>>,
283    pub descriptor: FontDescriptor,
284
285    /// The data for this font. And the index of the font within the data (in case it's a TTC)
286    /// This might be uninitialized for system fonts.
287    data_and_index: OnceLock<FontDataAndIndex>,
288
289    shaper: OnceLock<Shaper>,
290    cached_shape_data: RwLock<CachedShapeData>,
291    font_instance_key: RwLock<FxHashMap<PainterId, FontInstanceKey>>,
292
293    /// If this is a synthesized small caps font, then this font reference is for
294    /// the version of the font used to replace lowercase ASCII letters. It's up
295    /// to the consumer of this font to properly use this reference.
296    pub(crate) synthesized_small_caps: Option<FontRef>,
297
298    /// Whether or not this font supports color bitmaps or a COLR table. This is
299    /// essentially equivalent to whether or not we use it for emoji presentation.
300    /// This is cached, because getting table data is expensive.
301    has_color_bitmap_or_colr_table: OnceLock<bool>,
302
303    /// Whether or not this font can do fast shaping, ie whether or not it has
304    /// a kern table, but no GSUB and GPOS tables. When this is true, Servo will
305    /// shape Latin horizontal left-to-right text without using Harfbuzz.
306    ///
307    /// FIXME: This should be removed entirely in favor of better caching if necessary.
308    /// See <https://github.com/servo/servo/pull/11273#issuecomment-222332873>.
309    can_do_fast_shaping: OnceLock<bool>,
310
311    /// The family name of the font.
312    ///
313    /// This is the name as it is declared in the `name` table, *not* the name provided by the system
314    /// font service.
315    family_name: OnceLock<Result<Atom, NoUsableFamilyName>>,
316}
317
318/// An error indicating that the `name` table contained no usable family names.
319///
320/// For example, this can happen if the family name uses an incompatible or unknown encoding.
321#[derive(Clone)]
322struct NoUsableFamilyName;
323
324impl std::fmt::Debug for Font {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        f.debug_struct("Font")
327            .field("template", &self.template)
328            .field("descriptor", &self.descriptor)
329            .finish()
330    }
331}
332
333impl malloc_size_of::MallocSizeOf for Font {
334    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
335        // TODO: Collect memory usage for platform fonts and for shapers.
336        // This skips the template, because they are already stored in the template cache.
337
338        let metrics_size = self.metrics.get().map_or(0, |metrics| metrics.size_of(ops));
339        metrics_size +
340            self.descriptor.size_of(ops) +
341            self.cached_shape_data.read().size_of(ops) +
342            self.font_instance_key
343                .read()
344                .values()
345                .map(|key| key.size_of(ops))
346                .sum::<usize>()
347    }
348}
349
350impl Font {
351    pub fn new(
352        template: FontTemplateRef,
353        descriptor: FontDescriptor,
354        data: Option<FontData>,
355        synthesized_small_caps: Option<FontRef>,
356    ) -> Result<Font, &'static str> {
357        let synthetic_bold = {
358            let is_bold = descriptor.weight >= FontWeight::BOLD_THRESHOLD;
359            let allows_synthetic_bold = matches!(descriptor.synthesis_weight, FontSynthesis::Auto);
360
361            is_bold && allows_synthetic_bold
362        };
363
364        let handle = PlatformFont::new_from_template(
365            template.clone(),
366            Some(descriptor.pt_size),
367            &data,
368            synthetic_bold,
369        )?;
370        let variation_axes = VariationAxes::from_platform_font(&handle);
371
372        // Compute and apply the OpenType variations
373        let handle = if servo_config::pref!(layout_variable_fonts_enabled) &&
374            variation_axes.contains(VariationAxes::IS_VARIABLE)
375        {
376            let used_variations = compute_variations(&descriptor, &template, variation_axes);
377            handle.copy_with_variations(&template.identifier(), &used_variations)?
378        } else {
379            handle
380        };
381
382        Ok(Font {
383            handle,
384            template,
385            metrics: OnceLock::new(),
386            descriptor,
387            data_and_index: data
388                .map(|data| OnceLock::from(FontDataAndIndex { data, index: 0 }))
389                .unwrap_or_default(),
390            shaper: OnceLock::new(),
391            cached_shape_data: Default::default(),
392            font_instance_key: Default::default(),
393            synthesized_small_caps,
394            has_color_bitmap_or_colr_table: OnceLock::new(),
395            can_do_fast_shaping: OnceLock::new(),
396            family_name: Default::default(),
397        })
398    }
399
400    /// A unique identifier for the font, allowing comparison.
401    pub fn identifier(&self) -> AtomicRef<'_, FontIdentifier> {
402        self.template.identifier()
403    }
404
405    pub fn metrics(&self) -> &Arc<FontMetrics> {
406        self.metrics.get_or_init(|| Arc::new(self.handle.metrics()))
407    }
408
409    pub(crate) fn webrender_font_instance_flags(&self) -> FontInstanceFlags {
410        self.handle.webrender_font_instance_flags()
411    }
412
413    pub(crate) fn webrender_font_instance_platform_options(&self) -> FontInstancePlatformOptions {
414        self.handle.webrender_font_instance_platform_options()
415    }
416
417    pub(crate) fn has_color_bitmap_or_colr_table(&self) -> bool {
418        *self.has_color_bitmap_or_colr_table.get_or_init(|| {
419            self.table_for_tag(SBIX).is_some() ||
420                self.table_for_tag(CBDT).is_some() ||
421                self.table_for_tag(COLR).is_some()
422        })
423    }
424
425    pub fn key(&self, painter_id: PainterId, font_context: &FontContext) -> FontInstanceKey {
426        *self
427            .font_instance_key
428            .write()
429            .entry(painter_id)
430            .or_insert_with(|| font_context.create_font_instance_key(self, painter_id))
431    }
432
433    /// Return the data for this `Font`. Note that this is currently highly inefficient for system
434    /// fonts and should not be used except in legacy canvas code.
435    pub fn font_data_and_index(&self) -> Result<&FontDataAndIndex, FontDataError> {
436        if let Some(data_and_index) = self.data_and_index.get() {
437            return Ok(data_and_index);
438        }
439
440        let FontIdentifier::Local(local_font_identifier) = &*self.identifier() else {
441            unreachable!("All web fonts should already have initialized data");
442        };
443        let Some(data_and_index) = local_font_identifier.font_data_and_index() else {
444            return Err(FontDataError::FailedToLoad);
445        };
446
447        let data_and_index = self.data_and_index.get_or_init(move || data_and_index);
448        Ok(data_and_index)
449    }
450
451    pub(crate) fn variations(&self) -> &[FontVariation] {
452        self.handle.variations()
453    }
454}
455
456bitflags! {
457    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
458    pub struct ShapingFlags: u8 {
459        /// Set if we are to disable kerning.
460        const DISABLE_KERNING_SHAPING_FLAG = 1 << 3;
461        /// Text direction is right-to-left.
462        const RTL_FLAG = 1 << 4;
463        /// Set if word-break is set to keep-all.
464        const KEEP_ALL_FLAG = 1 << 5;
465    }
466}
467
468/// Various options that control text shaping.
469#[derive(Clone, Debug, Eq, Hash, PartialEq)]
470pub struct ShapingOptions {
471    /// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
472    ///
473    /// Letter spacing is not applied to all characters. Use [Self::letter_spacing_for_character] to
474    /// determine the amount of spacing to apply.
475    pub letter_spacing: Au,
476    /// Spacing to add between each word. Corresponds to the CSS 2.1 `word-spacing` property.
477    pub word_spacing: Au,
478    /// The Unicode script property of the characters in this run.
479    pub script: Script,
480    /// The preferred language, obtained from the `lang` attribute.
481    pub language: Language,
482    /// The value of the `font-variant-ligatures` property.
483    pub ligatures: FontVariantLigatures,
484    /// The value of the `font-variant-numeric` property.
485    pub numeric: FontVariantNumeric,
486    /// The value of the `font-variant-east-asian` property.
487    pub east_asian: FontVariantEastAsian,
488    /// The value of the `font-feature-settings` property.
489    pub feature_settings: FontFeatureSettings,
490    /// The value of the `font-variant-position` property.
491    pub position: FontVariantPosition,
492    /// The value of the `font-variant-alternates` property.
493    pub alternates: ResolvedFontVariantAlternates,
494    /// Various flags.
495    pub flags: ShapingFlags,
496}
497
498impl ShapingOptions {
499    pub(crate) fn letter_spacing_for_character(&self, character: char) -> Au {
500        // https://drafts.csswg.org/css-text/#letter-spacing-property
501        // Letter spacing ignores invisible zero-width formatting characters (such as those from the Unicode Cf category).
502        // Spacing must be added as if those characters did not exist in the document.
503        if GeneralCategory::for_char(character) == GeneralCategory::Format {
504            return Au::zero();
505        }
506        self.letter_spacing
507    }
508}
509
510/// An entry in the shape cache.
511#[derive(Clone, Debug, Eq, Hash, PartialEq)]
512struct ShapeCacheEntry {
513    text: String,
514    letter_spacing: Au,
515    word_spacing: Au,
516    script: Script,
517    language: Language,
518    font_features: Box<[(Tag, u32)]>,
519    flags: ShapingFlags,
520}
521
522impl Font {
523    #[servo_tracing::instrument(name = "Font::shape_text", skip_all)]
524    pub fn shape_text(&self, text: &str, options: &ShapingOptions) -> Arc<ShapedText> {
525        let font_features =
526            compute_used_font_features(options, self.template.borrow().font_face_rule.as_deref())
527                .collect();
528        let lookup_key = ShapeCacheEntry {
529            text: text.to_owned(),
530            letter_spacing: options.letter_spacing,
531            word_spacing: options.word_spacing,
532            script: options.script,
533            language: options.language,
534            flags: options.flags,
535            font_features,
536        };
537
538        if let Some(shaped_text) = self.cached_shape_data.read().shaped_text.get(&lookup_key) {
539            return shaped_text.clone();
540        }
541
542        let glyphs = if self.can_do_fast_shaping(text, options) {
543            debug!("shape_text: Using ASCII fast path.");
544            self.shape_text_fast(text, options)
545        } else {
546            debug!("shape_text: Using Harfbuzz.");
547            self.shaper.get_or_init(|| Shaper::new(self)).shape_text(
548                text,
549                options,
550                &lookup_key.font_features,
551            )
552        };
553
554        let shaped_text = Arc::new(glyphs);
555        let mut cache = self.cached_shape_data.write();
556        cache.shaped_text.insert(lookup_key, shaped_text.clone());
557
558        shaped_text
559    }
560
561    /// Whether not a particular text and [`ShapingOptions`] combination can use
562    /// "fast shaping" ie shaping without Harfbuzz.
563    ///
564    /// Note: This will eventually be removed.
565    pub fn can_do_fast_shaping(&self, text: &str, options: &ShapingOptions) -> bool {
566        options.script == Script::Latin &&
567            !options.flags.contains(ShapingFlags::RTL_FLAG) &&
568            *self.can_do_fast_shaping.get_or_init(|| {
569                self.table_for_tag(KERN).is_some() &&
570                    self.table_for_tag(GPOS).is_none() &&
571                    self.table_for_tag(GSUB).is_none()
572            }) &&
573            text.is_ascii()
574    }
575
576    /// Fast path for ASCII text that only needs simple horizontal LTR kerning.
577    fn shape_text_fast(&self, text: &str, options: &ShapingOptions) -> ShapedText {
578        let mut glyph_store = ShapedText::new(text.len(), false /* is_rtl */);
579        let mut prev_glyph_id = None;
580        for (string_byte_offset, byte) in text.bytes().enumerate() {
581            let character = byte as char;
582            let Some(glyph_id) = self.glyph_index(character) else {
583                continue;
584            };
585
586            let mut advance = Au::from_f64_px(self.glyph_h_advance(glyph_id));
587            let offset = prev_glyph_id.map(|prev| {
588                let h_kerning = Au::from_f64_px(self.glyph_h_kerning(prev, glyph_id));
589                advance += h_kerning;
590                Point2D::new(h_kerning, Au::zero())
591            });
592
593            let mut glyph = ShapedGlyph {
594                glyph_id,
595                string_byte_offset,
596                advance,
597                offset,
598            };
599            glyph.adjust_for_character(character, options);
600
601            glyph_store.add_glyph(character, &glyph);
602            prev_glyph_id = Some(glyph_id);
603        }
604        glyph_store
605    }
606
607    pub(crate) fn table_for_tag(&self, tag: Tag) -> Option<FontTable> {
608        let result = self.handle.table_for_tag(tag);
609        let status = if result.is_some() {
610            "Found"
611        } else {
612            "Didn't find"
613        };
614
615        debug!(
616            "{} font table[{}] in {:?},",
617            status,
618            str::from_utf8(tag.as_ref()).unwrap(),
619            self.identifier()
620        );
621        result
622    }
623
624    #[inline]
625    pub fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
626        {
627            let cache = self.cached_shape_data.read();
628            if let Some(glyph) = cache.glyph_indices.get(&codepoint) {
629                return *glyph;
630            }
631        }
632        let codepoint = match self.descriptor.variant {
633            font_variant_caps::T::SmallCaps => codepoint.to_ascii_uppercase(),
634            font_variant_caps::T::Normal => codepoint,
635        };
636        let glyph_index = self.handle.glyph_index(codepoint);
637
638        let mut cache = self.cached_shape_data.write();
639        cache.glyph_indices.insert(codepoint, glyph_index);
640        glyph_index
641    }
642
643    pub fn has_glyph_for(&self, codepoint: char) -> bool {
644        self.glyph_index(codepoint).is_some()
645    }
646
647    pub(crate) fn glyph_h_kerning(
648        &self,
649        first_glyph: GlyphId,
650        second_glyph: GlyphId,
651    ) -> FractionalPixel {
652        self.handle.glyph_h_kerning(first_glyph, second_glyph)
653    }
654
655    pub fn glyph_h_advance(&self, glyph_id: GlyphId) -> FractionalPixel {
656        {
657            let cache = self.cached_shape_data.read();
658            if let Some(width) = cache.glyph_advances.get(&glyph_id) {
659                return *width;
660            }
661        }
662
663        let new_width = self
664            .handle
665            .glyph_h_advance(glyph_id)
666            .unwrap_or(LAST_RESORT_GLYPH_ADVANCE as FractionalPixel);
667        let mut cache = self.cached_shape_data.write();
668        cache.glyph_advances.insert(glyph_id, new_width);
669        new_width
670    }
671
672    pub fn typographic_bounds(&self, glyph_id: GlyphId) -> Rect<f32> {
673        self.handle.typographic_bounds(glyph_id)
674    }
675
676    /// Get the [`FontBaseline`] for this font.
677    pub fn baseline(&self) -> Option<FontBaseline> {
678        self.shaper.get_or_init(|| Shaper::new(self)).baseline()
679    }
680
681    #[cfg(not(target_os = "macos"))]
682    pub(crate) fn find_fallback_using_system_font_api(
683        &self,
684        _: &FallbackFontSelectionOptions,
685    ) -> Option<FontRef> {
686        None
687    }
688
689    fn get_family_name_from_font_data(&self) -> Result<Atom, NoUsableFamilyName> {
690        let name_table = self.table_for_tag(NAME).ok_or(NoUsableFamilyName)?;
691        let name_table = NameTable::read(read_fonts::FontData::new(name_table.buffer()))
692            .map_err(|_| NoUsableFamilyName)?;
693
694        // Find the most usable family name entry, preferring "en-US" > "en" > "everything else".
695        // TODO: If we ever have a way to get a read_fonts::FontRef out of a PlatformFont then skrifa can
696        // do this for us with LocalizedStrings::english_or_first.
697        //
698        // https://docs.rs/skrifa/latest/skrifa/string/struct.LocalizedStrings.html#method.english_or_first
699        let mut best_rank = -1;
700        let mut best_string = None;
701        for (index, name_record) in name_table
702            .name_record()
703            .iter()
704            .filter(|name_record| name_record.name_id() == NameId::FAMILY_NAME)
705            .enumerate()
706        {
707            let localized_string = LocalizedString::new(&name_table, name_record);
708            let rank = match (index, localized_string.language()) {
709                (_, Some("en-US")) => {
710                    best_string = Some(localized_string);
711                    break;
712                },
713                (_, Some("en")) => 2,
714                (_, None) => 1,
715                (0, _) => 0,
716                _ => continue,
717            };
718            if rank > best_rank {
719                best_rank = rank;
720                best_string = Some(localized_string);
721            }
722        }
723
724        best_string
725            .map(|best_string| best_string.chars().collect::<String>().into())
726            .ok_or(NoUsableFamilyName)
727    }
728
729    /// Return the font's declared family name:
730    ///  - Platform: fonts: A value from OpenType `name` table, or `None` if either the platform
731    ///    does not support that query or the font does not have a usable name.
732    ///  - Web fonts: the family name specified in the `@font-face` rule
733    pub fn family_name(&self) -> Option<Atom> {
734        self.template
735            .font_face_rule()
736            .and_then(|font_face_rule| {
737                AtomicRef::filter_map(font_face_rule, |rule| rule.descriptors.font_family.as_ref())
738            })
739            .map(|font_family| font_family.name.clone())
740            .or_else(|| {
741                self.family_name
742                    .get_or_init(|| self.get_family_name_from_font_data())
743                    .clone()
744                    .ok()
745            })
746    }
747}
748
749#[derive(Clone, Debug, MallocSizeOf)]
750pub struct FontRef(#[conditional_malloc_size_of] pub(crate) Arc<Font>);
751
752impl PartialEq for FontRef {
753    fn eq(&self, other: &Self) -> bool {
754        Arc::ptr_eq(self, other)
755    }
756}
757
758impl Deref for FontRef {
759    type Target = Arc<Font>;
760    fn deref(&self) -> &Self::Target {
761        &self.0
762    }
763}
764
765#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
766pub struct FallbackKey {
767    script: Script,
768    unicode_block: Option<UnicodeBlock>,
769    language: Language,
770}
771
772impl FallbackKey {
773    fn new(options: &FallbackFontSelectionOptions) -> Self {
774        Self {
775            script: Script::from(options.character),
776            unicode_block: options.character.block(),
777            language: options.language,
778        }
779    }
780}
781
782/// A `FontGroup` is a prioritised list of fonts for a given set of font styles. It is used by
783/// `TextRun` to decide which font to render a character with. If none of the fonts listed in the
784/// styles are suitable, a fallback font may be used.
785#[derive(MallocSizeOf)]
786pub struct FontGroup {
787    /// The [`FontDescriptor`] which describes the properties of the fonts that should
788    /// be loaded for this [`FontGroup`].
789    descriptor: FontDescriptor,
790    /// The families that have been loaded for this [`FontGroup`]. This correponds to the
791    /// list of fonts specified in CSS.
792    families: SmallVec<[FontGroupFamily; 8]>,
793    /// A list of fallbacks that have been used in this [`FontGroup`]. Currently this
794    /// can grow indefinitely, but maybe in the future it should be an LRU cache.
795    /// It's unclear if this is the right thing to do. Perhaps fallbacks should
796    /// always be stored here as it's quite likely that they will be used again.
797    fallbacks: RwLock<HashMap<FallbackKey, FontRef>>,
798}
799
800impl FontGroup {
801    pub(crate) fn new(style: &FontStyleStruct, descriptor: FontDescriptor) -> FontGroup {
802        let families: SmallVec<[FontGroupFamily; 8]> = style
803            .font_family
804            .families
805            .iter()
806            .map(FontGroupFamily::local_or_web)
807            .collect();
808
809        FontGroup {
810            descriptor,
811            families,
812            fallbacks: Default::default(),
813        }
814    }
815
816    /// Finds the first font, or else the first fallback font, which contains a glyph for
817    /// `codepoint`. If no such font is found, returns the first available font or fallback font
818    /// (which will cause a "glyph not found" character to be rendered). If no font at all can be
819    /// found, returns None.
820    pub fn find_by_codepoint(
821        &self,
822        font_context: &FontContext,
823        codepoint: char,
824        next_codepoint: Option<char>,
825        language: Language,
826    ) -> Option<FontRef> {
827        // Tab characters are converted into spaces when rendering.
828        // TODO: We should not render a tab character. Instead they should be converted into tab stops
829        // based upon the width of a space character in inline formatting contexts.
830        let codepoint = match codepoint {
831            '\t' => ' ',
832            _ => codepoint,
833        };
834
835        let options = FallbackFontSelectionOptions::new(codepoint, next_codepoint, language);
836
837        let should_look_for_small_caps = self.descriptor.variant == font_variant_caps::T::SmallCaps &&
838            options.character.is_ascii_lowercase();
839        let font_or_synthesized_small_caps = |font: FontRef| {
840            if should_look_for_small_caps && font.synthesized_small_caps.is_some() {
841                return font.synthesized_small_caps.clone();
842            }
843            Some(font)
844        };
845
846        let font_has_glyph_and_presentation = |font: &FontRef| {
847            // Do not select this font if it goes against our emoji preference.
848            match options.presentation_preference {
849                EmojiPresentationPreference::Text if font.has_color_bitmap_or_colr_table() => {
850                    return false;
851                },
852                EmojiPresentationPreference::Emoji if !font.has_color_bitmap_or_colr_table() => {
853                    return false;
854                },
855                _ => {},
856            }
857            font.has_glyph_for(options.character)
858        };
859
860        let char_in_template =
861            |template: FontTemplateRef| template.char_in_unicode_range(options.character);
862
863        if let Some(font) = self.find(
864            font_context,
865            &char_in_template,
866            &font_has_glyph_and_presentation,
867        ) {
868            return font_or_synthesized_small_caps(font);
869        }
870
871        let fallback_key = FallbackKey::new(&options);
872        if let Some(fallback) = self.fallbacks.read().get(&fallback_key) &&
873            char_in_template(fallback.template.clone()) &&
874            font_has_glyph_and_presentation(fallback)
875        {
876            return font_or_synthesized_small_caps(fallback.clone());
877        }
878
879        if let Some(font) = self.find_fallback_using_system_font_list(
880            font_context,
881            options.clone(),
882            &char_in_template,
883            &font_has_glyph_and_presentation,
884        ) {
885            let fallback = font_or_synthesized_small_caps(font);
886            if let Some(fallback) = fallback.clone() {
887                self.fallbacks.write().insert(fallback_key, fallback);
888            }
889            return fallback;
890        }
891
892        let first_font = self.first(font_context);
893        if let Some(fallback) = first_font
894            .as_ref()
895            .and_then(|font| font.find_fallback_using_system_font_api(&options)) &&
896            font_has_glyph_and_presentation(&fallback)
897        {
898            return Some(fallback);
899        }
900
901        first_font
902    }
903
904    /// Find the first available font in the group, or the first available fallback font.
905    pub fn first(&self, font_context: &FontContext) -> Option<FontRef> {
906        // From https://drafts.csswg.org/css-fonts/#first-available-font:
907        // > The first available font, used for example in the definition of font-relative lengths
908        // > such as ex or in the definition of the line-height property, is defined to be the first
909        // > font for which the character U+0020 (space) is not excluded by a unicode-range, given the
910        // > font families in the font-family list (or a user agent’s default font if none are
911        // > available).
912        // > Note: it does not matter whether that font actually has a glyph for the space character.
913        let space_in_template = |template: FontTemplateRef| template.char_in_unicode_range(' ');
914        let font_predicate = |_: &FontRef| true;
915        self.find(font_context, &space_in_template, &font_predicate)
916            .or_else(|| {
917                self.find_fallback_using_system_font_list(
918                    font_context,
919                    FallbackFontSelectionOptions::default(),
920                    &space_in_template,
921                    &font_predicate,
922                )
923            })
924    }
925
926    /// Attempts to find a font which matches the given `template_predicate` and `font_predicate`.
927    /// This method mutates because we may need to load new font data in the process of finding
928    /// a suitable font.
929    fn find(
930        &self,
931        font_context: &FontContext,
932        template_predicate: &impl Fn(FontTemplateRef) -> bool,
933        font_predicate: &impl Fn(&FontRef) -> bool,
934    ) -> Option<FontRef> {
935        self.families
936            .iter()
937            .flat_map(|family| family.templates(font_context, &self.descriptor))
938            .find_map(|template| {
939                template.font_if_matches(
940                    font_context,
941                    &self.descriptor,
942                    template_predicate,
943                    font_predicate,
944                )
945            })
946    }
947
948    /// Attempts to find a suitable fallback font which matches the given `template_predicate` and
949    /// `font_predicate` using the system font list. The default family (i.e. "serif") will be tried
950    /// first, followed by platform-specific family names. If a `codepoint` is provided, then its
951    /// Unicode block may be used to refine
952    /// the list of family names which will be tried.
953    fn find_fallback_using_system_font_list(
954        &self,
955        font_context: &FontContext,
956        options: FallbackFontSelectionOptions,
957        template_predicate: &impl Fn(FontTemplateRef) -> bool,
958        font_predicate: &impl Fn(&FontRef) -> bool,
959    ) -> Option<FontRef> {
960        iter::once(FontFamilyDescriptor::default())
961            .chain(
962                fallback_font_families(options)
963                    .into_iter()
964                    .map(|family_name| {
965                        let family = SingleFontFamily::FamilyName(FamilyName {
966                            name: family_name.into(),
967                            syntax: FontFamilyNameSyntax::Quoted,
968                        });
969                        FontFamilyDescriptor::new(family, FontSearchScope::Local)
970                    }),
971            )
972            .find_map(|family_descriptor| {
973                FontGroupFamily::from(family_descriptor)
974                    .templates(font_context, &self.descriptor)
975                    .find_map(|template| {
976                        template.font_if_matches(
977                            font_context,
978                            &self.descriptor,
979                            template_predicate,
980                            font_predicate,
981                        )
982                    })
983            })
984    }
985}
986
987/// A [`FontGroupFamily`] can have multiple associated `FontTemplate`s if it is a
988/// "composite face", meaning that it is defined by multiple `@font-face`
989/// declarations which vary only by their `unicode-range` descriptors. In this case,
990/// font selection will select a single member that contains the necessary unicode
991/// character. Unicode ranges are specified by the [`FontGroupFamilyTemplate::template`]
992/// member.
993#[derive(MallocSizeOf)]
994struct FontGroupFamilyTemplate {
995    #[ignore_malloc_size_of = "This measured in the FontContext template cache."]
996    template: FontTemplateRef,
997    #[ignore_malloc_size_of = "This measured in the FontContext font cache."]
998    font: OnceLock<Option<FontRef>>,
999}
1000
1001impl From<FontTemplateRef> for FontGroupFamilyTemplate {
1002    fn from(template: FontTemplateRef) -> Self {
1003        Self {
1004            template,
1005            font: Default::default(),
1006        }
1007    }
1008}
1009
1010impl FontGroupFamilyTemplate {
1011    fn font(
1012        &self,
1013        font_context: &FontContext,
1014        font_descriptor: &FontDescriptor,
1015    ) -> Option<FontRef> {
1016        self.font
1017            .get_or_init(|| font_context.font(self.template.clone(), font_descriptor))
1018            .clone()
1019    }
1020
1021    fn font_if_matches(
1022        &self,
1023        font_context: &FontContext,
1024        font_descriptor: &FontDescriptor,
1025        template_predicate: &impl Fn(FontTemplateRef) -> bool,
1026        font_predicate: &impl Fn(&FontRef) -> bool,
1027    ) -> Option<FontRef> {
1028        if !template_predicate(self.template.clone()) {
1029            return None;
1030        }
1031        self.font(font_context, font_descriptor)
1032            .filter(font_predicate)
1033    }
1034}
1035
1036/// A `FontGroupFamily` is a single font family in a `FontGroup`. It corresponds to one of the
1037/// families listed in the `font-family` CSS property. The corresponding font data is lazy-loaded,
1038/// only if actually needed. A single `FontGroupFamily` can have multiple fonts, in the case that
1039/// individual fonts only cover part of the Unicode range.
1040#[derive(MallocSizeOf)]
1041struct FontGroupFamily {
1042    family_descriptor: FontFamilyDescriptor,
1043    members: OnceLock<Vec<FontGroupFamilyTemplate>>,
1044}
1045
1046impl From<FontFamilyDescriptor> for FontGroupFamily {
1047    fn from(family_descriptor: FontFamilyDescriptor) -> Self {
1048        Self {
1049            family_descriptor,
1050            members: Default::default(),
1051        }
1052    }
1053}
1054
1055impl FontGroupFamily {
1056    fn local_or_web(family: &SingleFontFamily) -> FontGroupFamily {
1057        FontFamilyDescriptor::new(family.clone(), FontSearchScope::Any).into()
1058    }
1059
1060    fn templates(
1061        &self,
1062        font_context: &FontContext,
1063        font_descriptor: &FontDescriptor,
1064    ) -> impl Iterator<Item = &FontGroupFamilyTemplate> {
1065        self.members
1066            .get_or_init(|| {
1067                font_context
1068                    .matching_templates(font_descriptor, &self.family_descriptor)
1069                    .into_iter()
1070                    .map(Into::into)
1071                    .collect()
1072            })
1073            .iter()
1074    }
1075}
1076
1077/// The scope within which we will look for a font.
1078#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
1079pub enum FontSearchScope {
1080    /// All fonts will be searched, including those specified via `@font-face` rules.
1081    Any,
1082
1083    /// Only local system fonts will be searched.
1084    Local,
1085}
1086
1087/// The font family parameters for font selection.
1088#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
1089pub struct FontFamilyDescriptor {
1090    pub(crate) family: SingleFontFamily,
1091    pub(crate) scope: FontSearchScope,
1092}
1093
1094impl FontFamilyDescriptor {
1095    pub fn new(family: SingleFontFamily, scope: FontSearchScope) -> FontFamilyDescriptor {
1096        FontFamilyDescriptor { family, scope }
1097    }
1098
1099    fn default() -> FontFamilyDescriptor {
1100        FontFamilyDescriptor {
1101            family: SingleFontFamily::Generic(GenericFontFamily::None),
1102            scope: FontSearchScope::Local,
1103        }
1104    }
1105}
1106
1107pub struct FontBaseline {
1108    pub ideographic_baseline: f32,
1109    pub alphabetic_baseline: f32,
1110    pub hanging_baseline: f32,
1111}
1112
1113/// Given a mapping array `mapping` and a value, map that value onto
1114/// the value specified by the array. For instance, for FontConfig
1115/// values of weights, we would map these onto the CSS [0..1000] range
1116/// by creating an array as below. Values that fall between two mapped
1117/// values, will be adjusted by the weighted mean.
1118///
1119/// ```ignore
1120/// let mapping = [
1121///     (0., 0.),
1122///     (FC_WEIGHT_REGULAR as f64, 400 as f64),
1123///     (FC_WEIGHT_BOLD as f64, 700 as f64),
1124///     (FC_WEIGHT_EXTRABLACK as f64, 1000 as f64),
1125/// ];
1126/// let mapped_weight = apply_font_config_to_style_mapping(&mapping, weight as f64);
1127/// ```
1128#[cfg(all(
1129    any(target_os = "linux", target_os = "macos", target_os = "freebsd"),
1130    not(target_env = "ohos")
1131))]
1132pub(crate) fn map_platform_values_to_style_values(mapping: &[(f64, f64)], value: f64) -> f64 {
1133    if value < mapping[0].0 {
1134        return mapping[0].1;
1135    }
1136
1137    for window in mapping.windows(2) {
1138        let (font_config_value_a, css_value_a) = window[0];
1139        let (font_config_value_b, css_value_b) = window[1];
1140
1141        if value >= font_config_value_a && value <= font_config_value_b {
1142            let ratio = (value - font_config_value_a) / (font_config_value_b - font_config_value_a);
1143            return css_value_a + ((css_value_b - css_value_a) * ratio);
1144        }
1145    }
1146
1147    mapping[mapping.len() - 1].1
1148}
1149
1150/// <https://drafts.csswg.org/css-fonts-4/#apply-font-matching-variations>
1151fn compute_variations(
1152    descriptor: &FontDescriptor,
1153    template: &FontTemplateRef,
1154    variation_axes: VariationAxes,
1155) -> Vec<FontVariation> {
1156    let font_face_rule = template.font_face_rule();
1157
1158    // The steps in this algorithm are inverted order because they are listed in ascending order of precedence.
1159    let mut variations: Vec<FontVariation> = vec![];
1160
1161    let mut add_variation = |variation: FontVariation| {
1162        if !variations
1163            .iter()
1164            .any(|existing_variation| existing_variation.tag == variation.tag)
1165        {
1166            variations.push(variation);
1167        }
1168    };
1169
1170    // Step 12. Font variations implied by the value of the font-variation-settings property are applied.
1171    // These values should be clamped to the values that are supported by the font.
1172    // NOTE: Clamping happens inside the PlatformFont.
1173    descriptor
1174        .variation_settings
1175        .iter()
1176        .copied()
1177        .for_each(&mut add_variation);
1178
1179    // Step 9. Font variations implied by the value of the font-optical-sizing property are applied.
1180    // NOTE The precise behaviour of font-optical-sizing:auto is not defined.
1181    // We choose to set "opsz" to the font size if it's not already set elsewhere. This is the easiest
1182    // at the end of this function, so we move this step down.
1183
1184    if let Some(font_face_rule) = &font_face_rule {
1185        // Step 6. If the font is defined via an @font-face rule, the font variations implied by the font-variation-settings
1186        // descriptor in the @font-face rule are applied.
1187        if let Some(variation_settings) =
1188            font_face_rule.descriptors.font_variation_settings.as_ref()
1189        {
1190            variation_settings
1191                .0
1192                .iter()
1193                .map(|variation| FontVariation {
1194                    tag: variation.tag.0,
1195                    value: variation.value.get().expect(
1196                        "The value is enforced to be resolvable at parse time \
1197                        (see FontVariationSettings::parse_for_font_face_rule).",
1198                    ),
1199                })
1200                .for_each(&mut add_variation);
1201        }
1202    }
1203
1204    // Step 2. Font variations as enabled by the font-weight, font-width, and font-style properties are applied.
1205    //
1206    // The application of the value enabled by font-style is affected by font selection, because this property might
1207    // select an italic or an oblique font. The value applied is the closest matching value as determined by the font
1208    // matching algorithm. User agents must apply at most one value due to the font-style property; both "ital" and
1209    // "slnt" values must not be set together.
1210    //
1211    // If the selected font is defined in an @font-face rule, then the values applied at this step should be clamped
1212    // to the value of the font-weight, font-width, and font-style descriptors in that @font-face rule.
1213    // TODO: Clamp weight/width to the descriptors from the @font-face rule, if any
1214    add_variation(FontVariation {
1215        tag: Tag::new(b"wght").to_u32(),
1216        value: descriptor.weight.value(),
1217    });
1218
1219    add_variation(FontVariation {
1220        tag: Tag::new(b"wdth").to_u32(),
1221        value: descriptor.width.0.to_float(),
1222    });
1223
1224    if variation_axes.intersects(VariationAxes::ITAL | VariationAxes::SLNT) {
1225        let clamped_font_style = font_face_rule
1226            .as_ref()
1227            .and_then(|font_face_rule| font_face_rule.descriptors.font_style.as_ref())
1228            .map(|font_style_range| {
1229                let computed_font_style_range =
1230                    font_style_range.compute().expect("never returns None");
1231
1232                if computed_font_style_range.0 == FontStyle::ITALIC {
1233                    debug_assert_eq!(computed_font_style_range.1, FontStyle::ITALIC);
1234                    return FontStyle::ITALIC;
1235                }
1236                debug_assert_ne!(computed_font_style_range.1, FontStyle::ITALIC);
1237
1238                let specified_angle = if descriptor.style == FontStyle::ITALIC {
1239                    FontStyle::DEFAULT_OBLIQUE_DEGREES as f32
1240                } else {
1241                    descriptor.style.oblique_degrees()
1242                };
1243
1244                let clamped_angle = specified_angle
1245                    .min(computed_font_style_range.1.oblique_degrees())
1246                    .max(computed_font_style_range.0.oblique_degrees());
1247                FontStyle::oblique(clamped_angle)
1248            })
1249            .unwrap_or(descriptor.style);
1250
1251        // TODO: We should recognize when a font has neither a ital nor a slnt axis and then
1252        // synthesize an appropriate font face if allowed by font-synthesis.
1253
1254        // When both a `ital` and a `slnt` axis are available then we prefer `ital` for
1255        // `font-style: italic` and `slnt` for `font-style: oblique`.
1256        let use_ital_axis = (variation_axes.contains(VariationAxes::ITAL) &&
1257            clamped_font_style == FontStyle::ITALIC) ||
1258            !variation_axes.contains(VariationAxes::SLNT);
1259        if use_ital_axis {
1260            add_variation(FontVariation {
1261                tag: ITAL.to_u32(),
1262                value: (clamped_font_style != FontStyle::NORMAL) as u32 as f32,
1263            });
1264        } else {
1265            // Note: CSS and OpenType measure slnt in opposite directions, so we need to negate the
1266            // angles.
1267            if clamped_font_style == FontStyle::ITALIC {
1268                add_variation(FontVariation {
1269                    tag: SLNT.to_u32(),
1270                    value: (-FontStyle::DEFAULT_OBLIQUE_DEGREES) as f32,
1271                });
1272            } else {
1273                add_variation(FontVariation {
1274                    tag: SLNT.to_u32(),
1275                    value: -clamped_font_style.oblique_degrees(),
1276                });
1277            }
1278        }
1279    }
1280
1281    // This is the implementation for Step 9. Refer to the note on Step 9 for an explanation of why it's here.
1282    if descriptor.optical_sizing == FontOpticalSizing::Auto {
1283        add_variation(FontVariation {
1284            tag: Tag::new(b"opsz").to_u32(),
1285            value: descriptor.pt_size.to_f32_px(),
1286        });
1287    }
1288
1289    variations
1290}
1291
1292bitflags! {
1293    #[derive(Clone, Copy, Debug)]
1294    struct VariationAxes: u8 {
1295        /// Whether the font has any variation axes at all.
1296        const IS_VARIABLE = 1;
1297
1298        /// Whether the font as a `ital` axis.
1299        ///
1300        /// Never set without `IS_VARIABLE`.
1301        const ITAL = 1 << 1;
1302
1303        /// Whether the font as a `slnt` axis.
1304        ///
1305        /// Never set without `IS_VARIABLE`.
1306        const SLNT = 1 << 2;
1307    }
1308}
1309
1310impl VariationAxes {
1311    fn from_platform_font(platform_font: &PlatformFont) -> Self {
1312        let Some(fvar_table) = platform_font.table_for_tag(FVAR) else {
1313            // This is not a variable font.
1314            return VariationAxes::empty();
1315        };
1316
1317        let Some(variation_axes) = fvar_table
1318            .parse_as_specific_table::<Fvar<'_>>()
1319            .ok()
1320            .and_then(|fvar: Fvar<'_>| fvar.axis_instance_arrays().ok())
1321            .map(|instance_arrays| instance_arrays.axes())
1322        else {
1323            // The fvar table is malformed.
1324            return VariationAxes::empty();
1325        };
1326
1327        if variation_axes.is_empty() {
1328            return VariationAxes::empty();
1329        }
1330
1331        let mut result = VariationAxes::IS_VARIABLE;
1332        for axis in variation_axes {
1333            match axis.axis_tag() {
1334                ITAL => result |= VariationAxes::ITAL,
1335                SLNT => result |= VariationAxes::SLNT,
1336                _ => {},
1337            }
1338        }
1339
1340        result
1341    }
1342}