Skip to main content

webfont_generator/memory/
options.rs

1use std::collections::{HashMap, HashSet};
2use std::io::{Error, ErrorKind};
3use std::sync::Arc;
4
5use crate::input::{
6    GenerationOptions, LoadedSvgSource, ResolvedGenerationOptions, ResolvedVariants,
7    VariantMetadata, resolve_variant_metadata, validate_format_options, validate_glyph_names,
8    validate_variants,
9};
10use crate::{ColorGlyphSelection, FontType, FormatOptions, MissingGlyphOptions};
11
12/// Named SVG text. `id` is an opaque, nonempty cache key, never a file path.
13#[derive(Clone, serde::Deserialize)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct SvgSource {
16    /// Stable identity. Repeated IDs across variants must have identical names and text.
17    pub id: String,
18    /// Glyph name: nonempty Unicode letters/numbers, hyphens or underscores.
19    /// This subset is safe in the engine's built-in SVG/CSS/HTML renderers.
20    pub name: String,
21    /// UTF-8 SVG document. External image paths are not loaded.
22    pub svg: String,
23}
24
25/// One ordered design in a text-input variant family.
26#[derive(Clone, serde::Deserialize)]
27#[serde(rename_all = "camelCase", deny_unknown_fields)]
28pub struct SourceFontVariant {
29    /// Unique variant name; follows the file API's variant naming rules.
30    pub name: String,
31    /// Ordered sources. IDs and glyph names must be unique within this design.
32    pub sources: Vec<SvgSource>,
33    /// Optional weight in 1..=1000; omitted weights use shared interpolation rules.
34    pub weight: Option<u16>,
35    /// Exactly one variant must set this to `Some(true)`.
36    pub default: Option<bool>,
37}
38
39/// Filesystem-free generation settings. Defaults match the file API's `GenerateWebfontsOptions`.
40/// Templates are text (`Some("")` renders empty), not paths. No output is written.
41/// Deserializes camelCase fields, rejecting unknown fields and defaulting omitted settings.
42#[derive(Clone, Default, serde::Deserialize)]
43#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
44pub struct GenerateWebfontsFromSourcesOptions {
45    /// Enable the persistent cache with bytes from an earlier result's
46    /// [`cache_bytes`](super::GenerateWebfontsFromSourcesResult::cache_bytes), or an empty vector
47    /// to start cold. Unchanged icons skip parsing and processing, and unchanged formats are
48    /// reused. Output is byte-identical to an uncached run. Bytes from another engine version or
49    /// other settings are ignored. Not deserialized: pass binary data directly.
50    #[serde(skip)]
51    pub cache: Option<Vec<u8>>,
52    /// TTF `head` timestamp used when `formatOptions.ttf.ts` is unset, for environments without a
53    /// system clock (WebAssembly). Unlike `ts` it isn't part of cache keys, so cached fonts keep
54    /// the timestamp of the run that created them. Not deserialized.
55    #[serde(skip)]
56    pub fallback_ts: Option<i64>,
57    /// Ordered sources for ordinary generation; empty when `variants` is supplied.
58    pub sources: Vec<SvgSource>,
59    /// Ordered variant designs, using the same font encoders as file generation.
60    pub variants: Option<Vec<SourceFontVariant>>,
61    /// Optional CSS Handlebars source; compiled on first CSS render.
62    pub css_template: Option<String>,
63    /// Optional HTML Handlebars source; compiled on first HTML render.
64    pub html_template: Option<String>,
65    /// Color glyph selection; supports TTF, WOFF and WOFF2.
66    #[serde(
67        default,
68        deserialize_with = "crate::types::color::deserialize_selection"
69    )]
70    pub color_glyphs: Option<ColorGlyphSelection>,
71    /// Named #RRGGBB palette slots, indexed in ASCII name order; requires active color glyphs.
72    pub color_slots: Option<HashMap<String, String>>,
73    /// Font ascent.
74    pub ascent: Option<f64>,
75    /// Center glyphs horizontally.
76    pub center_horizontally: Option<bool>,
77    /// Center glyphs vertically.
78    pub center_vertically: Option<bool>,
79    /// Include CSS in collected outputs (default true).
80    pub css: Option<bool>,
81    /// Explicit glyph name to Unicode assignments.
82    pub codepoints: Option<HashMap<String, u32>>,
83    /// URL prefix; absent means relative font filenames in both CSS and HTML.
84    pub css_fonts_url: Option<String>,
85    /// Font descent.
86    pub descent: Option<f64>,
87    /// Use a common advance width.
88    pub fixed_width: Option<bool>,
89    /// Format-specific encoding settings.
90    pub format_options: Option<FormatOptions>,
91    /// Include HTML in collected outputs (default false).
92    pub html: Option<bool>,
93    /// Include the fixed TypeScript source module in collected outputs. Defaults to false.
94    pub ts: Option<bool>,
95    /// Retain geometry and encoder caches for subsequent source updates.
96    pub incremental: Option<bool>,
97    /// Output font height.
98    pub font_height: Option<f64>,
99    /// Logical font name (default `iconfont`).
100    pub font_name: Option<String>,
101    /// Font style.
102    pub font_style: Option<String>,
103    /// Ordinary font weight; incompatible with variants.
104    pub font_weight: Option<String>,
105    /// Enable name ligatures (default true).
106    pub ligature: Option<bool>,
107    /// Variant missing-glyph policy.
108    pub missing_glyphs: Option<MissingGlyphOptions>,
109    /// Normalize glyph dimensions (default true).
110    pub normalize: Option<bool>,
111    /// CSS font format preference order.
112    pub order: Option<Vec<FontType>>,
113    /// Optimize SVG output paths.
114    pub optimize_output: Option<bool>,
115    /// Preserve source aspect ratios.
116    pub preserve_aspect_ratio: Option<bool>,
117    /// Coordinate rounding precision.
118    pub round: Option<f64>,
119    /// First automatic codepoint (default U+F101).
120    pub start_codepoint: Option<u32>,
121    /// Additional template context values.
122    pub template_options: Option<serde_json::Map<String, serde_json::Value>>,
123    /// How many threads do glyph work at once (never more than CPU cores), including later source
124    /// updates. WOFF2's Brotli worker isn't counted. Unset uses Rayon's global pool
125    /// (`RAYON_NUM_THREADS` applies). Output is identical for every value. Not deserialized.
126    #[serde(skip)]
127    pub threads: Option<std::num::NonZeroUsize>,
128    /// Requested formats; ordinary defaults EOT/WOFF/WOFF2, variants WOFF/WOFF2.
129    pub types: Option<Vec<FontType>>,
130    /// Variant CSS modifier prefix (default `icon--`).
131    pub variant_class_prefix: Option<String>,
132}
133
134#[derive(Clone)]
135pub(super) struct ResolvedSourceOptions {
136    pub common: ResolvedGenerationOptions,
137    pub variants: Option<ResolvedVariants>,
138    pub sources: Vec<Vec<LoadedSvgSource>>,
139    pub css_template: Option<Arc<str>>,
140    pub html_template: Option<Arc<str>>,
141}
142
143pub(super) fn resolve(
144    options: GenerateWebfontsFromSourcesOptions,
145) -> std::io::Result<ResolvedSourceOptions> {
146    let settings = GenerationOptions {
147        color_glyphs: options.color_glyphs,
148        color_slots: options.color_slots,
149        ascent: options.ascent,
150        center_horizontally: options.center_horizontally,
151        center_vertically: options.center_vertically,
152        css: options.css,
153        codepoints: options.codepoints,
154        css_fonts_url: options.css_fonts_url,
155        descent: options.descent,
156        fixed_width: options.fixed_width,
157        format_options: options.format_options,
158        html: options.html,
159        ts: options.ts,
160        incremental: options.incremental,
161        font_height: options.font_height,
162        font_name: options.font_name,
163        font_style: options.font_style,
164        font_weight: options.font_weight,
165        ligature: options.ligature,
166        missing_glyphs: options.missing_glyphs,
167        normalize: options.normalize,
168        order: options.order,
169        optimize_output: options.optimize_output,
170        preserve_aspect_ratio: options.preserve_aspect_ratio,
171        round: options.round,
172        start_codepoint: options.start_codepoint,
173        template_options: options.template_options,
174        threads: options.threads,
175        types: options.types,
176        variant_class_prefix: options.variant_class_prefix,
177    };
178    let (variants, designs) = if let Some(variants) = options.variants {
179        if !options.sources.is_empty() {
180            return Err(invalid(
181                "options.sources must be empty when variants are supplied",
182            ));
183        }
184        let metadata = variants
185            .iter()
186            .map(|variant| VariantMetadata {
187                name: &variant.name,
188                weight: variant.weight,
189                default: variant.default,
190                source_count: variant.sources.len(),
191            })
192            .collect::<Vec<_>>();
193        validate_variants(&settings, &metadata, "sources")?;
194        validate_format_options(settings.format_options.as_ref())?;
195        let resolved = resolve_variant_metadata(
196            &metadata,
197            variants
198                .iter()
199                .map(|variant| {
200                    variant
201                        .sources
202                        .iter()
203                        .map(|source| source.id.clone())
204                        .collect()
205                })
206                .collect(),
207            settings.variant_class_prefix.as_deref().unwrap_or("icon--"),
208        )?;
209        (
210            Some(resolved),
211            variants
212                .into_iter()
213                .map(|variant| variant.sources)
214                .collect(),
215        )
216    } else {
217        if options.sources.is_empty() {
218            return Err(invalid("options.sources is empty"));
219        }
220        settings.validate_without_variants()?;
221        validate_format_options(settings.format_options.as_ref())?;
222        (None, vec![options.sources])
223    };
224    let mut common = settings.resolve(variants.is_some())?;
225    common.filesystem_free = true;
226    common.fallback_ts = options.fallback_ts;
227    for name in common.explicit_codepoints.keys() {
228        validate_name(name)?;
229    }
230    let sources = normalize_sources(designs)?;
231    Ok(ResolvedSourceOptions {
232        common,
233        variants,
234        sources,
235        css_template: options.css_template.map(Arc::from),
236        html_template: options.html_template.map(Arc::from),
237    })
238}
239
240fn normalize_sources(designs: Vec<Vec<SvgSource>>) -> std::io::Result<Vec<Vec<LoadedSvgSource>>> {
241    let mut definitions = HashMap::<String, LoadedSvgSource>::new();
242    designs
243        .into_iter()
244        .map(|sources| {
245            let mut ids = HashSet::new();
246            let sources = sources
247                .into_iter()
248                .map(|source| {
249                    if source.id.is_empty() || !ids.insert(source.id.clone()) {
250                        return Err(invalid(format!(
251                            "Source ID {:?} must be nonempty and unique within a design",
252                            source.id
253                        )));
254                    }
255                    validate_name(&source.name)?;
256                    if let Some(existing) = definitions.get(&source.id) {
257                        if existing.glyph_name != source.name
258                            || existing.contents.as_ref() != source.svg
259                        {
260                            return Err(invalid(format!(
261                                "Conflicting definitions for source ID {:?}",
262                                source.id
263                            )));
264                        }
265                        return Ok(existing.clone());
266                    }
267                    let loaded = LoadedSvgSource {
268                        id: source.id,
269                        glyph_name: source.name,
270                        contents: source.svg.into(),
271                    };
272                    definitions.insert(loaded.id.clone(), loaded.clone());
273                    Ok(loaded)
274                })
275                .collect::<std::io::Result<Vec<_>>>()?;
276            validate_glyph_names(&sources)?;
277            Ok(sources)
278        })
279        .collect()
280}
281
282pub(super) fn invalid(message: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Error {
283    Error::new(ErrorKind::InvalidInput, message)
284}
285
286pub(super) fn validate_name(name: &str) -> std::io::Result<()> {
287    if name.is_empty()
288        || !name
289            .chars()
290            .all(|character| character.is_alphanumeric() || matches!(character, '-' | '_'))
291    {
292        return Err(invalid(format!(
293            "Glyph name {name:?} must contain only Unicode letters/numbers, hyphens or underscores"
294        )));
295    }
296    Ok(())
297}