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