1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
//! Command line arguments
use std::path::PathBuf;
use clap::{ArgAction, Parser};
use fontc::{DisableFlags, Input, Options};
use fontir::orchestration::Flags;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use crate::Error;
/// What font can we build for you today?
#[derive(Serialize, Deserialize, Parser, Debug, Clone, PartialEq)]
#[command(version = fontc::version())]
pub struct Args {
/// A designspace, ufo, or glyphs file
#[arg(
conflicts_with = "source",
required_unless_present("source"),
required_unless_present("verbose_version")
)]
pub input_source: Option<PathBuf>,
/// DEPRECATED: old name for positional input file
#[arg(short, long)]
source: Option<PathBuf>,
/// Output file name (default: build/font.ttf)
#[arg(short, long)]
pub output_file: Option<PathBuf>,
/// Whether to write additional debug files to disk.
#[arg(long, default_value = "false")]
pub emit_debug: bool,
/// In cases where a source glyph uses a mixture of components and contours, convert
/// all the components to contours.
#[arg(long, default_value = "true", action = ArgAction::Set)]
pub prefer_simple_glyphs: bool,
/// Flatten nested components to reference only simple (contour) glyphs.
///
/// Omit to use source default; use =true or =false to override.
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub flatten_components: Option<bool>,
/// Erase open corners (Glyphs-native feature).
///
/// Omit to use source default; use =true or =false to override.
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub erase_open_corners: Option<bool>,
/// Propagate anchors from components to composites.
///
/// Omit to use source default; use =true or =false to override.
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub propagate_anchors: Option<bool>,
/// Whether all components with a non-identity 2x2 transform will be converted to outlines.
// Named to match the ufo2ft flag as suggested in <https://github.com/googlefonts/fontc/pull/480#discussion_r1343801553>
#[arg(long, default_value = "false")]
pub decompose_transformed_components: bool,
/// Whether to decompose all components (superseding the above two flags).
#[arg(long, default_value = "false")]
pub decompose_components: bool,
/// Whether to output timing data in Chrome trace format (written to trace.json in build dir).
///
/// The generated JSON file can be viewed at https://ui.perfetto.dev/.
#[arg(long, default_value = "false")]
pub emit_timing: bool,
/// Working directory for the build process.
#[arg(short, long, default_value = "build")]
pub build_dir: PathBuf,
/// Glyph names must match this regex to be processed
#[arg(short, long, default_value = None, value_parser = ValidatedRegex::parse)]
pub glyph_name_filter: Option<ValidatedRegex>,
/// Set to skip compilation of OpenType Layout features
#[arg(long, default_value = "false")]
pub skip_features: bool,
/// Emit a Debg table with source-level lookup debug info
#[arg(long, default_value = "false")]
pub emit_lookup_debug_info: bool,
/// Whether to keep the original glyph contour direction (TTF only).
///
/// TrueType contours are recommended to follow clockwise orientation;
/// By default, we assume the source were drawn in counter-clockwise direction
/// as in PostScript outlines, and we flip the direction.
// Named to match fontmake's homonymous flag:
// https://github.com/googlefonts/fontmake/blob/6a8b2907/Lib/fontmake/__main__.py#L443
#[arg(long, default_value = "false")]
pub keep_direction: bool,
/// Don't rename glyphs with production names
// Named to match fontmake's homonymous flag:
// https://github.com/googlefonts/fontmake/blob/6a8b2907/Lib/fontmake/__main__.py#L602
#[arg(long, default_value = "false")]
pub no_production_names: bool,
/// Print verbose version information for debugging
// Includes fontc git commit, rustc host triple, rustc version and channel, llvm version,
// cargo profile, and cargo optimization level.
#[arg(long = "vv", default_value = "false")]
pub verbose_version: bool,
/// Set the log level, either globally or per module. Defaults to warn.
///
/// See <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives> for format.
#[arg(long)]
pub log: Option<String>,
}
/// A wrapper around a validated regex string
///
/// This is a wrapper because the Regex type itself does not implement PartialEq or
/// the serde traits, which we require.
#[derive(Clone, Debug)]
pub struct ValidatedRegex(Regex);
impl Args {
/// Collect various relevant flags into a [`Flags`] object.
pub fn flags(&self) -> Flags {
let mut flags = Flags::default();
flags.set(Flags::PREFER_SIMPLE_GLYPHS, self.prefer_simple_glyphs);
// Tri-state flags: Some(true) enables here; Some(false) handled by flags_to_disable()
if self.flatten_components == Some(true) {
flags.set(Flags::FLATTEN_COMPONENTS, true);
}
if self.erase_open_corners == Some(true) {
flags.set(Flags::ERASE_OPEN_CORNERS, true);
}
if self.propagate_anchors == Some(true) {
flags.set(Flags::PROPAGATE_ANCHORS, true);
}
flags.set(
Flags::DECOMPOSE_TRANSFORMED_COMPONENTS,
self.decompose_transformed_components,
);
flags.set(Flags::DECOMPOSE_COMPONENTS, self.decompose_components);
flags.set(Flags::KEEP_DIRECTION, self.keep_direction);
flags.set(Flags::PRODUCTION_NAMES, !self.no_production_names);
flags
}
/// Returns flags that should be explicitly disabled, overriding source defaults.
///
/// Tri-state flags use this logic:
/// - `None` = use source default
/// - `Some(true)` = force enable (handled in `flags()`)
/// - `Some(false)` = force disable (returned here)
pub fn flags_to_disable(&self) -> DisableFlags {
let mut disable = Flags::empty();
if self.flatten_components == Some(false) {
disable.set(Flags::FLATTEN_COMPONENTS, true);
}
if self.erase_open_corners == Some(false) {
disable.set(Flags::ERASE_OPEN_CORNERS, true);
}
if self.propagate_anchors == Some(false) {
disable.set(Flags::PROPAGATE_ANCHORS, true);
}
disable.into()
}
/// The input source to compile.
pub fn source(&self) -> Result<Input, Error> {
// safe to unwrap because clap ensures that the input_source is
// required_unless_present("source")
let path = self
.source
.as_ref()
.unwrap_or_else(|| self.input_source.as_ref().unwrap());
Input::try_from(path.as_path())
}
/// Returns the trace path if timing output is enabled.
pub fn trace_path(&self) -> Option<PathBuf> {
self.emit_timing.then(|| self.build_dir.join("trace.json"))
}
}
impl ValidatedRegex {
/// Create a new regex from a raw string.
///
/// We use a string as the error type because this is mainly
/// designed to be used with clap.
pub fn parse(s: &str) -> Result<Self, String> {
Regex::new(s).map_err(|e| e.to_string()).map(ValidatedRegex)
}
}
impl PartialEq for ValidatedRegex {
fn eq(&self, other: &Self) -> bool {
self.0.as_str() == other.0.as_str()
}
}
impl<'de> Deserialize<'de> for ValidatedRegex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Regex::new(&s)
.map_err(|e| serde::de::Error::custom(format!("invalid regex: '{e}'")))
.map(ValidatedRegex)
}
}
impl Serialize for ValidatedRegex {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.as_str().serialize(serializer)
}
}
impl TryInto<Options> for Args {
type Error = Error;
fn try_into(self) -> Result<Options, Self::Error> {
let flags = self.flags();
let flags_to_disable = self.flags_to_disable();
let debug_dir = self.emit_debug.then(|| self.build_dir.join("debug/"));
Ok(Options {
flags,
flags_to_disable,
skip_features: self.skip_features,
compile_debg: self.emit_lookup_debug_info,
output_file: self
.output_file
.or_else(|| Some(self.build_dir.join("font.ttf"))),
debug_dir,
})
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use fontir::orchestration::Flags;
use crate::args::Args;
// It's awkward to get the Flags::default values into #[arg] so test for consistency
#[test]
fn arg_default_matches_flags_default() {
let arg_default = Args::parse_from(vec!["program", "--source", "dont.care"]).flags();
let flags_default = Flags::default();
assert_eq!(
flags_default.bits(),
arg_default.bits(),
"mismatch in defaults. flags_default: {:#032b}. args_default: {:#032b}",
flags_default.bits(),
arg_default.bits(),
);
}
}