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