Skip to main content

fonts/platform/freetype/
font.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::fs::File;
6
7use app_units::Au;
8use euclid::default::{Point2D, Rect, Size2D};
9use fonts_traits::{FontIdentifier, FontTemplateDescriptor, LocalFontIdentifier};
10use freetype_sys::{
11    FT_F26Dot6, FT_Get_Char_Index, FT_Get_Kerning, FT_GlyphSlot, FT_KERNING_DEFAULT,
12    FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_Load_Glyph, FT_Size_Metrics, FT_SizeRec, FT_UInt,
13    FT_ULong, FT_Vector,
14};
15use log::debug;
16use memmap2::Mmap;
17use parking_lot::ReentrantMutex;
18use read_fonts::types::Tag;
19use read_fonts::{FontRef, ReadError, TableProvider};
20use servo_arc::Arc;
21use skrifa::attribute::Weight;
22use style::Zero;
23use webrender_api::{FontInstanceFlags, FontInstancePlatformOptions, FontVariation};
24
25use super::library_handle::FreeTypeLibraryHandle;
26use crate::FontData;
27use crate::font::{FontMetrics, FontTableMethods, FractionalPixel, PlatformFontMethods};
28use crate::glyph::GlyphId;
29use crate::platform::freetype::freetype_face::{
30    FALLBACK_HINTING_STYLE, FontBackingStore, FreeTypeFace,
31};
32
33const SEMI_BOLD_U16: u16 = Weight::SEMI_BOLD.value() as u16;
34
35/// Convert FreeType-style 26.6 fixed point to an [`f64`].
36fn fixed_26_dot_6_to_float(fixed: FT_F26Dot6) -> f64 {
37    fixed as f64 / 64.0
38}
39
40#[derive(Debug)]
41pub struct FontTable {
42    data: FreeTypeFaceTableProviderData,
43    tag: Tag,
44}
45
46impl FontTableMethods for FontTable {
47    fn buffer(&self) -> &[u8] {
48        let font_ref = self.data.font_ref().expect("Font checked before creating");
49        let table_data = font_ref
50            .table_data(self.tag)
51            .expect("Table existence checked before creating");
52        table_data.as_bytes()
53    }
54}
55
56#[derive(Debug)]
57pub struct PlatformFont {
58    face: ReentrantMutex<FreeTypeFace>,
59    requested_face_size: Au,
60    actual_face_size: Au,
61    variations: Vec<FontVariation>,
62    synthetic_bold: bool,
63
64    /// A member that allows using `skrifa` to read values from this font.
65    table_provider_data: FreeTypeFaceTableProviderData,
66}
67
68impl PlatformFontMethods for PlatformFont {
69    fn new_from_data(
70        _font_identifier: FontIdentifier,
71        font_data: &FontData,
72        requested_size: Option<Au>,
73        synthetic_bold: bool,
74    ) -> Result<PlatformFont, &'static str> {
75        let library = FreeTypeLibraryHandle::get().lock();
76        let data = FontBackingStore::Web(font_data.clone());
77        let face = FreeTypeFace::new_from_memory(&library, data, 0)?;
78
79        let (requested_face_size, actual_face_size) = match requested_size {
80            Some(requested_size) => (requested_size, face.set_size(requested_size)?),
81            None => (Au::zero(), Au::zero()),
82        };
83
84        let table_provider_data = FreeTypeFaceTableProviderData::Web(font_data.clone());
85
86        let synthetic_bold = table_provider_data.should_apply_synthetic_bold(synthetic_bold);
87
88        Ok(PlatformFont {
89            face: ReentrantMutex::new(face),
90            requested_face_size,
91            actual_face_size,
92            table_provider_data,
93            variations: vec![],
94            synthetic_bold,
95        })
96    }
97
98    fn new_from_local_font_identifier(
99        font_identifier: LocalFontIdentifier,
100        requested_size: Option<Au>,
101        synthetic_bold: bool,
102    ) -> Result<PlatformFont, &'static str> {
103        let library = FreeTypeLibraryHandle::get().lock();
104
105        let Ok(memory_mapped_font_data) = File::open(&*font_identifier.path)
106            .and_then(|file| unsafe { Mmap::map(&file) })
107            .map(Arc::new)
108        else {
109            return Err("Could not memory map font");
110        };
111
112        let face_index = font_identifier.face_index_for_freetype();
113        let face = FreeTypeFace::new_from_memory(
114            &library,
115            FontBackingStore::Local(memory_mapped_font_data.clone()),
116            face_index,
117        )?;
118
119        let (requested_face_size, actual_face_size) = match requested_size {
120            Some(requested_size) => (requested_size, face.set_size(requested_size)?),
121            None => (Au::zero(), Au::zero()),
122        };
123
124        let table_provider_data =
125            FreeTypeFaceTableProviderData::Local(memory_mapped_font_data, font_identifier.index());
126
127        let synthetic_bold = table_provider_data.should_apply_synthetic_bold(synthetic_bold);
128
129        Ok(PlatformFont {
130            face: ReentrantMutex::new(face),
131            requested_face_size,
132            actual_face_size,
133            table_provider_data,
134            variations: vec![],
135            synthetic_bold,
136        })
137    }
138
139    fn copy_with_variations(
140        mut self,
141        _: &FontIdentifier,
142        variations: &[FontVariation],
143    ) -> Result<Self, &'static str> {
144        let library = FreeTypeLibraryHandle::get().lock();
145        self.variations = self
146            .face
147            .lock()
148            .set_variations_for_font(variations, &library)?;
149        Ok(self)
150    }
151
152    fn descriptor(&self) -> FontTemplateDescriptor {
153        let Ok(font_ref) = self.table_provider_data.font_ref() else {
154            return FontTemplateDescriptor::default();
155        };
156        let Ok(os2) = font_ref.os2() else {
157            return FontTemplateDescriptor::default();
158        };
159        Self::descriptor_from_os2_table(&os2)
160    }
161
162    fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
163        let face = self.face.lock();
164
165        unsafe {
166            let idx = FT_Get_Char_Index(face.as_ptr(), codepoint as FT_ULong);
167            if idx != 0 as FT_UInt {
168                Some(idx as GlyphId)
169            } else {
170                debug!(
171                    "Invalid codepoint: U+{:04X} ('{}')",
172                    codepoint as u32, codepoint
173                );
174                None
175            }
176        }
177    }
178
179    fn glyph_h_kerning(&self, first_glyph: GlyphId, second_glyph: GlyphId) -> FractionalPixel {
180        let face = self.face.lock();
181
182        let mut delta = FT_Vector { x: 0, y: 0 };
183        unsafe {
184            FT_Get_Kerning(
185                face.as_ptr(),
186                first_glyph,
187                second_glyph,
188                FT_KERNING_DEFAULT,
189                &mut delta,
190            );
191        }
192        fixed_26_dot_6_to_float(delta.x) * self.unscalable_font_metrics_scale()
193    }
194
195    fn glyph_h_advance(&self, glyph: GlyphId) -> Option<FractionalPixel> {
196        let face = self.face.lock();
197
198        let load_flags = face.glyph_load_flags();
199        let result = unsafe { FT_Load_Glyph(face.as_ptr(), glyph as FT_UInt, load_flags) };
200        if 0 != result {
201            debug!("Unable to load glyph {}. reason: {:?}", glyph, result);
202            return None;
203        }
204
205        let void_glyph = face.as_ref().glyph;
206        let slot: FT_GlyphSlot = void_glyph;
207        if void_glyph.is_null() {
208            return None;
209        }
210
211        if self.synthetic_bold {
212            mozilla_glyphslot_embolden_less(slot);
213        }
214
215        let advance = unsafe { (*slot).metrics.horiAdvance };
216        Some(fixed_26_dot_6_to_float(advance) * self.unscalable_font_metrics_scale())
217    }
218
219    fn metrics(&self) -> FontMetrics {
220        let face = self.face.lock();
221        let font_ref = self.table_provider_data.font_ref();
222
223        // face.size is a *c_void in the bindings, presumably to avoid recursive structural types
224        let freetype_size: &FT_SizeRec = unsafe { &*face.as_ref().size };
225        let freetype_metrics: &FT_Size_Metrics = &(freetype_size).metrics;
226
227        let mut max_advance;
228        let mut max_ascent;
229        let mut max_descent;
230        let mut line_height;
231        let mut y_scale = 0.0;
232        let mut em_height;
233        if face.scalable() {
234            // Prefer FT_Size_Metrics::y_scale to y_ppem as y_ppem does not have subpixel accuracy.
235            //
236            // FT_Size_Metrics::y_scale is in 16.16 fixed point format.  Its (fractional) value is a
237            // factor that converts vertical metrics from design units to units of 1/64 pixels, so
238            // that the result may be interpreted as pixels in 26.6 fixed point format.
239            //
240            // This converts the value to a float without losing precision.
241            y_scale = freetype_metrics.y_scale as f64 / 65536.0 / 64.0;
242
243            max_advance = (face.as_ref().max_advance_width as f64) * y_scale;
244            max_ascent = (face.as_ref().ascender as f64) * y_scale;
245            max_descent = -(face.as_ref().descender as f64) * y_scale;
246            line_height = (face.as_ref().height as f64) * y_scale;
247            em_height = (face.as_ref().units_per_EM as f64) * y_scale;
248        } else {
249            max_advance = fixed_26_dot_6_to_float(freetype_metrics.max_advance);
250            max_ascent = fixed_26_dot_6_to_float(freetype_metrics.ascender);
251            max_descent = -fixed_26_dot_6_to_float(freetype_metrics.descender);
252            line_height = fixed_26_dot_6_to_float(freetype_metrics.height);
253
254            em_height = freetype_metrics.y_ppem as f64;
255            // FT_Face doc says units_per_EM and a bunch of following fields are "only relevant to
256            // scalable outlines". If it's an sfnt, we can get units_per_EM from the 'head' table
257            // instead; otherwise, we don't have a unitsPerEm value so we can't compute y_scale and
258            // x_scale.
259            if let Ok(head) = font_ref.clone().and_then(|font_ref| font_ref.head()) {
260                // Bug 1267909 - Even if the font is not explicitly scalable, if the face has color
261                // bitmaps, it should be treated as scalable and scaled to the desired size. Metrics
262                // based on y_ppem need to be rescaled for the adjusted size.
263                if face.color() {
264                    em_height = self.requested_face_size.to_f64_px();
265                    let adjust_scale = em_height / (freetype_metrics.y_ppem as f64);
266                    max_advance *= adjust_scale;
267                    max_descent *= adjust_scale;
268                    max_ascent *= adjust_scale;
269                    line_height *= adjust_scale;
270                }
271                y_scale = em_height / head.units_per_em() as f64;
272            }
273        }
274
275        // 'leading' is supposed to be the vertical distance between two baselines,
276        // reflected by the height attribute in freetype. On OS X (w/ CTFont),
277        // leading represents the distance between the bottom of a line descent to
278        // the top of the next line's ascent or: (line_height - ascent - descent),
279        // see http://stackoverflow.com/a/5635981 for CTFont implementation.
280        // Convert using a formula similar to what CTFont returns for consistency.
281        let leading = line_height - (max_ascent + max_descent);
282
283        let underline_size = face.as_ref().underline_thickness as f64 * y_scale;
284        let underline_offset = face.as_ref().underline_position as f64 * y_scale + 0.5;
285
286        // The default values for strikeout size and offset. Use OpenType spec's suggested position
287        // for Roman font as the default for offset.
288        let mut strikeout_size = underline_size;
289        let mut strikeout_offset = em_height * 409.0 / 2048.0 + 0.5 * strikeout_size;
290
291        // CSS 2.1, section 4.3.2 Lengths: "In the cases where it is
292        // impossible or impractical to determine the x-height, a value of
293        // 0.5em should be used."
294        let mut x_height = 0.5 * em_height;
295        let mut average_advance = 0.0;
296
297        if let Ok(os2) = font_ref.and_then(|font_ref| font_ref.os2()) {
298            let y_strikeout_size = os2.y_strikeout_size();
299            let y_strikeout_position = os2.y_strikeout_position();
300            if !y_strikeout_size.is_zero() && !y_strikeout_position.is_zero() {
301                strikeout_size = y_strikeout_size as f64 * y_scale;
302                strikeout_offset = y_strikeout_position as f64 * y_scale;
303            }
304
305            let sx_height = os2.sx_height().unwrap_or(0);
306            if !sx_height.is_zero() {
307                x_height = sx_height as f64 * y_scale;
308            }
309
310            let x_average_char_width = os2.x_avg_char_width();
311            if !x_average_char_width.is_zero() {
312                average_advance = x_average_char_width as f64 * y_scale;
313            }
314        }
315
316        if average_advance.is_zero() {
317            average_advance = self
318                .glyph_index('0')
319                .and_then(|idx| self.glyph_h_advance(idx))
320                .unwrap_or(max_advance);
321        }
322
323        let zero_horizontal_advance = self
324            .glyph_index('0')
325            .and_then(|idx| self.glyph_h_advance(idx))
326            .map(Au::from_f64_px);
327        let ic_horizontal_advance = self
328            .glyph_index('\u{6C34}')
329            .and_then(|idx| self.glyph_h_advance(idx))
330            .map(Au::from_f64_px);
331        let space_advance = self
332            .glyph_index(' ')
333            .and_then(|idx| self.glyph_h_advance(idx))
334            .unwrap_or(average_advance);
335
336        FontMetrics {
337            underline_size: Au::from_f64_px(underline_size),
338            underline_offset: Au::from_f64_px(underline_offset),
339            strikeout_size: Au::from_f64_px(strikeout_size),
340            strikeout_offset: Au::from_f64_px(strikeout_offset),
341            leading: Au::from_f64_px(leading),
342            x_height: Au::from_f64_px(x_height),
343            em_size: Au::from_f64_px(em_height),
344            ascent: Au::from_f64_px(max_ascent),
345            descent: Au::from_f64_px(max_descent),
346            max_advance: Au::from_f64_px(max_advance),
347            average_advance: Au::from_f64_px(average_advance),
348            line_gap: Au::from_f64_px(line_height),
349            zero_horizontal_advance,
350            ic_horizontal_advance,
351            space_advance: Au::from_f64_px(space_advance),
352        }
353    }
354
355    fn table_for_tag(&self, tag: Tag) -> Option<FontTable> {
356        let font_ref = self.table_provider_data.font_ref().ok()?;
357        let _table_data = font_ref.table_data(tag)?;
358        Some(FontTable {
359            data: self.table_provider_data.clone(),
360            tag,
361        })
362    }
363
364    fn typographic_bounds(&self, glyph_id: GlyphId) -> Rect<f32> {
365        let face = self.face.lock();
366
367        let load_flags = FT_LOAD_DEFAULT | FT_LOAD_NO_HINTING;
368        let result = unsafe { FT_Load_Glyph(face.as_ptr(), glyph_id as FT_UInt, load_flags) };
369        if 0 != result {
370            debug!("Unable to load glyph {}. reason: {:?}", glyph_id, result);
371            return Rect::default();
372        }
373
374        let metrics = unsafe { &(*face.as_ref().glyph).metrics };
375
376        Rect::new(
377            Point2D::new(
378                metrics.horiBearingX as f32,
379                (metrics.horiBearingY - metrics.height) as f32,
380            ),
381            Size2D::new(metrics.width as f32, metrics.height as f32),
382        ) * (1. / 64.)
383    }
384
385    fn webrender_font_instance_flags(&self) -> FontInstanceFlags {
386        // On other platforms, we only pass this when we know that we are loading a font with
387        // color characters, but not passing this flag simply *prevents* WebRender from
388        // loading bitmaps. There's no harm to always passing it.
389        let mut flags = FontInstanceFlags::EMBEDDED_BITMAPS;
390
391        // TODO: Add support for synthetic italics.
392        // <https://github.com/servo/servo/issues/39637>
393        if self.synthetic_bold {
394            flags |= FontInstanceFlags::SYNTHETIC_BOLD;
395        }
396
397        flags
398    }
399
400    fn webrender_font_instance_platform_options(&self) -> FontInstancePlatformOptions {
401        FontInstancePlatformOptions {
402            // TODO: We should eventually read the hinting style from the system when
403            // possible such as from Fontconfig.
404            hinting: FALLBACK_HINTING_STYLE,
405            ..Default::default()
406        }
407    }
408
409    fn variations(&self) -> &[FontVariation] {
410        &self.variations
411    }
412}
413
414impl PlatformFont {
415    /// Find the scale to use for metrics of unscalable fonts. Unscalable fonts, those using bitmap
416    /// glyphs, are scaled after glyph rasterization. In order for metrics to match the final scaled
417    /// font, we need to scale them based on the final size and the actual font size.
418    fn unscalable_font_metrics_scale(&self) -> f64 {
419        self.requested_face_size.to_f64_px() / self.actual_face_size.to_f64_px()
420    }
421}
422
423#[derive(Clone)]
424enum FreeTypeFaceTableProviderData {
425    Web(FontData),
426    Local(Arc<Mmap>, u32),
427}
428
429impl FreeTypeFaceTableProviderData {
430    fn font_ref(&self) -> Result<FontRef<'_>, ReadError> {
431        match self {
432            Self::Web(ipc_shared_memory) => FontRef::new(ipc_shared_memory.as_ref()),
433            Self::Local(mmap, index) => FontRef::from_index(mmap, *index),
434        }
435    }
436
437    fn should_apply_synthetic_bold(&self, synthetic_bold: bool) -> bool {
438        // Ensures that a font face is not emboldened if it's a variable font or
439        // if it's already bold.
440        let face_is_bold = self
441            .font_ref()
442            .and_then(|font_ref| font_ref.os2())
443            .is_ok_and(|table| table.us_weight_class() >= SEMI_BOLD_U16);
444        let is_variable_font = self
445            .font_ref()
446            .and_then(|font_ref| font_ref.fvar())
447            .is_ok_and(|table| table.axis_count() > 0);
448        !face_is_bold && !is_variable_font && synthetic_bold
449    }
450}
451
452impl std::fmt::Debug for FreeTypeFaceTableProviderData {
453    fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454        Ok(())
455    }
456}
457
458// This is copied from the webrender glyph rasterizer
459// https://github.com/servo/webrender/blob/c4bd5b47d8f5cd684334b445e67a1f945d106848/wr_glyph_rasterizer/src/platform/unix/font.rs#L115
460//
461// Custom version of FT_GlyphSlot_Embolden to be less aggressive with outline
462// fonts than the default implementation in FreeType.
463fn mozilla_glyphslot_embolden_less(slot: FT_GlyphSlot) {
464    use freetype_sys::{
465        FT_GLYPH_FORMAT_OUTLINE, FT_GlyphSlot_Embolden, FT_Long, FT_MulFix, FT_Outline_Embolden,
466    };
467
468    if slot.is_null() {
469        return;
470    }
471
472    let slot_ = unsafe { &mut *slot };
473    let format = slot_.format;
474    if format != FT_GLYPH_FORMAT_OUTLINE {
475        // For non-outline glyphs, just fall back to FreeType's function.
476        unsafe { FT_GlyphSlot_Embolden(slot) };
477        return;
478    }
479
480    let face_ = unsafe { &*slot_.face };
481
482    // FT_GlyphSlot_Embolden uses a divisor of 24 here; we'll be only half as
483    // bold.
484    let size_ = unsafe { &*face_.size };
485    let strength = unsafe { FT_MulFix(face_.units_per_EM as FT_Long, size_.metrics.y_scale) / 48 };
486    unsafe { FT_Outline_Embolden(&raw mut slot_.outline, strength) };
487
488    // Adjust metrics to suit the fattened glyph.
489    if slot_.advance.x != 0 {
490        slot_.advance.x += strength;
491    }
492    if slot_.advance.y != 0 {
493        slot_.advance.y += strength;
494    }
495    slot_.metrics.width += strength;
496    slot_.metrics.height += strength;
497    slot_.metrics.horiAdvance += strength;
498    slot_.metrics.vertAdvance += strength;
499    slot_.metrics.horiBearingY += strength;
500}