1#[cfg(feature = "bench")]
80pub mod bench_support;
81#[cfg(any(feature = "fs", feature = "in_memory"))]
82mod byte_helpers;
83#[cfg(any(feature = "fs", feature = "in_memory"))]
84mod cache;
85#[cfg(any(feature = "fs", feature = "in_memory"))]
86mod formats;
87#[cfg(feature = "fs")]
88mod hooks;
89#[cfg(any(feature = "fs", feature = "in_memory"))]
90mod incremental;
91#[cfg(any(feature = "fs", feature = "in_memory"))]
92mod input;
93#[cfg(feature = "in_memory")]
94mod memory;
95mod output;
96#[cfg(any(feature = "fs", feature = "in_memory"))]
97mod pipeline;
98#[cfg(any(feature = "fs", feature = "in_memory"))]
99mod rendering;
100#[cfg(any(feature = "fs", feature = "in_memory"))]
101mod result;
102#[cfg(any(feature = "fs", feature = "in_memory"))]
103mod sfnt;
104#[cfg(any(feature = "fs", feature = "in_memory"))]
105mod svg;
106#[cfg(all(test, any(feature = "fs", feature = "in_memory")))]
107mod test_helpers;
108#[cfg(any(feature = "fs", feature = "in_memory"))]
109mod threads;
110mod types;
111
112#[cfg(feature = "fs")]
113pub use cache::{ClearCacheReport, clear_cache};
114#[cfg(feature = "fs")]
115pub use hooks::GenerationHooks;
116#[cfg(feature = "fs")]
117use std::sync::Mutex;
118
119#[cfg(feature = "fs")]
120use input::{
121 ResolvedFileGenerationOptions, finalize_generate_webfonts_options,
122 resolve_validated_generate_webfonts_options,
123};
124#[cfg(any(feature = "fs", feature = "in_memory"))]
125use input::{
126 ResolvedGenerationOptions, ResolvedVariants, build_variant_family_sources,
127 resolve_missing_glyphs,
128};
129#[cfg(feature = "fs")]
130use input::{load_svg_files_with_hooks, load_variant_svg_files_with_hooks};
131#[cfg(feature = "in_memory")]
132pub use memory::{
133 GenerateWebfontsFromSourcesOptions, GenerateWebfontsFromSourcesResult, SourceChange,
134 SourceFontVariant, SourceMembership, SvgSource, VariantSourceSet, generate_from_sources_sync,
135};
136#[cfg(all(feature = "in_memory", feature = "async"))]
137pub use memory::{SourceRegenerateError, generate_from_sources};
138pub use output::collect::{CollectedOutput, OutputContents, OutputKind};
139#[cfg(feature = "fs")]
140use output::write_generate_webfonts_result;
141#[cfg(feature = "fs")]
142use pipeline::{generate_variant_webfonts_sync, generate_webfonts_sync};
143#[cfg(feature = "fs")]
144use rendering::{
145 CachedTemplateData, SharedTemplateData, build_css_context, build_html_context,
146 load_html_template,
147};
148#[cfg(feature = "fs")]
149pub use result::{GenerateWebfontsResult, RegenerateError};
150pub use types::{
151 ColorGlyphSelection, FontType, FormatOptions, MissingGlyphBehavior, MissingGlyphOptions,
152 SvgFormatOptions, TemplateVariant, TtfFormatOptions, Woff2FormatOptions, WoffFormatOptions,
153};
154#[cfg(feature = "fs")]
155pub use types::{
156 CssContext, FontVariant, GenerateWebfontsOptions, GlyphChange, GlyphChangeEntry, HtmlContext,
157 RegenerationFiles, VariantFileSet,
158};
159
160#[cfg(feature = "fs")]
161type PreparedVariantInput = (
162 svg::types::PreparedVariantFamily,
163 Vec<input::LoadedSvgSource>,
164 Option<svg::VariantGlyphCache>,
165);
166
167#[cfg(feature = "fs")]
168fn prepare_variant_family(
169 options: &mut ResolvedFileGenerationOptions,
170 source_files: Vec<Vec<input::LoadedSvgSource>>,
171) -> std::io::Result<PreparedVariantInput> {
172 let mut cache = options.incremental.then(svg::VariantGlyphCache::default);
173 let family = prepare_variant_family_cached(
174 options,
175 source_files.iter().map(Vec::as_slice).collect(),
176 cache.as_mut(),
177 )?;
178 let sources = source_files.into_iter().flatten().collect();
179 Ok((family, sources, cache))
180}
181
182#[cfg(feature = "fs")]
183fn prepare_variant_family_cached(
184 options: &mut ResolvedFileGenerationOptions,
185 source_files: Vec<&[input::LoadedSvgSource]>,
186 cache: Option<&mut svg::VariantGlyphCache>,
187) -> std::io::Result<svg::types::PreparedVariantFamily> {
188 let variants = options
189 .variants
190 .as_ref()
191 .expect("variant source preparation requires resolved variants");
192 prepare_content_variant_family(&mut options.common, variants, source_files, cache)
193}
194
195#[cfg(feature = "fs")]
196fn prepare_content_variant_family(
197 options: &mut ResolvedGenerationOptions,
198 variants: &ResolvedVariants,
199 source_files: Vec<&[input::LoadedSvgSource]>,
200 cache: Option<&mut svg::VariantGlyphCache>,
201) -> std::io::Result<svg::types::PreparedVariantFamily> {
202 let family = resolve_content_variant_sources(options, variants, source_files)?;
203 let svg_options = svg::svg_options_from_options(options);
204 match cache {
205 Some(cache) => svg::prepare_variant_svg_family_cached(&svg_options, &family, Some(cache)),
206 None => svg::prepare_variant_svg_family(&svg_options, &family),
207 }
208}
209
210#[cfg(any(feature = "fs", feature = "in_memory"))]
211fn resolve_content_variant_sources<'a>(
212 options: &mut ResolvedGenerationOptions,
213 variants: &ResolvedVariants,
214 source_files: Vec<&'a [input::LoadedSvgSource]>,
215) -> std::io::Result<input::VariantFamilySources<&'a [input::LoadedSvgSource]>> {
216 let (mut family, codepoints) = build_variant_family_sources(
217 source_files,
218 &options.explicit_codepoints,
219 options.start_codepoint,
220 )?;
221 input::validate_color_names(
222 options,
223 family.glyphs.iter().map(|glyph| glyph.name.as_str()),
224 )?;
225 let variant_names = variants
226 .variants
227 .iter()
228 .map(|variant| variant.name.as_str())
229 .collect::<Vec<_>>();
230 let fallback_index = options.missing_glyphs.variant.as_deref().map(|name| {
231 variant_names
232 .iter()
233 .position(|variant_name| *variant_name == name)
234 .expect("validated fallback must name a resolved variant")
235 });
236 resolve_missing_glyphs(
237 &mut family,
238 options.missing_glyphs.behavior,
239 fallback_index,
240 &variant_names,
241 )?;
242 options.codepoints = codepoints;
243 Ok(family)
244}
245
246#[cfg(feature = "fs")]
247fn variant_preparation_join_error(error: tokio::task::JoinError) -> std::io::Error {
248 std::io::Error::other(format!("Native variant preparation task failed: {error}"))
249}
250
251#[cfg(test)]
252#[tokio::test]
253#[cfg(feature = "fs")]
254async fn variant_preparation_reports_panicking_worker() {
255 let join_error = tokio::task::spawn_blocking(|| panic!("variant preparation panic"))
256 .await
257 .unwrap_err();
258 let error = variant_preparation_join_error(join_error);
259
260 assert!(
261 error
262 .to_string()
263 .starts_with("Native variant preparation task failed:")
264 );
265}
266
267#[cfg(test)]
268#[tokio::test]
269#[cfg(feature = "fs")]
270async fn hooked_variant_generation_succeeds_after_preparing_sources() {
271 let path = test_helpers::webfont_fixture("add.svg");
272 let variants = vec![
273 FontVariant {
274 name: "small".to_owned(),
275 files: vec![path.clone()],
276 weight: Some(300),
277 default: Some(true),
278 },
279 FontVariant {
280 name: "large".to_owned(),
281 files: vec![path.clone()],
282 weight: Some(700),
283 default: None,
284 },
285 ];
286 let options = GenerateWebfontsOptions {
287 dest: "artifacts".to_owned(),
288 files: vec![],
289 types: Some(vec![FontType::Woff2]),
290 variants: Some(variants.clone()),
291 write_files: Some(false),
292 ..Default::default()
293 };
294
295 let result = generate_with_hooks(options, &())
296 .await
297 .expect("variant generation should succeed");
298 assert!(result.woff2_bytes().is_some());
299 assert!(result.eot_bytes().is_none());
300
301 let mut duplicate = variants;
302 duplicate[0].files.push(path);
303 let error = generate_with_hooks(
304 GenerateWebfontsOptions {
305 dest: "artifacts".to_owned(),
306 files: vec![],
307 types: Some(vec![FontType::Woff2]),
308 variants: Some(duplicate),
309 ..Default::default()
310 },
311 &(),
312 )
313 .await
314 .err()
315 .expect("duplicate names within one variant should fail");
316 assert!(error.to_string().contains("must be unique"));
317}
318
319#[cfg(test)]
320#[tokio::test]
321#[cfg(feature = "fs")]
322async fn hooked_generation_keeps_the_ordinary_source_path() {
323 let result = generate_with_hooks(
324 GenerateWebfontsOptions {
325 css: Some(false),
326 dest: "artifacts".to_owned(),
327 files: vec![test_helpers::webfont_fixture("add.svg")],
328 html: Some(false),
329 types: Some(vec![FontType::Svg]),
330 write_files: Some(false),
331 ..Default::default()
332 },
333 &(),
334 )
335 .await
336 .expect("ordinary generation with hooks should succeed");
337
338 assert!(result.svg_string().is_some());
339}
340
341#[cfg(test)]
342#[tokio::test]
343#[cfg(feature = "fs")]
344async fn shared_variant_path_keeps_per_membership_hook_names() {
345 struct Names;
346 impl GenerationHooks for Names {
347 type Error = std::io::Error;
348
349 async fn rename(&self, paths: &[String]) -> std::io::Result<Option<Vec<String>>> {
350 assert_eq!(paths.len(), 2);
351 assert_eq!(paths[0], paths[1]);
352 Ok(Some(vec!["small-icon".into(), "large-icon".into()]))
353 }
354 }
355
356 let path = test_helpers::webfont_fixture("add.svg");
357 let mut result = generate_with_hooks(
358 GenerateWebfontsOptions {
359 dest: "artifacts".into(),
360 variants: Some(
361 ["small", "large"]
362 .into_iter()
363 .enumerate()
364 .map(|(index, name)| FontVariant {
365 name: name.into(),
366 files: vec![path.clone()],
367 weight: Some(300 + index as u16 * 400),
368 default: Some(index == 0),
369 })
370 .collect(),
371 ),
372 types: Some(vec![FontType::Ttf]),
373 write_files: Some(false),
374 incremental: Some(true),
375 ..Default::default()
376 },
377 &Names,
378 )
379 .await
380 .unwrap();
381
382 assert!(result.ttf_bytes().is_some());
383 assert_eq!(result.source_files[0].id, result.source_files[1].id);
384 assert_eq!(result.source_files[0].glyph_name, "small-icon");
385 assert_eq!(result.source_files[1].glyph_name, "large-icon");
386 assert_eq!(result.options.codepoints.len(), 2);
387 result
388 .regenerate(
389 &RegenerationFiles::Variants(vec![
390 VariantFileSet {
391 variant: "small".into(),
392 files: vec![path.clone(), test_helpers::webfont_fixture("test.svg")],
393 },
394 VariantFileSet {
395 variant: "large".into(),
396 files: vec![path],
397 },
398 ]),
399 &[],
400 )
401 .unwrap();
402 assert_eq!(result.source_files[0].id, result.source_files[2].id);
403 assert_eq!(result.source_files[0].glyph_name, "small-icon");
404 assert_eq!(result.source_files[2].glyph_name, "large-icon");
405}
406
407#[cfg(test)]
408#[tokio::test]
409#[cfg(feature = "fs")]
410async fn variant_update_replaces_a_collapsed_duplicate_hook_occurrence() {
411 struct Names;
412 impl GenerationHooks for Names {
413 type Error = std::io::Error;
414 async fn rename(&self, _: &[String]) -> std::io::Result<Option<Vec<String>>> {
415 Ok(Some(vec!["first".into(), "last".into(), "other".into()]))
416 }
417 }
418 let path = test_helpers::webfont_fixture("add.svg");
419 let mut result = generate_with_hooks(
420 GenerateWebfontsOptions {
421 dest: "artifacts".into(),
422 write_files: Some(false),
423 incremental: Some(true),
424 types: Some(vec![FontType::Ttf]),
425 variants: Some(vec![
426 FontVariant {
427 name: "light".into(),
428 files: vec![path.clone(), path.clone()],
429 weight: Some(300),
430 default: Some(true),
431 },
432 FontVariant {
433 name: "bold".into(),
434 files: vec![path.clone()],
435 weight: Some(700),
436 default: None,
437 },
438 ]),
439 ..Default::default()
440 },
441 &Names,
442 )
443 .await
444 .unwrap();
445 result
446 .regenerate(
447 &RegenerationFiles::Variants(
448 ["light", "bold"]
449 .map(|variant| VariantFileSet {
450 variant: variant.into(),
451 files: vec![path.clone()],
452 })
453 .into(),
454 ),
455 &[(
456 path,
457 GlyphChange::Changed {
458 name: Some("updated".into()),
459 },
460 )],
461 )
462 .unwrap();
463 assert_eq!(result.source_files.len(), 2);
464 assert!(
465 result
466 .source_files
467 .iter()
468 .all(|source| source.glyph_name == "updated")
469 );
470 assert_eq!(result.options.codepoints.len(), 1);
471}
472
473#[cfg(feature = "fs")]
488pub async fn generate_with_hooks<H: GenerationHooks>(
489 options: GenerateWebfontsOptions,
490 hooks: &H,
491) -> Result<GenerateWebfontsResult, H::Error> {
492 let mut resolved_options = resolve_validated_generate_webfonts_options(options)?;
493 let threads = resolved_options.threads;
494 let mut result = if let Some(variants) = &resolved_options.variants {
495 let variant_paths = variants
496 .variants
497 .iter()
498 .map(|variant| variant.source_ids.clone())
499 .collect::<Vec<_>>();
500 let source_files =
501 load_variant_svg_files_with_hooks(&variant_paths, hooks, threads).await?;
502 let generation = tokio::task::spawn_blocking(move || {
503 crate::threads::install(threads, || {
504 generate_variant_webfonts_sync(resolved_options, source_files)
505 })
506 });
507 generation.await.map_err(variant_preparation_join_error)??
508 } else {
509 let source_files =
510 load_svg_files_with_hooks(&resolved_options.files, hooks, true, threads).await?;
511 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
512 tokio::task::spawn_blocking(move || {
513 crate::threads::install(threads, || {
514 generate_webfonts_sync(resolved_options, source_files)
515 })
516 })
517 .await
518 .map_err(|error| {
519 std::io::Error::other(format!("Native webfont generation task failed: {error}"))
520 })??
521 };
522
523 if hooks.has_css_context() || hooks.has_html_context() {
527 let shared = SharedTemplateData::from_files(&result.options, &result.source_files)?;
528
529 let mut css_ctx = build_css_context(&result.options, &shared);
530 if hooks.has_css_context() {
531 css_ctx = hooks.css_context(css_ctx).await?;
532 css_ctx.insert(
533 "__webfontVariantMode".to_owned(),
534 serde_json::Value::Bool(result.options.variants.is_some()),
535 );
536 result.css_context = Some(css_ctx.clone());
537 }
538
539 let mut html_ctx =
540 if result.options.html || hooks.has_html_context() || result.options.variants.is_some()
541 {
542 if result.options.variants.is_some() && hooks.has_css_context() {
543 rendering::build_html_context_with_css(
544 &result.options,
545 &shared,
546 &result.source_files,
547 &css_ctx,
548 )?
549 } else {
550 build_html_context(&result.options, &shared, &result.source_files, None)?
551 }
552 } else {
553 serde_json::Map::new()
554 };
555 if hooks.has_html_context() {
556 html_ctx = hooks.html_context(html_ctx).await?;
557 result.html_context = Some(html_ctx.clone());
558 }
559
560 let html_template = load_html_template(&result.options)?;
562 let css_hbs_context =
563 handlebars::Context::wraps(&css_ctx).map_err(std::io::Error::other)?;
564 let html_hbs_context =
565 handlebars::Context::wraps(&html_ctx).map_err(std::io::Error::other)?;
566 let _ = result.cached.set(Ok(CachedTemplateData {
567 shared,
568 css_context: css_ctx,
569 css_hbs_context: Mutex::new(css_hbs_context),
570 html_context: html_ctx,
571 html_hbs_context: Mutex::new(html_hbs_context),
572 html_template,
573 render_cache: Mutex::new(Default::default()),
574 #[cfg(test)]
575 html_render_count: Default::default(),
576 }));
577 }
578
579 if result.options.write_files
580 && let Some(written) = write_generate_webfonts_result(&result).await?
581 {
582 result.seed_written_outputs(written);
584 }
585
586 Ok(result)
587}
588
589#[cfg(feature = "fs")]
591pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
592
593#[cfg(feature = "fs")]
598pub async fn generate(
599 options: GenerateWebfontsOptions,
600 rename: Option<RenameFn>,
601) -> std::io::Result<GenerateWebfontsResult> {
602 generate_with_hooks(options, &hooks::RenameHooks(rename.as_deref())).await
603}
604
605#[cfg(feature = "fs")]
608pub fn generate_sync(
609 options: GenerateWebfontsOptions,
610 rename: Option<RenameFn>,
611) -> std::io::Result<GenerateWebfontsResult> {
612 tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
613}