webfont_generator/memory/
options.rs1use 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#[derive(Clone, serde::Deserialize)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct SvgSource {
16 pub id: String,
18 pub name: String,
21 pub svg: String,
23}
24
25#[derive(Clone, serde::Deserialize)]
27#[serde(rename_all = "camelCase", deny_unknown_fields)]
28pub struct SourceFontVariant {
29 pub name: String,
31 pub sources: Vec<SvgSource>,
33 pub weight: Option<u16>,
35 pub default: Option<bool>,
37}
38
39#[derive(Clone, Default, serde::Deserialize)]
43#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
44pub struct GenerateWebfontsFromSourcesOptions {
45 #[serde(skip)]
51 pub cache: Option<Vec<u8>>,
52 #[serde(skip)]
56 pub fallback_ts: Option<i64>,
57 pub sources: Vec<SvgSource>,
59 pub variants: Option<Vec<SourceFontVariant>>,
61 pub css_template: Option<String>,
63 pub html_template: Option<String>,
65 #[serde(
67 default,
68 deserialize_with = "crate::types::color::deserialize_selection"
69 )]
70 pub color_glyphs: Option<ColorGlyphSelection>,
71 pub color_slots: Option<HashMap<String, String>>,
73 pub ascent: Option<f64>,
75 pub center_horizontally: Option<bool>,
77 pub center_vertically: Option<bool>,
79 pub css: Option<bool>,
81 pub codepoints: Option<HashMap<String, u32>>,
83 pub css_fonts_url: Option<String>,
85 pub descent: Option<f64>,
87 pub fixed_width: Option<bool>,
89 pub format_options: Option<FormatOptions>,
91 pub html: Option<bool>,
93 pub ts: Option<bool>,
95 pub incremental: Option<bool>,
97 pub font_height: Option<f64>,
99 pub font_name: Option<String>,
101 pub font_style: Option<String>,
103 pub font_weight: Option<String>,
105 pub ligature: Option<bool>,
107 pub missing_glyphs: Option<MissingGlyphOptions>,
109 pub normalize: Option<bool>,
111 pub order: Option<Vec<FontType>>,
113 pub optimize_output: Option<bool>,
115 pub preserve_aspect_ratio: Option<bool>,
117 pub round: Option<f64>,
119 pub start_codepoint: Option<u32>,
121 pub template_options: Option<serde_json::Map<String, serde_json::Value>>,
123 #[serde(skip)]
127 pub threads: Option<std::num::NonZeroUsize>,
128 pub types: Option<Vec<FontType>>,
130 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}