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::atomic::{AtomicUsize, Ordering};
10use std::sync::{Arc, OnceLock};
11use std::time::Instant;
12use std::{iter, str};
13
14use app_units::Au;
15use bitflags::bitflags;
16use euclid::default::{Point2D, Rect};
17use euclid::num::Zero;
18use fonts_traits::FontDescriptor;
19use icu_locid::subtags::Language;
20use log::debug;
21use malloc_size_of_derive::MallocSizeOf;
22use parking_lot::RwLock;
23use read_fonts::tables::os2::{Os2, SelectionFlags};
24use read_fonts::types::Tag;
25use rustc_hash::FxHashMap;
26use serde::{Deserialize, Serialize};
27use servo_base::id::PainterId;
28use servo_base::text::{UnicodeBlock, UnicodeBlockMethod};
29use smallvec::SmallVec;
30use style::computed_values::font_variant_caps;
31use style::properties::style_structs::Font as FontStyleStruct;
32use style::values::computed::font::{
33    FamilyName, FontFamilyNameSyntax, GenericFontFamily, SingleFontFamily,
34};
35use style::values::computed::{FontStretch, FontStyle, FontSynthesis, FontWeight};
36use unicode_script::Script;
37use webrender_api::{FontInstanceFlags, FontInstanceKey, FontVariation};
38
39use crate::platform::font::{FontTable, PlatformFont};
40use crate::platform::font_list::fallback_font_families;
41use crate::{
42    EmojiPresentationPreference, FallbackFontSelectionOptions, FontContext, FontData,
43    FontDataAndIndex, FontDataError, FontIdentifier, FontTemplateDescriptor, FontTemplateRef,
44    FontTemplateRefMethods, GlyphId, LocalFontIdentifier, ShapedGlyph, ShapedText, Shaper,
45};
46
47pub(crate) const GPOS: Tag = Tag::new(b"GPOS");
48pub(crate) const GSUB: Tag = Tag::new(b"GSUB");
49pub(crate) const KERN: Tag = Tag::new(b"kern");
50pub(crate) const SBIX: Tag = Tag::new(b"sbix");
51pub(crate) const CBDT: Tag = Tag::new(b"CBDT");
52pub(crate) const COLR: Tag = Tag::new(b"COLR");
53pub(crate) const BASE: Tag = Tag::new(b"BASE");
54pub(crate) const LIGA: Tag = Tag::new(b"liga");
55
56pub const LAST_RESORT_GLYPH_ADVANCE: FractionalPixel = 10.0;
57
58/// Nanoseconds spent shaping text across all layout threads.
59static TEXT_SHAPING_PERFORMANCE_COUNTER: AtomicUsize = AtomicUsize::new(0);
60
61// PlatformFont encapsulates access to the platform's font API,
62// e.g. quartz, FreeType. It provides access to metrics and tables
63// needed by the text shaper as well as access to the underlying font
64// resources needed by the graphics layer to draw glyphs.
65
66pub trait PlatformFontMethods: Sized {
67    #[servo_tracing::instrument(name = "PlatformFontMethods::new_from_template", skip_all)]
68    fn new_from_template(
69        template: FontTemplateRef,
70        pt_size: Option<Au>,
71        variations: &[FontVariation],
72        data: &Option<FontData>,
73        synthetic_bold: bool,
74    ) -> Result<PlatformFont, &'static str> {
75        let template = template.borrow();
76        let font_identifier = template.identifier.clone();
77
78        match font_identifier {
79            FontIdentifier::Local(font_identifier) => Self::new_from_local_font_identifier(
80                font_identifier,
81                pt_size,
82                variations,
83                synthetic_bold,
84            ),
85            FontIdentifier::Web(_) | FontIdentifier::ArrayBuffer(_) => Self::new_from_data(
86                font_identifier,
87                data.as_ref()
88                    .expect("Should never create a web font without data."),
89                pt_size,
90                variations,
91                synthetic_bold,
92            ),
93        }
94    }
95
96    fn new_from_local_font_identifier(
97        font_identifier: LocalFontIdentifier,
98        pt_size: Option<Au>,
99        variations: &[FontVariation],
100        synthetic_bold: bool,
101    ) -> Result<PlatformFont, &'static str>;
102
103    fn new_from_data(
104        font_identifier: FontIdentifier,
105        data: &FontData,
106        pt_size: Option<Au>,
107        variations: &[FontVariation],
108        synthetic_bold: bool,
109    ) -> Result<PlatformFont, &'static str>;
110
111    /// Get a [`FontTemplateDescriptor`] from a [`PlatformFont`]. This is used to get
112    /// descriptors for web fonts.
113    fn descriptor(&self) -> FontTemplateDescriptor;
114
115    fn glyph_index(&self, codepoint: char) -> Option<GlyphId>;
116    fn glyph_h_advance(&self, _: GlyphId) -> Option<FractionalPixel>;
117    fn glyph_h_kerning(&self, glyph0: GlyphId, glyph1: GlyphId) -> FractionalPixel;
118
119    fn metrics(&self) -> FontMetrics;
120    fn table_for_tag(&self, _: Tag) -> Option<FontTable>;
121    fn typographic_bounds(&self, _: GlyphId) -> Rect<f32>;
122
123    /// Get the necessary [`FontInstanceFlags`]` for this font.
124    fn webrender_font_instance_flags(&self) -> FontInstanceFlags;
125
126    /// Return all the variation values that the font was instantiated with.
127    fn variations(&self) -> &[FontVariation];
128
129    fn descriptor_from_os2_table(os2: &Os2) -> FontTemplateDescriptor {
130        let mut style = FontStyle::NORMAL;
131        if os2.fs_selection().contains(SelectionFlags::ITALIC) {
132            style = FontStyle::ITALIC;
133        }
134
135        let weight = FontWeight::from_float(os2.us_weight_class() as f32);
136        let stretch = match os2.us_width_class() {
137            1 => FontStretch::ULTRA_CONDENSED,
138            2 => FontStretch::EXTRA_CONDENSED,
139            3 => FontStretch::CONDENSED,
140            4 => FontStretch::SEMI_CONDENSED,
141            5 => FontStretch::NORMAL,
142            6 => FontStretch::SEMI_EXPANDED,
143            7 => FontStretch::EXPANDED,
144            8 => FontStretch::EXTRA_EXPANDED,
145            9 => FontStretch::ULTRA_EXPANDED,
146            _ => FontStretch::NORMAL,
147        };
148
149        FontTemplateDescriptor::new(weight, stretch, style)
150    }
151}
152
153// Used to abstract over the shaper's choice of fixed int representation.
154pub(crate) type FractionalPixel = f64;
155
156pub(crate) trait FontTableMethods {
157    fn buffer(&self) -> &[u8];
158}
159
160#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
161pub struct FontMetrics {
162    pub underline_size: Au,
163    pub underline_offset: Au,
164    pub strikeout_size: Au,
165    pub strikeout_offset: Au,
166    pub leading: Au,
167    pub x_height: Au,
168    pub em_size: Au,
169    pub ascent: Au,
170    pub descent: Au,
171    pub max_advance: Au,
172    pub average_advance: Au,
173    pub line_gap: Au,
174    pub zero_horizontal_advance: Option<Au>,
175    pub ic_horizontal_advance: Option<Au>,
176    /// The advance of the space character (' ') in this font or if there is no space,
177    /// the average char advance.
178    pub space_advance: Au,
179}
180
181impl FontMetrics {
182    /// Create an empty [`FontMetrics`] mainly to be used in situations where
183    /// no font can be found.
184    pub fn empty() -> Arc<Self> {
185        static EMPTY: OnceLock<Arc<FontMetrics>> = OnceLock::new();
186        EMPTY.get_or_init(Default::default).clone()
187    }
188
189    /// Whether or not the block metrics of the two `FontMetrics` instances differ in a way
190    /// that requires the resulting block size of a containing inline box to change.
191    pub fn block_metrics_meaningfully_differ(&self, other: &Self) -> bool {
192        self.ascent != other.ascent ||
193            self.descent != other.descent ||
194            self.line_gap != other.line_gap
195    }
196}
197
198#[derive(Debug, Default)]
199struct CachedShapeData {
200    glyph_advances: HashMap<GlyphId, FractionalPixel>,
201    glyph_indices: HashMap<char, Option<GlyphId>>,
202    shaped_text: HashMap<ShapeCacheEntry, Arc<ShapedText>>,
203}
204
205impl malloc_size_of::MallocSizeOf for CachedShapeData {
206    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
207        // Estimate the size of the shaped text cache. This will be smaller, because
208        // HashMap has some overhead, but we are mainly interested in the actual data.
209        let shaped_text_size = self
210            .shaped_text
211            .iter()
212            .map(|(key, value)| key.size_of(ops) + (*value).size_of(ops))
213            .sum::<usize>();
214        self.glyph_advances.size_of(ops) + self.glyph_indices.size_of(ops) + shaped_text_size
215    }
216}
217
218pub struct Font {
219    pub(crate) handle: PlatformFont,
220    pub(crate) template: FontTemplateRef,
221    pub metrics: Arc<FontMetrics>,
222    pub descriptor: FontDescriptor,
223
224    /// The data for this font. And the index of the font within the data (in case it's a TTC)
225    /// This might be uninitialized for system fonts.
226    data_and_index: OnceLock<FontDataAndIndex>,
227
228    shaper: OnceLock<Shaper>,
229    cached_shape_data: RwLock<CachedShapeData>,
230    font_instance_key: RwLock<FxHashMap<PainterId, FontInstanceKey>>,
231
232    /// If this is a synthesized small caps font, then this font reference is for
233    /// the version of the font used to replace lowercase ASCII letters. It's up
234    /// to the consumer of this font to properly use this reference.
235    pub(crate) synthesized_small_caps: Option<FontRef>,
236
237    /// Whether or not this font supports color bitmaps or a COLR table. This is
238    /// essentially equivalent to whether or not we use it for emoji presentation.
239    /// This is cached, because getting table data is expensive.
240    has_color_bitmap_or_colr_table: OnceLock<bool>,
241
242    /// Whether or not this font can do fast shaping, ie whether or not it has
243    /// a kern table, but no GSUB and GPOS tables. When this is true, Servo will
244    /// shape Latin horizontal left-to-right text without using Harfbuzz.
245    ///
246    /// FIXME: This should be removed entirely in favor of better caching if necessary.
247    /// See <https://github.com/servo/servo/pull/11273#issuecomment-222332873>.
248    can_do_fast_shaping: OnceLock<bool>,
249}
250
251impl std::fmt::Debug for Font {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.debug_struct("Font")
254            .field("template", &self.template)
255            .field("descriptor", &self.descriptor)
256            .finish()
257    }
258}
259
260impl malloc_size_of::MallocSizeOf for Font {
261    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
262        // TODO: Collect memory usage for platform fonts and for shapers.
263        // This skips the template, because they are already stored in the template cache.
264
265        self.metrics.size_of(ops) +
266            self.descriptor.size_of(ops) +
267            self.cached_shape_data.read().size_of(ops) +
268            self.font_instance_key
269                .read()
270                .values()
271                .map(|key| key.size_of(ops))
272                .sum::<usize>()
273    }
274}
275
276impl Font {
277    pub fn new(
278        template: FontTemplateRef,
279        descriptor: FontDescriptor,
280        data: Option<FontData>,
281        synthesized_small_caps: Option<FontRef>,
282    ) -> Result<Font, &'static str> {
283        let synthetic_bold = {
284            let is_bold = descriptor.weight >= FontWeight::BOLD_THRESHOLD;
285            let allows_synthetic_bold = matches!(descriptor.synthesis_weight, FontSynthesis::Auto);
286
287            is_bold && allows_synthetic_bold
288        };
289
290        let handle = PlatformFont::new_from_template(
291            template.clone(),
292            Some(descriptor.pt_size),
293            &descriptor.variation_settings,
294            &data,
295            synthetic_bold,
296        )?;
297        let metrics = Arc::new(handle.metrics());
298
299        Ok(Font {
300            handle,
301            template,
302            metrics,
303            descriptor,
304            data_and_index: data
305                .map(|data| OnceLock::from(FontDataAndIndex { data, index: 0 }))
306                .unwrap_or_default(),
307            shaper: OnceLock::new(),
308            cached_shape_data: Default::default(),
309            font_instance_key: Default::default(),
310            synthesized_small_caps,
311            has_color_bitmap_or_colr_table: OnceLock::new(),
312            can_do_fast_shaping: OnceLock::new(),
313        })
314    }
315
316    /// A unique identifier for the font, allowing comparison.
317    pub fn identifier(&self) -> FontIdentifier {
318        self.template.identifier()
319    }
320
321    pub(crate) fn webrender_font_instance_flags(&self) -> FontInstanceFlags {
322        self.handle.webrender_font_instance_flags()
323    }
324
325    pub(crate) fn has_color_bitmap_or_colr_table(&self) -> bool {
326        *self.has_color_bitmap_or_colr_table.get_or_init(|| {
327            self.table_for_tag(SBIX).is_some() ||
328                self.table_for_tag(CBDT).is_some() ||
329                self.table_for_tag(COLR).is_some()
330        })
331    }
332
333    pub fn key(&self, painter_id: PainterId, font_context: &FontContext) -> FontInstanceKey {
334        *self
335            .font_instance_key
336            .write()
337            .entry(painter_id)
338            .or_insert_with(|| font_context.create_font_instance_key(self, painter_id))
339    }
340
341    /// Return the data for this `Font`. Note that this is currently highly inefficient for system
342    /// fonts and should not be used except in legacy canvas code.
343    pub fn font_data_and_index(&self) -> Result<&FontDataAndIndex, FontDataError> {
344        if let Some(data_and_index) = self.data_and_index.get() {
345            return Ok(data_and_index);
346        }
347
348        let FontIdentifier::Local(local_font_identifier) = self.identifier() else {
349            unreachable!("All web fonts should already have initialized data");
350        };
351        let Some(data_and_index) = local_font_identifier.font_data_and_index() else {
352            return Err(FontDataError::FailedToLoad);
353        };
354
355        let data_and_index = self.data_and_index.get_or_init(move || data_and_index);
356        Ok(data_and_index)
357    }
358
359    pub(crate) fn variations(&self) -> &[FontVariation] {
360        self.handle.variations()
361    }
362}
363
364bitflags! {
365    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
366    pub struct ShapingFlags: u8 {
367        /// Set if the text is entirely whitespace.
368        const IS_WHITESPACE_SHAPING_FLAG = 1 << 0;
369        /// Set if the text ends with whitespace.
370        const ENDS_WITH_WHITESPACE_SHAPING_FLAG = 1 << 1;
371        /// Set if we are to ignore ligatures.
372        const IGNORE_LIGATURES_SHAPING_FLAG = 1 << 2;
373        /// Set if we are to disable kerning.
374        const DISABLE_KERNING_SHAPING_FLAG = 1 << 3;
375        /// Text direction is right-to-left.
376        const RTL_FLAG = 1 << 4;
377        /// Set if word-break is set to keep-all.
378        const KEEP_ALL_FLAG = 1 << 5;
379    }
380}
381
382/// Various options that control text shaping.
383#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
384pub struct ShapingOptions {
385    /// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
386    /// NB: You will probably want to set the `IGNORE_LIGATURES_SHAPING_FLAG` if this is non-null.
387    ///
388    /// Letter spacing is not applied to all characters. Use [Self::letter_spacing_for_character] to
389    /// determine the amount of spacing to apply.
390    pub letter_spacing: Option<Au>,
391    /// Spacing to add between each word. Corresponds to the CSS 2.1 `word-spacing` property.
392    pub word_spacing: Option<Au>,
393    /// The Unicode script property of the characters in this run.
394    pub script: Script,
395    /// The preferred language, obtained from the `lang` attribute.
396    pub language: Language,
397    /// Various flags.
398    pub flags: ShapingFlags,
399}
400
401impl ShapingOptions {
402    pub(crate) fn letter_spacing_for_character(&self, character: char) -> Option<Au> {
403        // https://drafts.csswg.org/css-text/#letter-spacing-property
404        // Letter spacing ignores invisible zero-width formatting characters (such as those from the Unicode Cf category).
405        // Spacing must be added as if those characters did not exist in the document.
406        self.letter_spacing.filter(|_| {
407            icu_properties::maps::general_category().get(character) !=
408                icu_properties::GeneralCategory::Format
409        })
410    }
411}
412
413/// An entry in the shape cache.
414#[derive(Clone, Debug, Eq, Hash, PartialEq)]
415struct ShapeCacheEntry {
416    text: String,
417    options: ShapingOptions,
418}
419
420impl Font {
421    pub fn shape_text(&self, text: &str, options: &ShapingOptions) -> Arc<ShapedText> {
422        let lookup_key = ShapeCacheEntry {
423            text: text.to_owned(),
424            options: *options,
425        };
426        {
427            let cache = self.cached_shape_data.read();
428            if let Some(shaped_text) = cache.shaped_text.get(&lookup_key) {
429                return shaped_text.clone();
430            }
431        }
432
433        let start_time = Instant::now();
434        let glyphs = if self.can_do_fast_shaping(text, options) {
435            debug!("shape_text: Using ASCII fast path.");
436            self.shape_text_fast(text, options)
437        } else {
438            debug!("shape_text: Using Harfbuzz.");
439            self.shaper
440                .get_or_init(|| Shaper::new(self))
441                .shape_text(text, options)
442        };
443
444        let shaped_text = Arc::new(glyphs);
445        let mut cache = self.cached_shape_data.write();
446        cache.shaped_text.insert(lookup_key, shaped_text.clone());
447
448        TEXT_SHAPING_PERFORMANCE_COUNTER.fetch_add(
449            ((Instant::now() - start_time).as_nanos()) as usize,
450            Ordering::Relaxed,
451        );
452
453        shaped_text
454    }
455
456    /// Whether not a particular text and [`ShapingOptions`] combination can use
457    /// "fast shaping" ie shaping without Harfbuzz.
458    ///
459    /// Note: This will eventually be removed.
460    pub fn can_do_fast_shaping(&self, text: &str, options: &ShapingOptions) -> bool {
461        options.script == Script::Latin &&
462            !options.flags.contains(ShapingFlags::RTL_FLAG) &&
463            *self.can_do_fast_shaping.get_or_init(|| {
464                self.table_for_tag(KERN).is_some() &&
465                    self.table_for_tag(GPOS).is_none() &&
466                    self.table_for_tag(GSUB).is_none()
467            }) &&
468            text.is_ascii()
469    }
470
471    /// Fast path for ASCII text that only needs simple horizontal LTR kerning.
472    fn shape_text_fast(&self, text: &str, options: &ShapingOptions) -> ShapedText {
473        let mut glyph_store = ShapedText::new(text.len(), options);
474        let mut prev_glyph_id = None;
475        for (string_byte_offset, byte) in text.bytes().enumerate() {
476            let character = byte as char;
477            let Some(glyph_id) = self.glyph_index(character) else {
478                continue;
479            };
480
481            let mut advance = Au::from_f64_px(self.glyph_h_advance(glyph_id));
482            let offset = prev_glyph_id.map(|prev| {
483                let h_kerning = Au::from_f64_px(self.glyph_h_kerning(prev, glyph_id));
484                advance += h_kerning;
485                Point2D::new(h_kerning, Au::zero())
486            });
487
488            let mut glyph = ShapedGlyph {
489                glyph_id,
490                string_byte_offset,
491                advance,
492                offset,
493            };
494            glyph.adjust_for_character(character, options);
495
496            glyph_store.add_glyph(character, &glyph);
497            prev_glyph_id = Some(glyph_id);
498        }
499        glyph_store
500    }
501
502    pub(crate) fn table_for_tag(&self, tag: Tag) -> Option<FontTable> {
503        let result = self.handle.table_for_tag(tag);
504        let status = if result.is_some() {
505            "Found"
506        } else {
507            "Didn't find"
508        };
509
510        debug!(
511            "{} font table[{}] in {:?},",
512            status,
513            str::from_utf8(tag.as_ref()).unwrap(),
514            self.identifier()
515        );
516        result
517    }
518
519    #[inline]
520    pub fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
521        {
522            let cache = self.cached_shape_data.read();
523            if let Some(glyph) = cache.glyph_indices.get(&codepoint) {
524                return *glyph;
525            }
526        }
527        let codepoint = match self.descriptor.variant {
528            font_variant_caps::T::SmallCaps => codepoint.to_ascii_uppercase(),
529            font_variant_caps::T::Normal => codepoint,
530        };
531        let glyph_index = self.handle.glyph_index(codepoint);
532
533        let mut cache = self.cached_shape_data.write();
534        cache.glyph_indices.insert(codepoint, glyph_index);
535        glyph_index
536    }
537
538    pub(crate) fn has_glyph_for(&self, codepoint: char) -> bool {
539        self.glyph_index(codepoint).is_some()
540    }
541
542    pub(crate) fn glyph_h_kerning(
543        &self,
544        first_glyph: GlyphId,
545        second_glyph: GlyphId,
546    ) -> FractionalPixel {
547        self.handle.glyph_h_kerning(first_glyph, second_glyph)
548    }
549
550    pub fn glyph_h_advance(&self, glyph_id: GlyphId) -> FractionalPixel {
551        {
552            let cache = self.cached_shape_data.read();
553            if let Some(width) = cache.glyph_advances.get(&glyph_id) {
554                return *width;
555            }
556        }
557
558        let new_width = self
559            .handle
560            .glyph_h_advance(glyph_id)
561            .unwrap_or(LAST_RESORT_GLYPH_ADVANCE as FractionalPixel);
562        let mut cache = self.cached_shape_data.write();
563        cache.glyph_advances.insert(glyph_id, new_width);
564        new_width
565    }
566
567    pub fn typographic_bounds(&self, glyph_id: GlyphId) -> Rect<f32> {
568        self.handle.typographic_bounds(glyph_id)
569    }
570
571    /// Get the [`FontBaseline`] for this font.
572    pub fn baseline(&self) -> Option<FontBaseline> {
573        self.shaper.get_or_init(|| Shaper::new(self)).baseline()
574    }
575
576    #[cfg(not(target_os = "macos"))]
577    pub(crate) fn find_fallback_using_system_font_api(
578        &self,
579        _: &FallbackFontSelectionOptions,
580    ) -> Option<FontRef> {
581        None
582    }
583}
584
585#[derive(Clone, Debug, MallocSizeOf)]
586pub struct FontRef(#[conditional_malloc_size_of] pub(crate) Arc<Font>);
587
588impl Deref for FontRef {
589    type Target = Arc<Font>;
590    fn deref(&self) -> &Self::Target {
591        &self.0
592    }
593}
594
595#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
596pub struct FallbackKey {
597    script: Script,
598    unicode_block: Option<UnicodeBlock>,
599    language: Language,
600}
601
602impl FallbackKey {
603    fn new(options: &FallbackFontSelectionOptions) -> Self {
604        Self {
605            script: Script::from(options.character),
606            unicode_block: options.character.block(),
607            language: options.language,
608        }
609    }
610}
611
612/// A `FontGroup` is a prioritised list of fonts for a given set of font styles. It is used by
613/// `TextRun` to decide which font to render a character with. If none of the fonts listed in the
614/// styles are suitable, a fallback font may be used.
615#[derive(MallocSizeOf)]
616pub struct FontGroup {
617    /// The [`FontDescriptor`] which describes the properties of the fonts that should
618    /// be loaded for this [`FontGroup`].
619    descriptor: FontDescriptor,
620    /// The families that have been loaded for this [`FontGroup`]. This correponds to the
621    /// list of fonts specified in CSS.
622    families: SmallVec<[FontGroupFamily; 8]>,
623    /// A list of fallbacks that have been used in this [`FontGroup`]. Currently this
624    /// can grow indefinitely, but maybe in the future it should be an LRU cache.
625    /// It's unclear if this is the right thing to do. Perhaps fallbacks should
626    /// always be stored here as it's quite likely that they will be used again.
627    fallbacks: RwLock<HashMap<FallbackKey, FontRef>>,
628}
629
630impl FontGroup {
631    pub(crate) fn new(style: &FontStyleStruct, descriptor: FontDescriptor) -> FontGroup {
632        let families: SmallVec<[FontGroupFamily; 8]> = style
633            .font_family
634            .families
635            .iter()
636            .map(FontGroupFamily::local_or_web)
637            .collect();
638
639        FontGroup {
640            descriptor,
641            families,
642            fallbacks: Default::default(),
643        }
644    }
645
646    /// Finds the first font, or else the first fallback font, which contains a glyph for
647    /// `codepoint`. If no such font is found, returns the first available font or fallback font
648    /// (which will cause a "glyph not found" character to be rendered). If no font at all can be
649    /// found, returns None.
650    pub fn find_by_codepoint(
651        &self,
652        font_context: &FontContext,
653        codepoint: char,
654        next_codepoint: Option<char>,
655        language: Language,
656    ) -> Option<FontRef> {
657        // Tab characters are converted into spaces when rendering.
658        // TODO: We should not render a tab character. Instead they should be converted into tab stops
659        // based upon the width of a space character in inline formatting contexts.
660        let codepoint = match codepoint {
661            '\t' => ' ',
662            _ => codepoint,
663        };
664
665        let options = FallbackFontSelectionOptions::new(codepoint, next_codepoint, language);
666
667        let should_look_for_small_caps = self.descriptor.variant == font_variant_caps::T::SmallCaps &&
668            options.character.is_ascii_lowercase();
669        let font_or_synthesized_small_caps = |font: FontRef| {
670            if should_look_for_small_caps && font.synthesized_small_caps.is_some() {
671                return font.synthesized_small_caps.clone();
672            }
673            Some(font)
674        };
675
676        let font_has_glyph_and_presentation = |font: &FontRef| {
677            // Do not select this font if it goes against our emoji preference.
678            match options.presentation_preference {
679                EmojiPresentationPreference::Text if font.has_color_bitmap_or_colr_table() => {
680                    return false;
681                },
682                EmojiPresentationPreference::Emoji if !font.has_color_bitmap_or_colr_table() => {
683                    return false;
684                },
685                _ => {},
686            }
687            font.has_glyph_for(options.character)
688        };
689
690        let char_in_template =
691            |template: FontTemplateRef| template.char_in_unicode_range(options.character);
692
693        if let Some(font) = self.find(
694            font_context,
695            &char_in_template,
696            &font_has_glyph_and_presentation,
697        ) {
698            return font_or_synthesized_small_caps(font);
699        }
700
701        let fallback_key = FallbackKey::new(&options);
702        if let Some(fallback) = self.fallbacks.read().get(&fallback_key) {
703            if char_in_template(fallback.template.clone()) &&
704                font_has_glyph_and_presentation(fallback)
705            {
706                return font_or_synthesized_small_caps(fallback.clone());
707            }
708        }
709
710        if let Some(font) = self.find_fallback_using_system_font_list(
711            font_context,
712            options.clone(),
713            &char_in_template,
714            &font_has_glyph_and_presentation,
715        ) {
716            let fallback = font_or_synthesized_small_caps(font);
717            if let Some(fallback) = fallback.clone() {
718                self.fallbacks.write().insert(fallback_key, fallback);
719            }
720            return fallback;
721        }
722
723        let first_font = self.first(font_context);
724        if let Some(fallback) = first_font
725            .as_ref()
726            .and_then(|font| font.find_fallback_using_system_font_api(&options))
727        {
728            if font_has_glyph_and_presentation(&fallback) {
729                return Some(fallback);
730            }
731        }
732
733        first_font
734    }
735
736    /// Find the first available font in the group, or the first available fallback font.
737    pub fn first(&self, font_context: &FontContext) -> Option<FontRef> {
738        // From https://drafts.csswg.org/css-fonts/#first-available-font:
739        // > The first available font, used for example in the definition of font-relative lengths
740        // > such as ex or in the definition of the line-height property, is defined to be the first
741        // > font for which the character U+0020 (space) is not excluded by a unicode-range, given the
742        // > font families in the font-family list (or a user agent’s default font if none are
743        // > available).
744        // > Note: it does not matter whether that font actually has a glyph for the space character.
745        let space_in_template = |template: FontTemplateRef| template.char_in_unicode_range(' ');
746        let font_predicate = |_: &FontRef| true;
747        self.find(font_context, &space_in_template, &font_predicate)
748            .or_else(|| {
749                self.find_fallback_using_system_font_list(
750                    font_context,
751                    FallbackFontSelectionOptions::default(),
752                    &space_in_template,
753                    &font_predicate,
754                )
755            })
756    }
757
758    /// Attempts to find a font which matches the given `template_predicate` and `font_predicate`.
759    /// This method mutates because we may need to load new font data in the process of finding
760    /// a suitable font.
761    fn find(
762        &self,
763        font_context: &FontContext,
764        template_predicate: &impl Fn(FontTemplateRef) -> bool,
765        font_predicate: &impl Fn(&FontRef) -> bool,
766    ) -> Option<FontRef> {
767        self.families
768            .iter()
769            .flat_map(|family| family.templates(font_context, &self.descriptor))
770            .find_map(|template| {
771                template.font_if_matches(
772                    font_context,
773                    &self.descriptor,
774                    template_predicate,
775                    font_predicate,
776                )
777            })
778    }
779
780    /// Attempts to find a suitable fallback font which matches the given `template_predicate` and
781    /// `font_predicate` using the system font list. The default family (i.e. "serif") will be tried
782    /// first, followed by platform-specific family names. If a `codepoint` is provided, then its
783    /// Unicode block may be used to refine
784    /// the list of family names which will be tried.
785    fn find_fallback_using_system_font_list(
786        &self,
787        font_context: &FontContext,
788        options: FallbackFontSelectionOptions,
789        template_predicate: &impl Fn(FontTemplateRef) -> bool,
790        font_predicate: &impl Fn(&FontRef) -> bool,
791    ) -> Option<FontRef> {
792        iter::once(FontFamilyDescriptor::default())
793            .chain(
794                fallback_font_families(options)
795                    .into_iter()
796                    .map(|family_name| {
797                        let family = SingleFontFamily::FamilyName(FamilyName {
798                            name: family_name.into(),
799                            syntax: FontFamilyNameSyntax::Quoted,
800                        });
801                        FontFamilyDescriptor::new(family, FontSearchScope::Local)
802                    }),
803            )
804            .find_map(|family_descriptor| {
805                FontGroupFamily::from(family_descriptor)
806                    .templates(font_context, &self.descriptor)
807                    .find_map(|template| {
808                        template.font_if_matches(
809                            font_context,
810                            &self.descriptor,
811                            template_predicate,
812                            font_predicate,
813                        )
814                    })
815            })
816    }
817}
818
819/// A [`FontGroupFamily`] can have multiple associated `FontTemplate`s if it is a
820/// "composite face", meaning that it is defined by multiple `@font-face`
821/// declarations which vary only by their `unicode-range` descriptors. In this case,
822/// font selection will select a single member that contains the necessary unicode
823/// character. Unicode ranges are specified by the [`FontGroupFamilyTemplate::template`]
824/// member.
825#[derive(MallocSizeOf)]
826struct FontGroupFamilyTemplate {
827    #[ignore_malloc_size_of = "This measured in the FontContext template cache."]
828    template: FontTemplateRef,
829    #[ignore_malloc_size_of = "This measured in the FontContext font cache."]
830    font: OnceLock<Option<FontRef>>,
831}
832
833impl From<FontTemplateRef> for FontGroupFamilyTemplate {
834    fn from(template: FontTemplateRef) -> Self {
835        Self {
836            template,
837            font: Default::default(),
838        }
839    }
840}
841
842impl FontGroupFamilyTemplate {
843    fn font(
844        &self,
845        font_context: &FontContext,
846        font_descriptor: &FontDescriptor,
847    ) -> Option<FontRef> {
848        self.font
849            .get_or_init(|| font_context.font(self.template.clone(), font_descriptor))
850            .clone()
851    }
852
853    fn font_if_matches(
854        &self,
855        font_context: &FontContext,
856        font_descriptor: &FontDescriptor,
857        template_predicate: &impl Fn(FontTemplateRef) -> bool,
858        font_predicate: &impl Fn(&FontRef) -> bool,
859    ) -> Option<FontRef> {
860        if !template_predicate(self.template.clone()) {
861            return None;
862        }
863        self.font(font_context, font_descriptor)
864            .filter(font_predicate)
865    }
866}
867
868/// A `FontGroupFamily` is a single font family in a `FontGroup`. It corresponds to one of the
869/// families listed in the `font-family` CSS property. The corresponding font data is lazy-loaded,
870/// only if actually needed. A single `FontGroupFamily` can have multiple fonts, in the case that
871/// individual fonts only cover part of the Unicode range.
872#[derive(MallocSizeOf)]
873struct FontGroupFamily {
874    family_descriptor: FontFamilyDescriptor,
875    members: OnceLock<Vec<FontGroupFamilyTemplate>>,
876}
877
878impl From<FontFamilyDescriptor> for FontGroupFamily {
879    fn from(family_descriptor: FontFamilyDescriptor) -> Self {
880        Self {
881            family_descriptor,
882            members: Default::default(),
883        }
884    }
885}
886
887impl FontGroupFamily {
888    fn local_or_web(family: &SingleFontFamily) -> FontGroupFamily {
889        FontFamilyDescriptor::new(family.clone(), FontSearchScope::Any).into()
890    }
891
892    fn templates(
893        &self,
894        font_context: &FontContext,
895        font_descriptor: &FontDescriptor,
896    ) -> impl Iterator<Item = &FontGroupFamilyTemplate> {
897        self.members
898            .get_or_init(|| {
899                font_context
900                    .matching_templates(font_descriptor, &self.family_descriptor)
901                    .into_iter()
902                    .map(Into::into)
903                    .collect()
904            })
905            .iter()
906    }
907}
908
909/// The scope within which we will look for a font.
910#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
911pub enum FontSearchScope {
912    /// All fonts will be searched, including those specified via `@font-face` rules.
913    Any,
914
915    /// Only local system fonts will be searched.
916    Local,
917}
918
919/// The font family parameters for font selection.
920#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
921pub struct FontFamilyDescriptor {
922    pub(crate) family: SingleFontFamily,
923    pub(crate) scope: FontSearchScope,
924}
925
926impl FontFamilyDescriptor {
927    pub fn new(family: SingleFontFamily, scope: FontSearchScope) -> FontFamilyDescriptor {
928        FontFamilyDescriptor { family, scope }
929    }
930
931    fn default() -> FontFamilyDescriptor {
932        FontFamilyDescriptor {
933            family: SingleFontFamily::Generic(GenericFontFamily::None),
934            scope: FontSearchScope::Local,
935        }
936    }
937}
938
939pub struct FontBaseline {
940    pub ideographic_baseline: f32,
941    pub alphabetic_baseline: f32,
942    pub hanging_baseline: f32,
943}
944
945/// Given a mapping array `mapping` and a value, map that value onto
946/// the value specified by the array. For instance, for FontConfig
947/// values of weights, we would map these onto the CSS [0..1000] range
948/// by creating an array as below. Values that fall between two mapped
949/// values, will be adjusted by the weighted mean.
950///
951/// ```rust
952/// let mapping = [
953///     (0., 0.),
954///     (FC_WEIGHT_REGULAR as f64, 400 as f64),
955///     (FC_WEIGHT_BOLD as f64, 700 as f64),
956///     (FC_WEIGHT_EXTRABLACK as f64, 1000 as f64),
957/// ];
958/// let mapped_weight = apply_font_config_to_style_mapping(&mapping, weight as f64);
959/// ```
960#[cfg(all(
961    any(target_os = "linux", target_os = "macos", target_os = "freebsd"),
962    not(target_env = "ohos")
963))]
964pub(crate) fn map_platform_values_to_style_values(mapping: &[(f64, f64)], value: f64) -> f64 {
965    if value < mapping[0].0 {
966        return mapping[0].1;
967    }
968
969    for window in mapping.windows(2) {
970        let (font_config_value_a, css_value_a) = window[0];
971        let (font_config_value_b, css_value_b) = window[1];
972
973        if value >= font_config_value_a && value <= font_config_value_b {
974            let ratio = (value - font_config_value_a) / (font_config_value_b - font_config_value_a);
975            return css_value_a + ((css_value_b - css_value_a) * ratio);
976        }
977    }
978
979    mapping[mapping.len() - 1].1
980}