Skip to content

Commit b566bad

Browse files
Initial release
– implement packing – improve code – improve cli
1 parent 0511656 commit b566bad

File tree

6 files changed

+338
-101
lines changed

6 files changed

+338
-101
lines changed

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
[package]
22
name = "glyphspack"
3-
version = "0.1.0"
3+
version = "1.0.0"
44
authors = ["Florian Pircher <florian@addpixel.net>"]
55
edition = "2018"
66
description = "Convert between .glyphs and .glyphspackage files."
77
license = "MIT OR Apache-2.0"
88

99
[dependencies]
1010
anyhow = "1.0"
11-
clap = "2.33.3"
1211
pest = "2.1.3"
1312
pest_derive = "2.1.0"
1413
rayon = "1.5.1"
14+
regex = "1.5.4"
15+
16+
[dependencies.clap]
17+
version = "2.33.3"
18+
default-features = false
19+
features = [ "suggestions", "wrap_help" ]

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
# glyphspack
22

3-
Convert between .glyphs and .glyphspackage files.
3+
`glyphspack` converts between the `.glyphs` and `.glyphspackage` file format flavors of the [Glyphs font editor](https://glyphsapp.com).
4+
5+
In Glyphs, save a file to a different format with _File_ → _Save As…_ → _File Format_.
6+
7+
## Usage
8+
9+
```sh
10+
$ glyphspack SomeFont.glyphspackage
11+
Unpacking SomeFont.glyphspackage into SomeFont.glyphs.
12+
$ glyphspack OtherFont.glyphs
13+
Packing OtherFont.glyphs into OtherFont.glyphspackage.
14+
```
15+
16+
Options:
17+
18+
- Set the output file name with `-o`/`--out`.
19+
- Overwrite any existing files with `-f`/`--force`.
20+
- Suppress log messages with `-q`/`--quiet`.
21+
- Print the path of the exported file to stdout with `-p`/`--print-path`.
22+
23+
Run with `--help` for a complete parameter description.
424

525
## License
626

src/main.rs

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use anyhow::{bail, Result};
1+
use anyhow::{bail, Context, Result};
22
use clap::{App, Arg};
3+
use std::io::Write;
4+
use std::os::unix::prelude::OsStrExt;
35
use std::path::Path;
46

57
#[macro_use]
@@ -9,45 +11,80 @@ mod pack;
911
mod plist;
1012
mod unpack;
1113

14+
const ARG_KEY_OUTFILE: &str = "OUT";
15+
const ARG_KEY_FILE: &str = "IN";
16+
const ARG_KEY_FORCE: &str = "FORCE";
17+
const ARG_KEY_QUIET: &str = "QUIET";
18+
const ARG_KEY_PRINT_PATH: &str = "PRINTPATH";
19+
20+
const FILE_EXT_STANDALONE: &str = "glyphs";
21+
const FILE_EXT_PACKAGE: &str = "glyphspackage";
22+
const FILE_EXT_GLYPH: &str = "glyph";
23+
24+
const FILE_PACKAGE_FONTINFO: &str = "fontinfo.plist";
25+
const FILE_PACKAGE_ORDER: &str = "order.plist";
26+
const FILE_PACKAGE_UI_STATE: &str = "UIState.plist";
27+
const FILE_PACKAGE_GLYPHS: &str = "glyphs";
28+
29+
const KEY_DISPLAY_STRINGS_PACKAGE: &str = "displayStrings";
30+
const KEY_DISPLAY_STRINGS_STANDALONE: &str = "DisplayStrings";
31+
const KEY_GLYPH_NAME: &str = "glyphname";
32+
const KEY_GLYPHS: &str = "glyphs";
33+
1234
enum Operation {
1335
Pack,
1436
Unpack,
1537
}
1638

1739
fn main() -> Result<()> {
1840
let config = App::new("glyphspack")
19-
.version("0.2")
41+
.version("1.0")
2042
.author("Florian Pircher <florian@addpixel.net>")
21-
.about("Convert between .glyphs and .glyphspackage files.")
43+
.about("Convert between .glyphs and .glyphspackage files. The conversion direction is automatically detected depending on whether <FILE> is a directory or not.")
44+
.after_help("See the Glyphs Handbook <https://glyphsapp.com/learn> for details on the standalone and the package format flavors.")
2245
.arg(
23-
Arg::with_name("OUT")
46+
Arg::with_name(ARG_KEY_OUTFILE)
2447
.short("o")
2548
.long("out")
2649
.help("The output file")
2750
.value_name("OUTFILE")
2851
.takes_value(true),
2952
)
3053
.arg(
31-
Arg::with_name("FORCE")
54+
Arg::with_name(ARG_KEY_FORCE)
3255
.short("f")
3356
.long("force")
34-
.help("Overwrite output file if it already exists"),
57+
.help("Overwrites output file if it already exists"),
58+
)
59+
.arg(
60+
Arg::with_name(ARG_KEY_QUIET)
61+
.short("q")
62+
.long("quiet")
63+
.help("Suppresses log messages"),
64+
)
65+
.arg(
66+
Arg::with_name(ARG_KEY_PRINT_PATH)
67+
.short("p")
68+
.long("print-path")
69+
.help("Prints the path of the exported file to stdout"),
3570
)
3671
.arg(
37-
Arg::with_name("IN")
72+
Arg::with_name(ARG_KEY_FILE)
3873
.help("The input file")
3974
.value_name("FILE")
4075
.required(true)
4176
.index(1),
4277
)
4378
.get_matches();
44-
let force = config.is_present("FORCE");
45-
let out_file = config.value_of("OUT");
46-
let in_file = config.value_of("IN").unwrap();
79+
let force = config.is_present(ARG_KEY_FORCE);
80+
let quiet = config.is_present(ARG_KEY_QUIET);
81+
let print_path = config.is_present(ARG_KEY_PRINT_PATH);
82+
let out_file = config.value_of(ARG_KEY_OUTFILE);
83+
let in_file = config.value_of(ARG_KEY_FILE).unwrap();
4784
let in_path = Path::new(in_file);
4885

4986
if !in_path.exists() {
50-
bail!("FILE does not exist {}", in_path.display());
87+
bail!("<FILE> does not exist: {}", in_path.display());
5188
}
5289

5390
let operation = if in_path.is_dir() {
@@ -59,17 +96,37 @@ fn main() -> Result<()> {
5996
let out_path = match out_file {
6097
Some(file) => Path::new(file).to_owned(),
6198
None => match operation {
62-
Operation::Pack => in_path.with_extension("glyphspackage"),
63-
Operation::Unpack => in_path.with_extension("glyphs"),
99+
Operation::Pack => in_path.with_extension(FILE_EXT_PACKAGE),
100+
Operation::Unpack => in_path.with_extension(FILE_EXT_STANDALONE),
64101
},
65102
};
66103

67104
if !force && out_path.exists() {
68-
bail!("OUTFILE already exists {}", out_path.display());
105+
bail!("<OUTFILE> already exists: {}", out_path.display());
106+
}
107+
108+
if print_path {
109+
std::io::stdout()
110+
.write_all(out_path.as_os_str().as_bytes())
111+
.with_context(|| format!("cannot write path of OUTFILE: {}", out_path.display()))?;
69112
}
70113

71114
match operation {
72-
Operation::Pack => pack::pack(in_path, &out_path),
73-
Operation::Unpack => unpack::unpack(in_path, &out_path),
115+
Operation::Pack => {
116+
if !quiet {
117+
eprintln!("Packing {} into {}.", in_path.display(), out_path.display());
118+
}
119+
pack::pack(in_path, &out_path, force)
120+
}
121+
Operation::Unpack => {
122+
if !quiet {
123+
eprintln!(
124+
"Unpacking {} into {}.",
125+
in_path.display(),
126+
out_path.display()
127+
);
128+
}
129+
unpack::unpack(in_path, &out_path)
130+
}
74131
}
75132
}

src/pack.rs

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,137 @@
1-
use anyhow::Result;
1+
use anyhow::{bail, Context, Result};
2+
use rayon::prelude::*;
3+
use regex::{Captures, Regex};
4+
use std::fs;
25
use std::path::Path;
36

4-
pub fn pack(in_path: &Path, out_path: &Path) -> Result<()> {
5-
eprintln!("Packing {} into {}.", in_path.display(), out_path.display());
7+
use crate::{
8+
plist, FILE_EXT_GLYPH, FILE_PACKAGE_FONTINFO, FILE_PACKAGE_GLYPHS, FILE_PACKAGE_ORDER,
9+
FILE_PACKAGE_UI_STATE, KEY_DISPLAY_STRINGS_PACKAGE, KEY_DISPLAY_STRINGS_STANDALONE, KEY_GLYPHS,
10+
KEY_GLYPH_NAME,
11+
};
12+
13+
pub fn pack(in_path: &Path, out_path: &Path, force: bool) -> Result<()> {
14+
// Read Standalong File
15+
16+
let standalone_code = fs::read_to_string(&in_path)
17+
.with_context(|| format!("cannot read font info at {}", in_path.display()))?;
18+
let standalone = plist::parse(plist::Root::Dict, &standalone_code)
19+
.with_context(|| format!("cannot parse font info at {}", in_path.display()))?;
20+
let standalone = match standalone.value {
21+
plist::Value::Dict(x) => x,
22+
_ => unreachable!(),
23+
};
24+
25+
let mut fontinfo: Vec<&str> = Vec::new();
26+
let mut order: Vec<&str> = Vec::new();
27+
let mut ui_state: Vec<String> = Vec::new();
28+
let mut glyphs: Vec<(&str, &str)> = Vec::new();
29+
30+
for (key, slice, code) in standalone {
31+
match key {
32+
KEY_DISPLAY_STRINGS_STANDALONE => {
33+
let code = format!("{} = (\n{}\n);", KEY_DISPLAY_STRINGS_PACKAGE, slice.code);
34+
ui_state.push(code);
35+
}
36+
KEY_GLYPHS => {
37+
let glyph_slices = match slice.value {
38+
plist::Value::Array(items) => items,
39+
_ => bail!("non-array `{}` in {}", KEY_GLYPHS, in_path.display()),
40+
};
41+
for glyph_slice in glyph_slices {
42+
let glyph = match glyph_slice.value {
43+
plist::Value::Dict(pairs) => pairs,
44+
_ => bail!("non-dict glyph in {}", in_path.display()),
45+
};
46+
let mut glyph_name: Option<&str> = None;
47+
for (key, slice, _) in glyph {
48+
if key == KEY_GLYPH_NAME {
49+
match slice.value {
50+
plist::Value::String(name) => glyph_name = Some(name),
51+
_ => bail!(
52+
"non-string `{}` in {}",
53+
KEY_GLYPH_NAME,
54+
in_path.display()
55+
),
56+
};
57+
}
58+
}
59+
let glyph_name = glyph_name.with_context(|| {
60+
format!(
61+
"missing `{}` in glyph in {}",
62+
KEY_GLYPH_NAME,
63+
in_path.display()
64+
)
65+
})?;
66+
67+
order.push(glyph_name);
68+
glyphs.push((glyph_name, glyph_slice.code));
69+
}
70+
}
71+
_ => {
72+
fontinfo.push(code);
73+
}
74+
}
75+
}
76+
77+
// Create Directories
78+
79+
if force && out_path.is_dir() {
80+
fs::remove_dir_all(&out_path).with_context(|| {
81+
format!(
82+
"cannot overwrite existing directory at {}",
83+
out_path.display()
84+
)
85+
})?;
86+
}
87+
88+
fs::create_dir_all(&out_path)
89+
.with_context(|| format!("cannot create package at {}", out_path.display()))?;
90+
91+
let glyphs_path = out_path.join(FILE_PACKAGE_GLYPHS);
92+
fs::create_dir(&glyphs_path).with_context(|| {
93+
format!(
94+
"cannot create glyphs diractory at {}",
95+
glyphs_path.display()
96+
)
97+
})?;
98+
99+
// Write Font Info
100+
101+
let fontinfo_path = out_path.join(FILE_PACKAGE_FONTINFO);
102+
plist::write_dict_file(&fontinfo_path, &fontinfo)?;
103+
104+
// Write Order
105+
106+
let order_path = out_path.join(FILE_PACKAGE_ORDER);
107+
plist::write_array_file(&order_path, &order)?;
108+
109+
// Write UI State
110+
111+
if !ui_state.is_empty() {
112+
let ui_state_path = out_path.join(FILE_PACKAGE_UI_STATE);
113+
plist::write_dict_file(&ui_state_path, &ui_state.iter().map(|x| &**x).collect())?;
114+
}
115+
116+
// Write Glyphs
117+
118+
let init_dot_regex = Regex::new(r"^\.").unwrap();
119+
let capital_regex = Regex::new(r"[A-Z]").unwrap();
120+
121+
glyphs.into_par_iter().try_for_each(|(glyphname, code)| {
122+
let file_stem = glyphname;
123+
let file_stem = init_dot_regex.replacen(file_stem, 1, "_");
124+
let file_stem = capital_regex.replace_all(file_stem.as_ref(), |captures: &Captures| {
125+
format!(
126+
"{}_",
127+
captures.get(0).unwrap().as_str().to_ascii_uppercase()
128+
)
129+
});
130+
131+
let glyph_path = glyphs_path.join(format!("{}.{}", file_stem, FILE_EXT_GLYPH));
132+
plist::write_dict_file(&glyph_path, &vec![code])?;
133+
Ok::<(), anyhow::Error>(())
134+
})?;
135+
6136
Ok(())
7137
}

0 commit comments

Comments
 (0)