fc-fontations/attributes.rs | 176 ++++++++++++++++++++++++++++++++++ fc-fontations/foundries.rs | 6 - fc-fontations/mod.rs | 19 +++ fc-fontations/pattern_bindings/mod.rs | 12 +- test/test_fontations_ft_query.py | 3 5 files changed, 209 insertions(+), 7 deletions(-) New commits: commit 752eb3d1c8423de634c3f61cbdd3ae13c7fdab78 Merge: 2fdcfb4 0ddd01e Author: Akira TAGOH <akira@xxxxxxxxx> Date: Wed May 7 09:26:34 2025 +0000 Merge branch 'attributes' into 'main' [Fontations] Add attributes weight, width, slant to Pattern See merge request fontconfig/fontconfig!396 commit 0ddd01e33e18f9d597aa011e755981d5af9d35f4 Author: Dominik Röttsches <drott@xxxxxxxxxxxx> Date: Mon Apr 28 12:37:53 2025 +0300 [Fontations] Add attributes weight, width, slant to Pattern * Parse style attributes through Fontations Attributes and OS/2 table information. * Add an instance mode enum as preparation for handling variable fonts and named instances. * Enable comparison testing for "size", "weight", "width", "slant". diff --git a/fc-fontations/attributes.rs b/fc-fontations/attributes.rs new file mode 100644 index 0000000..e0ee28c --- /dev/null +++ b/fc-fontations/attributes.rs @@ -0,0 +1,176 @@ +extern crate fc_fontations_bindgen; + +use fc_fontations_bindgen::{ + fcint::{ + FC_INDEX_OBJECT, FC_NAMED_INSTANCE_OBJECT, FC_SLANT_OBJECT, FC_VARIABLE_OBJECT, + FC_WEIGHT_OBJECT, FC_WIDTH_OBJECT, + }, + FcWeightFromOpenTypeDouble, FC_SLANT_ITALIC, FC_SLANT_OBLIQUE, FC_SLANT_ROMAN, FC_WEIGHT_BLACK, + FC_WEIGHT_BOLD, FC_WEIGHT_EXTRABOLD, FC_WEIGHT_EXTRALIGHT, FC_WEIGHT_LIGHT, FC_WEIGHT_MEDIUM, + FC_WEIGHT_NORMAL, FC_WEIGHT_SEMIBOLD, FC_WEIGHT_THIN, FC_WIDTH_CONDENSED, FC_WIDTH_EXPANDED, + FC_WIDTH_EXTRACONDENSED, FC_WIDTH_EXTRAEXPANDED, FC_WIDTH_NORMAL, FC_WIDTH_SEMICONDENSED, + FC_WIDTH_SEMIEXPANDED, FC_WIDTH_ULTRACONDENSED, FC_WIDTH_ULTRAEXPANDED, +}; + +use crate::{ + pattern_bindings::{FcPatternBuilder, PatternElement}, + InstanceMode, +}; +use read_fonts::TableProvider; +use skrifa::{ + attribute::{Attributes, Stretch, Style, Weight}, + FontRef, +}; + +fn fc_weight(skrifa_weight: Weight) -> f64 { + (match skrifa_weight { + Weight::THIN => FC_WEIGHT_THIN, + Weight::EXTRA_LIGHT => FC_WEIGHT_EXTRALIGHT, + Weight::LIGHT => FC_WEIGHT_LIGHT, + Weight::NORMAL => FC_WEIGHT_NORMAL, + Weight::MEDIUM => FC_WEIGHT_MEDIUM, + Weight::SEMI_BOLD => FC_WEIGHT_SEMIBOLD, + Weight::BOLD => FC_WEIGHT_BOLD, + Weight::EXTRA_BOLD => FC_WEIGHT_EXTRABOLD, + Weight::BLACK => FC_WEIGHT_BLACK, + // See fcfreetype.c: When weight is not available, set to medium. + // This would mean a font did not have a parseable OS/2 table or + // a weight value could not be retrieved from it. + _ => FC_WEIGHT_MEDIUM, + }) as f64 +} + +fn fc_slant(skrifa_style: Style) -> u32 { + match skrifa_style { + Style::Italic => FC_SLANT_ITALIC, + Style::Oblique(_) => FC_SLANT_OBLIQUE, + _ => FC_SLANT_ROMAN, + } +} + +fn fc_width(skrifa_stretch: Stretch) -> f64 { + (match skrifa_stretch { + Stretch::ULTRA_CONDENSED => FC_WIDTH_ULTRACONDENSED, + Stretch::EXTRA_CONDENSED => FC_WIDTH_EXTRACONDENSED, + Stretch::CONDENSED => FC_WIDTH_CONDENSED, + Stretch::SEMI_CONDENSED => FC_WIDTH_SEMICONDENSED, + Stretch::NORMAL => FC_WIDTH_NORMAL, + Stretch::SEMI_EXPANDED => FC_WIDTH_SEMIEXPANDED, + Stretch::EXPANDED => FC_WIDTH_EXPANDED, + Stretch::EXTRA_EXPANDED => FC_WIDTH_EXTRAEXPANDED, + Stretch::ULTRA_EXPANDED => FC_WIDTH_ULTRAEXPANDED, + _ => FC_WIDTH_NORMAL, + } as f64) +} + +fn fc_weight_from_os2(font_ref: &FontRef) -> Option<f64> { + let us_weight = font_ref.os2().ok()?.us_weight_class() as f64; + unsafe { + let result = FcWeightFromOpenTypeDouble(us_weight); + if result == -1.0 { + None + } else { + Some(result) + } + } +} + +fn fc_width_from_os2(font_ref: &FontRef) -> Option<f64> { + let us_width = font_ref.os2().ok()?.us_width_class(); + let converted = match us_width { + 1 => FC_WIDTH_ULTRACONDENSED, + 2 => FC_WIDTH_EXTRACONDENSED, + 3 => FC_WIDTH_CONDENSED, + 4 => FC_WIDTH_SEMICONDENSED, + 5 => FC_WIDTH_NORMAL, + 6 => FC_WIDTH_SEMIEXPANDED, + 7 => FC_WIDTH_EXPANDED, + 8 => FC_WIDTH_EXTRAEXPANDED, + 9 => FC_WIDTH_ULTRAEXPANDED, + _ => FC_WIDTH_NORMAL, + }; + Some(converted as f64) +} + +struct AttributesToPattern { + weight_from_os2: Option<f64>, + width_from_os2: Option<f64>, + attributes: Attributes, +} + +impl AttributesToPattern { + fn new(font: &FontRef) -> Self { + Self { + weight_from_os2: fc_weight_from_os2(font), + width_from_os2: fc_width_from_os2(font), + attributes: Attributes::new(font), + } + } + + fn static_weight(&self) -> PatternElement { + self.weight_from_os2.map_or_else( + || { + PatternElement::new( + FC_WEIGHT_OBJECT as i32, + fc_weight(self.attributes.weight).into(), + ) + }, + |os2_weight| PatternElement::new(FC_WEIGHT_OBJECT as i32, os2_weight.into()), + ) + } + + fn static_width(&self) -> PatternElement { + self.width_from_os2.map_or_else( + || { + PatternElement::new( + FC_WIDTH_OBJECT as i32, + fc_width(self.attributes.stretch).into(), + ) + }, + |os2_width| PatternElement::new(FC_WIDTH_OBJECT as i32, os2_width.into()), + ) + } + + fn static_slant(&self) -> PatternElement { + PatternElement::new( + FC_SLANT_OBJECT as i32, + (fc_slant(self.attributes.style) as i32).into(), + ) + } +} + +pub fn append_style_elements( + font: &FontRef, + instance_mode: InstanceMode, + ttc_index: Option<i32>, + pattern: &mut FcPatternBuilder, +) { + // TODO: fcfreetype.c seems to prefer parsing information from the WWS name table entry, + // but falls back to flags if those are not found. So far, I haven't identified test fonts + // for which the WWS code path would trigger. + + let attributes_converter = AttributesToPattern::new(font); + + match instance_mode { + InstanceMode::Default => { + pattern.append_element(attributes_converter.static_weight()); + pattern.append_element(attributes_converter.static_width()); + pattern.append_element(attributes_converter.static_slant()); + + pattern.append_element(PatternElement::new(FC_VARIABLE_OBJECT as i32, false.into())); + pattern.append_element(PatternElement::new( + FC_INDEX_OBJECT as i32, + ttc_index.unwrap_or_default().into(), + )); + + pattern.append_element(PatternElement::new( + FC_NAMED_INSTANCE_OBJECT as i32, + false.into(), + )); + } + _ => { + // TODO: Variable and named instances not implemented yet. + unreachable!() + } + } +} diff --git a/fc-fontations/foundries.rs b/fc-fontations/foundries.rs index ad60647..536331a 100644 --- a/fc-fontations/foundries.rs +++ b/fc-fontations/foundries.rs @@ -63,7 +63,7 @@ pub fn make_foundry(font: &FontRef) -> Option<CString> { return CString::new(os2.ach_vend_id().to_be_bytes()).ok(); } - map_foundry_from_name_entry(&mut font.localized_strings(StringId::TRADEMARK)).or_else( - || map_foundry_from_name_entry(&mut font.localized_strings(StringId::MANUFACTURER)), - ) + map_foundry_from_name_entry(&mut font.localized_strings(StringId::TRADEMARK)).or_else(|| { + map_foundry_from_name_entry(&mut font.localized_strings(StringId::MANUFACTURER)) + }) } diff --git a/fc-fontations/mod.rs b/fc-fontations/mod.rs index 26754ff..1c4780d 100644 --- a/fc-fontations/mod.rs +++ b/fc-fontations/mod.rs @@ -22,10 +22,12 @@ * PERFORMANCE OF THIS SOFTWARE. */ +mod attributes; mod foundries; mod names; mod pattern_bindings; +use attributes::append_style_elements; use foundries::make_foundry; use names::add_names; @@ -80,6 +82,18 @@ pub unsafe extern "C" fn add_patterns_to_fontset( 1 } +/// Used for controlling FontConfig's behavior per font instance. +/// +/// We add one pattern for the default instance, one for each named instance, +/// and one for using the font as a variable font, with ranges of values where applicable. +#[derive(Copy, Clone)] +#[allow(dead_code)] +enum InstanceMode { + Default, + Named(i32), + Variable, +} + fn fonts_and_indices( file_ref: Option<FileRef>, ) -> impl Iterator<Item = (FontRef<'_>, Option<i32>)> { @@ -136,7 +150,7 @@ fn has_hint(font_ref: &FontRef) -> bool { fn build_patterns_for_font( font: &FontRef, _: *const libc::c_char, - _: Option<i32>, + ttc_index: Option<i32>, ) -> Vec<*mut FcPattern> { let mut pattern = FcPatternBuilder::new(); @@ -200,6 +214,9 @@ fn build_patterns_for_font( version.into(), )); + // TODO: Handle variable instance and named instances. + append_style_elements(font, InstanceMode::Default, ttc_index, &mut pattern); + pattern .create_fc_pattern() .map(|p| p.into_raw() as *mut FcPattern) diff --git a/fc-fontations/pattern_bindings/mod.rs b/fc-fontations/pattern_bindings/mod.rs index 29359e9..24f7158 100644 --- a/fc-fontations/pattern_bindings/mod.rs +++ b/fc-fontations/pattern_bindings/mod.rs @@ -55,15 +55,21 @@ impl From<CString> for PatternValue { } } +impl From<i32> for PatternValue { + fn from(item: i32) -> Self { + PatternValue::Integer(item) + } +} + impl From<bool> for PatternValue { fn from(item: bool) -> Self { PatternValue::Boolean(item) } } -impl From<i32> for PatternValue { - fn from(item: i32) -> Self { - PatternValue::Integer(item) +impl From<f64> for PatternValue { + fn from(item: f64) -> Self { + PatternValue::Double(item) } } diff --git a/test/test_fontations_ft_query.py b/test/test_fontations_ft_query.py index c08b16e..9a00e3e 100644 --- a/test/test_fontations_ft_query.py +++ b/test/test_fontations_ft_query.py @@ -62,6 +62,9 @@ def test_fontations_freetype_fcquery_equal(font_file): "fonthashint", "foundry", "version", + "weight", + "width", + "slant", ] format_string = ":".join( "%{" + entity + "}" for entity in supported_format_entitites