Skip to main content

fonts/shapers/
mod.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
5mod harfbuzz;
6
7use app_units::Au;
8use euclid::default::Point2D;
9pub(crate) use harfbuzz::Shaper;
10use read_fonts::types::Tag;
11use rustc_hash::FxHashMap;
12use style::computed_values::font_variant_position::T as FontVariantPosition;
13use style::properties::generated::font_face::Descriptors as FontFaceRuleDescriptors;
14use style::values::computed::{FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric};
15
16use crate::{
17    AFRC, CALT, CLIG, CWSH, DLIG, FRAC, FWID, GlyphId, HIST, HLIG, JP04, JP78, JP83, JP90, KERN,
18    LIGA, LNUM, NALT, ONUM, ORDN, ORNM, PNUM, PWID, RUBY, SALT, SMPL, SUBS, SUPS, SWSH,
19    ShapingFlags, ShapingOptions, TNUM, TRAD, ZERO,
20};
21
22/// The highest acceptable value for `styleset` values in `font-variant-alternates`.
23///
24/// <https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop>:
25/// > OpenType currently defines ss01 through ss20
26const HIGHEST_DEFINED_STYLESET_INDEX: u32 = 20;
27
28/// The highest acceptable value for `character-variant` values in
29/// `font-variant-alternates`.
30///
31/// <https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop>:
32/// > OpenType currently defines cv01 through cv99
33const HIGHEST_DEFINED_CHARACTER_VARIANT_INDEX: u32 = 99;
34
35/// Utility function to convert a `unicode_script::Script` enum into the corresponding `c_uint` tag that
36/// harfbuzz uses to represent unicode scipts.
37fn unicode_script_to_iso15924_tag(script: unicode_script::Script) -> u32 {
38    let bytes: [u8; 4] = match script {
39        unicode_script::Script::Unknown => *b"Zzzz",
40        _ => {
41            let short_name = script.short_name();
42            short_name.as_bytes().try_into().unwrap()
43        },
44    };
45
46    u32::from_be_bytes(bytes)
47}
48
49#[derive(Debug)]
50pub(crate) struct ShapedGlyph {
51    /// The actual glyph to render for this [`ShapedGlyph`].
52    pub glyph_id: GlyphId,
53    /// The original byte offset in the input buffer of the character that this
54    /// glyph belongs to. More than one glyph can share the same character and
55    /// one character can produce multiple glyphs.
56    pub string_byte_offset: usize,
57    /// The advance the direction of the writing mode that this glyph needs.
58    pub advance: Au,
59    /// An offset that should be applied when rendering this glyph.
60    pub offset: Option<Point2D<Au>>,
61}
62
63/// Holds the results of shaping. Abstracts over HarfBuzz and HarfRust which return data in very similar
64/// form but with different types
65pub(crate) trait GlyphShapingResult {
66    /// The number of shaped glyphs
67    fn len(&self) -> usize;
68    /// Whether or not the result is right-to-left.
69    fn is_rtl(&self) -> bool;
70    /// An iterator of the shaped glyphs of this data.
71    fn iter(&self) -> impl Iterator<Item = ShapedGlyph>;
72}
73
74/// Determine which OpenType features are applied for the font.
75///
76/// The order of precedence for resolving font-specific font feature properties is specified in
77/// <https://drafts.csswg.org/css-fonts-4/#apply-font-matching-variations>.
78pub(crate) fn compute_used_font_features(
79    options: &ShapingOptions,
80    font_face_rule: Option<&FontFaceRuleDescriptors>,
81) -> impl Iterator<Item = (Tag, u32)> {
82    let mut features = FxHashMap::default();
83
84    let mut add_feature = |tag, value| {
85        features.entry(tag).insert_entry(value);
86    };
87
88    // Step 1. Font features enabled by default are applied, including features required
89    // for a given script. See § 7.1 Default features for a description of these.
90    add_feature(LIGA, 1);
91    add_feature(CLIG, 1);
92
93    // Step 7. If the font is defined via an @font-face rule, the font features implied
94    // by the font-feature-settings descriptor in the @font-face rule are applied.
95    if let Some(font_feature_settings) =
96        font_face_rule.and_then(|rule| rule.font_feature_settings.as_ref())
97    {
98        for feature_setting in font_feature_settings.0.iter() {
99            add_feature(
100                Tag::from_u32(feature_setting.tag.0),
101                feature_setting.value.resolve().expect(
102                    "The value is enforced to be resolvable at parse time \
103                    (see FontFeatureSettings::parse_for_font_face_rule).",
104                ) as u32,
105            )
106        }
107    }
108
109    // Step 10. Font features implied by the value of the font-variant property,
110    // the related font-variant subproperties and any other CSS property that uses
111    // OpenType features (e.g. the font-kerning property) are applied.
112    if options.ligatures == FontVariantLigatures::NONE {
113        add_feature(LIGA, 0);
114        add_feature(CLIG, 0);
115        add_feature(DLIG, 0);
116        add_feature(HLIG, 0);
117        add_feature(CALT, 0);
118    } else {
119        if options
120            .ligatures
121            .contains(FontVariantLigatures::COMMON_LIGATURES)
122        {
123            add_feature(LIGA, 1);
124            add_feature(CLIG, 1);
125        } else if options
126            .ligatures
127            .contains(FontVariantLigatures::NO_COMMON_LIGATURES)
128        {
129            add_feature(LIGA, 0);
130            add_feature(CLIG, 0);
131        }
132
133        if options
134            .ligatures
135            .contains(FontVariantLigatures::DISCRETIONARY_LIGATURES)
136        {
137            add_feature(DLIG, 1);
138        } else if options
139            .ligatures
140            .contains(FontVariantLigatures::NO_DISCRETIONARY_LIGATURES)
141        {
142            add_feature(DLIG, 0);
143        }
144
145        if options
146            .ligatures
147            .contains(FontVariantLigatures::HISTORICAL_LIGATURES)
148        {
149            add_feature(HLIG, 1);
150        } else if options
151            .ligatures
152            .contains(FontVariantLigatures::NO_HISTORICAL_LIGATURES)
153        {
154            add_feature(HLIG, 0);
155        }
156
157        if options.ligatures.contains(FontVariantLigatures::CONTEXTUAL) {
158            add_feature(CALT, 1);
159        } else if options
160            .ligatures
161            .contains(FontVariantLigatures::NO_CONTEXTUAL)
162        {
163            add_feature(CALT, 0);
164        }
165    }
166
167    if options.numeric != FontVariantNumeric::NORMAL {
168        if options.numeric.contains(FontVariantNumeric::LINING_NUMS) {
169            add_feature(LNUM, 1);
170        } else if options.numeric.contains(FontVariantNumeric::OLDSTYLE_NUMS) {
171            add_feature(ONUM, 1);
172        }
173        if options
174            .numeric
175            .contains(FontVariantNumeric::PROPORTIONAL_NUMS)
176        {
177            add_feature(PNUM, 1);
178        } else if options.numeric.contains(FontVariantNumeric::TABULAR_NUMS) {
179            add_feature(TNUM, 1);
180        }
181        if options
182            .numeric
183            .contains(FontVariantNumeric::DIAGONAL_FRACTIONS)
184        {
185            add_feature(FRAC, 1);
186        } else if options
187            .numeric
188            .contains(FontVariantNumeric::STACKED_FRACTIONS)
189        {
190            add_feature(AFRC, 1);
191        }
192        if options.numeric.contains(FontVariantNumeric::ORDINAL) {
193            add_feature(ORDN, 1);
194        }
195        if options.numeric.contains(FontVariantNumeric::SLASHED_ZERO) {
196            add_feature(ZERO, 1);
197        }
198    }
199
200    if options.east_asian != FontVariantEastAsian::NORMAL {
201        if options.east_asian.contains(FontVariantEastAsian::JIS78) {
202            add_feature(JP78, 1);
203        } else if options.east_asian.contains(FontVariantEastAsian::JIS83) {
204            add_feature(JP83, 1);
205        } else if options.east_asian.contains(FontVariantEastAsian::JIS90) {
206            add_feature(JP90, 1);
207        } else if options.east_asian.contains(FontVariantEastAsian::JIS04) {
208            add_feature(JP04, 1);
209        } else if options
210            .east_asian
211            .contains(FontVariantEastAsian::SIMPLIFIED)
212        {
213            add_feature(SMPL, 1);
214        } else if options
215            .east_asian
216            .contains(FontVariantEastAsian::TRADITIONAL)
217        {
218            add_feature(TRAD, 1);
219        }
220
221        if options
222            .east_asian
223            .contains(FontVariantEastAsian::FULL_WIDTH)
224        {
225            add_feature(FWID, 1);
226        } else if options
227            .east_asian
228            .contains(FontVariantEastAsian::PROPORTIONAL_WIDTH)
229        {
230            add_feature(PWID, 1);
231        }
232
233        if options.east_asian.contains(FontVariantEastAsian::RUBY) {
234            add_feature(RUBY, 1);
235        }
236    }
237
238    match options.position {
239        FontVariantPosition::Normal => {},
240        FontVariantPosition::Sub => add_feature(SUBS, 1),
241        FontVariantPosition::Super => add_feature(SUPS, 1),
242    }
243
244    if options.alternates.historical_forms {
245        add_feature(HIST, 1);
246    }
247
248    if let Some(stylistic) = &options.alternates.stylistic {
249        add_feature(SALT, stylistic.0);
250    }
251
252    for styleset in &options.alternates.styleset {
253        if *styleset > HIGHEST_DEFINED_STYLESET_INDEX {
254            continue;
255        }
256
257        let tag = Tag::new(&[
258            b's',
259            b's',
260            b'0' + (*styleset / 10) as u8,
261            b'0' + (*styleset % 10) as u8,
262        ]);
263        add_feature(tag, 1);
264    }
265
266    for character_variant in &options.alternates.character_variant {
267        if character_variant.0 > HIGHEST_DEFINED_CHARACTER_VARIANT_INDEX {
268            continue;
269        }
270
271        let tag = Tag::new(&[
272            b'c',
273            b'v',
274            b'0' + (character_variant.0 / 10) as u8,
275            b'0' + (character_variant.0 % 10) as u8,
276        ]);
277        add_feature(tag, character_variant.1.unwrap_or(1));
278    }
279
280    if let Some(swash) = &options.alternates.swash {
281        add_feature(SWSH, swash.0);
282        add_feature(CWSH, swash.0);
283    }
284
285    if let Some(ornaments) = &options.alternates.ornaments {
286        add_feature(ORNM, ornaments.0);
287    }
288
289    if let Some(annotation) = &options.alternates.annotation {
290        add_feature(NALT, annotation.0);
291    }
292
293    if options
294        .flags
295        .contains(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
296    {
297        add_feature(KERN, 0);
298    }
299
300    // Step 13. Font features implied by the value of font-feature-settings property are applied.
301    for feature_setting in options.feature_settings.0.iter() {
302        add_feature(
303            Tag::from_u32(feature_setting.tag.0),
304            feature_setting.value as u32,
305        )
306    }
307
308    features.into_iter()
309}