M .gitignore => .gitignore +1 -1
@@ 1,2 1,2 @@
-/target
+target
**/*.rs.bk
M Cargo.lock => Cargo.lock +18 -0
@@ 316,6 316,14 @@ dependencies = [
]
[[package]]
+name = "glyphstool"
+version = "0.1.0"
+dependencies = [
+ "kurbo 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "plist_derive 0.1.0",
+]
+
+[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 328,6 336,7 @@ name = "interp-toy"
version = "0.1.0"
dependencies = [
"druid 0.3.0 (git+https://github.com/xi-editor/druid?rev=cfbde68ca16c67b20268a5999da469ed76999b82)",
+ "glyphstool 0.1.0",
"nalgebra 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rbf-interp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ 602,6 611,15 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "plist_derive"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "proc-macro-hack"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
M Cargo.toml => Cargo.toml +2 -0
@@ 10,3 10,5 @@ edition = "2018"
druid = {git = "https://github.com/xi-editor/druid", rev = "cfbde68ca16c67b20268a5999da469ed76999b82" }
rbf-interp = "0.1.3"
nalgebra = "0.18"
+
+glyphstool = { path = "glyphstool" }
A glyphstool/Cargo.lock => glyphstool/Cargo.lock +79 -0
@@ 0,0 1,79 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "arrayvec"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "glyphstool"
+version = "0.1.0"
+dependencies = [
+ "kurbo 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "plist_derive 0.1.0",
+]
+
+[[package]]
+name = "kurbo"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "nodrop"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "plist_derive"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[metadata]
+"checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba"
+"checksum kurbo 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2f0caeb26248a62abf92dea93aad4f8244f54668e2f1060ed9cd9fd1d5545723"
+"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
+"checksum proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afdc77cc74ec70ed262262942ebb7dac3d479e9e5cfa2da1841c0806f6cdabcc"
+"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
+"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
+"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
A glyphstool/Cargo.toml => glyphstool/Cargo.toml +12 -0
@@ 0,0 1,12 @@
+[package]
+name = "glyphstool"
+version = "0.1.0"
+license = "MIT/Apache-2.0"
+authors = ["Raph Levien <raph.levien@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+kurbo = "0.5.1"
+plist_derive = { path = "plist_derive" }
A glyphstool/plist_derive/Cargo.toml => glyphstool/plist_derive/Cargo.toml +15 -0
@@ 0,0 1,15 @@
+[package]
+name = "plist_derive"
+version = "0.1.0"
+authors = ["Raph Levien <raph.levien@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = "1.0.5"
+quote = "1.0.2"
+proc-macro2 = "1.0"
A glyphstool/plist_derive/src/lib.rs => glyphstool/plist_derive/src/lib.rs +175 -0
@@ 0,0 1,175 @@
+extern crate proc_macro;
+
+use proc_macro2::TokenStream;
+use quote::{quote, quote_spanned};
+use syn::spanned::Spanned;
+use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields};
+
+#[proc_macro_derive(FromPlist, attributes(rest))]
+pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let name = input.ident;
+
+ let deser = add_deser(&input.data);
+
+ let expanded = quote! {
+ impl crate::from_plist::FromPlist for #name {
+ fn from_plist(plist: crate::plist::Plist) -> Self {
+ let mut hashmap = plist.into_hashmap();
+ #name {
+ #deser
+ }
+ }
+ }
+ };
+ proc_macro::TokenStream::from(expanded)
+}
+
+#[proc_macro_derive(ToPlist, attributes(rest))]
+pub fn derive_to(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+ let name = input.ident;
+
+ let ser_rest = add_ser_rest(&input.data);
+ let ser = add_ser(&input.data);
+
+ let expanded = quote! {
+ impl crate::to_plist::ToPlist for #name {
+ fn to_plist(self) -> crate::plist::Plist {
+ #ser_rest
+ #ser
+ hashmap.into()
+ }
+ }
+ };
+ proc_macro::TokenStream::from(expanded)
+}
+
+fn add_deser(data: &Data) -> TokenStream {
+ match *data {
+ Data::Struct(ref data) => match data.fields {
+ Fields::Named(ref fields) => {
+ let recurse = fields.named.iter().filter_map(|f| {
+ if !is_rest(&f.attrs) {
+ let name = &f.ident;
+ let name_str = name.as_ref().unwrap().to_string();
+ let snake_name = snake_to_camel_case(&name_str);
+ Some(quote_spanned! {f.span() =>
+ #name: crate::from_plist::FromPlistOpt::from_plist(
+ hashmap.remove(#snake_name)
+ ),
+ })
+ } else {
+ None
+ }
+ });
+ let recurse_rest = fields.named.iter().filter_map(|f| {
+ if is_rest(&f.attrs) {
+ let name = &f.ident;
+ Some(quote_spanned! {f.span() =>
+ #name: hashmap,
+ })
+ } else {
+ None
+ }
+ });
+ quote! {
+ #( #recurse )*
+ #( #recurse_rest )*
+ }
+ }
+ _ => unimplemented!(),
+ },
+ _ => unimplemented!(),
+ }
+}
+
+fn add_ser(data: &Data) -> TokenStream {
+ match *data {
+ Data::Struct(ref data) => match data.fields {
+ Fields::Named(ref fields) => {
+ let recurse = fields.named.iter().filter_map(|f| {
+ if !is_rest(&f.attrs) {
+ let name = &f.ident;
+ let name_str = name.as_ref().unwrap().to_string();
+ let snake_name = snake_to_camel_case(&name_str);
+ Some(quote_spanned! {f.span() =>
+ if let Some(plist) = crate::to_plist::ToPlistOpt::to_plist(self.#name) {
+ hashmap.insert(#snake_name.to_string(), plist);
+ }
+ })
+ } else {
+ None
+ }
+ });
+ quote! {
+ #( #recurse )*
+ }
+ }
+ _ => unimplemented!(),
+ },
+ _ => unimplemented!(),
+ }
+}
+
+fn add_ser_rest(data: &Data) -> TokenStream {
+ match *data {
+ Data::Struct(ref data) => match data.fields {
+ Fields::Named(ref fields) => {
+ for f in fields.named.iter() {
+ if is_rest(&f.attrs) {
+ let name = &f.ident;
+ return quote_spanned! { f.span() =>
+ let mut hashmap = self.#name;
+ }
+ }
+ }
+ quote! { let mut hashmap = HashMap::new(); }
+ }
+ _ => unimplemented!(),
+ },
+ _ => unimplemented!(),
+ }
+}
+
+fn is_rest(attrs: &[Attribute]) -> bool {
+ attrs.iter().any(|attr| {
+ attr.path
+ .get_ident()
+ .map(|ident| ident == "rest")
+ .unwrap_or(false)
+ })
+}
+
+fn snake_to_camel_case(id: &str) -> String {
+ let mut result = String::new();
+ let mut hump = false;
+ for c in id.chars() {
+ if c == '_' {
+ hump = true;
+ } else {
+ if hump {
+ result.push(c.to_ascii_uppercase());
+ } else {
+ result.push(c);
+ }
+ hump = false;
+ }
+ }
+ result
+}
+
+/*
+fn to_snake_case(id: &str) -> String {
+ let mut result = String::new();
+ for c in id.chars() {
+ if c.is_ascii_uppercase() {
+ result.push('_');
+ result.push(c.to_ascii_lowercase());
+ } else {
+ result.push(c);
+ }
+ }
+ result
+}
+*/
A glyphstool/src/font.rs => glyphstool/src/font.rs +164 -0
@@ 0,0 1,164 @@
+//! The general strategy is just to use a plist for storage. Also, lots of
+//! unwrapping.
+//!
+//! There are lots of other ways this could go, including something serde-like
+//! where it gets serialized to more Rust-native structures, proc macros, etc.
+
+use std::collections::HashMap;
+
+use kurbo::{Affine, Point};
+
+use crate::from_plist::FromPlist;
+use crate::plist::Plist;
+use crate::to_plist::ToPlist;
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct Font {
+ pub glyphs: Vec<Glyph>,
+ #[rest]
+ pub other_stuff: HashMap<String, Plist>,
+}
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct Glyph {
+ pub layers: Vec<Layer>,
+ pub glyphname: String,
+ #[rest]
+ pub other_stuff: HashMap<String, Plist>,
+}
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct Layer {
+ pub layer_id: String,
+ pub width: f64,
+ pub paths: Option<Vec<Path>>,
+ pub components: Option<Vec<Component>>,
+ pub anchors: Option<Vec<Anchor>>,
+ pub guide_lines: Option<Vec<GuideLine>>,
+ #[rest]
+ pub other_stuff: HashMap<String, Plist>,
+}
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct Path {
+ pub closed: bool,
+ pub nodes: Vec<Node>,
+}
+
+#[derive(Debug)]
+pub struct Node {
+ pub pt: Point,
+ pub node_type: NodeType,
+}
+
+#[derive(Debug)]
+pub enum NodeType {
+ Line,
+ OffCurve,
+ Curve,
+ CurveSmooth,
+}
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct Component {
+ pub name: String,
+ pub transform: Option<Affine>,
+ #[rest]
+ pub other_stuff: HashMap<String, Plist>,
+}
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct Anchor {
+ pub name: String,
+ pub position: Point,
+}
+
+#[derive(Debug, FromPlist, ToPlist)]
+pub struct GuideLine {
+ pub angle: Option<f64>,
+ pub position: Point,
+}
+
+impl FromPlist for Node {
+ fn from_plist(plist: Plist) -> Self {
+ let mut spl = plist.as_str().unwrap().split(' ');
+ let x = spl.next().unwrap().parse().unwrap();
+ let y = spl.next().unwrap().parse().unwrap();
+ let pt = Point::new(x, y);
+ let node_type = spl.next().unwrap().parse().unwrap();
+ Node { pt, node_type }
+ }
+}
+
+impl std::str::FromStr for NodeType {
+ type Err = String;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "LINE" => Ok(NodeType::Line),
+ "OFFCURVE" => Ok(NodeType::OffCurve),
+ "CURVE" => Ok(NodeType::Curve),
+ "CURVE SMOOTH" => Ok(NodeType::CurveSmooth),
+ _ => Err(format!("unknown node type {}", s)),
+ }
+ }
+}
+
+impl NodeType {
+ fn glyphs_str(&self) -> &'static str {
+ match self {
+ NodeType::Line => "LINE",
+ NodeType::OffCurve => "OFFCURVE",
+ NodeType::Curve => "CURVE",
+ NodeType::CurveSmooth => "CURVE SMOOTH",
+ }
+ }
+}
+
+impl ToPlist for Node {
+ fn to_plist(self) -> Plist {
+ format!(
+ "{} {} {}",
+ self.pt.x,
+ self.pt.y,
+ self.node_type.glyphs_str()
+ )
+ .into()
+ }
+}
+
+impl FromPlist for Affine {
+ fn from_plist(plist: Plist) -> Self {
+ let raw = plist.as_str().unwrap();
+ let raw = &raw[1..raw.len() - 1];
+ let coords: Vec<f64> = raw.split(", ").map(|c| c.parse().unwrap()).collect();
+ Affine::new([
+ coords[0], coords[1], coords[2], coords[3], coords[4], coords[5],
+ ])
+ }
+}
+
+impl ToPlist for Affine {
+ fn to_plist(self) -> Plist {
+ let c = self.as_coeffs();
+ format!(
+ "{{{}, {}, {}, {}, {}, {}}}",
+ c[0], c[1], c[2], c[3], c[4], c[5]
+ )
+ .into()
+ }
+}
+
+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<f64> = raw.split(", ").map(|c| c.parse().unwrap()).collect();
+ Point::new(coords[0], coords[1])
+ }
+}
+
+impl ToPlist for Point {
+ fn to_plist(self) -> Plist {
+ format!("{{{}, {}}}", self.x, self.y).into()
+ }
+}
A glyphstool/src/from_plist.rs => glyphstool/src/from_plist.rs +60 -0
@@ 0,0 1,60 @@
+pub use plist_derive::FromPlist;
+
+use crate::plist::Plist;
+
+pub trait FromPlist {
+ // Consider using result type; just unwrap for now.
+ fn from_plist(plist: Plist) -> Self;
+}
+
+pub trait FromPlistOpt {
+ // Consider using result type; just unwrap for now.
+ fn from_plist(plist: Option<Plist>) -> Self;
+}
+
+impl FromPlist for String {
+ fn from_plist(plist: Plist) -> Self {
+ plist.into_string()
+ }
+}
+
+impl FromPlist for bool {
+ fn from_plist(plist: Plist) -> Self {
+ // TODO: maybe error or warn on values other than 0, 1
+ plist.as_i64().expect("expected integer") != 0
+ }
+}
+
+impl FromPlist for i64 {
+ fn from_plist(plist: Plist) -> Self {
+ plist.as_i64().expect("expected integer")
+ }
+}
+
+impl FromPlist for f64 {
+ fn from_plist(plist: Plist) -> Self {
+ plist.as_f64().expect("expected float")
+ }
+}
+
+impl<T: FromPlist> FromPlist for Vec<T> {
+ fn from_plist(plist: Plist) -> Self {
+ let mut result = Vec::new();
+ for element in plist.into_vec() {
+ result.push(FromPlist::from_plist(element));
+ }
+ result
+ }
+}
+
+impl<T: FromPlist> FromPlistOpt for T {
+ fn from_plist(plist: Option<Plist>) -> Self {
+ FromPlist::from_plist(plist.unwrap())
+ }
+}
+
+impl<T: FromPlist> FromPlistOpt for Option<T> {
+ fn from_plist(plist: Option<Plist>) -> Self {
+ plist.map(FromPlist::from_plist)
+ }
+}
A glyphstool/src/lib.rs => glyphstool/src/lib.rs +13 -0
@@ 0,0 1,13 @@
+//! Lightweight library for reading and writing Glyphs font files.
+
+mod font;
+mod from_plist;
+mod plist;
+mod stretch;
+mod to_plist;
+
+pub use font::Font;
+pub use from_plist::FromPlist;
+pub use plist::Plist;
+pub use stretch::stretch;
+pub use to_plist::ToPlist;
A glyphstool/src/main.rs => glyphstool/src/main.rs +39 -0
@@ 0,0 1,39 @@
+use std::env;
+use std::fs;
+
+use glyphstool::{stretch, Font, FromPlist, Plist, ToPlist};
+
+fn usage() {
+ eprintln!("usage: glyphstool font.glyphs");
+}
+
+fn main() {
+ let mut filename = None;
+ for arg in env::args().skip(1) {
+ if filename.is_none() {
+ filename = Some(arg);
+ }
+ }
+ if filename.is_none() {
+ usage();
+ return;
+ }
+ let filename = filename.unwrap();
+ let contents = fs::read_to_string(filename).expect("error reading font");
+ let plist = Plist::parse(&contents).expect("parse error");
+ //println!("Plist: {:?}", plist);
+ /*
+ let font = Font::from_plist(plist);
+ for glyph in font.glyphs() {
+ println!("glyphname: {}", glyph.glyphname());
+ for layer in glyph.layers() {
+ println!(" layer: {}, width = {}", layer.layer_id(), layer.width());
+ }
+ }
+ */
+ let mut font: Font = FromPlist::from_plist(plist);
+ //println!("{:?}", font);
+ stretch::stretch(&mut font, 0.5, "051EFAE4-8BBE-4FBB-A016-4335C3E52F59");
+ let plist = font.to_plist();
+ println!("{}", plist.to_string());
+}
A glyphstool/src/plist.rs => glyphstool/src/plist.rs +403 -0
@@ 0,0 1,403 @@
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+/// An enum representing a property list.
+#[derive(Clone, Debug)]
+pub enum Plist {
+ Dictionary(HashMap<String, Plist>),
+ Array(Vec<Plist>),
+ String(String),
+ Integer(i64),
+ Float(f64),
+}
+
+#[derive(Debug)]
+pub enum Error {
+ UnexpectedChar(char),
+ UnclosedString,
+ UnknownEscape,
+ NotAString,
+ ExpectedEquals,
+ ExpectedComma,
+ ExpectedSemicolon,
+ SomethingWentWrong,
+}
+
+enum Token<'a> {
+ Eof,
+ OpenBrace,
+ OpenParen,
+ String(Cow<'a, str>),
+ Atom(&'a str),
+}
+
+fn is_numeric(b: u8) -> bool {
+ (b >= b'0' && b <= b'9') || b == b'.' || b == b'-'
+}
+
+fn is_alnum(b: u8) -> bool {
+ is_numeric(b) || (b >= b'A' && b <= b'Z') || (b >= b'a' && b <= b'z') || b == b'_'
+}
+
+// Used for serialization; make sure UUID's get quoted
+fn is_alnum_strict(b: u8) -> bool {
+ is_alnum(b) && b != b'-'
+}
+
+fn is_ascii_whitespace(b: u8) -> bool {
+ b == b' ' || b == b'\t' || b == b'\r' || b == b'\n'
+}
+
+fn numeric_ok(s: &str) -> bool {
+ if s.is_empty() {
+ return false;
+ }
+ if s.len() > 1 && s.as_bytes()[0] == b'0' {
+ return !s.as_bytes().iter().all(|&b| b >= b'0' && b <= b'9');
+ }
+ true
+}
+
+fn skip_ws(s: &str, mut ix: usize) -> usize {
+ while ix < s.len() && is_ascii_whitespace(s.as_bytes()[ix]) {
+ ix += 1;
+ }
+ ix
+}
+
+fn escape_string(buf: &mut String, s: &str) {
+ if !s.is_empty() && s.as_bytes().iter().all(|&b| is_alnum_strict(b)) {
+ buf.push_str(s);
+ } else {
+ buf.push('"');
+ let mut start = 0;
+ let mut ix = start;
+ while ix < s.len() {
+ let b = s.as_bytes()[ix];
+ match b {
+ b'"' | b'\\' => {
+ buf.push_str(&s[start..ix]);
+ buf.push('\\');
+ start = ix;
+ }
+ _ => (),
+ }
+ ix += 1;
+ }
+ buf.push_str(&s[start..]);
+ buf.push('"');
+ }
+}
+
+impl Plist {
+ pub fn parse(s: &str) -> Result<Plist, Error> {
+ let (plist, _ix) = Plist::parse_rec(s, 0)?;
+ // TODO: check that we're actually at eof
+ Ok(plist)
+ }
+
+ #[allow(unused)]
+ pub fn as_dict(&self) -> Option<&HashMap<String, Plist>> {
+ match self {
+ Plist::Dictionary(d) => Some(d),
+ _ => None,
+ }
+ }
+
+ #[allow(unused)]
+ pub fn get(&self, key: &str) -> Option<&Plist> {
+ match self {
+ Plist::Dictionary(d) => d.get(key),
+ _ => None,
+ }
+ }
+
+ #[allow(unused)]
+ pub fn as_array(&self) -> Option<&[Plist]> {
+ match self {
+ Plist::Array(a) => Some(a),
+ _ => None,
+ }
+ }
+
+ #[allow(unused)]
+ pub fn as_str(&self) -> Option<&str> {
+ match self {
+ Plist::String(s) => Some(s),
+ _ => None,
+ }
+ }
+
+ pub fn as_i64(&self) -> Option<i64> {
+ match self {
+ Plist::Integer(i) => Some(*i),
+ _ => None,
+ }
+ }
+
+ pub fn as_f64(&self) -> Option<f64> {
+ match self {
+ Plist::Integer(i) => Some(*i as f64),
+ Plist::Float(f) => Some(*f),
+ _ => None,
+ }
+ }
+
+ pub fn into_string(self) -> String {
+ match self {
+ Plist::String(s) => s,
+ _ => panic!("expected string"),
+ }
+ }
+
+ pub fn into_vec(self) -> Vec<Plist> {
+ match self {
+ Plist::Array(a) => a,
+ _ => panic!("expected array"),
+ }
+ }
+
+ pub fn into_hashmap(self) -> HashMap<String, Plist> {
+ match self {
+ Plist::Dictionary(d) => d,
+ _ => panic!("expected dictionary"),
+ }
+ }
+
+ fn parse_rec(s: &str, ix: usize) -> Result<(Plist, usize), Error> {
+ let (tok, mut ix) = Token::lex(s, ix)?;
+ match tok {
+ Token::Atom(s) => Ok((Plist::parse_atom(s), ix)),
+ Token::String(s) => Ok((Plist::String(s.into()), ix)),
+ Token::OpenBrace => {
+ let mut dict = HashMap::new();
+ loop {
+ if let Some(ix) = Token::expect(s, ix, b'}') {
+ return Ok((Plist::Dictionary(dict), ix));
+ }
+ let (key, next) = Token::lex(s, ix)?;
+ let key_str = Token::try_into_string(key)?;
+ let next = Token::expect(s, next, b'=');
+ if next.is_none() {
+ return Err(Error::ExpectedEquals);
+ }
+ let (val, next) = Self::parse_rec(s, next.unwrap())?;
+ dict.insert(key_str, val);
+ if let Some(next) = Token::expect(s, next, b';') {
+ ix = next;
+ } else {
+ return Err(Error::ExpectedSemicolon);
+ }
+ }
+ }
+ Token::OpenParen => {
+ let mut list = Vec::new();
+ if let Some(ix) = Token::expect(s, ix, b')') {
+ return Ok((Plist::Array(list), ix));
+ }
+ loop {
+ let (val, next) = Self::parse_rec(s, ix)?;
+ list.push(val);
+ if let Some(ix) = Token::expect(s, next, b')') {
+ return Ok((Plist::Array(list), ix));
+ }
+ if let Some(next) = Token::expect(s, next, b',') {
+ ix = next;
+ } else {
+ return Err(Error::ExpectedComma);
+ }
+ }
+ }
+ _ => Err(Error::SomethingWentWrong),
+ }
+ }
+
+ fn parse_atom(s: &str) -> Plist {
+ if numeric_ok(s) {
+ if let Ok(num) = s.parse() {
+ return Plist::Integer(num);
+ }
+ if let Ok(num) = s.parse() {
+ return Plist::Float(num);
+ }
+ }
+ Plist::String(s.into())
+ }
+
+ pub fn to_string(&self) -> String {
+ let mut s = String::new();
+ self.push_to_string(&mut s);
+ s
+ }
+
+ fn push_to_string(&self, s: &mut String) {
+ match self {
+ Plist::Array(a) => {
+ s.push_str("(");
+ let mut delim = "\n";
+ for el in a {
+ s.push_str(delim);
+ el.push_to_string(s);
+ delim = ",\n";
+ }
+ s.push_str("\n)");
+ }
+ Plist::Dictionary(a) => {
+ s.push_str("{\n");
+ // TODO: probably want to sort keys
+ for (k, el) in a {
+ // TODO: quote if needed?
+ escape_string(s, k);
+ s.push_str(" = ");
+ el.push_to_string(s);
+ s.push_str(";\n");
+ }
+ s.push_str("}");
+ }
+ Plist::String(st) => escape_string(s, st),
+ Plist::Integer(i) => {
+ s.push_str(&format!("{}", i));
+ }
+ Plist::Float(f) => {
+ s.push_str(&format!("{}", f));
+ }
+ }
+ }
+}
+
+impl<'a> Token<'a> {
+ fn lex(s: &'a str, ix: usize) -> Result<(Token<'a>, usize), Error> {
+ let start = skip_ws(s, ix);
+ if start == s.len() {
+ return Ok((Token::Eof, start));
+ }
+ let b = s.as_bytes()[start];
+ match b {
+ b'{' => Ok((Token::OpenBrace, start + 1)),
+ b'(' => Ok((Token::OpenParen, start + 1)),
+ b'"' => {
+ let mut ix = start + 1;
+ let mut cow_start = ix;
+ let mut buf = String::new();
+ while ix < s.len() {
+ let b = s.as_bytes()[ix];
+ match b {
+ b'"' => {
+ // End of string
+ let string = if buf.is_empty() {
+ s[cow_start..ix].into()
+ } else {
+ buf.push_str(&s[cow_start..ix]);
+ buf.into()
+ };
+ return Ok((Token::String(string), ix + 1));
+ }
+ b'\\' => {
+ buf.push_str(&s[cow_start..ix]);
+ ix += 1;
+ if ix == s.len() {
+ return Err(Error::UnclosedString);
+ }
+ let b = s.as_bytes()[ix];
+ match b {
+ b'"' | b'\\' => cow_start = ix,
+ b'n' => {
+ buf.push('\n');
+ cow_start = ix + 1;
+ }
+ b'r' => {
+ buf.push('\r');
+ cow_start = ix + 1;
+ }
+ _ => {
+ if b >= b'0' && b <= b'3' && ix + 2 < s.len() {
+ // octal escape
+ let b1 = s.as_bytes()[ix + 1];
+ let b2 = s.as_bytes()[ix + 2];
+ if b1 >= b'0' && b1 <= b'7' && b2 >= b'0' && b2 <= b'7' {
+ let oct =
+ (b - b'0') * 64 + (b1 - b'0') * 8 + (b2 - b'0');
+ buf.push(oct as char);
+ ix += 2;
+ cow_start = ix + 1;
+ } else {
+ return Err(Error::UnknownEscape);
+ }
+ } else {
+ return Err(Error::UnknownEscape);
+ }
+ }
+ }
+ ix += 1;
+ }
+ _ => ix += 1,
+ }
+ }
+ Err(Error::UnclosedString)
+ }
+ _ => {
+ if is_alnum(b) {
+ let mut ix = start + 1;
+ while ix < s.len() {
+ if !is_alnum(s.as_bytes()[ix]) {
+ break;
+ }
+ ix += 1;
+ }
+ Ok((Token::Atom(&s[start..ix]), ix))
+ } else {
+ Err(Error::UnexpectedChar(s[start..].chars().next().unwrap()))
+ }
+ }
+ }
+ }
+
+ fn try_into_string(self) -> Result<String, Error> {
+ match self {
+ Token::Atom(s) => Ok(s.into()),
+ Token::String(s) => Ok(s.into()),
+ _ => Err(Error::NotAString),
+ }
+ }
+
+ fn expect(s: &str, ix: usize, delim: u8) -> Option<usize> {
+ let ix = skip_ws(s, ix);
+ if ix < s.len() {
+ let b = s.as_bytes()[ix];
+ if b == delim {
+ return Some(ix + 1);
+ }
+ }
+ None
+ }
+}
+
+impl From<String> for Plist {
+ fn from(x: String) -> Plist {
+ Plist::String(x)
+ }
+}
+
+impl From<i64> for Plist {
+ fn from(x: i64) -> Plist {
+ Plist::Integer(x)
+ }
+}
+
+impl From<f64> for Plist {
+ fn from(x: f64) -> Plist {
+ Plist::Float(x)
+ }
+}
+
+impl From<Vec<Plist>> for Plist {
+ fn from(x: Vec<Plist>) -> Plist {
+ Plist::Array(x)
+ }
+}
+
+impl From<HashMap<String, Plist>> for Plist {
+ fn from(x: HashMap<String, Plist>) -> Plist {
+ Plist::Dictionary(x)
+ }
+}
A glyphstool/src/stretch.rs => glyphstool/src/stretch.rs +54 -0
@@ 0,0 1,54 @@
+//! A little logic to apply horizontal stretching to a font.
+
+use kurbo::Affine;
+
+use crate::font::{Font, Glyph, Layer};
+
+fn affine_stretch(stretch: f64) -> Affine {
+ Affine::new([stretch, 0., 0., 1., 0., 0.])
+}
+
+fn stretch_layer(layer: &mut Layer, stretch: f64) {
+ let a = affine_stretch(stretch);
+ let a_inv = affine_stretch(stretch.recip());
+ layer.width = (layer.width * stretch).round();
+ if let Some(ref mut paths) = layer.paths {
+ for path in paths {
+ for node in &mut path.nodes {
+ node.pt = (a * node.pt).round();
+ }
+ }
+ }
+ if let Some(ref mut anchors) = layer.anchors {
+ for anchor in anchors {
+ anchor.position = (a * anchor.position).round();
+ }
+ }
+ if let Some(ref mut guide_lines) = layer.guide_lines {
+ for guide_line in guide_lines {
+ guide_line.position = (a * guide_line.position).round();
+ }
+ }
+ if let Some(ref mut components) = layer.components {
+ for component in components {
+ if let Some(ref mut transform) = component.transform {
+ // TODO: round the translation component
+ *transform = a * *transform * a_inv;
+ }
+ }
+ }
+}
+
+fn stretch_glyph(glyph: &mut Glyph, stretch: f64, layer_id: &str) {
+ for layer in &mut glyph.layers {
+ if layer.layer_id == layer_id {
+ stretch_layer(layer, stretch);
+ }
+ }
+}
+
+pub fn stretch(font: &mut Font, stretch: f64, layer_id: &str) {
+ for glyph in &mut font.glyphs {
+ stretch_glyph(glyph, stretch, layer_id);
+ }
+}
A glyphstool/src/to_plist.rs => glyphstool/src/to_plist.rs +57 -0
@@ 0,0 1,57 @@
+pub use plist_derive::ToPlist;
+
+use crate::plist::Plist;
+
+pub trait ToPlist {
+ fn to_plist(self) -> Plist;
+}
+
+pub trait ToPlistOpt {
+ fn to_plist(self) -> Option<Plist>;
+}
+
+impl ToPlist for String {
+ fn to_plist(self) -> Plist {
+ self.into()
+ }
+}
+
+impl ToPlist for bool {
+ fn to_plist(self) -> Plist {
+ (self as i64).into()
+ }
+}
+
+impl ToPlist for i64 {
+ fn to_plist(self) -> Plist {
+ self.into()
+ }
+}
+
+impl ToPlist for f64 {
+ fn to_plist(self) -> Plist {
+ self.into()
+ }
+}
+
+impl<T: ToPlist> ToPlist for Vec<T> {
+ fn to_plist(self) -> Plist {
+ let mut result = Vec::new();
+ for element in self {
+ result.push(ToPlist::to_plist(element));
+ }
+ result.into()
+ }
+}
+
+impl<T: ToPlist> ToPlistOpt for T {
+ fn to_plist(self) -> Option<Plist> {
+ Some(ToPlist::to_plist(self))
+ }
+}
+
+impl<T: ToPlist> ToPlistOpt for Option<T> {
+ fn to_plist(self) -> Option<Plist> {
+ self.map(ToPlist::to_plist)
+ }
+}