diff --git a/fontdrasil/src/types.rs b/fontdrasil/src/types.rs index 8bcb80ff..aee721ae 100644 --- a/fontdrasil/src/types.rs +++ b/fontdrasil/src/types.rs @@ -48,3 +48,4 @@ impl Display for GlyphName { } pub type GroupName = GlyphName; +pub type AnchorName = GlyphName; diff --git a/fontir/src/ir.rs b/fontir/src/ir.rs index c74c764f..ac1cf778 100644 --- a/fontir/src/ir.rs +++ b/fontir/src/ir.rs @@ -13,7 +13,7 @@ use crate::{ use chrono::{DateTime, Utc}; use font_types::NameId; use font_types::Tag; -use fontdrasil::types::{GlyphName, GroupName}; +use fontdrasil::types::{AnchorName, GlyphName, GroupName}; use indexmap::IndexSet; use kurbo::{Affine, BezPath, PathEl, Point}; use log::warn; @@ -821,6 +821,21 @@ impl Features { } } +/// The complete set of anchor data +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Anchors { + pub anchors: HashMap>, +} + +/// A variable definition of an anchor. +/// +/// Must have at least one definition, at the default location. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Anchor { + pub name: AnchorName, + pub positions: HashMap, +} + /// A variable definition of a single glyph. /// /// Guarrantees at least one definition. Currently that must be at @@ -954,6 +969,16 @@ impl Persistable for Kerning { } } +impl Persistable for Anchors { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + /// A variable definition of a single glyph. /// /// If defined in many locations, presumed to vary continuously diff --git a/fontir/src/orchestration.rs b/fontir/src/orchestration.rs index 6643627a..626c4373 100644 --- a/fontir/src/orchestration.rs +++ b/fontir/src/orchestration.rs @@ -308,6 +308,7 @@ pub enum WorkId { GlyphOrder, Features, Kerning, + Anchors, } impl Identifier for WorkId {} @@ -370,6 +371,7 @@ pub struct Context { pub glyphs: FeContextMap, pub features: FeContextItem, pub kerning: FeContextItem, + pub anchors: FeContextItem, } pub fn set_cached(lock: &Arc>>>, value: T) { @@ -390,7 +392,8 @@ impl Context { global_metrics: self.global_metrics.clone_with_acl(acl.clone()), glyphs: self.glyphs.clone_with_acl(acl.clone()), features: self.features.clone_with_acl(acl.clone()), - kerning: self.kerning.clone_with_acl(acl), + kerning: self.kerning.clone_with_acl(acl.clone()), + anchors: self.anchors.clone_with_acl(acl), } } @@ -426,7 +429,8 @@ impl Context { ), glyphs: ContextMap::new(acl.clone(), persistent_storage.clone()), features: ContextItem::new(WorkId::Features, acl.clone(), persistent_storage.clone()), - kerning: ContextItem::new(WorkId::Kerning, acl, persistent_storage), + kerning: ContextItem::new(WorkId::Kerning, acl.clone(), persistent_storage.clone()), + anchors: ContextItem::new(WorkId::Anchors, acl, persistent_storage), } } diff --git a/fontir/src/paths.rs b/fontir/src/paths.rs index 5130a814..9e0c47f8 100644 --- a/fontir/src/paths.rs +++ b/fontir/src/paths.rs @@ -43,6 +43,7 @@ impl Paths { pub fn target_file(&self, id: &WorkId) -> PathBuf { match id { + WorkId::Anchors => self.build_dir.join("anchors.yml"), WorkId::StaticMetadata => self.build_dir.join("static_metadata.yml"), WorkId::PreliminaryGlyphOrder => self.build_dir.join("glyph_order.preliminary.yml"), WorkId::GlyphOrder => self.build_dir.join("glyph_order.yml"), diff --git a/glyphs-reader/src/font.rs b/glyphs-reader/src/font.rs index 785fa0f6..8c1c945a 100644 --- a/glyphs-reader/src/font.rs +++ b/glyphs-reader/src/font.rs @@ -91,6 +91,7 @@ pub struct Layer { pub layer_id: String, pub width: OrderedFloat, pub shapes: Vec, + pub anchors: Vec, } #[derive(Debug, PartialEq, Hash)] @@ -101,7 +102,7 @@ pub enum Shape { // The font you get directly from a plist, minimally modified // Types chosen specifically to accomodate plist translation. -#[derive(Debug, FromPlist, PartialEq, Eq)] +#[derive(Debug, FromPlist, PartialEq)] #[allow(non_snake_case)] struct RawFont { pub units_per_em: Option, @@ -166,7 +167,7 @@ pub struct Axis { pub hidden: Option, } -#[derive(Clone, Debug, FromPlist, PartialEq, Eq)] +#[derive(Clone, Debug, FromPlist, PartialEq)] pub struct RawGlyph { pub layers: Vec, pub glyphname: String, @@ -176,7 +177,7 @@ pub struct RawGlyph { pub other_stuff: BTreeMap, } -#[derive(Clone, Debug, FromPlist, PartialEq, Eq)] +#[derive(Clone, Debug, FromPlist, PartialEq)] pub struct RawLayer { pub layer_id: String, pub associated_master_id: Option, @@ -184,7 +185,7 @@ pub struct RawLayer { shapes: Option>, paths: Option>, components: Option>, - //pub anchors: Option>, + pub anchors: Option>, #[fromplist(rest)] pub other_stuff: BTreeMap, } @@ -273,10 +274,24 @@ pub enum NodeType { QCurveSmooth, } +#[derive(Clone, Debug, FromPlist, PartialEq)] +pub struct RawAnchor { + pub name: String, + pub pos: Option, // v3 + pub position: Option, // v2 +} + #[derive(Clone, Debug, FromPlist, PartialEq)] pub struct Anchor { pub name: String, - pub position: Point, + pub pos: Point, +} + +impl Hash for Anchor { + fn hash(&self, state: &mut H) { + self.name.hash(state); + PointForEqAndHash::new(self.pos).hash(state); + } } #[derive(Clone, Debug, PartialEq, Hash)] @@ -541,10 +556,17 @@ impl FromPlist for Affine { impl FromPlist for Point { fn from_plist(plist: Plist) -> Self { - let raw = plist.as_str().unwrap(); - let raw = &raw[1..raw.len() - 1]; - let coords: Vec = raw.split(", ").map(|c| c.parse().unwrap()).collect(); - Point::new(coords[0], coords[1]) + match plist { + Plist::Array(values) if values.len() == 2 => { + Point::new(values[0].as_f64().unwrap(), values[1].as_f64().unwrap()) + } + Plist::String(value) => { + let raw = &value[1..value.len() - 1]; + let coords: Vec = raw.split(", ").map(|c| c.parse().unwrap()).collect(); + Point::new(coords[0], coords[1]) + } + _ => panic!("Cannot parse point from {plist:?}"), + } } } @@ -1346,10 +1368,27 @@ impl TryFrom for Layer { shapes.push(raw_shape.try_into()?); } + let anchors = from + .anchors + .unwrap_or_default() + .into_iter() + .map(|ra| { + let pos = if let Some(pos) = ra.pos { + pos + } else if let Some(pos) = ra.position { + Point::from_plist(Plist::String(pos)) + } else { + panic!("No position for anchor {ra:?}"); + }; + Anchor { name: ra.name, pos } + }) + .collect(); + Ok(Layer { layer_id: from.layer_id, width: from.width, shapes, + anchors, }) } } @@ -1747,7 +1786,7 @@ mod tests { use pretty_assertions::assert_eq; - use kurbo::Affine; + use kurbo::{Affine, Point}; fn testdata_dir() -> PathBuf { // working dir varies CLI vs VSCode @@ -1863,6 +1902,11 @@ mod tests { assert_load_v2_matches_load_v3("WghtVar_OS2.glyphs"); } + #[test] + fn read_wght_var_anchors_2_and_3() { + assert_load_v2_matches_load_v3("WghtVar_Anchors.glyphs"); + } + fn only_shape_in_only_layer<'a>(font: &'a Font, glyph_name: &str) -> &'a Shape { let glyph = font.glyphs.get(glyph_name).unwrap(); assert_eq!(1, glyph.layers.len()); @@ -2285,4 +2329,26 @@ mod tests { (actual_groups, actual_kerning), ); } + + #[test] + fn read_simple_anchor() { + let font = Font::load(&glyphs3_dir().join("WghtVar_Anchors.glyphs")).unwrap(); + assert_eq!( + vec![ + ("m01", "top", Point::new(300.0, 700.0)), + ("l2", "top", Point::new(325.0, 725.0)) + ], + font.glyphs + .get("A") + .unwrap() + .layers + .iter() + .flat_map(|l| l.anchors.iter().map(|a| ( + l.layer_id.as_str(), + a.name.as_str(), + a.pos + ))) + .collect::>() + ); + } } diff --git a/resources/testdata/glyphs2/WghtVar_Anchors.glyphs b/resources/testdata/glyphs2/WghtVar_Anchors.glyphs new file mode 100644 index 00000000..be1f1d95 --- /dev/null +++ b/resources/testdata/glyphs2/WghtVar_Anchors.glyphs @@ -0,0 +1,179 @@ +{ +.appVersion = "3218"; +DisplayStrings = ( +A +); +customParameters = ( +{ +name = Axes; +value = ( +{ +Name = Weight; +Tag = wght; +} +); +} +); +date = "2022-12-01 04:52:20 +0000"; +familyName = WghtVar; +fontMaster = ( +{ +alignmentZones = ( +"{800, 16}", +"{0, -16}", +"{-200, -16}" +); +ascender = 800; +capHeight = 0; +descender = -200; +id = m01; +weightValue = 400; +xHeight = 0; +}, +{ +ascender = 800; +capHeight = 700; +descender = -200; +id = "l2"; +weight = Bold; +weightValue = 700; +xHeight = 500; +} +); +glyphs = ( +{ +glyphname = A; +lastChange = "2023-09-14 14:42:56 +0000"; +layers = ( +{ +anchors = ( +{ +name = top; +position = "{300, 700}"; +} +); +layerId = m01; +paths = ( +{ +closed = 1; +nodes = ( +"354 183 LINE", +"414 585 LINE", +"178 585 LINE", +"238 182 LINE" +); +}, +{ +closed = 1; +nodes = ( +"354 0 LINE", +"354 107 LINE", +"238 107 LINE", +"238 0 LINE" +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = top; +position = "{325, 725}"; +} +); +layerId = "l2"; +paths = ( +{ +closed = 1; +nodes = ( +"354 183 LINE", +"414 585 LINE", +"178 585 LINE", +"238 182 LINE" +); +}, +{ +closed = 1; +nodes = ( +"354 0 LINE", +"354 107 LINE", +"238 107 LINE", +"238 0 LINE" +); +} +); +width = 600; +} +); +unicode = 0041; +}, +{ +glyphname = space; +lastChange = "2022-12-01 04:58:12 +0000"; +layers = ( +{ +layerId = m01; +width = 200; +}, +{ +layerId = "l2"; +width = 600; +} +); +unicode = 0020; +}, +{ +glyphname = macroncomb; +lastChange = "2023-09-14 14:41:26 +0000"; +layers = ( +{ +anchors = ( +{ +name = _top; +position = "{300, 600}"; +} +); +layerId = m01; +paths = ( +{ +closed = 1; +nodes = ( +"140 661 LINE", +"454 661 LINE", +"454 724 LINE", +"140 724 LINE" +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = _top; +position = "{310, 610}"; +} +); +layerId = "l2"; +paths = ( +{ +closed = 1; +nodes = ( +"132 658 LINE", +"462 658 LINE", +"462 728 LINE", +"132 728 LINE" +); +} +); +width = 600; +} +); +unicode = 0304; +} +); +unitsPerEm = 1000; +versionMajor = 1; +versionMinor = 0; +} diff --git a/resources/testdata/glyphs3/WghtVar_Anchors.glyphs b/resources/testdata/glyphs3/WghtVar_Anchors.glyphs new file mode 100644 index 00000000..8d1b87b2 --- /dev/null +++ b/resources/testdata/glyphs3/WghtVar_Anchors.glyphs @@ -0,0 +1,218 @@ +{ +.appVersion = "3218"; +.formatVersion = 3; +DisplayStrings = ( +A +); +axes = ( +{ +name = Weight; +tag = wght; +} +); +date = "2022-12-01 04:52:20 +0000"; +familyName = WghtVar; +fontMaster = ( +{ +axesValues = ( +400 +); +id = m01; +metricValues = ( +{ +over = 16; +pos = 800; +}, +{ +over = -16; +}, +{ +over = -16; +pos = -200; +}, +{ +}, +{ +} +); +name = Regular; +}, +{ +axesValues = ( +700 +); +iconName = Bold; +id = "l2"; +metricValues = ( +{ +pos = 800; +}, +{ +}, +{ +pos = -200; +}, +{ +pos = 700; +}, +{ +pos = 500; +} +); +name = Bold; +} +); +glyphs = ( +{ +glyphname = A; +lastChange = "2023-09-14 14:42:56 +0000"; +layers = ( +{ +anchors = ( +{ +name = top; +pos = (300,700); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(354,183,l), +(414,585,l), +(178,585,l), +(238,182,l) +); +}, +{ +closed = 1; +nodes = ( +(354,0,l), +(354,107,l), +(238,107,l), +(238,0,l) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = top; +pos = (325,725); +} +); +layerId = "l2"; +shapes = ( +{ +closed = 1; +nodes = ( +(354,183,l), +(414,585,l), +(178,585,l), +(238,182,l) +); +}, +{ +closed = 1; +nodes = ( +(354,0,l), +(354,107,l), +(238,107,l), +(238,0,l) +); +} +); +width = 600; +} +); +unicode = 65; +}, +{ +glyphname = space; +lastChange = "2022-12-01 04:58:12 +0000"; +layers = ( +{ +layerId = m01; +width = 200; +}, +{ +layerId = "l2"; +width = 600; +} +); +unicode = 32; +}, +{ +glyphname = macroncomb; +lastChange = "2023-09-14 14:41:26 +0000"; +layers = ( +{ +anchors = ( +{ +name = _top; +pos = (300,600); +} +); +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(140,661,l), +(454,661,l), +(454,724,l), +(140,724,l) +); +} +); +width = 600; +}, +{ +anchors = ( +{ +name = _top; +pos = (310,610); +} +); +layerId = "l2"; +shapes = ( +{ +closed = 1; +nodes = ( +(132,658,l), +(462,658,l), +(462,728,l), +(132,728,l) +); +} +); +width = 600; +} +); +unicode = 772; +} +); +metrics = ( +{ +type = ascender; +}, +{ +type = baseline; +}, +{ +type = descender; +}, +{ +type = "cap height"; +}, +{ +type = "x-height"; +} +); +unitsPerEm = 1000; +versionMajor = 1; +versionMinor = 0; +}