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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
use std::collections::{HashMap, HashSet};
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use crate::input::{
GenerationOptions, LoadedSvgSource, ResolvedGenerationOptions, ResolvedVariants,
VariantMetadata, resolve_variant_metadata, validate_format_options, validate_glyph_names,
validate_variants,
};
use crate::{ColorGlyphSelection, FontType, FormatOptions, MissingGlyphOptions};
/// Named SVG text. `id` is an opaque, nonempty cache key, never a file path.
#[derive(Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SvgSource {
/// Stable identity. Repeated IDs across variants must have identical names and text.
pub id: String,
/// Glyph name: nonempty Unicode letters/numbers, hyphens or underscores.
/// This subset is safe in the engine's built-in SVG/CSS/HTML renderers.
pub name: String,
/// UTF-8 SVG document. External image paths are not loaded.
pub svg: String,
}
/// One ordered design in a text-input variant family.
#[derive(Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SourceFontVariant {
/// Unique variant name; follows the file API's variant naming rules.
pub name: String,
/// Ordered sources. IDs and glyph names must be unique within this design.
pub sources: Vec<SvgSource>,
/// Optional weight in 1..=1000; omitted weights use shared interpolation rules.
pub weight: Option<u16>,
/// Exactly one variant must set this to `Some(true)`.
pub default: Option<bool>,
}
/// Filesystem-free generation settings. Defaults match the file API's `GenerateWebfontsOptions`.
/// Templates are text (`Some("")` renders empty), not paths. No output is written.
/// Deserializes camelCase fields, rejecting unknown fields and defaulting omitted settings.
#[derive(Clone, Default, serde::Deserialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct GenerateWebfontsFromSourcesOptions {
/// Enable the persistent cache with bytes from an earlier result's
/// [`cache_bytes`](super::GenerateWebfontsFromSourcesResult::cache_bytes), or an empty vector
/// to start cold. Unchanged icons skip parsing and processing, and unchanged formats are
/// reused. Output is byte-identical to an uncached run. Bytes from another engine version or
/// other settings are ignored. Not deserialized: pass binary data directly.
#[serde(skip)]
pub cache: Option<Vec<u8>>,
/// TTF `head` timestamp used when `formatOptions.ttf.ts` is unset, for environments without a
/// system clock (WebAssembly). Unlike `ts` it isn't part of cache keys, so cached fonts keep
/// the timestamp of the run that created them. Not deserialized.
#[serde(skip)]
pub fallback_ts: Option<i64>,
/// Ordered sources for ordinary generation; empty when `variants` is supplied.
pub sources: Vec<SvgSource>,
/// Ordered variant designs, using the same font encoders as file generation.
pub variants: Option<Vec<SourceFontVariant>>,
/// Optional CSS Handlebars source; compiled on first CSS render.
pub css_template: Option<String>,
/// Optional HTML Handlebars source; compiled on first HTML render.
pub html_template: Option<String>,
/// Color glyph selection; supports TTF, WOFF and WOFF2.
#[serde(
default,
deserialize_with = "crate::types::color::deserialize_selection"
)]
pub color_glyphs: Option<ColorGlyphSelection>,
/// Named #RRGGBB palette slots, indexed in ASCII name order; requires active color glyphs.
pub color_slots: Option<HashMap<String, String>>,
/// Font ascent.
pub ascent: Option<f64>,
/// Center glyphs horizontally.
pub center_horizontally: Option<bool>,
/// Center glyphs vertically.
pub center_vertically: Option<bool>,
/// Include CSS in collected outputs (default true).
pub css: Option<bool>,
/// Explicit glyph name to Unicode assignments.
pub codepoints: Option<HashMap<String, u32>>,
/// URL prefix; absent means relative font filenames in both CSS and HTML.
pub css_fonts_url: Option<String>,
/// Font descent.
pub descent: Option<f64>,
/// Use a common advance width.
pub fixed_width: Option<bool>,
/// Format-specific encoding settings.
pub format_options: Option<FormatOptions>,
/// Include HTML in collected outputs (default false).
pub html: Option<bool>,
/// Include the fixed TypeScript source module in collected outputs. Defaults to false.
pub ts: Option<bool>,
/// Retain geometry and encoder caches for subsequent source updates.
pub incremental: Option<bool>,
/// Output font height.
pub font_height: Option<f64>,
/// Logical font name (default `iconfont`).
pub font_name: Option<String>,
/// Font style.
pub font_style: Option<String>,
/// Ordinary font weight; incompatible with variants.
pub font_weight: Option<String>,
/// Enable name ligatures (default true).
pub ligature: Option<bool>,
/// Variant missing-glyph policy.
pub missing_glyphs: Option<MissingGlyphOptions>,
/// Normalize glyph dimensions (default true).
pub normalize: Option<bool>,
/// CSS font format preference order.
pub order: Option<Vec<FontType>>,
/// Optimize SVG output paths.
pub optimize_output: Option<bool>,
/// Preserve source aspect ratios.
pub preserve_aspect_ratio: Option<bool>,
/// Coordinate rounding precision.
pub round: Option<f64>,
/// First automatic codepoint (default U+F101).
pub start_codepoint: Option<u32>,
/// Additional template context values.
pub template_options: Option<serde_json::Map<String, serde_json::Value>>,
/// How many threads do glyph work at once (never more than CPU cores), including later source
/// updates. WOFF2's Brotli worker isn't counted. Unset uses Rayon's global pool
/// (`RAYON_NUM_THREADS` applies). Output is identical for every value. Not deserialized.
#[serde(skip)]
pub threads: Option<std::num::NonZeroUsize>,
/// Requested formats; ordinary defaults EOT/WOFF/WOFF2, variants WOFF/WOFF2.
pub types: Option<Vec<FontType>>,
/// Variant CSS modifier prefix (default `icon--`).
pub variant_class_prefix: Option<String>,
}
#[derive(Clone)]
pub(super) struct ResolvedSourceOptions {
pub common: ResolvedGenerationOptions,
pub variants: Option<ResolvedVariants>,
pub sources: Vec<Vec<LoadedSvgSource>>,
pub css_template: Option<Arc<str>>,
pub html_template: Option<Arc<str>>,
}
pub(super) fn resolve(
options: GenerateWebfontsFromSourcesOptions,
) -> std::io::Result<ResolvedSourceOptions> {
let settings = GenerationOptions {
color_glyphs: options.color_glyphs,
color_slots: options.color_slots,
ascent: options.ascent,
center_horizontally: options.center_horizontally,
center_vertically: options.center_vertically,
css: options.css,
codepoints: options.codepoints,
css_fonts_url: options.css_fonts_url,
descent: options.descent,
fixed_width: options.fixed_width,
format_options: options.format_options,
html: options.html,
ts: options.ts,
incremental: options.incremental,
font_height: options.font_height,
font_name: options.font_name,
font_style: options.font_style,
font_weight: options.font_weight,
ligature: options.ligature,
missing_glyphs: options.missing_glyphs,
normalize: options.normalize,
order: options.order,
optimize_output: options.optimize_output,
preserve_aspect_ratio: options.preserve_aspect_ratio,
round: options.round,
start_codepoint: options.start_codepoint,
template_options: options.template_options,
threads: options.threads,
types: options.types,
variant_class_prefix: options.variant_class_prefix,
};
let (variants, designs) = if let Some(variants) = options.variants {
if !options.sources.is_empty() {
return Err(invalid(
"options.sources must be empty when variants are supplied",
));
}
let metadata = variants
.iter()
.map(|variant| VariantMetadata {
name: &variant.name,
weight: variant.weight,
default: variant.default,
source_count: variant.sources.len(),
})
.collect::<Vec<_>>();
validate_variants(&settings, &metadata, "sources")?;
validate_format_options(settings.format_options.as_ref())?;
let resolved = resolve_variant_metadata(
&metadata,
variants
.iter()
.map(|variant| {
variant
.sources
.iter()
.map(|source| source.id.clone())
.collect()
})
.collect(),
settings.variant_class_prefix.as_deref().unwrap_or("icon--"),
)?;
(
Some(resolved),
variants
.into_iter()
.map(|variant| variant.sources)
.collect(),
)
} else {
if options.sources.is_empty() {
return Err(invalid("options.sources is empty"));
}
settings.validate_without_variants()?;
validate_format_options(settings.format_options.as_ref())?;
(None, vec![options.sources])
};
let mut common = settings.resolve(variants.is_some())?;
common.filesystem_free = true;
common.fallback_ts = options.fallback_ts;
for name in common.explicit_codepoints.keys() {
validate_name(name)?;
}
let sources = normalize_sources(designs)?;
Ok(ResolvedSourceOptions {
common,
variants,
sources,
css_template: options.css_template.map(Arc::from),
html_template: options.html_template.map(Arc::from),
})
}
fn normalize_sources(designs: Vec<Vec<SvgSource>>) -> std::io::Result<Vec<Vec<LoadedSvgSource>>> {
let mut definitions = HashMap::<String, LoadedSvgSource>::new();
designs
.into_iter()
.map(|sources| {
let mut ids = HashSet::new();
let sources = sources
.into_iter()
.map(|source| {
if source.id.is_empty() || !ids.insert(source.id.clone()) {
return Err(invalid(format!(
"Source ID {:?} must be nonempty and unique within a design",
source.id
)));
}
validate_name(&source.name)?;
if let Some(existing) = definitions.get(&source.id) {
if existing.glyph_name != source.name
|| existing.contents.as_ref() != source.svg
{
return Err(invalid(format!(
"Conflicting definitions for source ID {:?}",
source.id
)));
}
return Ok(existing.clone());
}
let loaded = LoadedSvgSource {
id: source.id,
glyph_name: source.name,
contents: source.svg.into(),
};
definitions.insert(loaded.id.clone(), loaded.clone());
Ok(loaded)
})
.collect::<std::io::Result<Vec<_>>>()?;
validate_glyph_names(&sources)?;
Ok(sources)
})
.collect()
}
pub(super) fn invalid(message: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Error {
Error::new(ErrorKind::InvalidInput, message)
}
pub(super) fn validate_name(name: &str) -> std::io::Result<()> {
if name.is_empty()
|| !name
.chars()
.all(|character| character.is_alphanumeric() || matches!(character, '-' | '_'))
{
return Err(invalid(format!(
"Glyph name {name:?} must contain only Unicode letters/numbers, hyphens or underscores"
)));
}
Ok(())
}