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