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