]> git.ipfire.org Git - thirdparty/google/fonts.git/commitdiff
First cut at build_stat, no tests yet since they’re all TTX
authorSimon Cozens <simon@simon-cozens.org>
Tue, 21 Jan 2025 16:34:27 +0000 (16:34 +0000)
committerSimon Cozens <simon@simon-cozens.org>
Tue, 21 Jan 2025 16:34:27 +0000 (16:34 +0000)
src/lib.rs
src/nametable.rs
src/stat.rs [new file with mode: 0644]

index ab992c62b721bf747612fdd0e9f9ca6744c0eead..fa5c93ace2a02e3df681831a1b96bddda88b1a55 100644 (file)
@@ -12,7 +12,15 @@ use write_fonts::{
 include!(concat!(env!("OUT_DIR"), "/_.rs"));
 include!(concat!(env!("OUT_DIR"), "/data.rs"));
 
-// const LINKED_VALUES: [(&str, (f32, f32)); 2] = [("wght", (400.0, 700.0)), ("ital", (0.0, 1.0))];
+const LINKED_VALUES: [(&str, (f32, f32)); 2] = [("wght", (400.0, 700.0)), ("ital", (0.0, 1.0))];
+
+fn linked_value(axis: &str, value: f32) -> Option<f32> {
+    LINKED_VALUES
+        .iter()
+        .find(|(linked_axis, (cur, _link))| *linked_axis == axis && value == *cur)
+        .map(|(_, (_, link))| *link)
+}
+
 const GF_STATIC_STYLES: [(&str, u16); 18] = [
     ("Thin", 100),
     ("ExtraLight", 200),
@@ -212,6 +220,8 @@ mod monkeypatching;
 #[cfg(feature = "fontations")]
 mod nametable;
 #[cfg(feature = "fontations")]
+mod stat;
+#[cfg(feature = "fontations")]
 mod fontations {
     use super::*;
     use monkeypatching::{AxisValueNameId, SetAxisValueNameId};
@@ -219,6 +229,7 @@ mod fontations {
         add_name, best_familyname, best_subfamilyname, find_or_add_name, rewrite_or_insert,
     };
     use skrifa::{string::StringId, MetadataProvider, Tag};
+    use stat::{AxisLocation, AxisRecord, AxisValue, StatBuilder};
     use std::{cmp::Reverse, collections::HashMap};
     use write_fonts::{
         from_obj::ToOwnedTable,
@@ -697,6 +708,171 @@ mod fontations {
         new_font.add_table(&name_table)?;
         Ok(new_font.copy_missing_tables(font).build())
     }
+
+    // All right, let's do it
+    pub fn build_stat(
+        font: FontRef,
+        siblings: &[FontRef],
+    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
+        let mut new_font = FontBuilder::new();
+        let axes = font_axes(&font)?;
+        let axis_registry = AxisRegistry::new();
+        let fallbacks_in_fvar: HashMap<String, Vec<FallbackProto>> =
+            axis_registry.fallbacks(&axes).collect();
+        let mut fallbacks_in_siblings: Vec<(String, FallbackProto)> = vec![];
+        for fnt in siblings {
+            let family_name = best_familyname(fnt).unwrap_or("New Font".to_string());
+            let subfamily_name = best_subfamilyname(fnt).unwrap_or("Regular".to_string());
+            let font_axes = font_axes(fnt).unwrap_or_default();
+            fallbacks_in_siblings.extend(
+                axis_registry
+                    .name_table_fallbacks(&family_name, &subfamily_name, &font_axes)
+                    .map(|(tag, proto)| (tag.to_string(), proto.clone())),
+            )
+        }
+        // And for this font
+        let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
+        let subfamily_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
+        let fallbacks_in_names =
+            axis_registry.name_table_fallbacks(&family_name, &subfamily_name, &axes);
+
+        let fvar: Fvar = font.fvar().unwrap().to_owned_table();
+        let mut name: Name = font.name().unwrap().to_owned_table();
+        let fvar_name_ids: HashSet<StringId> = fvar
+            .axis_instance_arrays
+            .instances
+            .iter()
+            .map(|x| x.subfamily_name_id)
+            .chain(
+                fvar.axis_instance_arrays
+                    .axes
+                    .iter()
+                    .map(|x| x.axis_name_id),
+            )
+            .collect();
+        let keep = |name_id: StringId| -> bool {
+            name_id.to_u16() <= 25 || fvar_name_ids.contains(&name_id)
+        };
+        let mut delete_ids = vec![];
+        if let Ok(stat) = font.stat() {
+            for axis in stat.design_axes()?.iter() {
+                let id = axis.axis_name_id.get();
+                if !keep(id) {
+                    delete_ids.push(id);
+                }
+            }
+            if let Some(axis_values) = stat.offset_to_axis_values().transpose()? {
+                for axis_value in axis_values.axis_values().iter().flatten() {
+                    let id = axis_value.value_name_id();
+                    if !keep(id) {
+                        delete_ids.push(id);
+                    }
+                }
+            }
+        }
+        name.name_record
+            .retain(|record| !delete_ids.contains(&record.name_id));
+        let mut axis_records: Vec<AxisRecord> = vec![];
+        let mut values: Vec<AxisValue> = vec![];
+        let mut seen_axes = HashSet::new();
+
+        fn make_location(axis: Tag, value: f32, linked_value: Option<f32>) -> AxisLocation {
+            if let Some(linked_value) = linked_value {
+                AxisLocation::Three {
+                    tag: axis,
+                    value: Fixed::from_f64(value as f64),
+                    linked: Fixed::from_f64(linked_value as f64),
+                }
+            } else {
+                AxisLocation::One {
+                    tag: axis,
+                    value: Fixed::from_f64(value as f64),
+                }
+            }
+        }
+
+        for (axis, fallbacks) in fallbacks_in_fvar.iter() {
+            let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
+            let ar_axis = axis_registry.get(axis).unwrap();
+            seen_axes.insert(tag);
+            axis_records.push(AxisRecord {
+                tag,
+                name: ar_axis.display_name().to_string(),
+                ordering: 0,
+            });
+            let fallback_values = fallbacks.iter().map(|f| f.value()).collect::<Vec<f32>>();
+            for fallback in fallbacks.iter() {
+                values.push(AxisValue {
+                    flags: if fallback.value() == ar_axis.default_value() {
+                        0x2
+                    } else {
+                        0x0
+                    },
+                    name: fallback.name().to_string(),
+                    location: make_location(
+                        tag,
+                        fallback.value(),
+                        linked_value(axis, fallback.value())
+                            .filter(|value| fallback_values.contains(value)),
+                    ),
+                })
+            }
+        }
+
+        for (axis, fallback) in fallbacks_in_names {
+            let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
+            if seen_axes.contains(&tag) {
+                continue;
+            }
+            seen_axes.insert(tag);
+
+            let ar_axis = axis_registry.get(axis).unwrap();
+            axis_records.push(AxisRecord {
+                tag,
+                name: ar_axis.display_name().to_string(),
+                ordering: 0,
+            });
+            values.push(AxisValue {
+                flags: 0x0,
+                name: fallback.name().to_string(),
+                location: make_location(
+                    tag,
+                    fallback.value(),
+                    linked_value(axis, fallback.value()),
+                ),
+            });
+        }
+
+        for (axis, _fallback) in fallbacks_in_siblings {
+            let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
+            if seen_axes.contains(&tag) {
+                continue;
+            }
+            let ar_axis = axis_registry.get(&axis).unwrap();
+            let elided_value = ar_axis.default_value();
+            axis_records.push(AxisRecord {
+                tag,
+                name: ar_axis.display_name().to_string(),
+                ordering: 0,
+            });
+            if let Some(elided_fallback) = axis_registry.fallback_for_value(&axis, elided_value) {
+                values.push(AxisValue {
+                    flags: 0x2,
+                    name: elided_fallback.name().to_string(),
+                    location: make_location(tag, elided_value, linked_value(&axis, elided_value)),
+                })
+            }
+        }
+
+        let stat_builder = StatBuilder {
+            records: axis_records,
+            values,
+        };
+        let stat = stat_builder.build(&mut name.name_record);
+        new_font.add_table(&name)?;
+        new_font.add_table(&stat)?;
+        Ok(new_font.copy_missing_tables(font).build())
+    }
 }
 
 #[cfg(feature = "fontations")]
index 5e83bddab6682a58391e1908683bf0876c131ee3..31729b62a73feb0927cb80b0c5c9215e82f40675 100644 (file)
@@ -58,6 +58,7 @@ pub(crate) fn find_or_add_name(records: &mut Vec<NameRecord>, string: &str) -> S
             && r.platform_id == 3
             && r.encoding_id == 1
             && r.language_id == 0x409
+            && r.name_id.to_u16() >= 256
     }) {
         return existing.name_id;
     }
diff --git a/src/stat.rs b/src/stat.rs
new file mode 100644 (file)
index 0000000..8729c1c
--- /dev/null
@@ -0,0 +1,153 @@
+//! Building the STAT table
+
+// Largely stolen from fea-rs, where it's all private so we can't use it.
+// We're uninterested in anything other than 3/1/0x409, so we use Strings instead
+// of the more correct NameSpec.
+
+use std::collections::HashMap;
+use write_fonts::{
+    tables::{name::NameRecord, stat as write_stat},
+    types::{Fixed, NameId, Tag},
+};
+
+use crate::nametable::find_or_add_name;
+
+#[derive(Clone, Debug)]
+pub(crate) struct StatBuilder {
+    // pub name: StatFallbackName,
+    pub records: Vec<AxisRecord>,
+    pub values: Vec<AxisValue>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct AxisRecord {
+    pub tag: Tag,
+    pub name: String,
+    pub ordering: u16,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct AxisValue {
+    pub flags: u16,
+    pub name: String,
+    pub location: AxisLocation,
+}
+
+#[allow(dead_code)]
+#[derive(Clone, Debug)]
+pub enum AxisLocation {
+    One {
+        tag: Tag,
+        value: Fixed,
+    },
+    Two {
+        tag: Tag,
+        nominal: Fixed,
+        min: Fixed,
+        max: Fixed,
+    },
+    Three {
+        tag: Tag,
+        value: Fixed,
+        linked: Fixed,
+    },
+    Four(Vec<(Tag, Fixed)>),
+}
+
+impl StatBuilder {
+    pub(crate) fn build(&self, name_records: &mut Vec<NameRecord>) -> write_stat::Stat {
+        // let elided_fallback_name_id = match &self.name {
+        //     StatFallbackName::Id(id) => *id,
+        //     StatFallbackName::Record(name) => find_or_add_name(name_records, name),
+        // };
+
+        //HACK: we jump through a bunch of hoops to ensure our output matches
+        //feaLib's; in particular we want to add our name table entries grouped by
+        //axis.
+        let mut sorted_values = HashMap::<Tag, Vec<_>>::new();
+        let mut sorted_records = self.records.iter().collect::<Vec<_>>();
+        sorted_records.sort_by_key(|x| x.ordering);
+
+        for axis_value in &self.values {
+            match axis_value.location {
+                AxisLocation::One { tag, .. }
+                | AxisLocation::Two { tag, .. }
+                | AxisLocation::Three { tag, .. } => {
+                    sorted_values.entry(tag).or_default().push(axis_value)
+                }
+                AxisLocation::Four(_) => sorted_values
+                    .entry(Tag::default())
+                    .or_default()
+                    .push(axis_value),
+            }
+        }
+
+        let mut design_axes = Vec::with_capacity(self.records.len());
+        let mut axis_values = Vec::with_capacity(self.values.len());
+
+        for (i, record) in self.records.iter().enumerate() {
+            let name_id = find_or_add_name(name_records, &record.name);
+            let record = write_stat::AxisRecord {
+                axis_tag: record.tag,
+                axis_name_id: name_id,
+                axis_ordering: record.ordering,
+            };
+            for axis_value in sorted_values
+                .get(&record.axis_tag)
+                .iter()
+                .flat_map(|x| x.iter())
+            {
+                let flags = write_stat::AxisValueTableFlags::from_bits(axis_value.flags).unwrap();
+                let name_id = find_or_add_name(name_records, &axis_value.name);
+
+                let value = match &axis_value.location {
+                    AxisLocation::One { value, .. } => write_stat::AxisValue::format_1(
+                        //TODO: validate that all referenced tags refer to existing axes
+                        i as u16, flags, name_id, *value,
+                    ),
+                    AxisLocation::Two {
+                        nominal, min, max, ..
+                    } => write_stat::AxisValue::format_2(
+                        i as _, flags, name_id, *nominal, *min, *max,
+                    ),
+                    AxisLocation::Three { value, linked, .. } => {
+                        write_stat::AxisValue::format_3(i as _, flags, name_id, *value, *linked)
+                    }
+
+                    AxisLocation::Four(_) => panic!("assigned to separate group"),
+                };
+                axis_values.push(value);
+            }
+
+            design_axes.push(record);
+        }
+
+        let format4 = sorted_values
+            .remove(&Tag::default())
+            .unwrap_or_default()
+            .into_iter()
+            .map(|format4| {
+                let flags = write_stat::AxisValueTableFlags::from_bits(format4.flags).unwrap();
+                let name_id = find_or_add_name(name_records, &format4.name);
+
+                let AxisLocation::Four(values) = &format4.location else {
+                    panic!("only format 4 in this group")
+                };
+                let mapping = values
+                    .iter()
+                    .map(|(tag, value)| {
+                        let axis_index = design_axes
+                            .iter()
+                            .position(|rec| rec.axis_tag == *tag)
+                            .expect("validated");
+                        write_stat::AxisValueRecord::new(axis_index as _, *value)
+                    })
+                    .collect();
+                write_stat::AxisValue::format_4(flags, name_id, mapping)
+            });
+
+        //feaLib puts format4 records first
+        let axis_values = format4.chain(axis_values).collect();
+        write_stat::Stat::new(design_axes, axis_values, NameId::from(2))
+    }
+}