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    /// Ordered sources for ordinary generation; empty when `variants` is supplied.
46    pub sources: Vec<SvgSource>,
47    /// Ordered variant designs, using the same font encoders as file generation.
48    pub variants: Option<Vec<SourceFontVariant>>,
49    /// Optional CSS Handlebars source; compiled on first CSS render.
50    pub css_template: Option<String>,
51    /// Optional HTML Handlebars source; compiled on first HTML render.
52    pub html_template: Option<String>,
53    /// Color glyph selection; supports TTF, WOFF and WOFF2.
54    #[serde(
55        default,
56        deserialize_with = "crate::types::color::deserialize_selection"
57    )]
58    pub color_glyphs: Option<ColorGlyphSelection>,
59    /// Named #RRGGBB palette slots, indexed in ASCII name order; requires active color glyphs.
60    pub color_slots: Option<HashMap<String, String>>,
61    /// Font ascent.
62    pub ascent: Option<f64>,
63    /// Center glyphs horizontally.
64    pub center_horizontally: Option<bool>,
65    /// Center glyphs vertically.
66    pub center_vertically: Option<bool>,
67    /// Include CSS in collected outputs (default true).
68    pub css: Option<bool>,
69    /// Explicit glyph name to Unicode assignments.
70    pub codepoints: Option<HashMap<String, u32>>,
71    /// URL prefix; absent means relative font filenames in both CSS and HTML.
72    pub css_fonts_url: Option<String>,
73    /// Font descent.
74    pub descent: Option<f64>,
75    /// Use a common advance width.
76    pub fixed_width: Option<bool>,
77    /// Format-specific encoding settings.
78    pub format_options: Option<FormatOptions>,
79    /// Include HTML in collected outputs (default false).
80    pub html: Option<bool>,
81    /// Retain geometry and encoder caches for subsequent source updates.
82    pub incremental: Option<bool>,
83    /// Output font height.
84    pub font_height: Option<f64>,
85    /// Logical font name (default `iconfont`).
86    pub font_name: Option<String>,
87    /// Font style.
88    pub font_style: Option<String>,
89    /// Ordinary font weight; incompatible with variants.
90    pub font_weight: Option<String>,
91    /// Enable name ligatures (default true).
92    pub ligature: Option<bool>,
93    /// Variant missing-glyph policy.
94    pub missing_glyphs: Option<MissingGlyphOptions>,
95    /// Normalize glyph dimensions (default true).
96    pub normalize: Option<bool>,
97    /// CSS font format preference order.
98    pub order: Option<Vec<FontType>>,
99    /// Optimize SVG output paths.
100    pub optimize_output: Option<bool>,
101    /// Preserve source aspect ratios.
102    pub preserve_aspect_ratio: Option<bool>,
103    /// Coordinate rounding precision.
104    pub round: Option<f64>,
105    /// First automatic codepoint (default U+F101).
106    pub start_codepoint: Option<u32>,
107    /// Additional template context values.
108    pub template_options: Option<serde_json::Map<String, serde_json::Value>>,
109    /// Requested formats; ordinary defaults EOT/WOFF/WOFF2, variants WOFF/WOFF2.
110    pub types: Option<Vec<FontType>>,
111    /// Variant CSS modifier prefix (default `icon--`).
112    pub variant_class_prefix: Option<String>,
113}
114
115#[derive(Clone)]
116pub(super) struct ResolvedSourceOptions {
117    pub common: ResolvedGenerationOptions,
118    pub variants: Option<ResolvedVariants>,
119    pub sources: Vec<Vec<LoadedSvgSource>>,
120    pub css_template: Option<Arc<str>>,
121    pub html_template: Option<Arc<str>>,
122}
123
124pub(super) fn resolve(
125    options: GenerateWebfontsFromSourcesOptions,
126) -> std::io::Result<ResolvedSourceOptions> {
127    let settings = GenerationOptions {
128        color_glyphs: options.color_glyphs,
129        color_slots: options.color_slots,
130        ascent: options.ascent,
131        center_horizontally: options.center_horizontally,
132        center_vertically: options.center_vertically,
133        css: options.css,
134        codepoints: options.codepoints,
135        css_fonts_url: options.css_fonts_url,
136        descent: options.descent,
137        fixed_width: options.fixed_width,
138        format_options: options.format_options,
139        html: options.html,
140        incremental: options.incremental,
141        font_height: options.font_height,
142        font_name: options.font_name,
143        font_style: options.font_style,
144        font_weight: options.font_weight,
145        ligature: options.ligature,
146        missing_glyphs: options.missing_glyphs,
147        normalize: options.normalize,
148        order: options.order,
149        optimize_output: options.optimize_output,
150        preserve_aspect_ratio: options.preserve_aspect_ratio,
151        round: options.round,
152        start_codepoint: options.start_codepoint,
153        template_options: options.template_options,
154        types: options.types,
155        variant_class_prefix: options.variant_class_prefix,
156    };
157    let (variants, designs) = if let Some(variants) = options.variants {
158        if !options.sources.is_empty() {
159            return Err(invalid(
160                "options.sources must be empty when variants are supplied",
161            ));
162        }
163        let metadata = variants
164            .iter()
165            .map(|variant| VariantMetadata {
166                name: &variant.name,
167                weight: variant.weight,
168                default: variant.default,
169                source_count: variant.sources.len(),
170            })
171            .collect::<Vec<_>>();
172        validate_variants(&settings, &metadata, "sources")?;
173        validate_format_options(settings.format_options.as_ref())?;
174        let resolved = resolve_variant_metadata(
175            &metadata,
176            variants
177                .iter()
178                .map(|variant| {
179                    variant
180                        .sources
181                        .iter()
182                        .map(|source| source.id.clone())
183                        .collect()
184                })
185                .collect(),
186            settings.variant_class_prefix.as_deref().unwrap_or("icon--"),
187        )?;
188        (
189            Some(resolved),
190            variants
191                .into_iter()
192                .map(|variant| variant.sources)
193                .collect(),
194        )
195    } else {
196        if options.sources.is_empty() {
197            return Err(invalid("options.sources is empty"));
198        }
199        settings.validate_without_variants()?;
200        validate_format_options(settings.format_options.as_ref())?;
201        (None, vec![options.sources])
202    };
203    let mut common = settings.resolve(variants.is_some())?;
204    common.filesystem_free = true;
205    for name in common.explicit_codepoints.keys() {
206        validate_name(name)?;
207    }
208    let sources = normalize_sources(designs)?;
209    Ok(ResolvedSourceOptions {
210        common,
211        variants,
212        sources,
213        css_template: options.css_template.map(Arc::from),
214        html_template: options.html_template.map(Arc::from),
215    })
216}
217
218fn normalize_sources(designs: Vec<Vec<SvgSource>>) -> std::io::Result<Vec<Vec<LoadedSvgSource>>> {
219    let mut definitions = HashMap::<String, LoadedSvgSource>::new();
220    designs
221        .into_iter()
222        .map(|sources| {
223            let mut ids = HashSet::new();
224            let sources = sources
225                .into_iter()
226                .map(|source| {
227                    if source.id.is_empty() || !ids.insert(source.id.clone()) {
228                        return Err(invalid(format!(
229                            "Source ID {:?} must be nonempty and unique within a design",
230                            source.id
231                        )));
232                    }
233                    validate_name(&source.name)?;
234                    if let Some(existing) = definitions.get(&source.id) {
235                        if existing.glyph_name != source.name
236                            || existing.contents.as_ref() != source.svg
237                        {
238                            return Err(invalid(format!(
239                                "Conflicting definitions for source ID {:?}",
240                                source.id
241                            )));
242                        }
243                        return Ok(existing.clone());
244                    }
245                    let loaded = LoadedSvgSource {
246                        id: source.id,
247                        glyph_name: source.name,
248                        contents: source.svg.into(),
249                    };
250                    definitions.insert(loaded.id.clone(), loaded.clone());
251                    Ok(loaded)
252                })
253                .collect::<std::io::Result<Vec<_>>>()?;
254            validate_glyph_names(&sources)?;
255            Ok(sources)
256        })
257        .collect()
258}
259
260pub(super) fn invalid(message: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Error {
261    Error::new(ErrorKind::InvalidInput, message)
262}
263
264pub(super) fn validate_name(name: &str) -> std::io::Result<()> {
265    if name.is_empty()
266        || !name
267            .chars()
268            .all(|character| character.is_alphanumeric() || matches!(character, '-' | '_'))
269    {
270        return Err(invalid(format!(
271            "Glyph name {name:?} must contain only Unicode letters/numbers, hyphens or underscores"
272        )));
273    }
274    Ok(())
275}