Skip to main content

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