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