fonts: Start using fontations to read font tables (#37287)

Use `read-fonts` to read font tables for FreeType fonts. This is the
first step to using fontations throughout Servo. The main benefit here
is that we no longer need to provide our own table data structures and
we can read tables from these fonts without making copies of the table
contents.

Testing: This should not change observable behavior and is covered by
existing WPT tests. I have run some manual microbenchmarks and have not
noticed any changes in performance that are larger than the general
noise.
This adds a new memory map of the font file for local fonts, but this
should be very cheap as FreeType is already doing this internally and
subsequent maps should just reuuse the existing memory-mapped file.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-06-16 13:29:50 +02:00 committed by GitHub
parent 1044f8fbf5
commit 29e618dcf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 107 additions and 109 deletions

20
Cargo.lock generated
View file

@ -2175,6 +2175,15 @@ dependencies = [
"yeslogic-fontconfig-sys", "yeslogic-fontconfig-sys",
] ]
[[package]]
name = "font-types"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "fontconfig-parser" name = "fontconfig-parser"
version = "0.5.8" version = "0.5.8"
@ -2229,6 +2238,7 @@ dependencies = [
"parking_lot", "parking_lot",
"profile_traits", "profile_traits",
"range", "range",
"read-fonts",
"serde", "serde",
"servo_allocator", "servo_allocator",
"servo_arc", "servo_arc",
@ -6164,6 +6174,16 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "read-fonts"
version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
dependencies = [
"bytemuck",
"font-types",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"

View file

@ -112,6 +112,7 @@ rand_core = "0.6"
rand_isaac = "0.3" rand_isaac = "0.3"
raw-window-handle = "0.6" raw-window-handle = "0.6"
rayon = "1" rayon = "1"
read-fonts = "0.29.2"
regex = "1.11" regex = "1.11"
resvg = "0.45.0" resvg = "0.45.0"
rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] } rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] }

View file

@ -39,6 +39,7 @@ net_traits = { workspace = true }
num-traits = { workspace = true } num-traits = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
profile_traits = { workspace = true } profile_traits = { workspace = true }
read-fonts = { workspace = true }
range = { path = "../range" } range = { path = "../range" }
serde = { workspace = true } serde = { workspace = true }
servo_arc = { workspace = true } servo_arc = { workspace = true }

View file

@ -143,7 +143,7 @@ pub trait FontTableMethods {
fn buffer(&self) -> &[u8]; fn buffer(&self) -> &[u8];
} }
#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] #[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub struct FontMetrics { pub struct FontMetrics {
pub underline_size: Au, pub underline_size: Au,
pub underline_offset: Au, pub underline_offset: Au,

View file

