fonts_traits/
font_template.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::fmt::{Debug, Error, Formatter};
6use std::ops::{Deref, RangeInclusive};
7use std::sync::Arc;
8
9use atomic_refcell::AtomicRefCell;
10use malloc_size_of_derive::MallocSizeOf;
11use serde::{Deserialize, Serialize};
12use style::computed_values::font_stretch::T as FontStretch;
13use style::computed_values::font_style::T as FontStyle;
14use style::stylesheets::DocumentStyleSheet;
15use style::values::computed::font::FontWeight;
16
17use crate::{CSSFontFaceDescriptors, ComputedFontStyleDescriptor, FontDescriptor, FontIdentifier};
18
19/// A reference to a [`FontTemplate`] with shared ownership and mutability.
20#[derive(Clone, Debug, MallocSizeOf)]
21pub struct FontTemplateRef(#[conditional_malloc_size_of] Arc<AtomicRefCell<FontTemplate>>);
22
23impl FontTemplateRef {
24    pub fn new(template: FontTemplate) -> Self {
25        Self(Arc::new(AtomicRefCell::new(template)))
26    }
27}
28
29impl Deref for FontTemplateRef {
30    type Target = Arc<AtomicRefCell<FontTemplate>>;
31    fn deref(&self) -> &Self::Target {
32        &self.0
33    }
34}
35
36/// Describes how to select a font from a given family. This is very basic at the moment and needs
37/// to be expanded or refactored when we support more of the font styling parameters.
38///
39/// NB: If you change this, you will need to update `style::properties::compute_font_hash()`.
40#[derive(Clone, Debug, Deserialize, Hash, MallocSizeOf, PartialEq, Serialize)]
41pub struct FontTemplateDescriptor {
42    pub weight: (FontWeight, FontWeight),
43    pub stretch: (FontStretch, FontStretch),
44    pub style: (FontStyle, FontStyle),
45    #[ignore_malloc_size_of = "MallocSizeOf does not yet support RangeInclusive"]
46    pub unicode_range: Option<Vec<RangeInclusive<u32>>>,
47}
48
49impl Default for FontTemplateDescriptor {
50    fn default() -> Self {
51        Self::new(FontWeight::normal(), FontStretch::NORMAL, FontStyle::NORMAL)
52    }
53}
54
55/// FontTemplateDescriptor contains floats, which are not Eq because of NaN. However,
56/// we know they will never be NaN, so we can manually implement Eq.
57impl Eq for FontTemplateDescriptor {}
58
59impl FontTemplateDescriptor {
60    #[inline]
61    pub fn new(weight: FontWeight, stretch: FontStretch, style: FontStyle) -> Self {
62        Self {
63            weight: (weight, weight),
64            stretch: (stretch, stretch),
65            style: (style, style),
66            unicode_range: None,
67        }
68    }
69
70    pub fn is_variation_font(&self) -> bool {
71        self.weight.0 != self.weight.1 ||
72            self.stretch.0 != self.stretch.1 ||
73            self.style.0 != self.style.1
74    }
75
76    /// Returns a score indicating how far apart visually the two font descriptors are. This is
77    /// used for implmenting the CSS Font Matching algorithm:
78    /// <https://drafts.csswg.org/css-fonts/#font-matching-algorithm>.
79    ///
80    /// The smaller the score, the better the fonts match. 0 indicates an exact match. This must
81    /// be commutative (distance(A, B) == distance(B, A)).
82    #[inline]
83    fn distance_from(&self, target: &FontDescriptor) -> f32 {
84        let stretch_distance = target.stretch.match_distance(&self.stretch);
85        let style_distance = target.style.match_distance(&self.style);
86        let weight_distance = target.weight.match_distance(&self.weight);
87
88        // Sanity-check that the distances are within the expected range
89        // (update if implementation of the distance functions is changed).
90        assert!((0.0..=2000.0).contains(&stretch_distance));
91        assert!((0.0..=500.0).contains(&style_distance));
92        assert!((0.0..=1600.0).contains(&weight_distance));
93
94        // Factors used to weight the distances between the available and target font
95        // properties during font-matching. These ensure that we respect the CSS-fonts
96        // requirement that font-stretch >> font-style >> font-weight; and in addition,
97        // a mismatch between the desired and actual glyph presentation (emoji vs text)
98        // will take precedence over any of the style attributes.
99        //
100        // Also relevant for font selection is the emoji presentation preference, but this
101        // is handled later when filtering fonts based on the glyphs they contain.
102        const STRETCH_FACTOR: f32 = 1.0e8;
103        const STYLE_FACTOR: f32 = 1.0e4;
104        const WEIGHT_FACTOR: f32 = 1.0e0;
105
106        stretch_distance * STRETCH_FACTOR +
107            style_distance * STYLE_FACTOR +
108            weight_distance * WEIGHT_FACTOR
109    }
110
111    fn matches(&self, descriptor_to_match: &FontDescriptor) -> bool {
112        self.weight.0 <= descriptor_to_match.weight &&
113            self.weight.1 >= descriptor_to_match.weight &&
114            self.style.0 <= descriptor_to_match.style &&
115            self.style.1 >= descriptor_to_match.style &&
116            self.stretch.0 <= descriptor_to_match.stretch &&
117            self.stretch.1 >= descriptor_to_match.stretch
118    }
119
120    pub fn override_values_with_css_font_template_descriptors(
121        &mut self,
122        css_font_template_descriptors: &CSSFontFaceDescriptors,
123    ) {
124        if let Some(weight) = css_font_template_descriptors.weight {
125            self.weight = weight;
126        }
127        self.style = match css_font_template_descriptors.style {
128            Some(ComputedFontStyleDescriptor::Italic) => (FontStyle::ITALIC, FontStyle::ITALIC),
129            Some(ComputedFontStyleDescriptor::Oblique(angle_1, angle_2)) => (
130                FontStyle::oblique(angle_1.to_float()),
131                FontStyle::oblique(angle_2.to_float()),
132            ),
133            None => self.style,
134        };
135        if let Some(stretch) = css_font_template_descriptors.stretch {
136            self.stretch = stretch;
137        }
138        if let Some(ref unicode_range) = css_font_template_descriptors.unicode_range {
139            self.unicode_range = Some(unicode_range.clone());
140        }
141    }
142}
143
144/// This describes all the information needed to create
145/// font instance handles. It contains a unique
146/// FontTemplateData structure that is platform specific.
147#[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
148pub struct FontTemplate {
149    pub identifier: FontIdentifier,
150    pub descriptor: FontTemplateDescriptor,
151    /// If this font is a web font, this is a reference to the stylesheet that
152    /// created it. This will be used to remove this font from caches, when the
153    /// stylesheet is removed.
154    ///
155    /// This is not serialized, as it's only useful in the [`super::FontContext`]
156    /// that it is created in.
157    #[serde(skip)]
158    pub stylesheet: Option<DocumentStyleSheet>,
159}
160
161impl Debug for FontTemplate {
162    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
163        self.identifier.fmt(f)
164    }
165}
166
167/// Holds all of the template information for a font that
168/// is common, regardless of the number of instances of
169/// this font handle per thread.
170impl FontTemplate {
171    /// Create a new [`FontTemplate`].
172    pub fn new(
173        identifier: FontIdentifier,
174        descriptor: FontTemplateDescriptor,
175        stylesheet: Option<DocumentStyleSheet>,
176    ) -> FontTemplate {
177        FontTemplate {
178            identifier,
179            descriptor,
180            stylesheet,
181        }
182    }
183
184    /// Create a new [`FontTemplate`] for a `@font-family` with a `local(...)` `src`. This takes in
185    /// the template of the local font and creates a new one that reflects the properties specified
186    /// by `@font-family` in the stylesheet.
187    pub fn new_for_local_web_font(
188        local_template: FontTemplateRef,
189        css_font_template_descriptors: &CSSFontFaceDescriptors,
190        stylesheet: Option<DocumentStyleSheet>,
191    ) -> Result<FontTemplate, &'static str> {
192        let mut alias_template = local_template.borrow().clone();
193        alias_template
194            .descriptor
195            .override_values_with_css_font_template_descriptors(css_font_template_descriptors);
196        alias_template.stylesheet = stylesheet;
197        Ok(alias_template)
198    }
199
200    pub fn identifier(&self) -> &FontIdentifier {
201        &self.identifier
202    }
203}
204
205pub trait FontTemplateRefMethods {
206    /// Get the descriptor.
207    fn descriptor(&self) -> FontTemplateDescriptor;
208    /// Get the [`FontIdentifier`] for this template.
209    fn identifier(&self) -> FontIdentifier;
210    /// Returns true if the given descriptor matches the one in this [`FontTemplate`].
211    fn matches_font_descriptor(&self, descriptor_to_match: &FontDescriptor) -> bool;
212    /// Calculate the distance from this [`FontTemplate`]s descriptor and return it
213    /// or None if this is not a valid [`FontTemplate`].
214    fn descriptor_distance(&self, descriptor_to_match: &FontDescriptor) -> f32;
215    /// Whether or not this character is in the unicode ranges specified in
216    /// this temlates `@font-face` definition, if any.
217    fn char_in_unicode_range(&self, character: char) -> bool;
218}
219
220impl FontTemplateRefMethods for FontTemplateRef {
221    fn descriptor(&self) -> FontTemplateDescriptor {
222        self.borrow().descriptor.clone()
223    }
224
225    fn identifier(&self) -> FontIdentifier {
226        self.borrow().identifier.clone()
227    }
228
229    fn matches_font_descriptor(&self, descriptor_to_match: &FontDescriptor) -> bool {
230        self.descriptor().matches(descriptor_to_match)
231    }
232
233    fn descriptor_distance(&self, descriptor_to_match: &FontDescriptor) -> f32 {
234        self.descriptor().distance_from(descriptor_to_match)
235    }
236
237    fn char_in_unicode_range(&self, character: char) -> bool {
238        let character = character as u32;
239        self.borrow()
240            .descriptor
241            .unicode_range
242            .as_ref()
243            .is_none_or(|ranges| ranges.iter().any(|range| range.contains(&character)))
244    }
245}
246
247/// A trait for implementing the CSS font matching algorithm against various font features.
248/// See <https://drafts.csswg.org/css-fonts/#font-matching-algorithm>.
249///
250/// This implementation is ported from Gecko at:
251/// <https://searchfox.org/mozilla-central/rev/0529464f0d2981347ef581f7521ace8b7af7f7ac/gfx/thebes/gfxFontUtils.h#1217>.
252trait FontMatchDistanceMethod: Sized {
253    fn match_distance(&self, range: &(Self, Self)) -> f32;
254    fn to_float(&self) -> f32;
255}
256
257impl FontMatchDistanceMethod for FontStretch {
258    fn match_distance(&self, range: &(Self, Self)) -> f32 {
259        // stretch distance ==> [0,2000]
260        const REVERSE_DISTANCE: f32 = 1000.0;
261
262        let min_stretch = range.0;
263        let max_stretch = range.1;
264
265        // The stretch value is a (non-negative) percentage; currently we support
266        // values in the range 0 .. 1000. (If the upper limit is ever increased,
267        // the kReverseDistance value used here may need to be adjusted.)
268        // If aTargetStretch is >100, we prefer larger values if available;
269        // if <=100, we prefer smaller values if available.
270        if *self < min_stretch {
271            if *self > FontStretch::NORMAL {
272                return min_stretch.to_float() - self.to_float();
273            }
274            return (min_stretch.to_float() - self.to_float()) + REVERSE_DISTANCE;
275        }
276
277        if *self > max_stretch {
278            if *self <= FontStretch::NORMAL {
279                return self.to_float() - max_stretch.to_float();
280            }
281            return (self.to_float() - max_stretch.to_float()) + REVERSE_DISTANCE;
282        }
283        0.0
284    }
285
286    fn to_float(&self) -> f32 {
287        self.0.to_float()
288    }
289}
290
291impl FontMatchDistanceMethod for FontWeight {
292    // Calculate weight distance with values in the range (0..1000). In general,
293    // heavier weights match towards even heavier weights while lighter weights
294    // match towards even lighter weights. Target weight values in the range
295    // [400..500] are special, since they will first match up to 500, then down
296    // towards 0, then up again towards 999.
297    //
298    // Example: with target 600 and font weight 800, distance will be 200. With
299    // target 300 and font weight 600, distance will be 900, since heavier
300    // weights are farther away than lighter weights. If the target is 5 and the
301    // font weight 995, the distance would be 1590 for the same reason.
302
303    fn match_distance(&self, range: &(Self, Self)) -> f32 {
304        // weight distance ==> [0,1600]
305        const NOT_WITHIN_CENTRAL_RANGE: f32 = 100.0;
306        const REVERSE_DISTANCE: f32 = 600.0;
307
308        let min_weight = range.0;
309        let max_weight = range.1;
310
311        if *self >= min_weight && *self <= max_weight {
312            // Target is within the face's range, so it's a perfect match
313            return 0.0;
314        }
315
316        if *self < FontWeight::NORMAL {
317            // Requested a lighter-than-400 weight
318            if max_weight < *self {
319                return self.to_float() - max_weight.to_float();
320            }
321
322            // Add reverse-search penalty for bolder faces
323            return (min_weight.to_float() - self.to_float()) + REVERSE_DISTANCE;
324        }
325
326        if *self > FontWeight::from_float(500.) {
327            // Requested a bolder-than-500 weight
328            if min_weight > *self {
329                return min_weight.to_float() - self.to_float();
330            }
331            // Add reverse-search penalty for lighter faces
332            return (self.to_float() - max_weight.to_float()) + REVERSE_DISTANCE;
333        }
334
335        // Special case for requested weight in the [400..500] range
336        if min_weight > *self {
337            if min_weight <= FontWeight::from_float(500.) {
338                // Bolder weight up to 500 is first choice
339                return min_weight.to_float() - self.to_float();
340            }
341            // Other bolder weights get a reverse-search penalty
342            return (min_weight.to_float() - self.to_float()) + REVERSE_DISTANCE;
343        }
344        // Lighter weights are not as good as bolder ones within [400..500]
345        (self.to_float() - max_weight.to_float()) + NOT_WITHIN_CENTRAL_RANGE
346    }
347
348    fn to_float(&self) -> f32 {
349        self.value()
350    }
351}
352
353impl FontMatchDistanceMethod for FontStyle {
354    fn match_distance(&self, range: &(Self, Self)) -> f32 {
355        // style distance ==> [0,500]
356        let min_style = range.0;
357        if *self == min_style {
358            return 0.0; // styles match exactly ==> 0
359        }
360
361        // bias added to angle difference when searching in the non-preferred
362        // direction from a target angle
363        const REVERSE: f32 = 100.0;
364
365        // bias added when we've crossed from positive to negative angles or
366        // vice versa
367        const NEGATE: f32 = 200.0;
368
369        if *self == FontStyle::NORMAL {
370            if min_style.is_oblique() {
371                // to distinguish oblique 0deg from normal, we add 1.0 to the angle
372                let min_angle = min_style.oblique_degrees();
373                if min_angle >= 0.0 {
374                    return 1.0 + min_angle;
375                }
376                let max_style = range.1;
377                let max_angle = max_style.oblique_degrees();
378                if max_angle >= 0.0 {
379                    // [min,max] range includes 0.0, so just return our minimum
380                    return 1.0;
381                }
382                // negative oblique is even worse than italic
383                return NEGATE - max_angle;
384            }
385            // must be italic, which is worse than any non-negative oblique;
386            // treat as a match in the wrong search direction
387            assert!(min_style == FontStyle::ITALIC);
388            return REVERSE;
389        }
390
391        let default_oblique_angle = FontStyle::OBLIQUE.oblique_degrees();
392        if *self == FontStyle::ITALIC {
393            if min_style.is_oblique() {
394                let min_angle = min_style.oblique_degrees();
395                if min_angle >= default_oblique_angle {
396                    return 1.0 + (min_angle - default_oblique_angle);
397                }
398                let max_style = range.1;
399                let max_angle = max_style.oblique_degrees();
400                if max_angle >= default_oblique_angle {
401                    return 1.0;
402                }
403                if max_angle > 0.0 {
404                    // wrong direction but still > 0, add bias of 100
405                    return REVERSE + (default_oblique_angle - max_angle);
406                }
407                // negative oblique angle, add bias of 300
408                return REVERSE + NEGATE + (default_oblique_angle - max_angle);
409            }
410            // normal is worse than oblique > 0, but better than oblique <= 0
411            assert!(min_style == FontStyle::NORMAL);
412            return NEGATE;
413        }
414
415        // target is oblique <angle>: four different cases depending on
416        // the value of the <angle>, which determines the preferred direction
417        // of search
418        let target_angle = self.oblique_degrees();
419        if target_angle >= default_oblique_angle {
420            if min_style.is_oblique() {
421                let min_angle = min_style.oblique_degrees();
422                if min_angle >= target_angle {
423                    return min_angle - target_angle;
424                }
425                let max_style = range.1;
426                let max_angle = max_style.oblique_degrees();
427                if max_angle >= target_angle {
428                    return 0.0;
429                }
430                if max_angle > 0.0 {
431                    return REVERSE + (target_angle - max_angle);
432                }
433                return REVERSE + NEGATE + (target_angle - max_angle);
434            }
435            if min_style == FontStyle::ITALIC {
436                return REVERSE + NEGATE;
437            }
438            return REVERSE + NEGATE + 1.0;
439        }
440
441        if target_angle <= -default_oblique_angle {
442            if min_style.is_oblique() {
443                let max_style = range.1;
444                let max_angle = max_style.oblique_degrees();
445                if max_angle <= target_angle {
446                    return target_angle - max_angle;
447                }
448                let min_angle = min_style.oblique_degrees();
449                if min_angle <= target_angle {
450                    return 0.0;
451                }
452                if min_angle < 0.0 {
453                    return REVERSE + (min_angle - target_angle);
454                }
455                return REVERSE + NEGATE + (min_angle - target_angle);
456            }
457            if min_style == FontStyle::ITALIC {
458                return REVERSE + NEGATE;
459            }
460            return REVERSE + NEGATE + 1.0;
461        }
462
463        if target_angle >= 0.0 {
464            if min_style.is_oblique() {
465                let min_angle = min_style.oblique_degrees();
466                if min_angle > target_angle {
467                    return REVERSE + (min_angle - target_angle);
468                }
469                let max_style = range.1;
470                let max_angle = max_style.oblique_degrees();
471                if max_angle >= target_angle {
472                    return 0.0;
473                }
474                if max_angle > 0.0 {
475                    return target_angle - max_angle;
476                }
477                return REVERSE + NEGATE + (target_angle - max_angle);
478            }
479            if min_style == FontStyle::ITALIC {
480                return REVERSE + NEGATE - 2.0;
481            }
482            return REVERSE + NEGATE - 1.0;
483        }
484
485        // last case: (targetAngle < 0.0 && targetAngle > kDefaultAngle)
486        if min_style.is_oblique() {
487            let max_style = range.1;
488            let max_angle = max_style.oblique_degrees();
489            if max_angle < target_angle {
490                return REVERSE + (target_angle - max_angle);
491            }
492            let min_angle = min_style.oblique_degrees();
493            if min_angle <= target_angle {
494                return 0.0;
495            }
496            if min_angle < 0.0 {
497                return min_angle - target_angle;
498            }
499            return REVERSE + NEGATE + (min_angle - target_angle);
500        }
501        if min_style == FontStyle::ITALIC {
502            return REVERSE + NEGATE - 2.0;
503        }
504        REVERSE + NEGATE - 1.0
505    }
506
507    fn to_float(&self) -> f32 {
508        unimplemented!("Don't know how to convert FontStyle to float.");
509    }
510}
511
512pub trait IsOblique {
513    fn is_oblique(&self) -> bool;
514}
515
516impl IsOblique for FontStyle {
517    fn is_oblique(&self) -> bool {
518        *self != FontStyle::NORMAL && *self != FontStyle::ITALIC
519    }
520}