doc/fcfontations.fncs | 2 fc-fontations-bindgen/build.rs | 3 fc-fontations/attributes.rs | 173 +++++++++++++++++++++++++-- fc-fontations/instance_enumerate.rs | 164 +++++++++++++++++++++++++ fc-fontations/mod.rs | 92 +++++++------- fc-fontations/names.rs | 97 ++++++++++++++- fc-fontations/pattern_bindings/fc_wrapper.rs | 13 +- fc-fontations/pattern_bindings/mod.rs | 16 +- test/meson.build | 2 test/test_fontations_ft_query.py | 6 10 files changed, 494 insertions(+), 74 deletions(-) New commits: commit 3454d29ef0761978897de5f28e388e5b0a3489f4 Merge: 4e6cdf7 bd37dd6 Author: Akira TAGOH <akira@xxxxxxxxx> Date: Thu May 8 09:40:03 2025 +0000 Merge branch 'instances' into 'main' [Fontations] Process variable instances See merge request fontconfig/fontconfig!399 commit bd37dd6150b157329c240bcee07ef88242ebe29e Author: Dominik Röttsches <drott@xxxxxxxxxxxx> Date: Wed Apr 30 12:54:51 2025 +0300 [Fontations] Iterate over TrueType collections and named instances * Create an iterator for generating instance modes for variable fonts * Refactor attributes assignment to take into account variable axes * Refactor names assignment to take into account instance names * Enable Roboto Flex pattern comparison test diff --git a/doc/fcfontations.fncs b/doc/fcfontations.fncs index 9b3b5a5..12328cd 100644 --- a/doc/fcfontations.fncs +++ b/doc/fcfontations.fncs @@ -35,7 +35,7 @@ @PURPOSE@ compute all patterns from font file (and index) @DESC@ UNSTABLE feature. Constructs patterns found in 'file'. Currently pattern is incomplete -compared to FcFreeTypeQueryAll(). +compared to FcFreeTypeQueryAll() and may deviate from what FreeType indexing produces. Not supported yet, but will follow: If id is -1, then all patterns found in 'file' are added to 'set'. Otherwise, this function works exactly like FcFreeTypeQueryAll()/FcFreeTypeQuery(). diff --git a/fc-fontations/attributes.rs b/fc-fontations/attributes.rs index e0ee28c..41c853b 100644 --- a/fc-fontations/attributes.rs +++ b/fc-fontations/attributes.rs @@ -2,8 +2,8 @@ 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, + FC_INDEX_OBJECT, FC_NAMED_INSTANCE_OBJECT, FC_SIZE_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, @@ -13,13 +13,13 @@ use fc_fontations_bindgen::{ }; use crate::{ - pattern_bindings::{FcPatternBuilder, PatternElement}, + pattern_bindings::{fc_wrapper::FcRangeWrapper, FcPatternBuilder, PatternElement}, InstanceMode, }; use read_fonts::TableProvider; use skrifa::{ attribute::{Attributes, Stretch, Style, Weight}, - FontRef, + AxisCollection, FontRef, MetadataProvider, NamedInstance, Tag, }; fn fc_weight(skrifa_weight: Weight) -> f64 { @@ -92,21 +92,38 @@ fn fc_width_from_os2(font_ref: &FontRef) -> Option<f64> { Some(converted as f64) } -struct AttributesToPattern { +struct AttributesToPattern<'a> { weight_from_os2: Option<f64>, width_from_os2: Option<f64>, attributes: Attributes, + axes: AxisCollection<'a>, + named_instance: Option<NamedInstance<'a>>, } -impl AttributesToPattern { - fn new(font: &FontRef) -> Self { +impl<'a> AttributesToPattern<'a> { + fn new(font: &'a FontRef, instance_mode: &InstanceMode) -> Self { + let named_instance = match instance_mode { + InstanceMode::Named(index) => font.named_instances().get(*index as usize), + _ => None, + }; Self { weight_from_os2: fc_weight_from_os2(font), width_from_os2: fc_width_from_os2(font), attributes: Attributes::new(font), + axes: font.axes(), + named_instance, } } + fn user_coord_for_tag(&self, tag: Tag) -> Option<f64> { + let mut axis_coords = self + .axes + .iter() + .map(|axis| axis.tag()) + .zip(self.named_instance.clone()?.user_coords()); + Some(axis_coords.find(|item| item.0 == tag)?.1 as f64) + } + fn static_weight(&self) -> PatternElement { self.weight_from_os2.map_or_else( || { @@ -137,6 +154,89 @@ impl AttributesToPattern { (fc_slant(self.attributes.style) as i32).into(), ) } + + fn instance_weight(&self) -> Option<PatternElement> { + let named_instance_weight = self.user_coord_for_tag(Tag::new(b"wght"))?; + unsafe { + Some(PatternElement::new( + FC_WEIGHT_OBJECT as i32, + FcWeightFromOpenTypeDouble(named_instance_weight).into(), + )) + } + } + + fn instance_width(&self) -> Option<PatternElement> { + let named_instance_weight = self.user_coord_for_tag(Tag::new(b"wdth"))?; + + Some(PatternElement::new( + FC_WIDTH_OBJECT as i32, + named_instance_weight.into(), + )) + } + + fn instance_slant(&self) -> Option<PatternElement> { + let named_instance_slant = self.user_coord_for_tag(Tag::new(b"slnt"))?; + if named_instance_slant < 0.0 { + Some(PatternElement::new( + FC_SLANT_OBJECT as i32, + (FC_SLANT_ITALIC as i32).into(), + )) + } else { + Some(PatternElement::new( + FC_SLANT_OBJECT as i32, + (FC_SLANT_ROMAN as i32).into(), + )) + } + } + + fn instance_size(&self) -> Option<PatternElement> { + let named_instance_size = self.user_coord_for_tag(Tag::new(b"opsz"))?; + + Some(PatternElement::new( + FC_SIZE_OBJECT as i32, + named_instance_size.into(), + )) + } + + fn default_size(&self) -> Option<PatternElement> { + self.axes.get_by_tag(Tag::new(b"opsz")).map(|opsz_axis| { + PatternElement::new( + FC_SIZE_OBJECT as i32, + (opsz_axis.default_value() as f64).into(), + ) + }) + } + + fn variable_weight(&self) -> Option<PatternElement> { + let weight_axis = self.axes.get_by_tag(Tag::new(b"wght"))?; + unsafe { + Some(PatternElement::new( + FC_WEIGHT_OBJECT as i32, + FcRangeWrapper::new( + FcWeightFromOpenTypeDouble(weight_axis.min_value() as f64), + FcWeightFromOpenTypeDouble(weight_axis.max_value() as f64), + )? + .into(), + )) + } + } + + fn variable_width(&self) -> Option<PatternElement> { + let width_axis = self.axes.get_by_tag(Tag::new(b"wdth"))?; + Some(PatternElement::new( + FC_WIDTH_OBJECT as i32, + FcRangeWrapper::new(width_axis.min_value() as f64, width_axis.max_value() as f64)? + .into(), + )) + } + + fn variable_opsz(&self) -> Option<PatternElement> { + let opsz_axis = self.axes.get_by_tag(Tag::new(b"opsz"))?; + Some(PatternElement::new( + FC_SIZE_OBJECT as i32, + FcRangeWrapper::new(opsz_axis.min_value() as f64, opsz_axis.max_value() as f64)?.into(), + )) + } } pub fn append_style_elements( @@ -149,7 +249,7 @@ pub fn append_style_elements( // 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); + let attributes_converter = AttributesToPattern::new(font, &instance_mode); match instance_mode { InstanceMode::Default => { @@ -168,9 +268,60 @@ pub fn append_style_elements( false.into(), )); } - _ => { - // TODO: Variable and named instances not implemented yet. - unreachable!() + InstanceMode::Variable => { + if let Some(weight_to_add) = attributes_converter + .variable_weight() + .or(Some(attributes_converter.static_weight())) + { + pattern.append_element(weight_to_add); + } + if let Some(width_to_add) = attributes_converter + .variable_width() + .or(Some(attributes_converter.static_width())) + { + pattern.append_element(width_to_add); + } + if let Some(size) = attributes_converter.variable_opsz() { + pattern.append_element(size); + } + pattern.append_element(PatternElement::new(FC_VARIABLE_OBJECT as i32, true.into())); + + // TODO: Check if this should have a zero ttc index if not part of a collection. + 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(), + )); + pattern.append_element(attributes_converter.static_slant()); + } + InstanceMode::Named(index) => { + if let Some(weight) = attributes_converter.instance_weight() { + pattern.append_element(weight); + } + if let Some(width) = attributes_converter.instance_width() { + pattern.append_element(width); + } + 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() + ((index + 1) << 16)).into(), + )); + if let Some(size_element) = attributes_converter + .instance_size() + .or(attributes_converter.default_size()) + { + pattern.append_element(size_element); + }; + if let Some(slant_element) = attributes_converter.instance_slant() { + pattern.append_element(slant_element); + } + pattern.append_element(PatternElement::new( + FC_NAMED_INSTANCE_OBJECT as i32, + true.into(), + )); } } } diff --git a/fc-fontations/instance_enumerate.rs b/fc-fontations/instance_enumerate.rs new file mode 100644 index 0000000..6f13bf9 --- /dev/null +++ b/fc-fontations/instance_enumerate.rs @@ -0,0 +1,164 @@ +/* + * fontconfig/fc-fontations/mod.rs + * + * Copyright 2025 Google LLC. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of the author(s) not be used in + * advertising or publicity pertaining to distribution of the software without + * specific, written prior permission. The authors make no + * representations about the suitability of this software for any purpose. It + * is provided "as is" without express or implied warranty. + * + * THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO + * EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, + * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +use crate::InstanceMode::{self, Named}; +use read_fonts::{ + FileRef::{self, Collection, Font}, + FontRef, +}; +use skrifa::MetadataProvider; + +#[allow(unused)] +pub fn fonts_and_indices( + file_ref: Option<FileRef>, +) -> impl Iterator<Item = (FontRef<'_>, Option<i32>)> { + let (iter_one, iter_two) = match file_ref { + Some(Font(font)) => (Some((Ok(font.clone()), None)), None), + Some(Collection(collection)) => ( + None, + Some( + collection + .iter() + .enumerate() + .map(|entry| (entry.1, Some(entry.0 as i32))), + ), + ), + None => (None, None), + }; + iter_two + .into_iter() + .flatten() + .chain(iter_one) + .filter_map(|(font_result, index)| { + if let Ok(font) = font_result { + return Some((font, index)); + } + None + }) +} + +// Produces an iterator over the named instances of the font. +#[allow(unused)] +pub fn enumerate_named_instances<'a>(f: &'a FontRef) -> impl Iterator<Item = InstanceMode> + 'a { + let default_coords = f.axes().iter().map(|axis| axis.default_value()); + f.named_instances() + .iter() + .enumerate() + .filter_map(move |(i, instance)| { + let user_coords = instance.user_coords(); + if user_coords.eq(default_coords.clone()) { + None + } else { + Some(Named(i as i32)) + } + }) +} + +/// Used for producing the expected set of patterns out of variable fonts. +/// Produces an iterator over +/// * the default instance, +/// * the named instances, and the +/// * variable instance. +#[allow(unused)] +pub fn all_instances<'a>(f: &'a FontRef) -> Box<dyn Iterator<Item = InstanceMode> + 'a> { + if f.axes().is_empty() { + Box::new(std::iter::once(InstanceMode::Default)) + } else { + Box::new( + std::iter::once(InstanceMode::Default) + .chain(enumerate_named_instances(f)) + .chain(std::iter::once(InstanceMode::Variable)), + ) + } +} + +#[cfg(test)] +mod test { + use crate::{ + instance_enumerate::{all_instances, fonts_and_indices}, + InstanceMode, + }; + + use read_fonts::FileRef; + use std::path::PathBuf; + + fn testfontdir() -> PathBuf { + let default_build_dir = std::env::current_dir().unwrap().join("build"); + let build_dir = std::env::var("builddir").unwrap_or_else(|_| { + println!("builddir env var not set, using current directory + \"build/\""); + default_build_dir.to_string_lossy().to_string() + }); + PathBuf::from(build_dir) + .join("testfonts") + .join("roboto-flex-fonts") + .join("fonts") + .join("variable") + } + + fn variable_collection_testfont() -> PathBuf { + testfontdir().join( + "RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght].ttf", + ) + } + + const EXPECTED_INSTANCES: [InstanceMode; 21] = [ + InstanceMode::Default, + InstanceMode::Named(0), + InstanceMode::Named(1), + InstanceMode::Named(2), + InstanceMode::Named(4), + InstanceMode::Named(5), + InstanceMode::Named(6), + InstanceMode::Named(7), + InstanceMode::Named(8), + InstanceMode::Named(9), + InstanceMode::Named(10), + InstanceMode::Named(11), + InstanceMode::Named(12), + InstanceMode::Named(13), + InstanceMode::Named(14), + InstanceMode::Named(15), + InstanceMode::Named(16), + InstanceMode::Named(17), + InstanceMode::Named(18), + InstanceMode::Named(19), + InstanceMode::Variable, + ]; + + #[test] + fn test_instance_iterator() { + let bytes = std::fs::read(variable_collection_testfont()) + .ok() + .unwrap_or_default(); + let fileref = FileRef::new(&bytes).ok(); + let fonts = fonts_and_indices(fileref); + for (font, _ttc_index) in fonts { + all_instances(&font) + .zip(EXPECTED_INSTANCES.iter()) + .for_each(|(instance, expected_instance)| { + assert_eq!(instance, *expected_instance); + }) + } + } +} diff --git a/fc-fontations/mod.rs b/fc-fontations/mod.rs index 1c4780d..3f43266 100644 --- a/fc-fontations/mod.rs +++ b/fc-fontations/mod.rs @@ -24,6 +24,7 @@ mod attributes; mod foundries; +mod instance_enumerate; mod names; mod pattern_bindings; @@ -33,8 +34,8 @@ use names::add_names; use fc_fontations_bindgen::{ fcint::{ - FC_COLOR_OBJECT, FC_FONTFORMAT_OBJECT, FC_FONTVERSION_OBJECT, FC_FONT_HAS_HINT_OBJECT, - FC_FOUNDRY_OBJECT, FC_OUTLINE_OBJECT, FC_SCALABLE_OBJECT, + FC_COLOR_OBJECT, FC_DECORATIVE_OBJECT, FC_FONTFORMAT_OBJECT, FC_FONTVERSION_OBJECT, + FC_FONT_HAS_HINT_OBJECT, FC_FOUNDRY_OBJECT, FC_OUTLINE_OBJECT, FC_SCALABLE_OBJECT, }, FcFontSet, FcFontSetAdd, FcPattern, }; @@ -43,10 +44,7 @@ use font_types::Tag; use pattern_bindings::{FcPatternBuilder, PatternElement}; use std::str::FromStr; -use read_fonts::{ - FileRef::{self, Collection, Font}, - FontRef, TableProvider, -}; +use read_fonts::{FileRef, FontRef, TableProvider}; use std::{ ffi::{CStr, CString, OsStr}, @@ -54,6 +52,8 @@ use std::{ os::unix::ffi::OsStrExt, }; +use instance_enumerate::{all_instances, fonts_and_indices}; + #[no_mangle] /// Externally called in fcfontations.c as the file scanner function /// similar to the job that FreeType performs. @@ -86,7 +86,7 @@ pub unsafe extern "C" fn add_patterns_to_fontset( /// /// 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)] +#[derive(Copy, Clone, Debug, PartialEq)] #[allow(dead_code)] enum InstanceMode { Default, @@ -94,34 +94,6 @@ enum InstanceMode { Variable, } -fn fonts_and_indices( - file_ref: Option<FileRef>, -) -> impl Iterator<Item = (FontRef<'_>, Option<i32>)> { - let (iter_one, iter_two) = match file_ref { - Some(Font(font)) => (Some((Ok(font.clone()), None)), None), - Some(Collection(collection)) => ( - None, - Some( - collection - .iter() - .enumerate() - .map(|entry| (entry.1, Some(entry.0 as i32))), - ), - ), - None => (None, None), - }; - iter_two - .into_iter() - .flatten() - .chain(iter_one) - .filter_map(|(font_result, index)| { - if let Ok(font) = font_result { - return Some((font, index)); - } - None - }) -} - fn has_one_of_tables<I>(font_ref: &FontRef, tags: I) -> bool where I: IntoIterator, @@ -154,8 +126,6 @@ fn build_patterns_for_font( ) -> Vec<*mut FcPattern> { let mut pattern = FcPatternBuilder::new(); - add_names(font, &mut pattern); - let has_glyf = has_one_of_tables(font, ["glyf"]); let has_cff = has_one_of_tables(font, ["CFF ", "CFF2"]); let has_color = has_one_of_tables(font, ["COLR", "SVG ", "CBLC", "SBIX"]); @@ -214,14 +184,48 @@ fn build_patterns_for_font( version.into(), )); - // TODO: Handle variable instance and named instances. - append_style_elements(font, InstanceMode::Default, ttc_index, &mut pattern); + // So far the pattern elements applied to te whole font file, in the below, + // clone the current pattern state and add instance specific + // attributes. FontConfig for variable fonts produces a pattern for the + // default instance, each named instance, and a separate one for the + // "variable instance", which may contain ranges for pattern elements that + // describe variable aspects, such as weight of the font. + all_instances(font) + .flat_map(move |instance_mode| { + let mut instance_pattern = pattern.clone(); + + // Style names: fcfreetype adds TT_NAME_ID_WWS_SUBFAMILY, TT_NAME_ID_TYPOGRAPHIC_SUBFAMILY, + // TT_NAME_ID_FONT_SUBFAMILY as FC_STYLE_OBJECT, FC_STYLE_OBJECT_LANG unless a named instance + // is added,then the instance's name id is used as FC_STYLE_OBJECT. + + append_style_elements(font, instance_mode, ttc_index, &mut instance_pattern); + + // For variable fonts: + // Names (mainly postscript name and style), weight, width and opsz (font-size?) are affected. + // * Add the variable font itself, with ranges for weight, width, opsz. + // * Add an entry for each named instance + // * With instance name turning into FC_STYLE_OBJECT. + // * Fixed width, wgth, opsz + // * Add the default instance with fixed values. + let mut had_decoratve = false; + // Family and full name. + add_names( + font, + instance_mode, + &mut instance_pattern, + &mut had_decoratve, + ); + + instance_pattern.append_element(PatternElement::new( + FC_DECORATIVE_OBJECT as i32, + had_decoratve.into(), + )); - pattern - .create_fc_pattern() - .map(|p| p.into_raw() as *mut FcPattern) - .into_iter() - .collect() + instance_pattern + .create_fc_pattern() + .map(|wrapper| wrapper.into_raw() as *mut FcPattern) + }) + .collect::<Vec<_>>() } #[cfg(test)] diff --git a/fc-fontations/names.rs b/fc-fontations/names.rs index b4464b4..80fb980 100644 --- a/fc-fontations/names.rs +++ b/fc-fontations/names.rs @@ -25,10 +25,11 @@ use skrifa::{string::StringId, MetadataProvider}; use fc_fontations_bindgen::fcint::{ - FC_FAMILYLANG_OBJECT, FC_FAMILY_OBJECT, FC_INVALID_OBJECT, FC_POSTSCRIPT_NAME_OBJECT, + FC_FAMILYLANG_OBJECT, FC_FAMILY_OBJECT, FC_FULLNAMELANG_OBJECT, FC_FULLNAME_OBJECT, + FC_INVALID_OBJECT, FC_POSTSCRIPT_NAME_OBJECT, FC_STYLELANG_OBJECT, FC_STYLE_OBJECT, }; -use crate::{FcPatternBuilder, PatternElement}; +use crate::{FcPatternBuilder, InstanceMode, PatternElement}; use read_fonts::FontRef; use std::ffi::CString; @@ -39,7 +40,14 @@ fn objects_for_id(string_id: StringId) -> (i32, i32) { StringId::FAMILY_NAME | StringId::WWS_FAMILY_NAME | StringId::TYPOGRAPHIC_FAMILY_NAME => { (FC_FAMILY_OBJECT as i32, FC_FAMILYLANG_OBJECT as i32) } + StringId::FULL_NAME => (FC_FULLNAME_OBJECT as i32, FC_FULLNAMELANG_OBJECT as i32), StringId::POSTSCRIPT_NAME => (FC_POSTSCRIPT_NAME_OBJECT as i32, FC_INVALID_OBJECT as i32), + StringId::SUBFAMILY_NAME + | StringId::WWS_SUBFAMILY_NAME + | StringId::TYPOGRAPHIC_SUBFAMILY_NAME => { + (FC_STYLE_OBJECT as i32, FC_STYLELANG_OBJECT as i32) + } + _ => panic!("No equivalent FontConfig objects found for StringId."), } } @@ -52,12 +60,79 @@ fn normalize_name(name: &CString) -> String { .replace(' ', "") } -pub fn add_names(font: &FontRef, pattern: &mut FcPatternBuilder) { +fn mangle_postscript_name_for_named_instance( + font: &FontRef, + named_instance_id: i32, +) -> Option<CString> { + let instance_ps_name_id = font + .named_instances() + .get(named_instance_id as usize)? + .postscript_name_id()?; + let ps_name = font + .localized_strings(instance_ps_name_id) + .english_or_first()? + .clone() + .to_string(); + CString::new(ps_name).ok() +} + +fn mangle_subfamily_name_for_named_instance( + font: &FontRef, + named_instance_id: i32, +) -> Option<CString> { + let instance_subfamily_name_id = font + .named_instances() + .get(named_instance_id as usize)? + .subfamily_name_id(); + let subfamily = font + .localized_strings(instance_subfamily_name_id) + .english_or_first()? + .clone() + .to_string(); + CString::new(subfamily).ok() +} + +fn mangle_full_name_for_named_instance(font: &FontRef, named_instance_id: i32) -> Option<CString> { + let instance_subfamily_name_id = font + .named_instances() + .get(named_instance_id as usize)? + .subfamily_name_id(); + let full_name = font + .localized_strings(StringId::FAMILY_NAME) + .english_or_first()? + .to_string() + + " "; + let subfam = font + .localized_strings(instance_subfamily_name_id) + .english_or_first()? + .to_string(); + + CString::new(full_name + &subfam).ok() +} + +fn determine_decorative(object_id: i32, name: &Option<CString>) -> bool { + object_id == FC_STYLE_OBJECT as i32 + && name + .as_ref() + .is_some_and(|name| name.to_string_lossy().to_lowercase().contains("decorative")) +} + +pub fn add_names( + font: &FontRef, + instance_mode: InstanceMode, + pattern: &mut FcPatternBuilder, + had_decorative: &mut bool, +) { // Order of these is important for matching FreeType. Or we might need to sort these descending to achieve the same result. let string_ids = &[ + StringId::WWS_FAMILY_NAME, StringId::TYPOGRAPHIC_FAMILY_NAME, StringId::FAMILY_NAME, + StringId::FULL_NAME, StringId::POSTSCRIPT_NAME, + StringId::TYPOGRAPHIC_SUBFAMILY_NAME, + StringId::SUBFAMILY_NAME, + StringId::WWS_SUBFAMILY_NAME, ]; let mut already_encountered_names: HashSet<(i32, String)> = HashSet::new(); @@ -78,6 +153,22 @@ pub fn add_names(font: &FontRef, pattern: &mut FcPatternBuilder) { CString::new(lang).ok() }); + // Instance postscript name. + let name = match (instance_mode, string_id) { + (InstanceMode::Named(instance), &StringId::POSTSCRIPT_NAME) => { + mangle_postscript_name_for_named_instance(font, instance).or(name) + } + (InstanceMode::Named(instance), &StringId::SUBFAMILY_NAME) => { + mangle_subfamily_name_for_named_instance(font, instance).or(name) + } + (InstanceMode::Named(instance), &StringId::FULL_NAME) => { + mangle_full_name_for_named_instance(font, instance).or(name) + } + _ => name, + }; + + *had_decorative = determine_decorative(object_ids.0, &name); + if let (Some(name), Some(language)) = (name, language) { let normalized_name = normalize_name(&name); if already_encountered_names.contains(&(object_ids.0, normalized_name.clone())) { diff --git a/fc-fontations/pattern_bindings/fc_wrapper.rs b/fc-fontations/pattern_bindings/fc_wrapper.rs index 125b172..ccc3269 100644 --- a/fc-fontations/pattern_bindings/fc_wrapper.rs +++ b/fc-fontations/pattern_bindings/fc_wrapper.rs @@ -24,10 +24,11 @@ use fc_fontations_bindgen::{ fcint::{ - FcCharSet, FcCharSetCreate, FcCharSetDestroy, FcLangSet, FcPattern, FcPatternCreate, - FcPatternDestroy, FcRange, FcRangeCopy, FcRangeCreateDouble, FcRangeDestroy, + FcCharSet, FcCharSetCopy, FcCharSetCreate, FcCharSetDestroy, FcLangSet, FcPattern, + FcPatternCreate, FcPatternDestroy, FcRange, FcRangeCopy, FcRangeCreateDouble, + FcRangeDestroy, }, - FcLangSetCreate, FcLangSetDestroy, + FcLangSetCopy, FcLangSetCreate, FcLangSetDestroy, }; macro_rules! wrap_fc_object { @@ -128,7 +129,8 @@ impl FcPatternWrapper { wrap_fc_object! { FcCharSetWrapper, FcCharSet, - FcCharSetDestroy + FcCharSetDestroy, + FcCharSetCopy } impl FcCharSetWrapper { @@ -151,7 +153,8 @@ impl FcCharSetWrapper { wrap_fc_object! { FcLangSetWrapper, FcLangSet, - FcLangSetDestroy + FcLangSetDestroy, + FcLangSetCopy } impl FcLangSetWrapper { diff --git a/fc-fontations/pattern_bindings/mod.rs b/fc-fontations/pattern_bindings/mod.rs index 24f7158..32f604a 100644 --- a/fc-fontations/pattern_bindings/mod.rs +++ b/fc-fontations/pattern_bindings/mod.rs @@ -24,7 +24,7 @@ extern crate fc_fontations_bindgen; -mod fc_wrapper; +pub mod fc_wrapper; use std::ffi::CString; use std::fmt::Debug; @@ -35,10 +35,10 @@ use fc_fontations_bindgen::fcint::{ FcPatternObjectAddString, FC_FAMILY_OBJECT, }; -use self::fc_wrapper::{FcCharSetWrapper, FcLangSetWrapper, FcPatternWrapper, FcRangeWrapper}; +use fc_wrapper::{FcCharSetWrapper, FcLangSetWrapper, FcPatternWrapper, FcRangeWrapper}; #[allow(unused)] -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum PatternValue { String(CString), Boolean(bool), @@ -73,7 +73,13 @@ impl From<f64> for PatternValue { } } -#[derive(Debug)] +impl From<FcRangeWrapper> for PatternValue { + fn from(item: FcRangeWrapper) -> Self { + PatternValue::Range(item) + } +} + +#[derive(Debug, Clone)] pub struct PatternElement { object_id: i32, value: PatternValue, @@ -127,7 +133,7 @@ impl PatternElement { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct FcPatternBuilder { elements: Vec<PatternElement>, } diff --git a/test/meson.build b/test/meson.build index 36cf4fe..3a08da7 100644 --- a/test/meson.build +++ b/test/meson.build @@ -75,7 +75,7 @@ endforeach if get_option('fontations').enabled() rust = import('rust') - rust.test('fc_fontations_rust_tests', fc_fontations, link_with: [libfontconfig_internal], depends: fetch_test_fonts) + rust.test('fc_fontations_rust_tests', fc_fontations, link_with: [libfontconfig_internal], depends: fetch_test_fonts, env: ['builddir=@0@'.format(meson.project_build_root())],) endif fs = import('fs') diff --git a/test/test_fontations_ft_query.py b/test/test_fontations_ft_query.py index 9a00e3e..37a2aab 100644 --- a/test/test_fontations_ft_query.py +++ b/test/test_fontations_ft_query.py @@ -19,7 +19,7 @@ def list_test_fonts(): for root, _, files in os.walk(builddir() / "testfonts"): for file in files: # Variable .ttc not supported yet. - if file.endswith(".ttf") and not "RobotoFlex" in file: + if file.endswith(".ttf"): font_files.append(os.path.join(root, file)) return font_files @@ -53,8 +53,8 @@ def test_fontations_freetype_fcquery_equal(font_file): print(f"Testing with: {font_file}") # Example usage supported_format_entitites = [ - "family[0]", - "familylang[0]", + "family", + "familylang", "outline", "scalable", "fontformat", commit f0c0fbe16b0929c5a4e5ad4dea00842765cd0897 Author: Dominik Röttsches <drott@xxxxxxxxxxxx> Date: Wed Apr 30 12:53:25 2025 +0300 Cargo build improvements diff --git a/fc-fontations-bindgen/build.rs b/fc-fontations-bindgen/build.rs index 0591cf0..61877e4 100644 --- a/fc-fontations-bindgen/build.rs +++ b/fc-fontations-bindgen/build.rs @@ -11,6 +11,7 @@ fn main() { meson.arg("setup") .arg(build_dir.to_str().unwrap()) .arg("--reconfigure") + .arg("--default-library=static") .arg("-Dfontations=enabled"); let status = meson.status().expect("Failed to execute meson"); @@ -27,7 +28,7 @@ fn main() { } // Tell cargo to look for fontconfig in the build directory - println!("cargo:rustc-link-search=native={}", build_dir.join("src").display()); + println!("cargo:rustc-link-search=native={}", build_dir.display()); println!("cargo:rustc-link-lib=static=fontconfig"); // FreeType and Expat from the system.