@ -3,6 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::ffi::CString; use std::ffi::CString;
use std::fs::File;
use std::os::raw::c_long; use std::os::raw::c_long;
use std::{mem, ptr}; use std::{mem, ptr};
@ -10,14 +11,16 @@ use app_units::Au;
use euclid::default::{Point2D, Rect, Size2D}; use euclid::default::{Point2D, Rect, Size2D};
use freetype_sys::{ use freetype_sys::{
FT_Byte, FT_Done_Face, FT_Error, FT_F26Dot6, FT_FACE_FLAG_COLOR, FT_FACE_FLAG_FIXED_SIZES, FT_Byte, FT_Done_Face, FT_Error, FT_F26Dot6, FT_FACE_FLAG_COLOR, FT_FACE_FLAG_FIXED_SIZES,
FT_FACE_FLAG_SCALABLE, FT_Face, FT_Fixed, FT_Get_Char_Index, FT_Get_Kerning, FT_Get_Sfnt_Table, FT_FACE_FLAG_SCALABLE, FT_Face, FT_Get_Char_Index, FT_Get_Kerning, FT_GlyphSlot, FT_Int32,
FT_GlyphSlot, FT_Int32, FT_KERNING_DEFAULT, FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_KERNING_DEFAULT, FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_Load_Glyph, FT_Long,
FT_Load_Glyph, FT_Long, FT_MulFix, FT_New_Face, FT_New_Memory_Face, FT_Pos, FT_New_Face, FT_New_Memory_Face, FT_Pos, FT_Select_Size, FT_Set_Char_Size, FT_Size_Metrics,
FT_STYLE_FLAG_ITALIC, FT_Select_Size, FT_Set_Char_Size, FT_Short, FT_Size_Metrics, FT_SizeRec, FT_SizeRec, FT_UInt, FT_ULong, FT_Vector,
FT_UInt, FT_ULong, FT_UShort, FT_Vector, TT_OS2, ft_sfnt_head, ft_sfnt_os2,
}; };
use log::debug; use log::debug;
use memmap2::Mmap;
use parking_lot::ReentrantMutex; use parking_lot::ReentrantMutex;
use read_fonts::tables::os2::SelectionFlags;
use read_fonts::{FontRef, ReadError, TableProvider};
use style::Zero; use style::Zero;
use style::computed_values::font_stretch::T as FontStretch; use style::computed_values::font_stretch::T as FontStretch;
use style::computed_values::font_weight::T as FontWeight; use style::computed_values::font_weight::T as FontWeight;
@ -55,24 +58,15 @@ impl FontTableMethods for FontTable {
} }
} }
/// Data from the OS/2 table of an OpenType font.
/// See <https://www.microsoft.com/typography/otspec/os2.htm>
#[derive(Debug)]
struct OS2Table {
x_average_char_width: FT_Short,
us_weight_class: FT_UShort,
us_width_class: FT_UShort,
y_strikeout_size: FT_Short,
y_strikeout_position: FT_Short,
sx_height: FT_Short,
}
#[derive(Debug)] #[derive(Debug)]
#[allow(unused)] #[allow(unused)]
pub struct PlatformFont { pub struct PlatformFont {
face: ReentrantMutex<FT_Face>, face: ReentrantMutex<FT_Face>,
requested_face_size: Au, requested_face_size: Au,
actual_face_size: Au, actual_face_size: Au,
/// A member that allows using `skrifa` to read values from this font.
table_provider_data: FreeTypeFaceTableProviderData,
} }
// FT_Face can be used in multiple threads, but from only one thread at a time. // FT_Face can be used in multiple threads, but from only one thread at a time.
@ -129,6 +123,7 @@ impl PlatformFontMethods for PlatformFont {
face: ReentrantMutex::new(face), face: ReentrantMutex::new(face),
requested_face_size, requested_face_size,
actual_face_size, actual_face_size,
table_provider_data: FreeTypeFaceTableProviderData::Web(font_data.clone()),
}) })
} }
@ -158,29 +153,39 @@ impl PlatformFontMethods for PlatformFont {
None => (Au::zero(), Au::zero()), None => (Au::zero(), Au::zero()),
}; };
let Ok(memory_mapped_font_data) =
File::open(&*font_identifier.path).and_then(|file| unsafe { Mmap::map(&file) })
else {
return Err("Could not memory map");
};
Ok(PlatformFont { Ok(PlatformFont {
face: ReentrantMutex::new(face), face: ReentrantMutex::new(face),
requested_face_size, requested_face_size,
actual_face_size, actual_face_size,
table_provider_data: FreeTypeFaceTableProviderData::Local(
memory_mapped_font_data,
font_identifier.index(),
),
}) })
} }
fn descriptor(&self) -> FontTemplateDescriptor { fn descriptor(&self) -> FontTemplateDescriptor {
let face = self.face.lock(); let Ok(font_ref) = self.table_provider_data.font_ref() else {
let style = if unsafe { (**face).style_flags & FT_STYLE_FLAG_ITALIC as c_long != 0 } { return FontTemplateDescriptor::default();
FontStyle::ITALIC
} else {
FontStyle::NORMAL
}; };
let os2_table = face.os2_table(); let Ok(os2) = font_ref.os2() else {
let weight = os2_table return FontTemplateDescriptor::default();
.as_ref() };
.map(|os2| FontWeight::from_float(os2.us_weight_class as f32))
.unwrap_or_else(FontWeight::normal); let mut style = FontStyle::NORMAL;
let stretch = os2_table if os2.fs_selection().contains(SelectionFlags::ITALIC) {
.as_ref() style = FontStyle::ITALIC;
.map(|os2| match os2.us_width_class { }
let weight = FontWeight::from_float(os2.us_weight_class() as f32);
let stretch = match os2.us_width_class() {
1 => FontStretch::ULTRA_CONDENSED, 1 => FontStretch::ULTRA_CONDENSED,
2 => FontStretch::EXTRA_CONDENSED, 2 => FontStretch::EXTRA_CONDENSED,
3 => FontStretch::CONDENSED, 3 => FontStretch::CONDENSED,
@ -191,8 +196,7 @@ impl PlatformFontMethods for PlatformFont {
8 => FontStretch::EXTRA_EXPANDED, 8 => FontStretch::EXTRA_EXPANDED,
9 => FontStretch::ULTRA_EXPANDED, 9 => FontStretch::ULTRA_EXPANDED,
_ => FontStretch::NORMAL, _ => FontStretch::NORMAL,
}) };
.unwrap_or(FontStretch::NORMAL);
FontTemplateDescriptor::new(weight, stretch, style) FontTemplateDescriptor::new(weight, stretch, style)
} }
@ -254,6 +258,7 @@ impl PlatformFontMethods for PlatformFont {
fn metrics(&self) -> FontMetrics { fn metrics(&self) -> FontMetrics {
let face_ptr = *self.face.lock(); let face_ptr = *self.face.lock();
let face = unsafe { &*face_ptr }; let face = unsafe { &*face_ptr };
let font_ref = self.table_provider_data.font_ref();
// face.size is a *c_void in the bindings, presumably to avoid recursive structural types // face.size is a *c_void in the bindings, presumably to avoid recursive structural types
let freetype_size: &FT_SizeRec = unsafe { mem::transmute(&(*face.size)) }; let freetype_size: &FT_SizeRec = unsafe { mem::transmute(&(*face.size)) };
@ -291,8 +296,7 @@ impl PlatformFontMethods for PlatformFont {
// scalable outlines". If it's an sfnt, we can get units_per_EM from the 'head' table // scalable outlines". If it's an sfnt, we can get units_per_EM from the 'head' table
// instead; otherwise, we don't have a unitsPerEm value so we can't compute y_scale and // instead; otherwise, we don't have a unitsPerEm value so we can't compute y_scale and
// x_scale. // x_scale.
let head = unsafe { FT_Get_Sfnt_Table(face_ptr, ft_sfnt_head) as *mut TtHeader }; if let Ok(head) = font_ref.clone().and_then(|font_ref| font_ref.head()) {
if !head.is_null() && unsafe { (*head).table_version != 0xffff } {
// Bug 1267909 - Even if the font is not explicitly scalable, if the face has color // Bug 1267909 - Even if the font is not explicitly scalable, if the face has color
// bitmaps, it should be treated as scalable and scaled to the desired size. Metrics // bitmaps, it should be treated as scalable and scaled to the desired size. Metrics
// based on y_ppem need to be rescaled for the adjusted size. // based on y_ppem need to be rescaled for the adjusted size.
@ -304,8 +308,7 @@ impl PlatformFontMethods for PlatformFont {
max_ascent *= adjust_scale; max_ascent *= adjust_scale;
line_height *= adjust_scale; line_height *= adjust_scale;
} }
let units_per_em = unsafe { (*head).units_per_em } as f64; y_scale = em_height / head.units_per_em() as f64;
y_scale = em_height / units_per_em;
} }
} }
@ -330,22 +333,23 @@ impl PlatformFontMethods for PlatformFont {
// 0.5em should be used." // 0.5em should be used."
let mut x_height = 0.5 * em_height; let mut x_height = 0.5 * em_height;
let mut average_advance = 0.0; let mut average_advance = 0.0;
if let Some(os2) = face_ptr.os2_table() {
if !os2.y_strikeout_size.is_zero() && !os2.y_strikeout_position.is_zero() { if let Ok(os2) = font_ref.and_then(|font_ref| font_ref.os2()) {
strikeout_size = os2.y_strikeout_size as f64 * y_scale; let y_strikeout_size = os2.y_strikeout_size();
strikeout_offset = os2.y_strikeout_position as f64 * y_scale; let y_strikeout_position = os2.y_strikeout_position();
} if !y_strikeout_size.is_zero() && !y_strikeout_position.is_zero() {
if !os2.sx_height.is_zero() { strikeout_size = y_strikeout_size as f64 * y_scale;
x_height = os2.sx_height as f64 * y_scale; strikeout_offset = y_strikeout_position as f64 * y_scale;
} }
if !os2.x_average_char_width.is_zero() { let sx_height = os2.sx_height().unwrap_or(0);
average_advance = fixed_26_dot_6_to_float(unsafe { if !sx_height.is_zero() {
FT_MulFix( x_height = sx_height as f64 * y_scale;
os2.x_average_char_width as FT_F26Dot6, }
freetype_metrics.x_scale,
) let x_average_char_width = os2.x_avg_char_width();
}); if !x_average_char_width.is_zero() {
average_advance = x_average_char_width as f64 * y_scale;
} }
} }
@ -451,7 +455,6 @@ trait FreeTypeFaceHelpers {
fn color(self) -> bool; fn color(self) -> bool;
fn set_size(self, pt_size: Au) -> Result<Au, &'static str>; fn set_size(self, pt_size: Au) -> Result<Au, &'static str>;
fn glyph_load_flags(self) -> FT_Int32; fn glyph_load_flags(self) -> FT_Int32;
fn os2_table(self) -> Option<OS2Table>;
} }
impl FreeTypeFaceHelpers for FT_Face { impl FreeTypeFaceHelpers for FT_Face {
@ -524,53 +527,6 @@ impl FreeTypeFaceHelpers for FT_Face {
load_flags as FT_Int32 load_flags as FT_Int32
} }
fn os2_table(self) -> Option<OS2Table> {
unsafe {
let os2 = FT_Get_Sfnt_Table(self, ft_sfnt_os2) as *mut TT_OS2;
let valid = !os2.is_null() && (*os2).version != 0xffff;
if !valid {
return None;
}
Some(OS2Table {
x_average_char_width: (*os2).xAvgCharWidth,
us_weight_class: (*os2).usWeightClass,
us_width_class: (*os2).usWidthClass,
y_strikeout_size: (*os2).yStrikeoutSize,
y_strikeout_position: (*os2).yStrikeoutPosition,
sx_height: (*os2).sxHeight,
})
}
}
}
#[repr(C)]
struct TtHeader {
table_version: FT_Fixed,
font_revision: FT_Fixed,
checksum_adjust: FT_Long,
magic_number: FT_Long,
flags: FT_UShort,
units_per_em: FT_UShort,
created: [FT_ULong; 2],
modified: [FT_ULong; 2],
x_min: FT_Short,
y_min: FT_Short,
x_max: FT_Short,
y_max: FT_Short,
mac_style: FT_UShort,
lowest_rec_ppem: FT_UShort,
font_direction: FT_Short,
index_to_loc_format: FT_Short,
glyph_data_format: FT_Short,
} }
unsafe extern "C" { unsafe extern "C" {
@ -582,3 +538,23 @@ unsafe extern "C" {
length: *mut FT_ULong, length: *mut FT_ULong,
) -> FT_Error; ) -> FT_Error;
} }
enum FreeTypeFaceTableProviderData {
Web(FontData),
Local(Mmap, u32),
}
impl FreeTypeFaceTableProviderData {
fn font_ref(&self) -> Result<FontRef<'_>, ReadError> {
match self {
Self::Web(ipc_shared_memory) => FontRef::new(&ipc_shared_memory.0),
Self::Local(mmap, index) => FontRef::from_index(mmap, *index),
}
}
}
impl std::fmt::Debug for FreeTypeFaceTableProviderData {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}