fontconfig: Branch 'main' - 3 commits

GitLab Mirror gitlab-mirror at kemper.freedesktop.org
Thu May 8 09:40:39 UTC 2025


 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 at tagoh.org>
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 at chromium.org>
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 at chromium.org>
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.


More information about the Fontconfig mailing list