1#[cfg(feature = "bench")]
64pub mod bench_support;
65mod byte_helpers;
66mod formats;
67mod hooks;
68mod incremental;
69mod input;
70mod output;
71mod pipeline;
72mod rendering;
73mod result;
74mod sfnt;
75mod svg;
76#[cfg(test)]
77mod test_helpers;
78mod types;
79
80pub use hooks::GenerationHooks;
81use std::sync::Mutex;
82
83use input::{
84 ResolvedGenerateWebfontsOptions, build_variant_family_sources,
85 finalize_generate_webfonts_options, resolve_generate_webfonts_options, resolve_missing_glyphs,
86 validate_generate_webfonts_options,
87};
88use input::{load_svg_files_with_hooks, load_variant_svg_files_with_hooks};
89use output::write_generate_webfonts_result;
90use pipeline::{generate_variant_webfonts_sync, generate_webfonts_sync};
91use rendering::{
92 CachedTemplateData, SharedTemplateData, build_css_context, build_html_context,
93 build_html_registry_and_dependencies,
94};
95pub use result::{GenerateWebfontsResult, RegenerateError};
96pub use types::{
97 ColorGlyphSelection, CssContext, FontType, FontVariant, FormatOptions, GenerateWebfontsOptions,
98 GlyphChange, GlyphChangeEntry, HtmlContext, MissingGlyphBehavior, MissingGlyphOptions,
99 RegenerationFiles, SvgFormatOptions, TemplateVariant, TtfFormatOptions, VariantFileSet,
100 Woff2FormatOptions, WoffFormatOptions,
101};
102
103type PreparedVariantInput = (
104 svg::types::PreparedVariantFamily,
105 Vec<input::LoadedSvgFile>,
106 Option<svg::VariantGlyphCache>,
107);
108
109fn prepare_variant_family(
110 options: &mut ResolvedGenerateWebfontsOptions,
111 source_files: Vec<Vec<input::LoadedSvgFile>>,
112) -> std::io::Result<PreparedVariantInput> {
113 let mut cache = options.incremental.then(svg::VariantGlyphCache::default);
114 let family = prepare_variant_family_cached(
115 options,
116 source_files.iter().map(Vec::as_slice).collect(),
117 cache.as_mut(),
118 )?;
119 let sources = source_files.into_iter().flatten().collect();
120 Ok((family, sources, cache))
121}
122
123fn prepare_variant_family_cached(
124 options: &mut ResolvedGenerateWebfontsOptions,
125 source_files: Vec<&[input::LoadedSvgFile]>,
126 cache: Option<&mut svg::VariantGlyphCache>,
127) -> std::io::Result<svg::types::PreparedVariantFamily> {
128 let (mut family, codepoints) = build_variant_family_sources(
129 source_files,
130 &options.explicit_codepoints,
131 options.start_codepoint,
132 )?;
133 input::validate_color_names(
134 options,
135 family.glyphs.iter().map(|glyph| glyph.name.as_str()),
136 )?;
137 let variants = options
138 .variants
139 .as_ref()
140 .expect("variant source preparation requires resolved variants");
141 let variant_names = variants
142 .variants
143 .iter()
144 .map(|variant| variant.name.as_str())
145 .collect::<Vec<_>>();
146 let fallback_index = options.missing_glyphs.variant.as_deref().map(|name| {
147 variant_names
148 .iter()
149 .position(|variant_name| *variant_name == name)
150 .expect("validated fallback must name a resolved variant")
151 });
152 resolve_missing_glyphs(
153 &mut family,
154 options.missing_glyphs.behavior,
155 fallback_index,
156 &variant_names,
157 )?;
158 options.codepoints = codepoints;
159 let svg_options = svg::svg_options_from_options(options);
160 let prepared = match cache {
161 Some(cache) => svg::prepare_variant_svg_family_cached(&svg_options, &family, Some(cache))?,
162 None => svg::prepare_variant_svg_family(&svg_options, &family)?,
163 };
164 Ok(prepared)
165}
166
167fn variant_preparation_join_error(error: tokio::task::JoinError) -> std::io::Error {
168 std::io::Error::other(format!("Native variant preparation task failed: {error}"))
169}
170
171#[cfg(test)]
172#[tokio::test]
173async fn variant_preparation_reports_panicking_worker() {
174 let join_error = tokio::task::spawn_blocking(|| panic!("variant preparation panic"))
175 .await
176 .unwrap_err();
177 let error = variant_preparation_join_error(join_error);
178
179 assert!(
180 error
181 .to_string()
182 .starts_with("Native variant preparation task failed:")
183 );
184}
185
186#[cfg(test)]
187#[tokio::test]
188async fn hooked_variant_generation_succeeds_after_preparing_sources() {
189 let path = test_helpers::webfont_fixture("add.svg");
190 let variants = vec![
191 FontVariant {
192 name: "small".to_owned(),
193 files: vec![path.clone()],
194 weight: Some(300),
195 default: Some(true),
196 },
197 FontVariant {
198 name: "large".to_owned(),
199 files: vec![path.clone()],
200 weight: Some(700),
201 default: None,
202 },
203 ];
204 let options = GenerateWebfontsOptions {
205 dest: "artifacts".to_owned(),
206 files: vec![],
207 types: Some(vec![FontType::Woff2]),
208 variants: Some(variants.clone()),
209 write_files: Some(false),
210 ..Default::default()
211 };
212
213 let result = generate_with_hooks(options, &())
214 .await
215 .expect("variant generation should succeed");
216 assert!(result.woff2_bytes().is_some());
217 assert!(result.eot_bytes().is_none());
218
219 let mut duplicate = variants;
220 duplicate[0].files.push(path);
221 let error = generate_with_hooks(
222 GenerateWebfontsOptions {
223 dest: "artifacts".to_owned(),
224 files: vec![],
225 types: Some(vec![FontType::Woff2]),
226 variants: Some(duplicate),
227 ..Default::default()
228 },
229 &(),
230 )
231 .await
232 .err()
233 .expect("duplicate names within one variant should fail");
234 assert!(error.to_string().contains("must be unique"));
235}
236
237#[cfg(test)]
238#[tokio::test]
239async fn hooked_generation_keeps_the_ordinary_source_path() {
240 let result = generate_with_hooks(
241 GenerateWebfontsOptions {
242 css: Some(false),
243 dest: "artifacts".to_owned(),
244 files: vec![test_helpers::webfont_fixture("add.svg")],
245 html: Some(false),
246 types: Some(vec![FontType::Svg]),
247 write_files: Some(false),
248 ..Default::default()
249 },
250 &(),
251 )
252 .await
253 .expect("ordinary generation with hooks should succeed");
254
255 assert!(result.svg_string().is_some());
256}
257
258pub async fn generate_with_hooks<H: GenerationHooks>(
273 options: GenerateWebfontsOptions,
274 hooks: &H,
275) -> Result<GenerateWebfontsResult, H::Error> {
276 validate_generate_webfonts_options(&options)?;
277 let mut resolved_options = resolve_generate_webfonts_options(options)?;
278 let mut result = if let Some(variants) = &resolved_options.variants {
279 let variant_paths = variants
280 .variants
281 .iter()
282 .map(|variant| variant.files.clone())
283 .collect::<Vec<_>>();
284 let source_files = load_variant_svg_files_with_hooks(&variant_paths, hooks).await?;
285 let generation = tokio::task::spawn_blocking(move || {
286 let (family, source_files, cache) =
287 prepare_variant_family(&mut resolved_options, source_files)?;
288 generate_variant_webfonts_sync(resolved_options, source_files, family, cache)
289 });
290 generation.await.map_err(variant_preparation_join_error)??
291 } else {
292 let source_files = load_svg_files_with_hooks(&resolved_options.files, hooks, true).await?;
293 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
294 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
295 .await
296 .map_err(|error| {
297 std::io::Error::other(format!("Native webfont generation task failed: {error}"))
298 })??
299 };
300
301 if hooks.has_css_context() || hooks.has_html_context() {
305 let shared = SharedTemplateData::new(&result.options, &result.source_files)?;
306
307 let mut css_ctx = build_css_context(&result.options, &shared);
308 if hooks.has_css_context() {
309 css_ctx = hooks.css_context(css_ctx).await?;
310 css_ctx.insert(
311 "__webfontVariantMode".to_owned(),
312 serde_json::Value::Bool(result.options.variants.is_some()),
313 );
314 result.css_context = Some(css_ctx.clone());
315 }
316
317 let mut html_ctx =
318 if result.options.html || hooks.has_html_context() || result.options.variants.is_some()
319 {
320 if result.options.variants.is_some() && hooks.has_css_context() {
321 rendering::build_html_context_with_css(
322 &result.options,
323 &shared,
324 &result.source_files,
325 &css_ctx,
326 )?
327 } else {
328 build_html_context(&result.options, &shared, &result.source_files, None)?
329 }
330 } else {
331 serde_json::Map::new()
332 };
333 if hooks.has_html_context() {
334 html_ctx = hooks.html_context(html_ctx).await?;
335 result.html_context = Some(html_ctx.clone());
336 }
337
338 let (html_registry, html_template_dependencies) =
340 build_html_registry_and_dependencies(&result.options)?;
341 let css_hbs_context =
342 handlebars::Context::wraps(&css_ctx).map_err(std::io::Error::other)?;
343 let html_hbs_context =
344 handlebars::Context::wraps(&html_ctx).map_err(std::io::Error::other)?;
345 let _ = result.cached.set(Ok(CachedTemplateData {
346 shared,
347 css_context: css_ctx,
348 css_hbs_context: Mutex::new(css_hbs_context),
349 html_context: html_ctx,
350 html_hbs_context: Mutex::new(html_hbs_context),
351 html_template_dependencies,
352 html_registry,
353 render_cache: Mutex::new(Default::default()),
354 }));
355 }
356
357 if result.options.write_files
358 && let Some(written) = write_generate_webfonts_result(&result).await?
359 {
360 result.seed_written_outputs(written);
362 }
363
364 Ok(result)
365}
366
367pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
369
370pub async fn generate(
375 options: GenerateWebfontsOptions,
376 rename: Option<RenameFn>,
377) -> std::io::Result<GenerateWebfontsResult> {
378 generate_with_hooks(options, &hooks::RenameHooks(rename.as_deref())).await
379}
380
381pub fn generate_sync(
384 options: GenerateWebfontsOptions,
385 rename: Option<RenameFn>,
386) -> std::io::Result<GenerateWebfontsResult> {
387 tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
388}