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