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, 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::{FontBackingStore, FreeTypeFace};
30
31const SEMI_BOLD_U16: u16 = Weight::SEMI_BOLD.value() as u16;
32
33/// Convert FreeType-style 26.6 fixed point to an [`f64`].
34fn fixed_26_dot_6_to_float(fixed: FT_F26Dot6) -> f64 {
35    fixed as f64 / 64.0
36}
37
38#[derive(Debug)]
39pub struct FontTable {
40    data: FreeTypeFaceTableProviderData,
41    tag: Tag,
42}
43
44impl FontTableMethods for FontTable {
45    fn buffer(&self) -> &[u8] {
46        let font_ref = self.data.font_ref().expect("Font checked before creating");
47        let table_data = font_ref
48            .table_data(self.tag)
49            .expect("Table existence checked before creating");
50        table_data.as_bytes()
51    }
52}
53
54#[derive(Debug)]
55pub struct PlatformFont {
56    face: ReentrantMutex<FreeTypeFace>,
57    requested_face_size: Au,
58    actual_face_size: Au,
59    variations: Vec<FontVariation>,
60    synthetic_bold: bool,
61
62    /// A member that allows using `skrifa` to read values from this font.
63    table_provider_data: FreeTypeFaceTableProviderData,
64}
65
66impl PlatformFontMethods for PlatformFont {
67    fn new_from_data(
68        _font_identifier: FontIdentifier,
69        font_data: &FontData,
70        requested_size: Option<Au>,
71        variations: &[FontVariation],
72        synthetic_bold: bool,
73    ) -> Result<PlatformFont, &'static str> {
74        let library = FreeTypeLibraryHandle::get().lock();
75        let data = FontBackingStore::Web(font_data.clone());
76        let face = FreeTypeFace::new_from_memory(&library, data, 0)?;
77
78        let normalized_variations = face.set_variations_for_font(variations, &library)?;
79
80        let (requested_face_size, actual_face_size) = match requested_size {
81            Some(requested_size) => (requested_size, face.set_size(requested_size)?),
82            None => (Au::zero(), Au::zero()),
83        };
84
85        let table_provider_data = FreeTypeFaceTableProviderData::Web(font_data.clone());
86
87        let synthetic_bold = table_provider_data.should_apply_synthetic_bold(synthetic_bold);
88
89        Ok(PlatformFont {
90            face: ReentrantMutex::new(face),
91            requested_face_size,
92            actual_face_size,
93            table_provider_data,
94            variations: normalized_variations,
95            synthetic_bold,
96        })
97    }
98
99    fn new_from_local_font_identifier(
100        font_identifier: LocalFontIdentifier,
101        requested_size: Option<Au>,
102        variations: &[FontVariation],
103        synthetic_bold: bool,
104    ) -> Result<PlatformFont, &'static str> {
105        let library = FreeTypeLibraryHandle::get().lock();
106
107        let Ok(memory_mapped_font_data) = File::open(&*font_identifier.path)
108            .and_then(|file| unsafe { Mmap::map(&file) })
109            .map(Arc::new)
110        else {
111            return Err("Could not memory map font");
112        };
113
114        let face_index = font_identifier.face_index_for_freetype();
115        let face = FreeTypeFace::new_from_memory(
116            &library,
117            FontBackingStore::Local(memory_mapped_font_data.clone()),
118            face_index,
119        )?;
120
121        let normalized_variations = face.set_variations_for_font(variations, &library)?;
122
123        let (requested_face_size, actual_face_size) = match requested_size {
124            Some(requested_size) => (requested_size, face.set_size(requested_size)?),
125            None => (Au::zero(), Au::zero()),
126        };
127
128        let table_provider_data =
129            FreeTypeFaceTableProviderData::Local(memory_mapped_font_data, font_identifier.index());
130
131        let synthetic_bold = table_provider_data.should_apply_synthetic_bold(synthetic_bold);
132
133        Ok(PlatformFont {
134            face: ReentrantMutex::new(face),
135            requested_face_size,
136            actual_face_size,
137            table_provider_data,
138            variations: normalized_variations,
139            synthetic_bold,
140        })
141    }
142
143    fn descriptor(&self) -> FontTemplateDescriptor {
144        let Ok(font_ref) = self.table_provider_data.font_ref() else {
145            return FontTemplateDescriptor::default();
146        };
147        let Ok(os2) = font_ref.os2() else {
148            return FontTemplateDescriptor::default();
149        };
150        Self::descriptor_from_os2_table(&os2)
151    }
152
153    fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
154        let face = self.face.lock();
155
156        unsafe {
157            let idx = FT_Get_Char_Index(face.as_ptr(), codepoint as FT_ULong);
158            if idx != 0 as FT_UInt {
159                Some(idx as GlyphId)
160            } else {
161                debug!(
162                    "Invalid codepoint: U+{:04X} ('{}')",
163                    codepoint as u32, codepoint
164                );
165                None
166            }
167        }
168    }
169
170    fn glyph_h_kerning(&self, first_glyph: GlyphId, second_glyph: GlyphId) -> FractionalPixel {
171        let face = self.face.lock();
172
173        let mut delta = FT_Vector { x: 0, y: 0 };
174        unsafe {
175            FT_Get_Kerning(
176                face.as_ptr(),
177                first_glyph,
178                second_glyph,
179                FT_KERNING_DEFAULT,
180                &mut delta,
181            );
182        }
183        fixed_26_dot_6_to_float(delta.x) * self.unscalable_font_metrics_scale()
184    }
185
186    fn glyph_h_advance(&self, glyph: GlyphId) -> Option<FractionalPixel> {
187        let face = self.face.lock();
188
189        let load_flags = face.glyph_load_flags();
190        let result = unsafe { FT_Load_Glyph(face.as_ptr(), glyph as FT_UInt, load_flags) };
191        if 0 != result {
192            debug!("Unable to load glyph {}. reason: {:?}", glyph, result);
193            return None;
194        }
195
196        let void_glyph = face.as_ref().glyph;
197        let slot: FT_GlyphSlot = void_glyph;
198        if void_glyph.is_null() {
199            return None;
200        }
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}