fonts: Add font variation support for macOS (#38760)

This change adds font variation support for macOS. The main bulk of the
change is reading the default, min, and max values for each variation
axis from the font and instantiating a new CoreText font with the
appropriate values. 

In addition, fonts with variations are now properly cached in the
CoreText font cache.

Testing: There are no tests for this change as we do not run WPT tests
for
macOS and font variations are currently turned off by default.
Eventually,
when the feature is turned on there will be test for it. These changes
are just laying the groundwork for the full implementation.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-08-19 08:12:48 -07:00 committed by GitHub
parent ad3018a921
commit 2022831e4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 197 additions and 33 deletions

View file

@ -7,14 +7,14 @@ use std::sync::{Arc, OnceLock};
use app_units::Au; use app_units::Au;
use core_foundation::base::TCFType; use core_foundation::base::TCFType;
use core_foundation::string::CFString; use core_foundation::data::CFData;
use core_foundation::number::CFNumber;
use core_foundation::string::{CFString, CFStringRef};
use core_foundation::url::{CFURL, kCFURLPOSIXPathStyle}; use core_foundation::url::{CFURL, kCFURLPOSIXPathStyle};
use core_graphics::data_provider::CGDataProvider;
use core_graphics::display::CFDictionary; use core_graphics::display::CFDictionary;
use core_graphics::font::CGFont; use core_text::font_descriptor::{kCTFontURLAttribute, kCTFontVariationAttribute};
use core_text::font::CTFont;
use core_text::font_descriptor::kCTFontURLAttribute;
use parking_lot::RwLock; use parking_lot::RwLock;
use webrender_api::FontVariation;
use crate::FontData; use crate::FontData;
use crate::platform::font::PlatformFont; use crate::platform::font::PlatformFont;
@ -30,41 +30,86 @@ static CACHE: CoreTextFontCache = CoreTextFontCache(OnceLock::new());
/// A [`HashMap`] of cached [`CTFont`] for a single [`FontIdentifier`]. There is one [`CTFont`] /// A [`HashMap`] of cached [`CTFont`] for a single [`FontIdentifier`]. There is one [`CTFont`]
/// for each cached font size. /// for each cached font size.
type CachedCTFont = HashMap<Au, CTFont>; type CachedCTFont = HashMap<CoreTextFontCacheKey, PlatformFont>;
#[derive(Eq, Hash, PartialEq)]
struct CoreTextFontCacheKey {
size: Au,
variations: Vec<FontVariation>,
}
impl CoreTextFontCache { impl CoreTextFontCache {
pub(crate) fn core_text_font( pub(crate) fn core_text_font(
font_identifier: FontIdentifier, font_identifier: FontIdentifier,
data: Option<&FontData>, data: Option<&FontData>,
pt_size: f64, pt_size: f64,
variations: &[FontVariation],
) -> Option<PlatformFont> { ) -> Option<PlatformFont> {
//// If you pass a zero font size to one of the Core Text APIs, it'll replace it with //// If you pass a zero font size to one of the Core Text APIs, it'll replace it with
//// 12.0. We don't want that! (Issue #10492.) //// 12.0. We don't want that! (Issue #10492.)
let clamped_pt_size = pt_size.max(0.01); let clamped_pt_size = pt_size.max(0.01);
let au_size = Au::from_f64_px(clamped_pt_size); let au_size = Au::from_f64_px(clamped_pt_size);
let key = CoreTextFontCacheKey {
size: au_size,
variations: variations.to_owned(),
};
let cache = CACHE.0.get_or_init(Default::default); let cache = CACHE.0.get_or_init(Default::default);
{ {
let cache = cache.read(); let cache = cache.read();
if let Some(core_text_font) = cache if let Some(platform_font) = cache
.get(&font_identifier) .get(&font_identifier)
.and_then(|identifier_cache| identifier_cache.get(&au_size)) .and_then(|identifier_cache| identifier_cache.get(&key))
{ {
return Some(PlatformFont::new_with_ctfont(core_text_font.clone())); return Some(platform_font.clone());
} }
} }
if !key.variations.is_empty() {
let core_text_font_no_variations =
Self::core_text_font(font_identifier.clone(), data, clamped_pt_size, &[])?;
let mut cache = cache.write();
let entry = cache.entry(font_identifier.clone()).or_default();
// It could be that between the time of the cache miss above and now, after the write lock
// on the cache has been acquired, the cache was populated with the data that we need. Thus
// check again and return the CTFont if it is is already cached.
if let Some(core_text_font) = entry.get(&key) {
return Some(core_text_font.clone());
}
let platform_font = Self::add_variations_to_font(
core_text_font_no_variations,
&key.variations,
clamped_pt_size,
);
entry.insert(key, platform_font.clone());
return Some(platform_font);
}
let mut cache = cache.write(); let mut cache = cache.write();
let identifier_cache = cache.entry(font_identifier.clone()).or_default(); let identifier_cache = cache.entry(font_identifier.clone()).or_default();
// It could be that between the time of the cache miss above and now, after the write lock // It could be that between the time of the cache miss above and now, after the write lock
// on the cache has been acquired, the cache was populated with the data that we need. Thus // on the cache has been acquired, the cache was populated with the data that we need. Thus
// check again and return the CTFont if it is is already cached. // check again and return the CTFont if it is is already cached.
if let Some(core_text_font) = identifier_cache.get(&au_size) { if let Some(platform_font) = identifier_cache.get(&key) {
return Some(PlatformFont::new_with_ctfont(core_text_font.clone())); return Some(platform_font.clone());
} }
let core_text_font = match font_identifier { let platform_font =
Self::create_font_without_variations(font_identifier, data, clamped_pt_size)?;
identifier_cache.insert(key, platform_font.clone());
Some(platform_font)
}
pub(crate) fn create_font_without_variations(
font_identifier: FontIdentifier,
data: Option<&FontData>,
clamped_pt_size: f64,
) -> Option<PlatformFont> {
let descriptor = match font_identifier {
FontIdentifier::Local(local_font_identifier) => { FontIdentifier::Local(local_font_identifier) => {
// Other platforms can instantiate a platform font by loading the data // Other platforms can instantiate a platform font by loading the data
// from a file and passing an index in the case the file is a TTC bundle. // from a file and passing an index in the case the file is a TTC bundle.
@ -72,7 +117,7 @@ impl CoreTextFontCache {
// macOS is to create the font using a descriptor with both the PostScript // macOS is to create the font using a descriptor with both the PostScript
// name and path. // name and path.
let cf_name = CFString::new(&local_font_identifier.postscript_name); let cf_name = CFString::new(&local_font_identifier.postscript_name);
let mut descriptor = core_text::font_descriptor::new_from_postscript_name(&cf_name); let descriptor = core_text::font_descriptor::new_from_postscript_name(&cf_name);
let cf_path = CFString::new(&local_font_identifier.path); let cf_path = CFString::new(&local_font_identifier.path);
let url_attribute = unsafe { CFString::wrap_under_get_rule(kCTFontURLAttribute) }; let url_attribute = unsafe { CFString::wrap_under_get_rule(kCTFontURLAttribute) };
@ -80,25 +125,128 @@ impl CoreTextFontCache {
url_attribute, url_attribute,
CFURL::from_file_system_path(cf_path, kCFURLPOSIXPathStyle, false), CFURL::from_file_system_path(cf_path, kCFURLPOSIXPathStyle, false),
)]); )]);
if let Ok(descriptor_with_path) =
descriptor.create_copy_with_attributes(attributes.to_untyped())
{
descriptor = descriptor_with_path;
}
core_text::font::new_from_descriptor(&descriptor, clamped_pt_size) descriptor.create_copy_with_attributes(attributes.to_untyped())
}, },
FontIdentifier::Web(_) => { FontIdentifier::Web(_) => {
let data = data let data = data
.expect("Should always have FontData for web fonts") .expect("Should always have FontData for web fonts")
.clone(); .clone();
let provider = CGDataProvider::from_buffer(Arc::new(data)); let cf_data = CFData::from_arc(Arc::new(data));
let cgfont = CGFont::from_data_provider(provider).ok()?; core_text::font_manager::create_font_descriptor_with_data(cf_data)
core_text::font::new_from_CGFont(&cgfont, clamped_pt_size)
}, },
}; };
identifier_cache.insert(au_size, core_text_font.clone()); Some(PlatformFont::new_with_ctfont(
Some(PlatformFont::new_with_ctfont(core_text_font)) core_text::font::new_from_descriptor(&descriptor.ok()?, clamped_pt_size),
))
}
fn add_variations_to_font(
platform_font: PlatformFont,
specified_variations: &[FontVariation],
pt_size: f64,
) -> PlatformFont {
if specified_variations.is_empty() {
return platform_font;
}
let Some(variations) = Self::get_variation_axis_information(&platform_font) else {
return platform_font;
};
let mut modified_variations = false;
let variations: Vec<_> = variations
.iter()
.map(|variation| {
let value = specified_variations
.iter()
.find_map(|specified_variation| {
if variation.tag == specified_variation.tag as i64 {
Some(specified_variation.value as f64)
} else {
None
}
})
.unwrap_or(variation.default_value)
.clamp(variation.min_value, variation.max_value);
if value != variation.default_value {
modified_variations = true;
}
FontVariation {
tag: variation.tag as u32,
value: value as f32,
}
})
.collect();
if !modified_variations {
return platform_font;
}
let cftype_variations: Vec<_> = variations
.iter()
.map(|variation| {
(
CFNumber::from(variation.tag as i64),
CFNumber::from(variation.value as f64),
)
})
.collect();
let values_dict = CFDictionary::from_CFType_pairs(&cftype_variations);
let variation_attribute =
unsafe { CFString::wrap_under_get_rule(kCTFontVariationAttribute) };
let attrs_dict = CFDictionary::from_CFType_pairs(&[(variation_attribute, values_dict)]);
let ct_var_font_desc = platform_font
.ctfont
.copy_descriptor()
.create_copy_with_attributes(attrs_dict.to_untyped())
.unwrap();
let ctfont = core_text::font::new_from_descriptor(&ct_var_font_desc, pt_size);
PlatformFont::new_with_ctfont_and_variations(ctfont, variations)
}
fn get_variation_axis_information(
platform_font: &PlatformFont,
) -> Option<Vec<VariationAxisInformation>> {
Some(
platform_font
.ctfont
.get_variation_axes()?
.iter()
.filter_map(|axes| {
let tag = unsafe { axes.find(kCTFontVariationAxisIdentifierKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let max_value = unsafe { axes.find(kCTFontVariationAxisMaximumValueKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let min_value = unsafe { axes.find(kCTFontVariationAxisMinimumValueKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let default_value = unsafe { axes.find(kCTFontVariationAxisDefaultValueKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
Some(VariationAxisInformation {
tag: tag.to_i64()?,
max_value: max_value.to_f64()?,
min_value: min_value.to_f64()?,
default_value: default_value.to_f64()?,
})
})
.collect(),
)
} }
} }
#[derive(Clone, Default, Debug)]
struct VariationAxisInformation {
tag: i64,
max_value: f64,
min_value: f64,
default_value: f64,
}
unsafe extern "C" {
static kCTFontVariationAxisDefaultValueKey: CFStringRef;
static kCTFontVariationAxisIdentifierKey: CFStringRef;
static kCTFontVariationAxisMaximumValueKey: CFStringRef;
static kCTFontVariationAxisMinimumValueKey: CFStringRef;
}

View file

@ -31,6 +31,7 @@ use crate::{
const KERN_PAIR_LEN: usize = 6; const KERN_PAIR_LEN: usize = 6;
#[derive(Clone)]
pub struct FontTable { pub struct FontTable {
data: CFData, data: CFData,
} }
@ -52,9 +53,10 @@ impl FontTableMethods for FontTable {
} }
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct PlatformFont { pub struct PlatformFont {
ctfont: CTFont, pub(crate) ctfont: CTFont,
variations: Vec<FontVariation>,
h_kern_subtable: Option<CachedKernTable>, h_kern_subtable: Option<CachedKernTable>,
} }
@ -71,8 +73,16 @@ unsafe impl Send for PlatformFont {}
impl PlatformFont { impl PlatformFont {
pub(crate) fn new_with_ctfont(ctfont: CTFont) -> Self { pub(crate) fn new_with_ctfont(ctfont: CTFont) -> Self {
Self::new_with_ctfont_and_variations(ctfont, vec![])
}
pub(crate) fn new_with_ctfont_and_variations(
ctfont: CTFont,
variations: Vec<FontVariation>,
) -> PlatformFont {
Self { Self {
ctfont, ctfont,
variations,
h_kern_subtable: None, h_kern_subtable: None,
} }
} }
@ -81,13 +91,14 @@ impl PlatformFont {
font_identifier: FontIdentifier, font_identifier: FontIdentifier,
data: Option<&FontData>, data: Option<&FontData>,
requested_size: Option<Au>, requested_size: Option<Au>,
variations: &[FontVariation],
) -> Result<PlatformFont, &'static str> { ) -> Result<PlatformFont, &'static str> {
let size = match requested_size { let size = match requested_size {
Some(s) => s.to_f64_px(), Some(s) => s.to_f64_px(),
None => 0.0, None => 0.0,
}; };
let Some(mut platform_font) = let Some(mut platform_font) =
CoreTextFontCache::core_text_font(font_identifier, data, size) CoreTextFontCache::core_text_font(font_identifier, data, size, variations)
else { else {
return Err("Could not generate CTFont for FontTemplateData"); return Err("Could not generate CTFont for FontTemplateData");
}; };
@ -162,6 +173,7 @@ impl PlatformFont {
} }
} }
#[derive(Clone)]
struct CachedKernTable { struct CachedKernTable {
font_table: FontTable, font_table: FontTable,
pair_data_range: Range<usize>, pair_data_range: Range<usize>,
@ -201,17 +213,22 @@ impl PlatformFontMethods for PlatformFont {
font_identifier: FontIdentifier, font_identifier: FontIdentifier,
data: &FontData, data: &FontData,
requested_size: Option<Au>, requested_size: Option<Au>,
_variations: &[FontVariation], variations: &[FontVariation],
) -> Result<PlatformFont, &'static str> { ) -> Result<PlatformFont, &'static str> {
Self::new(font_identifier, Some(data), requested_size) Self::new(font_identifier, Some(data), requested_size, variations)
} }
fn new_from_local_font_identifier( fn new_from_local_font_identifier(
font_identifier: LocalFontIdentifier, font_identifier: LocalFontIdentifier,
requested_size: Option<Au>, requested_size: Option<Au>,
_variations: &[FontVariation], variations: &[FontVariation],
) -> Result<PlatformFont, &'static str> { ) -> Result<PlatformFont, &'static str> {
Self::new(FontIdentifier::Local(font_identifier), None, requested_size) Self::new(
FontIdentifier::Local(font_identifier),
None,
requested_size,
variations,
)
} }
fn descriptor(&self) -> FontTemplateDescriptor { fn descriptor(&self) -> FontTemplateDescriptor {
@ -357,8 +374,7 @@ impl PlatformFontMethods for PlatformFont {
} }
fn variations(&self) -> &[FontVariation] { fn variations(&self) -> &[FontVariation] {
// FIXME: Implement this for macos &self.variations
&[]
} }
} }