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),
#[cfg(feature = "fontations")]
mod nametable;
#[cfg(feature = "fontations")]
+mod stat;
+#[cfg(feature = "fontations")]
mod fontations {
use super::*;
use monkeypatching::{AxisValueNameId, SetAxisValueNameId};
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,
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")]
--- /dev/null
+//! 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))
+ }
+}