diff --git a/fontra2fontir/Cargo.toml b/fontra2fontir/Cargo.toml index d4bae3b0f..866209ddc 100644 --- a/fontra2fontir/Cargo.toml +++ b/fontra2fontir/Cargo.toml @@ -16,8 +16,6 @@ fontir = { version = "0.0.1", path = "../fontir" } write-fonts.workspace = true -percent-encoding = "2.3.1" - log.workspace = true env_logger.workspace = true diff --git a/fontra2fontir/src/fontra.rs b/fontra2fontir/src/fontra.rs index 45e8cb65b..8c5e7d696 100644 --- a/fontra2fontir/src/fontra.rs +++ b/fontra2fontir/src/fontra.rs @@ -3,21 +3,116 @@ //! See use std::{ - collections::HashMap, + collections::{BTreeMap, HashMap}, fs, path::{Path, PathBuf}, }; use fontdrasil::types::GlyphName; use fontir::error::Error; -use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use serde::Deserialize; use write_fonts::types::Tag; +const SEPARATOR_CHAR: char = '^'; + +fn is_reserved_char(c: char) -> bool { + matches!( + c, + '\0'..='\x1F' + | '\x7F' + | SEPARATOR_CHAR + | '>' + | '|' + | '[' + | '?' + | '+' + | '\\' + | '"' + | ':' + | '/' + | '<' + | '%' + | ']' + | '*' + ) +} + +fn is_reserved_filename(name: &str) -> bool { + matches!( + name.to_ascii_uppercase().as_str(), + "CON" + | "PRN" + | "AUX" + | "CLOCK$" + | "NUL" + | "COM1" + | "LPT1" + | "LPT2" + | "LPT3" + | "COM2" + | "COM3" + | "COM4" + ) +} + +const BASE_32_CHARS: [char; 32] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', +]; + +/// Matches +fn string_to_filename(string: &str, suffix: &str) -> String { + let string_bytes = string.as_bytes(); + let mut code_digits: Vec<_> = (0..string_bytes.len()) + .step_by(5) + .map(|i| { + let mut digit = 0; + let mut bit = 1; + let string = string.as_bytes(); + for byte in string[i..(i + 5).min(string.len())].iter() { + if byte.is_ascii_uppercase() { + digit |= bit + } + bit <<= 1; + } + digit + }) + .collect(); + while let Some(0) = code_digits.last() { + code_digits.pop(); + } + + let mut filename = String::new(); + for (i, c) in string.chars().enumerate() { + if i == 0 && c == '.' { + filename.push_str("%2E"); + } else if !is_reserved_char(c) { + filename.push(c); + } else { + filename.push_str(format!("%{:02X}", c as u32).as_str()); + } + } + + if code_digits.is_empty() && is_reserved_filename(string) { + code_digits.push(0); + } + + if !code_digits.is_empty() { + filename.push(SEPARATOR_CHAR); + for d in code_digits { + assert!(d < 32, "We've made a terrible mistake"); + filename.push(BASE_32_CHARS[d]); + } + } + + for c in suffix.chars() { + filename.push(c); + } + filename +} + pub(crate) fn glyph_file(glyph_dir: &Path, glyph: GlyphName) -> PathBuf { - // TODO(https://github.com/googlefonts/fontc/issues/688): fontra string => filename algorithm - let filename = utf8_percent_encode(glyph.as_str(), NON_ALPHANUMERIC); - glyph_dir.join(filename.to_string() + ".json") + glyph_dir.join(string_to_filename(glyph.as_str(), ".json")) } fn from_file(p: &Path) -> Result @@ -66,8 +161,26 @@ pub(crate) struct FontraAxis { #[allow(dead_code)] // TEMPORARY pub(crate) struct FontraGlyph { pub(crate) name: GlyphName, + /// Variable component, or glyph-local, axes + #[serde(default)] + pub(crate) axes: Vec, pub(crate) sources: Vec, - pub(crate) layers: HashMap, + pub(crate) layers: BTreeMap, +} + +/// An axis specific to a glyph meant to be used as a variable component +/// +/// +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] // TEMPORARY +pub(crate) struct FontraGlyphAxis { + pub(crate) name: String, + #[serde(rename = "minValue")] + pub(crate) min_value: f64, + #[serde(rename = "defaultValue")] + pub(crate) default_value: f64, + #[serde(rename = "maxValue")] + pub(crate) max_value: f64, } /// @@ -78,7 +191,7 @@ pub(crate) struct FontraSource { #[serde(rename = "layerName")] pub(crate) layer_name: String, #[serde(default)] - pub(crate) location: HashMap, + pub(crate) location: HashMap, // TODO: locationBase #[serde(default)] pub(crate) inactive: bool, @@ -100,6 +213,8 @@ pub(crate) struct FontraGlyphInstance { // TODO: Fontra has two representations, packed and unpacked. This only covers one. #[serde(default)] pub(crate) path: FontraPath, + #[serde(default)] + pub(crate) components: Vec, } impl FontraGlyph { @@ -113,6 +228,7 @@ impl FontraGlyph { #[derive(Default, Debug, Clone, Deserialize)] #[allow(dead_code)] // TEMPORARY pub(crate) struct FontraPath { + #[serde(default)] pub(crate) contours: Vec, } @@ -173,6 +289,51 @@ impl PointType { } } +/// +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] // TEMPORARY +pub(crate) struct FontraComponent { + pub(crate) name: GlyphName, + #[serde(default)] + pub(crate) transformation: FontraTransform, + // This location is in terms of axes defined by the referenced glyph + #[serde(default)] + pub(crate) location: HashMap, +} + +/// What FontTools calls a DecomposedTransform +/// +/// +#[derive(Default, Debug, Clone, Deserialize)] +#[allow(dead_code)] // TEMPORARY +pub(crate) struct FontraTransform { + #[serde(rename = "translateX", default)] + translate_x: f64, + #[serde(rename = "translateY", default)] + translate_y: f64, + /// in degrees counter-clockwise in font coordinate space + #[serde(default)] + rotation: f64, + #[serde(rename = "scaleX", default = "float_one")] + scale_x: f64, + #[serde(rename = "scaleY", default = "float_one")] + scale_y: f64, + /// in degrees clockwise in font coordinate space + #[serde(rename = "skewX", default)] + skew_x: f64, + /// in degrees counter-clockwise in font coordinate space + #[serde(rename = "skewY", default)] + skew_y: f64, + #[serde(rename = "tCenterX", default)] + t_center_x: f64, + #[serde(rename = "tCenterY", default)] + t_center_y: f64, +} + +fn float_one() -> f64 { + 1.0 +} + #[cfg(test)] mod tests { use std::collections::HashSet; @@ -200,6 +361,22 @@ mod tests { .collect::>() } + fn glyph_axis_tuples(glyph: &FontraGlyph) -> Vec<(&str, f64, f64, f64)> { + glyph + .axes + .iter() + .map(|a| (a.name.as_str(), a.min_value, a.default_value, a.max_value)) + .collect::>() + } + + fn read_test_glyph(fontra_dir: &str, glyph_name: &str) -> FontraGlyph { + let file = testdata_dir() + .join(fontra_dir) + .join("glyphs") + .join(string_to_filename(glyph_name, ".json")); + FontraGlyph::from_file(&file).unwrap_or_else(|e| panic!("Unable to read {file:?}: {e}")) + } + #[test] fn fontdata_of_minimal() { let font_data = @@ -238,9 +415,7 @@ mod tests { #[test] fn read_notdef() { - let glyph = - FontraGlyph::from_file(&testdata_dir().join("minimal.fontra/glyphs/%2Enotdef.json")) - .unwrap(); + let glyph = read_test_glyph("minimal.fontra", ".notdef"); assert_eq!(GlyphName::new(".notdef"), glyph.name, "{glyph:#?}"); assert_eq!( 1000.0, glyph.layers["foreground"].glyph.x_advance, @@ -249,10 +424,24 @@ mod tests { } #[test] - fn read_u20089() { - let glyph = - FontraGlyph::from_file(&testdata_dir().join("2glyphs.fontra/glyphs/u20089.json")) - .unwrap(); + fn read_varc_axes() { + let glyph = read_test_glyph("component.fontra", "VG_4E00_00"); + assert_eq!( + vec![ + ("width", 200.0, 1000.0, 1000.0), + ("weight", 10.0, 33.0, 50.0), + ("serif_height", -50.0, 0.0, 100.0), + ("serif_width", -100.0, 0.0, 100.0), + ("serif_top_moveH", -50.0, 0.0, 50.0), + ("H_L_length", -800.0, 0.0, 800.0), + ], + glyph_axis_tuples(&glyph), + ) + } + + #[test] + fn read_simple_contours() { + let glyph = read_test_glyph("2glyphs.fontra", "u20089"); assert_eq!(GlyphName::new("u20089"), glyph.name, "{glyph:#?}"); let mut layer_names: Vec<_> = glyph.layers.keys().map(|n| n.as_str()).collect(); layer_names.sort(); @@ -278,4 +467,48 @@ mod tests { contour.points[2].point_type().unwrap() ); } + + #[test] + fn read_simple_component() { + let glyph = read_test_glyph("component.fontra", "uni4E00"); + assert_eq!(GlyphName::new("uni4E00"), glyph.name, "{glyph:#?}"); + assert_eq!( + vec![ + ("foreground".to_string(), GlyphName::new("VG_4E00_00")), + ("wght=1".to_string(), GlyphName::new("VG_4E00_00")) + ], + glyph + .layers + .iter() + .flat_map(|(n, l)| l + .glyph + .components + .iter() + .map(|c| (n.clone(), c.name.clone()))) + .collect::>(), + ); + } + + #[test] + fn match_python_string_to_filename() { + // expected is as observed in Python with .json appended + let exemplars = vec![ + ("AUX", "AUX^7.json"), + (".notdef", "%2Enotdef.json"), + ("4E00", "4E00^2.json"), + ("VG_4E00_01", "VG_4E00_01^J.json"), + ("duck:goose/mallard", "duck%3Agoose%2Fmallard.json"), + ("Hi ❤️‍🔥 hru", "Hi ❤️\u{200d}🔥 hru^1.json"), + ]; + let mut errors = Vec::new(); + for (input, expected) in exemplars { + let actual = string_to_filename(input, ".json"); + if expected != actual { + errors.push(format!( + "\"{input}\" should convert to \"{expected}\" not \"{actual}\"" + )); + } + } + assert_eq!(0, errors.len(), "{errors:#?}"); + } } diff --git a/fontra2fontir/src/toir.rs b/fontra2fontir/src/toir.rs index 588e3b2c0..26906bb67 100644 --- a/fontra2fontir/src/toir.rs +++ b/fontra2fontir/src/toir.rs @@ -12,6 +12,7 @@ use fontir::{ }; use kurbo::BezPath; use log::trace; +use write_fonts::types::Tag; use crate::fontra::{FontraContour, FontraFontData, FontraGlyph, FontraPoint, PointType}; @@ -82,35 +83,44 @@ pub(crate) fn to_ir_static_metadata( .map_err(WorkError::VariationModelError) } +/// #[allow(dead_code)] // TEMPORARY fn to_ir_glyph( - default_location: NormalizedLocation, + global_axes: HashMap<&str, Tag>, codepoints: HashSet, fontra_glyph: &FontraGlyph, ) -> Result { + let _local_axes: HashMap<_, _> = fontra_glyph + .axes + .iter() + .map(|a| (a.name.as_str(), a)) + .collect(); + let layer_locations: HashMap<_, _> = fontra_glyph .sources .iter() - .map(|s| { - let mut location = default_location.clone(); - for (tag, pos) in s.location.iter() { - if !location.contains(*tag) { - return Err(WorkError::UnexpectedAxisPosition( - fontra_glyph.name.clone(), - tag.to_string(), - )); - } - location.insert(*tag, NormalizedCoord::new(*pos as f32)); - } - Ok((s.layer_name.as_str(), location)) - }) - .collect::>()?; + .map(|s| (s.layer_name.as_str(), &s.location)) + .collect(); let mut instances = HashMap::new(); for (layer_name, layer) in fontra_glyph.layers.iter() { + // TODO: we need IR VARC support to proceed + if !fontra_glyph.axes.is_empty() { + todo!("Support local axes"); + } + let Some(location) = layer_locations.get(layer_name.as_str()) else { return Err(WorkError::NoSourceForName(layer_name.clone())); }; + let global_location: NormalizedLocation = global_axes + .iter() + .map(|(name, tag)| { + ( + *tag, + NormalizedCoord::new(location.get(*name).copied().unwrap_or_default() as f32), + ) + }) + .collect(); let contours: Vec<_> = layer .glyph @@ -119,14 +129,22 @@ fn to_ir_glyph( .iter() .map(|c| to_ir_path(fontra_glyph.name.clone(), c)) .collect::>()?; - instances.insert( - location.clone(), - GlyphInstance { - width: layer.glyph.x_advance, - contours, - ..Default::default() - }, - ); + if instances + .insert( + global_location.clone(), + GlyphInstance { + width: layer.glyph.x_advance, + contours, + ..Default::default() + }, + ) + .is_some() + { + return Err(WorkError::DuplicateNormalizedLocation { + what: "Multiple glyph instances".to_string(), + loc: global_location, + }); + }; } Glyph::new(fontra_glyph.name.clone(), true, codepoints, instances) @@ -162,7 +180,7 @@ fn add_to_path<'a>( fn to_ir_path(glyph_name: GlyphName, contour: &FontraContour) -> Result { // Based on glyphs2fontir/src/toir.rs to_ir_path - // TODO: so similar a trait to to let things be added to GlyphPathBuilder would be nice + // TODO(https://github.com/googlefonts/fontc/issues/700): share code if contour.points.is_empty() { return Ok(BezPath::new()); } @@ -204,9 +222,9 @@ fn to_ir_path(glyph_name: GlyphName, contour: &FontraContour) -> Result", +"layerName": "foreground", +"customData": { +"fontra.development.status": 0 +} +} +], +"layers": { +"foreground": { +"glyph": { +"xAdvance": 1000 +} +} +} +} diff --git a/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_00.alt^J.json b/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_00.alt^J.json new file mode 100644 index 000000000..7a9beca23 --- /dev/null +++ b/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_00.alt^J.json @@ -0,0 +1,790 @@ +{ +"name": "VG_4E00_00.alt", +"axes": [ +{ +"name": "width", +"minValue": 200, +"defaultValue": 951, +"maxValue": 1000 +}, +{ +"name": "thickness", +"minValue": 10, +"defaultValue": 33, +"maxValue": 70 +}, +{ +"name": "head", +"minValue": 50, +"defaultValue": 105, +"maxValue": 150 +}, +{ +"name": "head-height", +"minValue": -20, +"defaultValue": 0, +"maxValue": 20 +} +], +"sources": [ +{ +"name": "", +"layerName": "foreground", +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "wght=1", +"layerName": "wght=1", +"location": { +"wght": 1 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "width=1000", +"layerName": "width=1000", +"location": { +"width": 1000 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "width=200", +"layerName": "width=200", +"location": { +"width": 200 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "thickness=70", +"layerName": "thickness=70", +"location": { +"thickness": 70 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "thickness=10", +"layerName": "thickness=10", +"location": { +"thickness": 10 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "head=50", +"layerName": "head=50", +"location": { +"head": 50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "head=150", +"layerName": "head=150", +"location": { +"head": 150 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "head-height=20", +"layerName": "head-height=20", +"location": { +"head-height": 20 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "head-height=-20", +"layerName": "head-height=-20", +"location": { +"head-height": -20 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "wght=1,head=50", +"layerName": "wght=1,head=50", +"location": { +"head": 50, +"wght": 1 +}, +"customData": { +"fontra.development.status": 0 +} +} +], +"layers": { +"foreground": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 848, +"y": 505 +}, +{ +"x": 793, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 912, +"y": 454, +"type": "cubic" +}, +{ +"x": 848, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"head-height=-20": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 848, +"y": 485 +}, +{ +"x": 793, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 912, +"y": 444, +"type": "cubic" +}, +{ +"x": 848, +"y": 485, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"head-height=20": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 848, +"y": 525 +}, +{ +"x": 793, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 912, +"y": 462, +"type": "cubic" +}, +{ +"x": 848, +"y": 525, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"head=150": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 803, +"y": 542 +}, +{ +"x": 722, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 869, +"y": 492, +"type": "cubic" +}, +{ +"x": 803, +"y": 542, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"head=50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 903, +"y": 460 +}, +{ +"x": 884, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 934, +"y": 434, +"type": "cubic" +}, +{ +"x": 903, +"y": 460, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"thickness=10": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 848, +"y": 505 +}, +{ +"x": 775, +"y": 411 +}, +{ +"x": 53, +"y": 411 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 912, +"y": 454, +"type": "cubic" +}, +{ +"x": 848, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"thickness=70": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 848, +"y": 505 +}, +{ +"x": 821, +"y": 471 +}, +{ +"x": 43, +"y": 471 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 923, +"y": 401, +"smooth": true +}, +{ +"x": 939, +"y": 401, +"type": "cubic" +}, +{ +"x": 950, +"y": 404, +"type": "cubic" +}, +{ +"x": 953, +"y": 416 +}, +{ +"x": 912, +"y": 454, +"type": "cubic" +}, +{ +"x": 848, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"wght=1": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 841, +"y": 565 +}, +{ +"x": 748, +"y": 433 +}, +{ +"x": 54, +"y": 433 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 973, +"y": 401, +"smooth": true +}, +{ +"x": 990, +"y": 401, +"type": "cubic" +}, +{ +"x": 1002, +"y": 406, +"type": "cubic" +}, +{ +"x": 1005, +"y": 417 +}, +{ +"x": 945, +"y": 474, +"type": "cubic" +}, +{ +"x": 841, +"y": 565, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"wght=1,head=50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 927, +"y": 489 +}, +{ +"x": 889, +"y": 433 +}, +{ +"x": 54, +"y": 433 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 973, +"y": 401, +"smooth": true +}, +{ +"x": 990, +"y": 401, +"type": "cubic" +}, +{ +"x": 1002, +"y": 406, +"type": "cubic" +}, +{ +"x": 1005, +"y": 417 +}, +{ +"x": 967, +"y": 454, +"type": "cubic" +}, +{ +"x": 927, +"y": 489, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"width=1000": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 948, +"y": 505 +}, +{ +"x": 893, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 1023, +"y": 401, +"smooth": true +}, +{ +"x": 1039, +"y": 401, +"type": "cubic" +}, +{ +"x": 1050, +"y": 404, +"type": "cubic" +}, +{ +"x": 1053, +"y": 416 +}, +{ +"x": 1012, +"y": 454, +"type": "cubic" +}, +{ +"x": 948, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"width=200": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 148, +"y": 505 +}, +{ +"x": 93, +"y": 434 +}, +{ +"x": 53, +"y": 434 +}, +{ +"x": 63, +"y": 401 +}, +{ +"x": 223, +"y": 401, +"smooth": true +}, +{ +"x": 239, +"y": 401, +"type": "cubic" +}, +{ +"x": 250, +"y": 404, +"type": "cubic" +}, +{ +"x": 253, +"y": 416 +}, +{ +"x": 212, +"y": 454, +"type": "cubic" +}, +{ +"x": 148, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +} +} +} diff --git a/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_00^J.json b/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_00^J.json new file mode 100644 index 000000000..d20c830bc --- /dev/null +++ b/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_00^J.json @@ -0,0 +1,939 @@ +{ +"name": "VG_4E00_00", +"axes": [ +{ +"name": "width", +"minValue": 200, +"defaultValue": 1000, +"maxValue": 1000 +}, +{ +"name": "weight", +"minValue": 10, +"defaultValue": 33, +"maxValue": 50 +}, +{ +"name": "serif_height", +"minValue": -50, +"defaultValue": 0, +"maxValue": 100 +}, +{ +"name": "serif_width", +"minValue": -100, +"defaultValue": 0, +"maxValue": 100 +}, +{ +"name": "serif_top_moveH", +"minValue": -50, +"defaultValue": 0, +"maxValue": 50 +}, +{ +"name": "H_L_length", +"minValue": -800, +"defaultValue": 0, +"maxValue": 800 +} +], +"sources": [ +{ +"name": "", +"layerName": "foreground", +"customData": { +"fontra.development.status": 4 +} +}, +{ +"name": "width=100", +"layerName": "width=100", +"location": { +"width": 100 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "weight=10", +"layerName": "weight=10", +"location": { +"weight": 10 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "weight=50", +"layerName": "weight=50", +"location": { +"weight": 50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_height=-50", +"layerName": "serif_height=-50", +"location": { +"serif_height": -50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_width=-50", +"layerName": "serif_width=-50", +"location": { +"serif_width": -50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_top_moveH=50", +"layerName": "serif_top_moveH=50", +"location": { +"serif_top_moveH": 50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_top_moveH=-50", +"layerName": "serif_top_moveH=-50", +"location": { +"serif_top_moveH": -50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_height=100", +"layerName": "serif_height=100", +"location": { +"serif_height": 100 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_width=100", +"layerName": "serif_width=100", +"location": { +"serif_width": 100 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "serif_width=-100", +"layerName": "serif_width=-100", +"location": { +"serif_width": -100 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "H_L_length=800", +"layerName": "H_L_length=800", +"location": { +"H_L_length": 800 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "H_L_length=-800", +"layerName": "H_L_length=-800", +"location": { +"H_L_length": -800 +}, +"customData": { +"fontra.development.status": 0 +} +} +], +"layers": { +"foreground": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 505 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 454, +"type": "cubic" +}, +{ +"x": 895, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"H_L_length=-800": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 505 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": 800, +"y": 434 +}, +{ +"x": 810, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 454, +"type": "cubic" +}, +{ +"x": 895, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"H_L_length=800": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 505 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": -800, +"y": 434 +}, +{ +"x": -790, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 454, +"type": "cubic" +}, +{ +"x": 895, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_height=-50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 455 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 433, +"type": "cubic" +}, +{ +"x": 895, +"y": 455, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_height=100": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 605 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 494, +"type": "cubic" +}, +{ +"x": 895, +"y": 605, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_top_moveH=-50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 845, +"y": 505 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 939, +"y": 454, +"type": "cubic" +}, +{ +"x": 845, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_top_moveH=50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 945, +"y": 505 +}, +{ +"x": 840, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 979, +"y": 454, +"type": "cubic" +}, +{ +"x": 945, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_width=-100": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 960, +"y": 505 +}, +{ +"x": 940, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 403, +"type": "cubic" +}, +{ +"x": 1000, +"y": 415 +}, +{ +"x": 985, +"y": 454, +"type": "cubic" +}, +{ +"x": 960, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_width=-50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 927, +"y": 505 +}, +{ +"x": 890, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 972, +"y": 454, +"type": "cubic" +}, +{ +"x": 927, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"serif_width=100": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 828, +"y": 505 +}, +{ +"x": 740, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 934, +"y": 454, +"type": "cubic" +}, +{ +"x": 828, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"weight=10": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 505 +}, +{ +"x": 822, +"y": 411 +}, +{ +"x": 0, +"y": 411 +}, +{ +"x": 3, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 454, +"type": "cubic" +}, +{ +"x": 895, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"weight=50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 895, +"y": 505 +}, +{ +"x": 853, +"y": 451 +}, +{ +"x": 0, +"y": 451 +}, +{ +"x": 16, +"y": 401 +}, +{ +"x": 970, +"y": 401, +"smooth": true +}, +{ +"x": 986, +"y": 401, +"type": "cubic" +}, +{ +"x": 997, +"y": 404, +"type": "cubic" +}, +{ +"x": 1000, +"y": 416 +}, +{ +"x": 959, +"y": 454, +"type": "cubic" +}, +{ +"x": 895, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"width=100": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 95, +"y": 505 +}, +{ +"x": 40, +"y": 434 +}, +{ +"x": 0, +"y": 434 +}, +{ +"x": 10, +"y": 401 +}, +{ +"x": 170, +"y": 401, +"smooth": true +}, +{ +"x": 186, +"y": 401, +"type": "cubic" +}, +{ +"x": 197, +"y": 404, +"type": "cubic" +}, +{ +"x": 200, +"y": 416 +}, +{ +"x": 159, +"y": 454, +"type": "cubic" +}, +{ +"x": 95, +"y": 505, +"type": "cubic" +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +} +} +} diff --git a/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_01^J.json b/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_01^J.json new file mode 100644 index 000000000..3ef6f3650 --- /dev/null +++ b/resources/testdata/fontra/component.fontra/glyphs/VG_4E00_01^J.json @@ -0,0 +1,224 @@ +{ +"name": "VG_4E00_01", +"axes": [ +{ +"name": "width", +"minValue": 50, +"defaultValue": 1000, +"maxValue": 1000 +}, +{ +"name": "weight", +"minValue": 10, +"defaultValue": 10, +"maxValue": 200 +}, +{ +"name": "L_cutangle", +"minValue": -200, +"defaultValue": 0, +"maxValue": 200 +} +], +"sources": [ +{ +"name": "", +"layerName": "foreground", +"customData": { +"fontra.development.status": 4 +} +}, +{ +"name": "width=50", +"layerName": "width=50", +"location": { +"width": 50 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "weight=200", +"layerName": "weight=200", +"location": { +"weight": 200 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "L_cutangle=200", +"layerName": "L_cutangle=200", +"location": { +"L_cutangle": 200 +}, +"customData": { +"fontra.development.status": 0 +} +}, +{ +"name": "L_cutangle=-200", +"layerName": "L_cutangle=-200", +"location": { +"L_cutangle": -200 +}, +"customData": { +"fontra.development.status": 0 +} +} +], +"layers": { +"foreground": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 0, +"y": 380 +}, +{ +"x": 1000, +"y": 380 +}, +{ +"x": 1000, +"y": 390 +}, +{ +"x": 0, +"y": 390 +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"L_cutangle=-200": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": -200, +"y": 380 +}, +{ +"x": 1000, +"y": 380 +}, +{ +"x": 1000, +"y": 390 +}, +{ +"x": 0, +"y": 390 +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"L_cutangle=200": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 200, +"y": 380 +}, +{ +"x": 1000, +"y": 380 +}, +{ +"x": 1000, +"y": 390 +}, +{ +"x": 0, +"y": 390 +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"weight=200": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 0, +"y": 380 +}, +{ +"x": 1000, +"y": 380 +}, +{ +"x": 1000, +"y": 580 +}, +{ +"x": 0, +"y": 580 +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +}, +"width=50": { +"glyph": { +"path": { +"contours": [ +{ +"points": [ +{ +"x": 0, +"y": 380 +}, +{ +"x": 50, +"y": 380 +}, +{ +"x": 50, +"y": 390 +}, +{ +"x": 0, +"y": 390 +} +], +"isClosed": true +} +] +}, +"xAdvance": 1000 +} +} +} +} diff --git a/resources/testdata/fontra/component.fontra/glyphs/uni4E00^G.json b/resources/testdata/fontra/component.fontra/glyphs/uni4E00^G.json new file mode 100644 index 000000000..61b03eed6 --- /dev/null +++ b/resources/testdata/fontra/component.fontra/glyphs/uni4E00^G.json @@ -0,0 +1,64 @@ +{ +"name": "uni4E00", +"sources": [ +{ +"name": "", +"layerName": "foreground", +"customData": { +"fontra.development.status": 4 +} +}, +{ +"name": "wght=1", +"layerName": "wght=1", +"location": { +"wght": 1.0 +}, +"customData": { +"fontra.development.status": 0 +} +} +], +"layers": { +"foreground": { +"glyph": { +"components": [ +{ +"name": "VG_4E00_00", +"transformation": { +"translateX": 53 +}, +"location": { +"serif_height": 0, +"serif_width": 0, +"weight": 33, +"width": 900 +} +} +], +"xAdvance": 1000 +} +}, +"wght=1": { +"glyph": { +"components": [ +{ +"name": "VG_4E00_00", +"transformation": { +"translateX": 29, +"translateY": -15 +}, +"location": { +"serif_height": 60, +"serif_top_moveH": 6, +"serif_width": 95, +"weight": 32, +"width": 950 +} +} +], +"xAdvance": 1000 +} +} +} +}