Skip to main content

style/
font_face.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
5//! The [`@font-face`][ff] at-rule.
6//!
7//! [ff]: https://drafts.csswg.org/css-fonts/#at-font-face-rule
8
9use crate::derives::*;
10use crate::error_reporting::ContextualParseError;
11use crate::parser::{Parse, ParserContext};
12use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
13use crate::values::generics::font::FontStyle as GenericFontStyle;
14use crate::values::specified::{url::SpecifiedUrl, Angle};
15use cssparser::{Parser, RuleBodyParser, SourceLocation};
16use std::fmt::{self, Write};
17use style_traits::{CssStringWriter, CssWriter, ParseError, StyleParseErrorKind, ToCss};
18
19pub use crate::properties::font_face::{DescriptorId, DescriptorParser, Descriptors};
20pub use crate::values::computed::font::{FamilyName, FontStretch};
21pub use crate::values::specified::font::{
22    AbsoluteFontWeight, FontFeatureSettings, FontLanguageOverride,
23    FontStretch as SpecifiedFontStretch, FontVariationSettings, MetricsOverride,
24    SpecifiedFontStyle,
25};
26
27/// A source for a font-face rule.
28#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
29#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)]
30pub enum Source {
31    /// A `url()` source.
32    Url(UrlSource),
33    /// A `local()` source.
34    #[css(function)]
35    Local(FamilyName),
36}
37
38/// A list of sources for the font-face src descriptor.
39#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)]
40#[css(comma)]
41pub struct SourceList(#[css(iterable)] pub Vec<Source>);
42
43// We can't just use OneOrMoreSeparated to derive Parse for the Source list,
44// because we want to filter out components that parsed as None, then fail if no
45// valid components remain. So we provide our own implementation here.
46impl Parse for SourceList {
47    fn parse<'i, 't>(
48        context: &ParserContext,
49        input: &mut Parser<'i, 't>,
50    ) -> Result<Self, ParseError<'i>> {
51        // Parse the comma-separated list, then let filter_map discard any None items.
52        let list = input
53            .parse_comma_separated(|input| {
54                let s = input.parse_entirely(|input| Source::parse(context, input));
55                while input.next().is_ok() {}
56                Ok(s.ok())
57            })?
58            .into_iter()
59            .filter_map(|s| s)
60            .collect::<Vec<Source>>();
61        if list.is_empty() {
62            Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
63        } else {
64            Ok(SourceList(list))
65        }
66    }
67}
68
69/// Keywords for the font-face src descriptor's format() function.
70/// ('None' and 'Unknown' are for internal use in gfx, not exposed to CSS.)
71#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
72#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
73#[repr(u8)]
74#[allow(missing_docs)]
75pub enum FontFaceSourceFormatKeyword {
76    #[css(skip)]
77    None,
78    Collection,
79    EmbeddedOpentype,
80    Opentype,
81    Svg,
82    Truetype,
83    Woff,
84    Woff2,
85    #[css(skip)]
86    Unknown,
87}
88
89/// Flags for the @font-face tech() function, indicating font technologies
90/// required by the resource.
91#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
92#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
93#[repr(C)]
94pub struct FontFaceSourceTechFlags(u16);
95bitflags! {
96    impl FontFaceSourceTechFlags: u16 {
97        /// Font requires OpenType feature support.
98        const FEATURES_OPENTYPE = 1 << 0;
99        /// Font requires Apple Advanced Typography support.
100        const FEATURES_AAT = 1 << 1;
101        /// Font requires Graphite shaping support.
102        const FEATURES_GRAPHITE = 1 << 2;
103        /// Font requires COLRv0 rendering support (simple list of colored layers).
104        const COLOR_COLRV0 = 1 << 3;
105        /// Font requires COLRv1 rendering support (graph of paint operations).
106        const COLOR_COLRV1 = 1 << 4;
107        /// Font requires SVG glyph rendering support.
108        const COLOR_SVG = 1 << 5;
109        /// Font has bitmap glyphs in 'sbix' format.
110        const COLOR_SBIX = 1 << 6;
111        /// Font has bitmap glyphs in 'CBDT' format.
112        const COLOR_CBDT = 1 << 7;
113        /// Font requires OpenType Variations support.
114        const VARIATIONS = 1 << 8;
115        /// Font requires CPAL palette selection support.
116        const PALETTES = 1 << 9;
117        /// Font requires support for incremental downloading.
118        const INCREMENTAL = 1 << 10;
119    }
120}
121
122impl FontFaceSourceTechFlags {
123    /// Parse a single font-technology keyword and return its flag.
124    pub fn parse_one<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
125        Ok(try_match_ident_ignore_ascii_case! { input,
126            "features-opentype" => Self::FEATURES_OPENTYPE,
127            "features-aat" => Self::FEATURES_AAT,
128            "features-graphite" => Self::FEATURES_GRAPHITE,
129            "color-colrv0" => Self::COLOR_COLRV0,
130            "color-colrv1" => Self::COLOR_COLRV1,
131            "color-svg" => Self::COLOR_SVG,
132            "color-sbix" => Self::COLOR_SBIX,
133            "color-cbdt" => Self::COLOR_CBDT,
134            "variations" => Self::VARIATIONS,
135            "palettes" => Self::PALETTES,
136            "incremental" => Self::INCREMENTAL,
137        })
138    }
139}
140
141impl Parse for FontFaceSourceTechFlags {
142    fn parse<'i, 't>(
143        _context: &ParserContext,
144        input: &mut Parser<'i, 't>,
145    ) -> Result<Self, ParseError<'i>> {
146        let location = input.current_source_location();
147        // We don't actually care about the return value of parse_comma_separated,
148        // because we insert the flags into result as we go.
149        let mut result = Self::empty();
150        input.parse_comma_separated(|input| {
151            let flag = Self::parse_one(input)?;
152            result.insert(flag);
153            Ok(())
154        })?;
155        if !result.is_empty() {
156            Ok(result)
157        } else {
158            Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
159        }
160    }
161}
162
163#[allow(unused_assignments)]
164impl ToCss for FontFaceSourceTechFlags {
165    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
166    where
167        W: fmt::Write,
168    {
169        let mut first = true;
170
171        macro_rules! write_if_flag {
172            ($s:expr => $f:ident) => {
173                if self.contains(Self::$f) {
174                    if first {
175                        first = false;
176                    } else {
177                        dest.write_str(", ")?;
178                    }
179                    dest.write_str($s)?;
180                }
181            };
182        }
183
184        write_if_flag!("features-opentype" => FEATURES_OPENTYPE);
185        write_if_flag!("features-aat" => FEATURES_AAT);
186        write_if_flag!("features-graphite" => FEATURES_GRAPHITE);
187        write_if_flag!("color-colrv0" => COLOR_COLRV0);
188        write_if_flag!("color-colrv1" => COLOR_COLRV1);
189        write_if_flag!("color-svg" => COLOR_SVG);
190        write_if_flag!("color-sbix" => COLOR_SBIX);
191        write_if_flag!("color-cbdt" => COLOR_CBDT);
192        write_if_flag!("variations" => VARIATIONS);
193        write_if_flag!("palettes" => PALETTES);
194        write_if_flag!("incremental" => INCREMENTAL);
195
196        Ok(())
197    }
198}
199
200/// <https://drafts.csswg.org/css-fonts/#font-face-rule>
201#[derive(Clone, Debug, ToShmem, PartialEq)]
202pub struct FontFaceRule {
203    /// The descriptors of the @font-face rule.
204    pub descriptors: Descriptors,
205    /// The parser location of the rule.
206    pub source_location: SourceLocation,
207}
208
209impl FontFaceRule {
210    /// Returns an empty rule.
211    pub fn empty(source_location: SourceLocation) -> Self {
212        Self {
213            descriptors: Default::default(),
214            source_location,
215        }
216    }
217}
218
219/// A POD representation for Gecko. All pointers here are non-owned and as such
220/// can't outlive the rule they came from, but we can't enforce that via C++.
221///
222/// All the strings are of course utf8.
223#[cfg(feature = "gecko")]
224#[derive(Clone, Copy, Debug, Eq, PartialEq)]
225#[repr(u8)]
226#[allow(missing_docs)]
227pub enum FontFaceSourceListComponent {
228    Url(*const crate::url::CssUrl),
229    Local(*mut crate::gecko_bindings::structs::nsAtom),
230    FormatHintKeyword(FontFaceSourceFormatKeyword),
231    FormatHintString {
232        length: usize,
233        utf8_bytes: *const u8,
234    },
235    TechFlags(FontFaceSourceTechFlags),
236}
237
238#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)]
239#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
240#[repr(u8)]
241#[allow(missing_docs)]
242pub enum FontFaceSourceFormat {
243    Keyword(FontFaceSourceFormatKeyword),
244    String(String),
245}
246
247/// A `UrlSource` represents a font-face source that has been specified with a
248/// `url()` function.
249///
250/// <https://drafts.csswg.org/css-fonts/#src-desc>
251#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
252#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
253pub struct UrlSource {
254    /// The specified url.
255    pub url: SpecifiedUrl,
256    /// The format hint specified with the `format()` function, if present.
257    pub format_hint: Option<FontFaceSourceFormat>,
258    /// The font technology flags specified with the `tech()` function, if any.
259    pub tech_flags: FontFaceSourceTechFlags,
260}
261
262impl ToCss for UrlSource {
263    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
264    where
265        W: fmt::Write,
266    {
267        self.url.to_css(dest)?;
268        if let Some(hint) = &self.format_hint {
269            dest.write_str(" format(")?;
270            hint.to_css(dest)?;
271            dest.write_char(')')?;
272        }
273        if !self.tech_flags.is_empty() {
274            dest.write_str(" tech(")?;
275            self.tech_flags.to_css(dest)?;
276            dest.write_char(')')?;
277        }
278        Ok(())
279    }
280}
281
282/// A font-display value for a @font-face rule.
283/// The font-display descriptor determines how a font face is displayed based
284/// on whether and when it is downloaded and ready to use.
285#[allow(missing_docs)]
286#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
287#[derive(
288    Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToShmem,
289)]
290#[repr(u8)]
291pub enum FontDisplay {
292    Auto,
293    Block,
294    Swap,
295    Fallback,
296    Optional,
297}
298
299macro_rules! impl_range {
300    ($range:ident, $component:ident) => {
301        impl Parse for $range {
302            fn parse<'i, 't>(
303                context: &ParserContext,
304                input: &mut Parser<'i, 't>,
305            ) -> Result<Self, ParseError<'i>> {
306                let first = $component::parse(context, input)?;
307                let second = input
308                    .try_parse(|input| $component::parse(context, input))
309                    .unwrap_or_else(|_| first.clone());
310                Ok($range(first, second))
311            }
312        }
313        impl ToCss for $range {
314            fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
315            where
316                W: fmt::Write,
317            {
318                self.0.to_css(dest)?;
319                if self.0 != self.1 {
320                    dest.write_char(' ')?;
321                    self.1.to_css(dest)?;
322                }
323                Ok(())
324            }
325        }
326    };
327}
328
329/// The font-weight descriptor:
330///
331/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-weight
332#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
333pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight);
334impl_range!(FontWeightRange, AbsoluteFontWeight);
335
336/// The computed representation of the above so Gecko can read them easily.
337///
338/// This one is needed because cbindgen doesn't know how to generate
339/// specified::Number.
340#[repr(C)]
341#[allow(missing_docs)]
342pub struct ComputedFontWeightRange(f32, f32);
343
344#[inline]
345fn sort_range<T: PartialOrd>(a: T, b: T) -> (T, T) {
346    if a > b {
347        (b, a)
348    } else {
349        (a, b)
350    }
351}
352
353impl FontWeightRange {
354    /// Returns a computed font-weight range, or None if either bound is an unresolvable calc.
355    pub fn compute(&self) -> Option<ComputedFontWeightRange> {
356        let (min, max) = sort_range(self.0.compute()?.value(), self.1.compute()?.value());
357        Some(ComputedFontWeightRange(min, max))
358    }
359}
360
361/// The font-stretch descriptor:
362///
363/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-stretch
364#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
365pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch);
366impl_range!(FontStretchRange, SpecifiedFontStretch);
367
368/// The computed representation of the above, so that Gecko can read them
369/// easily.
370#[repr(C)]
371#[allow(missing_docs)]
372pub struct ComputedFontStretchRange(FontStretch, FontStretch);
373
374impl FontStretchRange {
375    /// Returns a computed font-stretch range, or None if any value contains a calc
376    /// expression that cannot be resolved at parse time.
377    pub fn compute(&self) -> Option<ComputedFontStretchRange> {
378        fn compute_stretch(s: &SpecifiedFontStretch) -> Option<FontStretch> {
379            match *s {
380                SpecifiedFontStretch::Keyword(ref kw) => Some(kw.compute()),
381                SpecifiedFontStretch::Stretch(ref p) => {
382                    Some(FontStretch::from_percentage(p.compute()?.0))
383                },
384                SpecifiedFontStretch::System(..) => unreachable!(),
385            }
386        }
387
388        let (min, max) = sort_range(compute_stretch(&self.0)?, compute_stretch(&self.1)?);
389        Some(ComputedFontStretchRange(min, max))
390    }
391}
392
393/// The font-style descriptor:
394///
395/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-style
396#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
397#[allow(missing_docs)]
398pub enum FontStyle {
399    Italic,
400    Oblique(Angle, Angle),
401}
402
403/// The computed representation of the above, with angles in degrees, so that
404/// Gecko can read them easily.
405#[repr(u8)]
406#[allow(missing_docs)]
407pub enum ComputedFontStyleDescriptor {
408    Italic,
409    Oblique(f32, f32),
410}
411
412impl Parse for FontStyle {
413    fn parse<'i, 't>(
414        context: &ParserContext,
415        input: &mut Parser<'i, 't>,
416    ) -> Result<Self, ParseError<'i>> {
417        // We parse 'normal' explicitly here to distinguish it from 'oblique 0deg',
418        // because we must not accept a following angle.
419        if input
420            .try_parse(|i| i.expect_ident_matching("normal"))
421            .is_ok()
422        {
423            return Ok(FontStyle::Oblique(Angle::zero(), Angle::zero()));
424        }
425
426        let style = SpecifiedFontStyle::parse(context, input)?;
427        Ok(match style {
428            GenericFontStyle::Italic => FontStyle::Italic,
429            GenericFontStyle::Oblique(angle) => {
430                let second_angle = input
431                    .try_parse(|input| SpecifiedFontStyle::parse_angle(context, input))
432                    .unwrap_or_else(|_| angle.clone());
433
434                FontStyle::Oblique(angle, second_angle)
435            },
436        })
437    }
438}
439
440impl ToCss for FontStyle {
441    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
442    where
443        W: fmt::Write,
444    {
445        match *self {
446            FontStyle::Italic => dest.write_str("italic"),
447            FontStyle::Oblique(ref first, ref second) => {
448                // Not first.is_zero() because we don't want to serialize
449                // `oblique calc(0deg)` as `normal`.
450                if *first == Angle::zero() && first == second {
451                    return dest.write_str("normal");
452                }
453                dest.write_str("oblique")?;
454                if *first != SpecifiedFontStyle::default_angle() || first != second {
455                    dest.write_char(' ')?;
456                    first.to_css(dest)?;
457                }
458                if first != second {
459                    dest.write_char(' ')?;
460                    second.to_css(dest)?;
461                }
462                Ok(())
463            },
464        }
465    }
466}
467
468impl FontStyle {
469    /// Returns a computed font-style descriptor.
470    pub fn compute(&self) -> Option<ComputedFontStyleDescriptor> {
471        match *self {
472            FontStyle::Italic => Some(ComputedFontStyleDescriptor::Italic),
473            FontStyle::Oblique(ref first, ref second) => {
474                let first = SpecifiedFontStyle::compute_angle_degrees(first)?;
475                let second = SpecifiedFontStyle::compute_angle_degrees(second)?;
476                let (min, max) = sort_range(first, second);
477                Some(ComputedFontStyleDescriptor::Oblique(min, max))
478            },
479        }
480    }
481}
482
483/// Parse the block inside a `@font-face` rule.
484///
485/// Note that the prelude parsing code lives in the `stylesheets` module.
486pub fn parse_font_face_block(
487    context: &ParserContext,
488    input: &mut Parser,
489    source_location: SourceLocation,
490) -> FontFaceRule {
491    let mut rule = FontFaceRule::empty(source_location);
492    {
493        let mut parser = DescriptorParser {
494            context,
495            descriptors: &mut rule.descriptors,
496        };
497        let mut iter = RuleBodyParser::new(input, &mut parser);
498        while let Some(declaration) = iter.next() {
499            if let Err((error, slice)) = declaration {
500                let location = error.location;
501                let error = ContextualParseError::UnsupportedFontFaceDescriptor(slice, error);
502                context.log_css_error(location, error)
503            }
504        }
505    }
506    rule
507}
508
509impl Parse for Source {
510    fn parse<'i, 't>(
511        context: &ParserContext,
512        input: &mut Parser<'i, 't>,
513    ) -> Result<Source, ParseError<'i>> {
514        if input
515            .try_parse(|input| input.expect_function_matching("local"))
516            .is_ok()
517        {
518            return input
519                .parse_nested_block(|input| FamilyName::parse(context, input))
520                .map(Source::Local);
521        }
522
523        let url = SpecifiedUrl::parse(context, input)?;
524
525        // Parsing optional format()
526        let format_hint = if input
527            .try_parse(|input| input.expect_function_matching("format"))
528            .is_ok()
529        {
530            input.parse_nested_block(|input| {
531                if let Ok(kw) = input.try_parse(FontFaceSourceFormatKeyword::parse) {
532                    Ok(Some(FontFaceSourceFormat::Keyword(kw)))
533                } else {
534                    let s = input.expect_string()?.as_ref().to_owned();
535                    Ok(Some(FontFaceSourceFormat::String(s)))
536                }
537            })?
538        } else {
539            None
540        };
541
542        // Parse optional tech()
543        let tech_flags = if static_prefs::pref!("layout.css.font-tech.enabled")
544            && input
545                .try_parse(|input| input.expect_function_matching("tech"))
546                .is_ok()
547        {
548            input.parse_nested_block(|input| FontFaceSourceTechFlags::parse(context, input))?
549        } else {
550            FontFaceSourceTechFlags::empty()
551        };
552
553        Ok(Source::Url(UrlSource {
554            url,
555            format_hint,
556            tech_flags,
557        }))
558    }
559}
560
561impl ToCssWithGuard for FontFaceRule {
562    // Serialization of FontFaceRule is not specced.
563    fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
564        dest.write_str("@font-face { ")?;
565        self.descriptors.to_css(&mut CssWriter::new(dest))?;
566        dest.write_char('}')
567    }
568}