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