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 pub sources: Vec<SvgSource>,
47 pub variants: Option<Vec<SourceFontVariant>>,
49 pub css_template: Option<String>,
51 pub html_template: Option<String>,
53 #[serde(
55 default,
56 deserialize_with = "crate::types::color::deserialize_selection"
57 )]
58 pub color_glyphs: Option<ColorGlyphSelection>,
59 pub color_slots: Option<HashMap<String, String>>,
61 pub ascent: Option<f64>,
63 pub center_horizontally: Option<bool>,
65 pub center_vertically: Option<bool>,
67 pub css: Option<bool>,
69 pub codepoints: Option<HashMap<String, u32>>,
71 pub css_fonts_url: Option<String>,
73 pub descent: Option<f64>,
75 pub fixed_width: Option<bool>,
77 pub format_options: Option<FormatOptions>,
79 pub html: Option<bool>,
81 pub incremental: Option<bool>,
83 pub font_height: Option<f64>,
85 pub font_name: Option<String>,
87 pub font_style: Option<String>,
89 pub font_weight: Option<String>,
91 pub ligature: Option<bool>,
93 pub missing_glyphs: Option<MissingGlyphOptions>,
95 pub normalize: Option<bool>,
97 pub order: Option<Vec<FontType>>,
99 pub optimize_output: Option<bool>,
101 pub preserve_aspect_ratio: Option<bool>,
103 pub round: Option<f64>,
105 pub start_codepoint: Option<u32>,
107 pub template_options: Option<serde_json::Map<String, serde_json::Value>>,
109 pub types: Option<Vec<FontType>>,
111 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}