#[cfg(feature = "bench")]
pub mod bench_support;
mod byte_helpers;
mod formats;
mod hooks;
mod incremental;
mod input;
mod output;
mod pipeline;
mod rendering;
mod result;
mod sfnt;
mod svg;
#[cfg(test)]
mod test_helpers;
mod types;
pub use hooks::GenerationHooks;
use std::sync::Mutex;
use input::{
ResolvedGenerateWebfontsOptions, build_variant_family_sources,
finalize_generate_webfonts_options, resolve_generate_webfonts_options, resolve_missing_glyphs,
validate_generate_webfonts_options,
};
use input::{load_svg_files_with_hooks, load_variant_svg_files_with_hooks};
use output::write_generate_webfonts_result;
use pipeline::{generate_variant_webfonts_sync, generate_webfonts_sync};
use rendering::{
CachedTemplateData, SharedTemplateData, build_css_context, build_html_context,
build_html_registry_and_dependencies,
};
pub use result::{GenerateWebfontsResult, RegenerateError};
pub use types::{
ColorGlyphSelection, CssContext, FontType, FontVariant, FormatOptions, GenerateWebfontsOptions,
GlyphChange, GlyphChangeEntry, HtmlContext, MissingGlyphBehavior, MissingGlyphOptions,
RegenerationFiles, SvgFormatOptions, TemplateVariant, TtfFormatOptions, VariantFileSet,
Woff2FormatOptions, WoffFormatOptions,
};
type PreparedVariantInput = (
svg::types::PreparedVariantFamily,
Vec<input::LoadedSvgFile>,
Option<svg::VariantGlyphCache>,
);
fn prepare_variant_family(
options: &mut ResolvedGenerateWebfontsOptions,
source_files: Vec<Vec<input::LoadedSvgFile>>,
) -> std::io::Result<PreparedVariantInput> {
let mut cache = options.incremental.then(svg::VariantGlyphCache::default);
let family = prepare_variant_family_cached(
options,
source_files.iter().map(Vec::as_slice).collect(),
cache.as_mut(),
)?;
let sources = source_files.into_iter().flatten().collect();
Ok((family, sources, cache))
}
fn prepare_variant_family_cached(
options: &mut ResolvedGenerateWebfontsOptions,
source_files: Vec<&[input::LoadedSvgFile]>,
cache: Option<&mut svg::VariantGlyphCache>,
) -> std::io::Result<svg::types::PreparedVariantFamily> {
let (mut family, codepoints) = build_variant_family_sources(
source_files,
&options.explicit_codepoints,
options.start_codepoint,
)?;
input::validate_color_names(
options,
family.glyphs.iter().map(|glyph| glyph.name.as_str()),
)?;
let variants = options
.variants
.as_ref()
.expect("variant source preparation requires resolved variants");
let variant_names = variants
.variants
.iter()
.map(|variant| variant.name.as_str())
.collect::<Vec<_>>();
let fallback_index = options.missing_glyphs.variant.as_deref().map(|name| {
variant_names
.iter()
.position(|variant_name| *variant_name == name)
.expect("validated fallback must name a resolved variant")
});
resolve_missing_glyphs(
&mut family,
options.missing_glyphs.behavior,
fallback_index,
&variant_names,
)?;
options.codepoints = codepoints;
let svg_options = svg::svg_options_from_options(options);
let prepared = match cache {
Some(cache) => svg::prepare_variant_svg_family_cached(&svg_options, &family, Some(cache))?,
None => svg::prepare_variant_svg_family(&svg_options, &family)?,
};
Ok(prepared)
}
fn variant_preparation_join_error(error: tokio::task::JoinError) -> std::io::Error {
std::io::Error::other(format!("Native variant preparation task failed: {error}"))
}
#[cfg(test)]
#[tokio::test]
async fn variant_preparation_reports_panicking_worker() {
let join_error = tokio::task::spawn_blocking(|| panic!("variant preparation panic"))
.await
.unwrap_err();
let error = variant_preparation_join_error(join_error);
assert!(
error
.to_string()
.starts_with("Native variant preparation task failed:")
);
}
#[cfg(test)]
#[tokio::test]
async fn hooked_variant_generation_succeeds_after_preparing_sources() {
let path = test_helpers::webfont_fixture("add.svg");
let variants = vec![
FontVariant {
name: "small".to_owned(),
files: vec![path.clone()],
weight: Some(300),
default: Some(true),
},
FontVariant {
name: "large".to_owned(),
files: vec![path.clone()],
weight: Some(700),
default: None,
},
];
let options = GenerateWebfontsOptions {
dest: "artifacts".to_owned(),
files: vec![],
types: Some(vec![FontType::Woff2]),
variants: Some(variants.clone()),
write_files: Some(false),
..Default::default()
};
let result = generate_with_hooks(options, &())
.await
.expect("variant generation should succeed");
assert!(result.woff2_bytes().is_some());
assert!(result.eot_bytes().is_none());
let mut duplicate = variants;
duplicate[0].files.push(path);
let error = generate_with_hooks(
GenerateWebfontsOptions {
dest: "artifacts".to_owned(),
files: vec![],
types: Some(vec![FontType::Woff2]),
variants: Some(duplicate),
..Default::default()
},
&(),
)
.await
.err()
.expect("duplicate names within one variant should fail");
assert!(error.to_string().contains("must be unique"));
}
#[cfg(test)]
#[tokio::test]
async fn hooked_generation_keeps_the_ordinary_source_path() {
let result = generate_with_hooks(
GenerateWebfontsOptions {
css: Some(false),
dest: "artifacts".to_owned(),
files: vec![test_helpers::webfont_fixture("add.svg")],
html: Some(false),
types: Some(vec![FontType::Svg]),
write_files: Some(false),
..Default::default()
},
&(),
)
.await
.expect("ordinary generation with hooks should succeed");
assert!(result.svg_string().is_some());
}
pub async fn generate_with_hooks<H: GenerationHooks>(
options: GenerateWebfontsOptions,
hooks: &H,
) -> Result<GenerateWebfontsResult, H::Error> {
validate_generate_webfonts_options(&options)?;
let mut resolved_options = resolve_generate_webfonts_options(options)?;
let mut result = if let Some(variants) = &resolved_options.variants {
let variant_paths = variants
.variants
.iter()
.map(|variant| variant.files.clone())
.collect::<Vec<_>>();
let source_files = load_variant_svg_files_with_hooks(&variant_paths, hooks).await?;
let generation = tokio::task::spawn_blocking(move || {
let (family, source_files, cache) =
prepare_variant_family(&mut resolved_options, source_files)?;
generate_variant_webfonts_sync(resolved_options, source_files, family, cache)
});
generation.await.map_err(variant_preparation_join_error)??
} else {
let source_files = load_svg_files_with_hooks(&resolved_options.files, hooks, true).await?;
finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
.await
.map_err(|error| {
std::io::Error::other(format!("Native webfont generation task failed: {error}"))
})??
};
if hooks.has_css_context() || hooks.has_html_context() {
let shared = SharedTemplateData::new(&result.options, &result.source_files)?;
let mut css_ctx = build_css_context(&result.options, &shared);
if hooks.has_css_context() {
css_ctx = hooks.css_context(css_ctx).await?;
css_ctx.insert(
"__webfontVariantMode".to_owned(),
serde_json::Value::Bool(result.options.variants.is_some()),
);
result.css_context = Some(css_ctx.clone());
}
let mut html_ctx =
if result.options.html || hooks.has_html_context() || result.options.variants.is_some()
{
if result.options.variants.is_some() && hooks.has_css_context() {
rendering::build_html_context_with_css(
&result.options,
&shared,
&result.source_files,
&css_ctx,
)?
} else {
build_html_context(&result.options, &shared, &result.source_files, None)?
}
} else {
serde_json::Map::new()
};
if hooks.has_html_context() {
html_ctx = hooks.html_context(html_ctx).await?;
result.html_context = Some(html_ctx.clone());
}
let (html_registry, html_template_dependencies) =
build_html_registry_and_dependencies(&result.options)?;
let css_hbs_context =
handlebars::Context::wraps(&css_ctx).map_err(std::io::Error::other)?;
let html_hbs_context =
handlebars::Context::wraps(&html_ctx).map_err(std::io::Error::other)?;
let _ = result.cached.set(Ok(CachedTemplateData {
shared,
css_context: css_ctx,
css_hbs_context: Mutex::new(css_hbs_context),
html_context: html_ctx,
html_hbs_context: Mutex::new(html_hbs_context),
html_template_dependencies,
html_registry,
render_cache: Mutex::new(Default::default()),
}));
}
if result.options.write_files
&& let Some(written) = write_generate_webfonts_result(&result).await?
{
result.seed_written_outputs(written);
}
Ok(result)
}
pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
pub async fn generate(
options: GenerateWebfontsOptions,
rename: Option<RenameFn>,
) -> std::io::Result<GenerateWebfontsResult> {
generate_with_hooks(options, &hooks::RenameHooks(rename.as_deref())).await
}
pub fn generate_sync(
options: GenerateWebfontsOptions,
rename: Option<RenameFn>,
) -> std::io::Result<GenerateWebfontsResult> {
tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
}