fonts: Initial draft of synthetic bold face for FreeType (#39519)

This is an initial attempt at implementing synthetic bold face for font
families that lack actual bold faces. The overall approach borrowed
implementations from Chromium and FireFox. WPT expectations will be
updated after https://github.com/servo/stylo/pull/244 lands

Testing: There are existing WPT testcases for font synthesis
(`wpt/css/css-fonts/font-synthesis-*`)

Depends on: https://github.com/servo/stylo/pull/244

---------

Signed-off-by: Minghua Wu <michael.wu1107@gmail.com>
Signed-off-by: minghuaw <michael.wu1107@gmail.com>
This commit is contained in:
minghuaw 2025-10-02 05:54:12 +08:00 committed by GitHub
parent 4ea363277e
commit bcd8bbb142
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 141 additions and 151 deletions

View file

@ -30,7 +30,7 @@ use style::properties::style_structs::Font as FontStyleStruct;
use style::values::computed::font::{
FamilyName, FontFamilyNameSyntax, GenericFontFamily, SingleFontFamily,
};
use style::values::computed::{FontStretch, FontStyle, FontWeight};
use style::values::computed::{FontStretch, FontStyle, FontSynthesis, FontWeight};
use unicode_script::Script;
use webrender_api::{FontInstanceFlags, FontInstanceKey, FontVariation};
@ -68,20 +68,25 @@ pub trait PlatformFontMethods: Sized {
pt_size: Option<Au>,
variations: &[FontVariation],
data: &Option<FontData>,
synthetic_bold: bool,
) -> Result<PlatformFont, &'static str> {
let template = template.borrow();
let font_identifier = template.identifier.clone();
match font_identifier {
FontIdentifier::Local(font_identifier) => {
Self::new_from_local_font_identifier(font_identifier, pt_size, variations)
},
FontIdentifier::Local(font_identifier) => Self::new_from_local_font_identifier(
font_identifier,
pt_size,
variations,
synthetic_bold,
),
FontIdentifier::Web(_) => Self::new_from_data(
font_identifier,
data.as_ref()
.expect("Should never create a web font without data."),
pt_size,
variations,
synthetic_bold,
),
}
}
@ -90,6 +95,7 @@ pub trait PlatformFontMethods: Sized {
font_identifier: LocalFontIdentifier,
pt_size: Option<Au>,
variations: &[FontVariation],
synthetic_bold: bool,
) -> Result<PlatformFont, &'static str>;
fn new_from_data(
@ -97,6 +103,7 @@ pub trait PlatformFontMethods: Sized {
data: &FontData,
pt_size: Option<Au>,
variations: &[FontVariation],
synthetic_bold: bool,
) -> Result<PlatformFont, &'static str>;
/// Get a [`FontTemplateDescriptor`] from a [`PlatformFont`]. This is used to get
@ -269,11 +276,20 @@ impl Font {
data: Option<FontData>,
synthesized_small_caps: Option<FontRef>,
) -> Result<Font, &'static str> {
let synthetic_bold = {
let is_bold = descriptor.weight >= FontWeight::BOLD_THRESHOLD;
let template_is_bold = template.descriptor().weight.0 >= FontWeight::BOLD_THRESHOLD;
let allows_synthetic_bold = matches!(descriptor.synthesis_weight, FontSynthesis::Auto);
is_bold && !template_is_bold && allows_synthetic_bold
};
let handle = PlatformFont::new_from_template(
template.clone(),
Some(descriptor.pt_size),
&descriptor.variation_settings,
&data,
synthetic_bold,
)?;
let metrics = handle.metrics();

View file

@ -878,7 +878,8 @@ impl RemoteWebFontDownloader {
let url: ServoUrl = self.url.clone().into();
let identifier = FontIdentifier::Web(url.clone());
let Ok(handle) = PlatformFont::new_from_data(identifier, &font_data, None, &[]) else {
let Ok(handle) = PlatformFont::new_from_data(identifier, &font_data, None, &[], false)
else {
return false;
};
let state = self.take_state();

View file

@ -9,9 +9,9 @@ use app_units::Au;
use euclid::default::{Point2D, Rect, Size2D};
use fonts_traits::{FontIdentifier, FontTemplateDescriptor, LocalFontIdentifier};
use freetype_sys::{
FT_F26Dot6, FT_Get_Char_Index, FT_Get_Kerning, FT_GlyphSlot, FT_KERNING_DEFAULT,
FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_Load_Glyph, FT_Size_Metrics, FT_SizeRec, FT_UInt,
FT_ULong, FT_Vector,
FT_F26Dot6, FT_Get_Char_Index, FT_Get_Kerning, FT_GlyphSlot, FT_HAS_MULTIPLE_MASTERS,
FT_KERNING_DEFAULT, FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_Load_Glyph, FT_Size_Metrics,
FT_SizeRec, FT_UInt, FT_ULong, FT_Vector,
};
use log::debug;
use memmap2::Mmap;
@ -56,6 +56,7 @@ pub struct PlatformFont {
requested_face_size: Au,
actual_face_size: Au,
variations: Vec<FontVariation>,
synthetic_bold: bool,
/// A member that allows using `skrifa` to read values from this font.
table_provider_data: FreeTypeFaceTableProviderData,
@ -67,6 +68,7 @@ impl PlatformFontMethods for PlatformFont {
font_data: &FontData,
requested_size: Option<Au>,
variations: &[FontVariation],
mut synthetic_bold: bool,
) -> Result<PlatformFont, &'static str> {
let library = FreeTypeLibraryHandle::get().lock();
let data: &[u8] = font_data.as_ref();
@ -79,12 +81,22 @@ impl PlatformFontMethods for PlatformFont {
None => (Au::zero(), Au::zero()),
};
// Variable fonts, where the font designer has provided one or more axes of
// variation do not count as font synthesis and their use is not affected by
// the font-synthesis property.
//
// <https://www.w3.org/TR/css-fonts-4/#font-synthesis-intro>
if FT_HAS_MULTIPLE_MASTERS(face.as_ptr()) {
synthetic_bold = false;
}
Ok(PlatformFont {
face: ReentrantMutex::new(face),
requested_face_size,
actual_face_size,
table_provider_data: FreeTypeFaceTableProviderData::Web(font_data.clone()),
variations: normalized_variations,
synthetic_bold,
})
}
@ -92,6 +104,7 @@ impl PlatformFontMethods for PlatformFont {
font_identifier: LocalFontIdentifier,
requested_size: Option<Au>,
variations: &[FontVariation],
mut synthetic_bold: bool,
) -> Result<PlatformFont, &'static str> {
let library = FreeTypeLibraryHandle::get().lock();
let filename = CString::new(&*font_identifier.path).expect("filename contains NUL byte!");
@ -115,6 +128,15 @@ impl PlatformFontMethods for PlatformFont {
return Err("Could not memory map");
};
// Variable fonts, where the font designer has provided one or more axes of
// variation do not count as font synthesis and their use is not affected by
// the font-synthesis property.
//
// <https://www.w3.org/TR/css-fonts-4/#font-synthesis-intro>
if FT_HAS_MULTIPLE_MASTERS(face.as_ptr()) {
synthetic_bold = false;
}
Ok(PlatformFont {
face: ReentrantMutex::new(face),
requested_face_size,
@ -124,6 +146,7 @@ impl PlatformFontMethods for PlatformFont {
font_identifier.index(),
),
variations: normalized_variations,
synthetic_bold,
})
}
@ -184,6 +207,10 @@ impl PlatformFontMethods for PlatformFont {
let slot: FT_GlyphSlot = void_glyph;
assert!(!slot.is_null());
if self.synthetic_bold {
mozilla_glyphslot_embolden_less(slot);
}
let advance = unsafe { (*slot).metrics.horiAdvance };
Some(fixed_26_dot_6_to_float(advance) * self.unscalable_font_metrics_scale())
}
@ -358,7 +385,13 @@ impl PlatformFontMethods for PlatformFont {
// On other platforms, we only pass this when we know that we are loading a font with
// color characters, but not passing this flag simply *prevents* WebRender from
// loading bitmaps. There's no harm to always passing it.
FontInstanceFlags::EMBEDDED_BITMAPS
let mut flags = FontInstanceFlags::EMBEDDED_BITMAPS;
if self.synthetic_bold {
flags |= FontInstanceFlags::SYNTHETIC_BOLD;
}
flags
}
fn variations(&self) -> &[FontVariation] {
@ -395,3 +428,47 @@ impl std::fmt::Debug for FreeTypeFaceTableProviderData {
Ok(())
}
}
// This is copied from the webrender glyph rasterizer
// https://github.com/servo/webrender/blob/c4bd5b47d8f5cd684334b445e67a1f945d106848/wr_glyph_rasterizer/src/platform/unix/font.rs#L115
//
// Custom version of FT_GlyphSlot_Embolden to be less aggressive with outline
// fonts than the default implementation in FreeType.
fn mozilla_glyphslot_embolden_less(slot: FT_GlyphSlot) {
use freetype_sys::{
FT_GLYPH_FORMAT_OUTLINE, FT_GlyphSlot_Embolden, FT_Long, FT_MulFix, FT_Outline_Embolden,
};
if slot.is_null() {
return;
}
let slot_ = unsafe { &mut *slot };
let format = slot_.format;
if format != FT_GLYPH_FORMAT_OUTLINE {
// For non-outline glyphs, just fall back to FreeType's function.
unsafe { FT_GlyphSlot_Embolden(slot) };
return;
}
let face_ = unsafe { &*slot_.face };
// FT_GlyphSlot_Embolden uses a divisor of 24 here; we'll be only half as
// bold.
let size_ = unsafe { &*face_.size };
let strength = unsafe { FT_MulFix(face_.units_per_EM as FT_Long, size_.metrics.y_scale) / 48 };
unsafe { FT_Outline_Embolden(&raw mut slot_.outline, strength) };
// Adjust metrics to suit the fattened glyph.
if slot_.advance.x != 0 {
slot_.advance.x += strength;
}
if slot_.advance.y != 0 {
slot_.advance.y += strength;
}
slot_.metrics.width += strength;
slot_.metrics.height += strength;
slot_.metrics.horiAdvance += strength;
slot_.metrics.vertAdvance += strength;
slot_.metrics.horiBearingY += strength;
}

View file

@ -214,6 +214,7 @@ impl PlatformFontMethods for PlatformFont {
data: &FontData,
requested_size: Option<Au>,
variations: &[FontVariation],
_synthetic_bold: bool,
) -> Result<PlatformFont, &'static str> {
Self::new(font_identifier, Some(data), requested_size, variations)
}
@ -222,6 +223,7 @@ impl PlatformFontMethods for PlatformFont {
font_identifier: LocalFontIdentifier,
requested_size: Option<Au>,
variations: &[FontVariation],
_synthetic_bold: bool,
) -> Result<PlatformFont, &'static str> {
Self::new(
FontIdentifier::Local(font_identifier),

View file

@ -158,6 +158,7 @@ impl PlatformFontMethods for PlatformFont {
data: &FontData,
pt_size: Option<Au>,
variations: &[FontVariation],
_synthetic_bold: bool,
) -> Result<Self, &'static str> {
let font_face = FontFile::new_from_buffer(Arc::new(data.clone()))
.ok_or("Could not create FontFile")?
@ -170,6 +171,7 @@ impl PlatformFontMethods for PlatformFont {
font_identifier: LocalFontIdentifier,
pt_size: Option<Au>,
variations: &[FontVariation],
_synthetic_bold: bool,
) -> Result<PlatformFont, &'static str> {
let font_face = FontCollection::system()
.font_from_descriptor(&font_identifier.font_descriptor)

View file

@ -15,7 +15,7 @@ use fonts::{
};
use servo_url::ServoUrl;
use style::properties::longhands::font_variant_caps::computed_value::T as FontVariantCaps;
use style::values::computed::{FontStretch, FontStyle, FontWeight};
use style::values::computed::{FontStretch, FontStyle, FontSynthesis, FontWeight};
use unicode_script::Script;
fn make_font(path: PathBuf) -> Font {
@ -27,7 +27,8 @@ fn make_font(path: PathBuf) -> Font {
let data = FontData::from_bytes(&bytes);
let identifier = FontIdentifier::Web(ServoUrl::from_file_path(path).unwrap());
let platform_font = PlatformFont::new_from_data(identifier.clone(), &data, None, &[]).unwrap();
let platform_font =
PlatformFont::new_from_data(identifier.clone(), &data, None, &[], false).unwrap();
let template = FontTemplate {
identifier,
@ -41,6 +42,7 @@ fn make_font(path: PathBuf) -> Font {
variant: FontVariantCaps::Normal,
pt_size: Au::from_px(24),
variation_settings: vec![],
synthesis_weight: FontSynthesis::Auto,
};
Font::new(FontTemplateRef::new(template), descriptor, Some(data), None).unwrap()
}

View file

@ -32,7 +32,7 @@ mod font_context {
use style::properties::style_structs::Font as FontStyleStruct;
use style::values::computed::font::{
FamilyName, FontFamily, FontFamilyList, FontFamilyNameSyntax, FontStretch, FontStyle,
FontWeight, SingleFontFamily,
FontSynthesis, FontWeight, SingleFontFamily,
};
use stylo_atoms::Atom;
use webrender_api::{FontInstanceKey, FontKey, IdNamespace};
@ -190,6 +190,7 @@ mod font_context {
local_font_identifier.clone(),
None,
&[],
false,
)
.expect("Could not load test font");
@ -355,6 +356,7 @@ mod font_context {
variant: FontVariantCaps::Normal,
pt_size: Au(10),
variation_settings: vec![],
synthesis_weight: FontSynthesis::Auto,
};
let family = SingleFontFamily::FamilyName(FamilyName {

View file

@ -36,7 +36,7 @@ fn test_font_template_descriptor() {
.unwrap();
let data = FontData::from_bytes(&bytes);
let handle = PlatformFont::new_from_data(identifier, &data, None, &[]).unwrap();
let handle = PlatformFont::new_from_data(identifier, &data, None, &[], false).unwrap();
handle.descriptor()
}

View file

@ -934,6 +934,7 @@ malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::font::SingleFon
malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::JustifyContent);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::align::AlignFlags);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::box_::Overflow);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::font::FontSynthesis);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::TextDecorationLine);
malloc_size_of_is_stylo_malloc_size_of!(stylo_dom::ElementState);

View file

@ -10,7 +10,7 @@ use style::computed_values::font_variant_caps;
use style::font_face::{FontFaceRuleData, FontStyle as FontFaceStyle};
use style::properties::style_structs::Font as FontStyleStruct;
use style::values::computed::font::{FixedPoint, FontStyleFixedPoint};
use style::values::computed::{Au, FontStretch, FontStyle, FontWeight};
use style::values::computed::{Au, FontStretch, FontStyle, FontSynthesis, FontWeight};
use style::values::specified::FontStretch as SpecifiedFontStretch;
use webrender_api::FontVariation;
@ -26,6 +26,7 @@ pub struct FontDescriptor {
pub variant: font_variant_caps::T,
pub pt_size: Au,
pub variation_settings: Vec<FontVariation>,
pub synthesis_weight: FontSynthesis,
}
impl Eq for FontDescriptor {}
@ -41,6 +42,7 @@ impl<'a> From<&'a FontStyleStruct> for FontDescriptor {
value: setting.value,
})
.collect();
let synthesis_weight = style.clone_font_synthesis_weight();
FontDescriptor {
weight: style.font_weight,
stretch: style.font_stretch,
@ -48,6 +50,7 @@ impl<'a> From<&'a FontStyleStruct> for FontDescriptor {
variant: style.font_variant_caps,
pt_size: Au::from_f32_px(style.font_size.computed_size().px()),
variation_settings,
synthesis_weight,
}
}
}