From: Simon Cozens Date: Tue, 21 Jan 2025 16:34:27 +0000 (+0000) Subject: First cut at build_stat, no tests yet since they’re all TTX X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e6f1b68bf230d921d07d0bd99003fa42915e391f;p=thirdparty%2Fgoogle%2Ffonts.git First cut at build_stat, no tests yet since they’re all TTX --- diff --git a/src/lib.rs b/src/lib.rs index ab992c62b..fa5c93ace 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { + 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, Box> { + let mut new_font = FontBuilder::new(); + let axes = font_axes(&font)?; + let axis_registry = AxisRegistry::new(); + let fallbacks_in_fvar: HashMap> = + 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 = 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 = vec![]; + let mut values: Vec = vec![]; + let mut seen_axes = HashSet::new(); + + fn make_location(axis: Tag, value: f32, linked_value: Option) -> 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::>(); + 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")] diff --git a/src/nametable.rs b/src/nametable.rs index 5e83bddab..31729b62a 100644 --- a/src/nametable.rs +++ b/src/nametable.rs @@ -58,6 +58,7 @@ pub(crate) fn find_or_add_name(records: &mut Vec, 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 index 000000000..8729c1cc3 --- /dev/null +++ b/src/stat.rs @@ -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, + pub values: Vec, +} + +#[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) -> 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::>::new(); + let mut sorted_records = self.records.iter().collect::>(); + 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)) + } +}