]> git.ipfire.org Git - thirdparty/google/fonts.git/commitdiff
Initial commit
authorSimon Cozens <simon@simon-cozens.org>
Fri, 17 Jan 2025 12:12:46 +0000 (12:12 +0000)
committerSimon Cozens <simon@simon-cozens.org>
Fri, 17 Jan 2025 12:12:46 +0000 (12:12 +0000)
Cargo.toml [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/nametable.rs [new file with mode: 0644]

diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..f36018b
--- /dev/null
@@ -0,0 +1,33 @@
+[package]
+name = "google-fonts-axisregistry"
+version = "0.7.1"
+edition = "2021"
+description = "Google Fonts font axis support data"
+repository = "https://github.com/googlefonts/axisregistry"
+license-file = "LICENSE.txt"
+
+[features]
+default = ["fontations"]
+fontations = ["dep:write-fonts", "dep:skrifa"]
+
+[dependencies]
+bytes = "1.7.1"
+prost = "0.13"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+write-fonts = { version = "0.33.1", optional = true, features = ["read"] }
+skrifa = { version = "0.26.4", optional = true }
+indexmap = "2.7.0"
+
+[build-dependencies]
+prost-build = "0.13"
+protobuf-support = "3.7.1"
+protobuf = "3.7.1"
+protobuf-parse = "3.7.1"
+glob = "0"
+prettyplease = "0.2"
+quote = "1.0"
+proc-macro2 = "1.0"
+syn = "2.0"
+itertools = "0.13"
+serde_json = "1.0"
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..d4c995e
--- /dev/null
@@ -0,0 +1,478 @@
+use std::{collections::HashSet, ops::Index};
+
+use indexmap::IndexMap;
+#[cfg(feature = "fontations")]
+use write_fonts::{
+    read::FontRef,
+    read::{ReadError, TableProvider},
+    FontBuilder,
+};
+
+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 GF_STATIC_STYLES: [(&str, u16); 18] = [
+    ("Thin", 100),
+    ("ExtraLight", 200),
+    ("Light", 300),
+    ("Regular", 400),
+    ("Medium", 500),
+    ("SemiBold", 600),
+    ("Bold", 700),
+    ("ExtraBold", 800),
+    ("Black", 900),
+    ("Thin Italic", 100),
+    ("ExtraLight Italic", 200),
+    ("Light Italic", 300),
+    ("Italic", 400),
+    ("Medium Italic", 500),
+    ("SemiBold Italic", 600),
+    ("Bold Italic", 700),
+    ("ExtraBold Italic", 800),
+    ("Black Italic", 900),
+];
+
+pub struct AxisRegistry {
+    axes: BTreeMap<String, Box<AxisProto>>,
+}
+
+pub struct FontAxis {
+    pub tag: String,
+    pub min: f32,
+    pub max: f32,
+    pub default: f32,
+}
+
+pub struct NameParticle {
+    pub name: Option<String>,
+    pub value: f32,
+    pub elided: bool,
+}
+
+impl AxisRegistry {
+    pub fn new() -> Self {
+        Self {
+            axes: (*AXES).clone(),
+        }
+    }
+
+    pub fn get(&self, tag: &str) -> Option<&AxisProto> {
+        self.axes.get(tag).map(|v| &**v)
+    }
+
+    pub fn contains_key(&self, tag: &str) -> bool {
+        self.axes.contains_key(tag)
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = (&String, &AxisProto)> {
+        self.axes.iter().map(|(k, v)| (k, &**v))
+    }
+
+    pub fn get_fallback<'a>(&'a self, name: &str) -> Option<(&'a str, &'a FallbackProto)> {
+        self.axes
+            .iter()
+            .flat_map(|(tag, axis)| {
+                let fallback = axis
+                    .fallback
+                    .iter()
+                    .find(|f| f.name.as_deref() == Some(name));
+                fallback.map(|f| (tag.as_str(), f))
+            })
+            .next()
+    }
+
+    // This is fallbacks_in_fvar, but without assuming any particular font representation
+    pub fn fallbacks<'a>(
+        &'a self,
+        font_axes: &'a [FontAxis],
+    ) -> impl Iterator<Item = (String, Vec<FallbackProto>)> + 'a {
+        font_axes.iter().filter_map(|axis| {
+            self.get(&axis.tag).map(|registry_axis| {
+                (
+                    registry_axis.tag.clone().unwrap_or_default(),
+                    registry_axis
+                        .fallback
+                        .iter()
+                        .filter(|f| {
+                            f.value.unwrap_or(-f32::INFINITY) <= axis.min
+                                && f.value.unwrap_or(f32::INFINITY) >= axis.max
+                        })
+                        .cloned()
+                        .collect(),
+                )
+            })
+        })
+    }
+
+    // This is fallbacks_in_name_table, but without assuming any particular font representation
+    pub fn name_table_fallbacks<'a>(
+        &'a self,
+        family_name: &'a str,
+        subfamily_name: &'a str,
+        font_axes: &'a [FontAxis],
+    ) -> impl Iterator<Item = (&'a str, &'a FallbackProto)> + 'a {
+        let axis_names: HashSet<&str> = font_axes.iter().map(|axis| axis.tag.as_ref()).collect();
+        let tokens = family_name
+            .split_whitespace()
+            .skip(1)
+            .chain(subfamily_name.split_whitespace());
+        tokens
+            .flat_map(|token| self.get_fallback(token))
+            .filter(move |(tag, _)| axis_names.contains(tag))
+    }
+
+    pub fn fallback_for_value<'a>(
+        &'a self,
+        axis_tag: &str,
+        value: f32,
+    ) -> Option<&'a FallbackProto> {
+        self.get(axis_tag)
+            .and_then(|axis| axis.fallback.iter().find(|f| f.value == Some(value)))
+    }
+
+    pub fn axis_order(&self) -> Vec<&str> {
+        let mut axis_tags: Vec<&str> = self.axes.keys().map(|k| k.as_str()).collect();
+        axis_tags.sort();
+        axis_tags.extend(vec!["opsz", "wdth", "wght", "ital", "slnt"]);
+        axis_tags
+    }
+
+    pub fn name_particles<'a>(&self, font_axes: &'a [FontAxis]) -> IndexMap<&'a str, NameParticle> {
+        let mut particles = IndexMap::new();
+        for axis in font_axes {
+            if axis.tag == "opsz" {
+                particles.insert(
+                    "opsz",
+                    NameParticle {
+                        name: Some(format!("{}pt", axis.default)),
+                        value: axis.default,
+                        elided: false,
+                    },
+                );
+            } else if let Some(fallback) = self.fallback_for_value(&axis.tag, axis.default) {
+                particles.insert(
+                    &axis.tag,
+                    NameParticle {
+                        name: fallback.name.clone(),
+                        value: axis.default,
+                        elided: (fallback.value() == axis.default)
+                            && !(["Regular", "Italic", "14pt"].contains(&fallback.name())),
+                    },
+                );
+            } else {
+                particles.insert(
+                    &axis.tag,
+                    NameParticle {
+                        name: None,
+                        value: axis.default,
+                        elided: true,
+                    },
+                );
+            };
+        }
+        particles
+    }
+}
+
+impl Default for AxisRegistry {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Index<&str> for AxisRegistry {
+    type Output = AxisProto;
+
+    fn index(&self, tag: &str) -> &Self::Output {
+        self.get(tag).expect("No such axis")
+    }
+}
+
+#[cfg(feature = "fontations")]
+mod nametable;
+
+#[cfg(feature = "fontations")]
+mod fontations {
+    use super::*;
+    use nametable::{best_familyname, best_subfamilyname, find_or_add_name, rewrite_or_insert};
+    use skrifa::{string::StringId, Tag};
+    use std::{cmp::Reverse, collections::HashMap};
+    use write_fonts::{
+        from_obj::ToOwnedTable,
+        tables::{
+            name::{Name, NameRecord},
+            os2::Os2,
+            stat::{AxisValue, Stat},
+        },
+        types::Fixed,
+    };
+
+    pub fn build_name_table(
+        font: FontRef,
+        family_name: Option<&str>,
+        style_name: Option<&str>,
+        siblings: &[FontRef],
+    ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
+        let mut new_font = FontBuilder::new();
+        let family_name = family_name
+            .map(|x| x.to_string())
+            .unwrap_or_else(|| best_familyname(&font).unwrap_or("Unknown".to_string()));
+        let style_name = style_name
+            .map(|x| x.to_string())
+            .unwrap_or_else(|| best_subfamilyname(&font).unwrap_or("Regular".to_string()));
+
+        if font.table_data(Tag::new(b"fvar")).is_some() {
+            build_vf_name_table(&mut new_font, &font, &family_name, siblings)?;
+        } else {
+            // build_static_name_table_v1(&mut new_font, &font, &family_name, &style_name)
+        }
+
+        let mut styles: Vec<_> = GF_STATIC_STYLES.iter().collect();
+        styles.sort_by_key(|(name, _weight)| Reverse(name.len()));
+        for (name, weight) in styles.iter() {
+            if style_name.contains(name) {
+                let mut new_os2: Os2 = font.os2()?.to_owned_table();
+                new_os2.us_weight_class = *weight;
+                new_font.add_table(&new_os2)?;
+                break;
+            }
+        }
+        // Set RIBBI bits
+        Ok(new_font.copy_missing_tables(font).build())
+    }
+
+    fn fvar_instance_collisions(font: &FontRef, siblings: &[FontRef]) -> bool {
+        let fonts = siblings.iter().chain(std::iter::once(font));
+        let is_italic = fonts
+            .map(|f| {
+                f.post()
+                    .map(|post| post.italic_angle().abs() != Fixed::from_f64(0.0))
+                    .unwrap_or(false)
+            })
+            .collect::<Vec<_>>();
+        // Any duplicates?
+        is_italic.iter().any(|&x| x) && is_italic.iter().all(|&x| x)
+    }
+
+    fn build_vf_name_table(
+        newfont: &mut FontBuilder,
+        font: &FontRef,
+        family_name: &str,
+        siblings: &[FontRef],
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        let style_name = vf_style_name(font, family_name)?;
+        let mut new_name: Name = (if fvar_instance_collisions(font, siblings) {
+            build_static_name_table_v1(newfont, font, family_name, style_name)
+        } else {
+            build_static_name_table(newfont, font, family_name, style_name)
+        })?;
+        build_variations_ps_name(&mut new_name, font, Some(family_name));
+        newfont.add_table(&new_name)?;
+        Ok(())
+    }
+
+    fn build_variations_ps_name(newname: &mut Name, font: &FontRef, family_name: Option<&str>) {
+        let fallback = best_familyname(font);
+        let family_name = family_name.or(fallback.as_deref()).unwrap_or("New Font");
+        let subfamily_name = best_subfamilyname(font).unwrap_or("Regular".to_string());
+        let font_axes = font_axes(font).unwrap_or_default();
+        let registry = AxisRegistry::new();
+        let font_styles = registry.name_table_fallbacks(family_name, &subfamily_name, &font_axes);
+        let mut var_ps = family_name.replace(" ", "");
+        for (_, fallback) in font_styles {
+            let fallback_name = fallback.name();
+            if !var_ps.contains(fallback_name) {
+                var_ps.push_str(fallback_name);
+            }
+        }
+        rewrite_or_insert(&mut newname.name_record, StringId::POSTSCRIPT_NAME, var_ps);
+    }
+
+    fn build_static_name_table_v1(
+        _newfont: &mut FontBuilder,
+        _font: &FontRef,
+        _family_name: &str,
+        _style_name: String,
+    ) -> Result<Name, Box<dyn std::error::Error>> {
+        unimplemented!()
+    }
+
+    fn build_static_name_table(
+        newfont: &mut FontBuilder,
+        font: &FontRef,
+        family_name: &str,
+        style_name: String,
+    ) -> Result<Name, Box<dyn std::error::Error>> {
+        let mut name: Name = font.name()?.to_owned_table();
+        let mut records = name.name_record.into_iter().collect::<Vec<NameRecord>>();
+        records.retain(|record| record.platform_id != 1);
+        // let existing_name = best_familyname(font).unwrap_or("New Font".to_string());
+        let mut removed_names: HashMap<StringId, String> = HashMap::new();
+        let (new_family_name, new_style_name, removeable_name_ids) =
+            if ["Italic", "Bold Italic", "Bold", "Regular"].contains(&style_name.as_str()) {
+                (
+                    family_name.to_string(),
+                    style_name,
+                    vec![
+                        StringId::TYPOGRAPHIC_FAMILY_NAME,
+                        StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
+                        StringId::new(21),
+                        StringId::new(22),
+                    ],
+                )
+            } else {
+                let style_tokens = style_name.split_whitespace().collect::<Vec<_>>();
+                let mut new_family_name_tokens = family_name.split_whitespace().collect::<Vec<_>>();
+                let is_italic = style_tokens.contains(&"Italic");
+                let additional_tokens = (style_tokens.into_iter().filter(|token| {
+                    !(*token == "Regular"
+                        || *token == "Italic"
+                        || new_family_name_tokens.contains(token))
+                }))
+                .collect::<Vec<_>>();
+                new_family_name_tokens.extend(additional_tokens);
+                let new_family_name = new_family_name_tokens.join(" ");
+                let new_style_name = if is_italic { "Italic" } else { "Regular" };
+                rewrite_or_insert(
+                    &mut records,
+                    StringId::TYPOGRAPHIC_FAMILY_NAME,
+                    family_name.to_string(),
+                );
+                rewrite_or_insert(
+                    &mut records,
+                    StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
+                    style_name,
+                );
+                (
+                    new_family_name,
+                    new_style_name.to_string(),
+                    vec![StringId::new(21), StringId::new(22)],
+                )
+            };
+        let full_name = new_family_name.to_string() + " " + &new_style_name;
+        let ps_name = (new_family_name.to_string() + "-" + &new_style_name).replace(" ", "");
+        rewrite_or_insert(&mut records, StringId::FAMILY_NAME, family_name.to_string());
+        rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, new_style_name);
+        rewrite_or_insert(&mut records, StringId::FULL_NAME, full_name);
+        rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, ps_name);
+        let mut to_delete = vec![];
+        for name_id in removeable_name_ids.into_iter() {
+            if let Some(existing) = records.iter_mut().position(|r| {
+                r.name_id == name_id
+                    && r.platform_id == 3
+                    && r.encoding_id == 1
+                    && r.language_id == 0x409
+            }) {
+                removed_names.insert(name_id, records[existing].string.to_string());
+                to_delete.push(existing);
+            }
+        }
+        for i in to_delete.into_iter().rev() {
+            records.remove(i);
+        }
+
+        // If STAT table was using any removed names, add then back with a new ID
+        if !removed_names.is_empty() && font.table_data(Tag::from_be_bytes(*b"STAT")).is_some() {
+            let mut stat: Stat = font.stat()?.to_owned_table();
+            for axis in stat.design_axes.iter_mut() {
+                let id = axis.axis_name_id;
+                if let Some(old_name) = removed_names.get(&id) {
+                    axis.axis_name_id = find_or_add_name(&mut records, old_name);
+                }
+            }
+            // Also do the axis value array
+            if let Some(axis_values) = stat.offset_to_axis_values.as_deref_mut() {
+                for axis_value in axis_values.iter_mut() {
+                    match &mut **axis_value {
+                        AxisValue::Format1(ref mut axis_value) => {
+                            if let Some(old_name) = removed_names.get(&axis_value.value_name_id) {
+                                axis_value.value_name_id = find_or_add_name(&mut records, old_name);
+                            }
+                        }
+                        AxisValue::Format2(ref mut axis_value) => {
+                            if let Some(old_name) = removed_names.get(&axis_value.value_name_id) {
+                                axis_value.value_name_id = find_or_add_name(&mut records, old_name);
+                            }
+                        }
+                        AxisValue::Format3(ref mut axis_value) => {
+                            if let Some(old_name) = removed_names.get(&axis_value.value_name_id) {
+                                axis_value.value_name_id = find_or_add_name(&mut records, old_name);
+                            }
+                        }
+                        AxisValue::Format4(_axis_value_format4) => {
+                            // We don't do any magic here in Python; maybe we should...
+                        }
+                    }
+                }
+            }
+            newfont.add_table(&stat)?;
+        }
+        name.name_record = records;
+        Ok(name)
+    }
+
+    fn font_axes(font: &FontRef) -> Result<Vec<FontAxis>, ReadError> {
+        let fvar = font.fvar().unwrap();
+        let mut axes = vec![];
+        for axis in fvar.axes()? {
+            let tag = axis.axis_tag().to_string();
+            let min = axis.min_value().to_f32();
+            let max = axis.max_value().to_f32();
+            let default = axis.default_value().to_f32();
+            axes.push(FontAxis {
+                tag,
+                min,
+                max,
+                default,
+            });
+        }
+        Ok(axes)
+    }
+
+    fn vf_style_name(font: &FontRef, family_name: &str) -> Result<String, ReadError> {
+        let axisregistry = AxisRegistry::new();
+        let axes: Vec<_> = font_axes(font)?;
+        let fvar_dflts = axisregistry.name_particles(&axes);
+        let mut relevant_particles: Vec<String> = axisregistry
+            .axis_order()
+            .iter()
+            .flat_map(|tag| fvar_dflts.get(tag))
+            .filter(|particle| !particle.elided)
+            .flat_map(|particle| particle.name.clone())
+            .collect::<Vec<_>>();
+        let family_name_tokens = family_name.split_whitespace().collect::<HashSet<_>>();
+        let subfamily_name = best_subfamilyname(&font);
+        let font_styles = axisregistry
+            .name_table_fallbacks(
+                family_name,
+                subfamily_name.as_deref().unwrap_or("Regular"),
+                &axes,
+            )
+            .map(|(_tag, proto)| proto.name())
+            .filter(|name| !family_name_tokens.contains(name))
+            .map(|name| name.to_string())
+            .filter(|name| !relevant_particles.contains(&name))
+            .collect::<Vec<_>>();
+        relevant_particles.extend(font_styles);
+        let name = relevant_particles
+            .join(" ")
+            .replace("Regular Italic", "Italic");
+        Ok(name)
+    }
+}
+
+#[cfg(feature = "fontations")]
+pub use fontations::*;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn opsz() {
+        let ar = AxisRegistry::new();
+        assert!(ar.contains_key("opsz"));
+        assert_eq!(ar["opsz"].display_name.as_deref(), Some("Optical Size"));
+    }
+}
diff --git a/src/nametable.rs b/src/nametable.rs
new file mode 100644 (file)
index 0000000..eb268f5
--- /dev/null
@@ -0,0 +1,78 @@
+use std::collections::HashSet;
+
+use skrifa::{string::StringId, FontRef, MetadataProvider};
+use write_fonts::tables::name::NameRecord;
+
+/// Utility functions for name table handling.
+
+pub(crate) fn get_best_name(font: &FontRef, ids: &[StringId]) -> Option<String> {
+    for id in ids {
+        if let Some(name) = font.localized_strings(*id).english_or_first() {
+            return Some(name.chars().collect());
+        }
+    }
+    None
+}
+
+pub(crate) fn best_familyname(font: &FontRef) -> Option<String> {
+    get_best_name(
+        font,
+        &[
+            StringId::WWS_FAMILY_NAME,
+            StringId::TYPOGRAPHIC_FAMILY_NAME,
+            StringId::FAMILY_NAME,
+        ],
+    )
+}
+
+pub(crate) fn best_subfamilyname(font: &FontRef) -> Option<String> {
+    get_best_name(
+        font,
+        &[
+            StringId::WWS_SUBFAMILY_NAME,
+            StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
+            StringId::SUBFAMILY_NAME,
+        ],
+    )
+}
+
+pub(crate) fn rewrite_or_insert(records: &mut Vec<NameRecord>, name_id: StringId, string: String) {
+    if let Some(existing) = records.iter_mut().find(|r| {
+        r.name_id == name_id && r.platform_id == 3 && r.encoding_id == 1 && r.language_id == 0x409
+    }) {
+        *existing.string = string.into();
+    } else {
+        records.push(NameRecord {
+            platform_id: 3,
+            encoding_id: 1,
+            language_id: 0x409,
+            name_id,
+            string: string.into(),
+        });
+    }
+}
+
+pub(crate) fn find_or_add_name(records: &mut Vec<NameRecord>, string: &str) -> StringId {
+    if let Some(existing) = records.iter().find(|r| {
+        r.string.to_string() == string
+            && r.platform_id == 3
+            && r.encoding_id == 1
+            && r.language_id == 0x409
+    }) {
+        return existing.name_id;
+    }
+    let existing_ids: HashSet<u16> = records.iter().map(|r| r.name_id.to_u16()).collect();
+    let mut id: u16 = 256;
+    while existing_ids.contains(&id) {
+        id += 1;
+    }
+    let name_id = StringId::new(id);
+    records.push(NameRecord {
+        platform_id: 3,
+        encoding_id: 1,
+        language_id: 0x409,
+        name_id,
+        string: string.to_string().into(),
+    });
+    name_id
+}