#[cfg(feature = "bench")]
pub mod bench_support;
mod byte_helpers;
mod formats;
mod incremental;
mod input;
mod output;
mod pipeline;
mod rendering;
mod result;
mod sfnt;
mod svg;
#[cfg(test)]
mod test_helpers;
mod types;
#[cfg(feature = "napi")]
use napi::Status;
#[cfg(feature = "napi")]
use napi::threadsafe_function::ThreadsafeFunction;
#[cfg(feature = "napi")]
use napi_derive::napi;
#[cfg(feature = "napi")]
use std::sync::Mutex;
use input::{
ResolvedGenerateWebfontsOptions, build_variant_family_sources,
finalize_generate_webfonts_options, load_svg_files, load_variant_svg_files,
resolve_generate_webfonts_options, resolve_missing_glyphs, validate_generate_webfonts_options,
};
#[cfg(feature = "napi")]
use input::{load_svg_files_napi, load_variant_svg_files_napi};
use output::write_generate_webfonts_result;
use pipeline::{generate_variant_webfonts_sync, generate_webfonts_sync};
#[cfg(feature = "napi")]
use rendering::{
CachedTemplateData, SharedTemplateData, apply_context_function, build_css_context,
build_html_context, build_html_registry_and_dependencies,
};
#[cfg(feature = "napi")]
use result::to_napi_err;
pub use result::{GenerateWebfontsResult, RegenerateError};
pub use types::{
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,
)?;
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)
}
#[cfg(feature = "napi")]
fn variant_preparation_join_error(error: tokio::task::JoinError) -> napi::Error {
napi::Error::new(
Status::GenericFailure,
format!("Native variant preparation task failed: {error}"),
)
}
#[cfg(all(test, feature = "napi"))]
#[unsafe(no_mangle)]
extern "C" fn napi_call_threadsafe_function(
_: napi::sys::napi_threadsafe_function,
_: *mut std::ffi::c_void,
_: napi::sys::napi_threadsafe_function_call_mode,
) -> napi::sys::napi_status {
0
}
#[cfg(all(test, feature = "napi"))]
#[unsafe(no_mangle)]
extern "C" fn napi_release_threadsafe_function(
_: napi::sys::napi_threadsafe_function,
_: napi::sys::napi_threadsafe_function_release_mode,
) -> napi::sys::napi_status {
0
}
#[cfg(all(test, feature = "napi"))]
#[tokio::test]
async fn napi_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
.reason
.starts_with("Native variant preparation task failed:")
);
}
#[cfg(all(test, feature = "napi"))]
#[tokio::test]
async fn napi_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_webfonts(options, None, None, None)
.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_webfonts(
GenerateWebfontsOptions {
dest: "artifacts".to_owned(),
files: vec![],
types: Some(vec![FontType::Woff2]),
variants: Some(duplicate),
..Default::default()
},
None,
None,
None,
)
.await
.err()
.expect("duplicate names within one variant should fail");
assert!(error.reason.contains("must be unique"));
}
#[cfg(all(test, feature = "napi"))]
#[tokio::test]
async fn napi_generation_keeps_the_ordinary_source_path() {
let result = generate_webfonts(
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()
},
None,
None,
None,
)
.await
.expect("ordinary NAPI generation should succeed");
assert!(result.svg_string().is_some());
}
#[cfg(feature = "napi")]
#[napi]
#[allow(clippy::type_complexity)] pub async fn generate_webfonts(
options: GenerateWebfontsOptions,
rename: Option<ThreadsafeFunction<Vec<String>, Vec<String>, Vec<String>, Status, false>>,
css_context: Option<
ThreadsafeFunction<
serde_json::Map<String, serde_json::Value>,
serde_json::Map<String, serde_json::Value>,
serde_json::Map<String, serde_json::Value>,
Status,
false,
>,
>,
html_context: Option<
ThreadsafeFunction<
serde_json::Map<String, serde_json::Value>,
serde_json::Map<String, serde_json::Value>,
serde_json::Map<String, serde_json::Value>,
Status,
false,
>,
>,
) -> napi::Result<GenerateWebfontsResult> {
validate_generate_webfonts_options(&options)?;
let mut result = if options.variants.is_some() {
let mut resolved_options = resolve_generate_webfonts_options(options)?;
let variant_paths = resolved_options
.variants
.as_ref()
.expect("validated variant options must resolve variants")
.variants
.iter()
.map(|variant| variant.files.clone())
.collect::<Vec<_>>();
let source_files = load_variant_svg_files_napi(&variant_paths, rename.as_ref()).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_napi(&options.files, rename.as_ref(), true).await?;
let mut resolved_options = resolve_generate_webfonts_options(options)?;
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| {
napi::Error::new(
Status::GenericFailure,
format!("Native webfont generation task failed: {error}"),
)
})??
};
if css_context.is_some() || html_context.is_some() {
let shared =
SharedTemplateData::new(&result.options, &result.source_files).map_err(to_napi_err)?;
let mut css_ctx = build_css_context(&result.options, &shared);
if css_context.is_some() {
css_ctx = apply_context_function(css_ctx, css_context.as_ref())
.await
.map_err(to_napi_err)?;
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 || html_context.is_some() || result.options.variants.is_some() {
if result.options.variants.is_some() && css_context.is_some() {
rendering::build_html_context_with_css(
&result.options,
&shared,
&result.source_files,
&css_ctx,
)
.map_err(to_napi_err)?
} else {
build_html_context(&result.options, &shared, &result.source_files, None)
.map_err(to_napi_err)?
}
} else {
serde_json::Map::new()
};
if html_context.is_some() {
html_ctx = apply_context_function(html_ctx, html_context.as_ref())
.await
.map_err(to_napi_err)?;
result.html_context = Some(html_ctx.clone());
}
let (html_registry, html_template_dependencies) =
build_html_registry_and_dependencies(&result.options).map_err(to_napi_err)?;
let css_hbs_context = handlebars::Context::wraps(&css_ctx).map_err(to_napi_err)?;
let html_hbs_context = handlebars::Context::wraps(&html_ctx).map_err(to_napi_err)?;
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> {
validate_generate_webfonts_options(&options)?;
let result = if options.variants.is_some() {
let mut resolved_options = resolve_generate_webfonts_options(options)?;
let variant_paths = resolved_options
.variants
.as_ref()
.expect("validated variant options must resolve variants")
.variants
.iter()
.map(|variant| variant.files.clone())
.collect::<Vec<_>>();
let source_files = load_variant_svg_files(&variant_paths, rename.as_deref()).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(std::io::Error::other)??
} else {
let source_files = load_svg_files(&options.files, rename.as_deref()).await?;
let mut resolved_options = resolve_generate_webfonts_options(options)?;
finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
.await
.map_err(std::io::Error::other)??
};
if result.options.write_files
&& let Some(written) = write_generate_webfonts_result(&result).await?
{
result.seed_written_outputs(written);
}
Ok(result)
}
pub fn generate_sync(
options: GenerateWebfontsOptions,
rename: Option<RenameFn>,
) -> std::io::Result<GenerateWebfontsResult> {
tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
}