1#[cfg(feature = "bench")]
63pub mod bench_support;
64mod byte_helpers;
65mod formats;
66mod incremental;
67mod input;
68mod output;
69mod pipeline;
70mod rendering;
71mod result;
72mod sfnt;
73mod svg;
74#[cfg(test)]
75mod test_helpers;
76mod types;
77
78#[cfg(feature = "napi")]
79use napi::Status;
80#[cfg(feature = "napi")]
81use napi::threadsafe_function::ThreadsafeFunction;
82#[cfg(feature = "napi")]
83use napi_derive::napi;
84#[cfg(feature = "napi")]
85use std::sync::Mutex;
86
87use input::{
88 ResolvedGenerateWebfontsOptions, build_variant_family_sources,
89 finalize_generate_webfonts_options, load_svg_files, load_variant_svg_files,
90 resolve_generate_webfonts_options, resolve_missing_glyphs, validate_generate_webfonts_options,
91};
92#[cfg(feature = "napi")]
93use input::{load_svg_files_napi, load_variant_svg_files_napi};
94use output::write_generate_webfonts_result;
95use pipeline::{generate_variant_webfonts_sync, generate_webfonts_sync};
96#[cfg(feature = "napi")]
97use rendering::{
98 CachedTemplateData, SharedTemplateData, apply_context_function, build_css_context,
99 build_html_context, build_html_registry_and_dependencies,
100};
101#[cfg(feature = "napi")]
102use result::to_napi_err;
103pub use result::{GenerateWebfontsResult, RegenerateError};
104pub use types::{
105 CssContext, FontType, FontVariant, FormatOptions, GenerateWebfontsOptions, GlyphChange,
106 GlyphChangeEntry, HtmlContext, MissingGlyphBehavior, MissingGlyphOptions, RegenerationFiles,
107 SvgFormatOptions, TemplateVariant, TtfFormatOptions, VariantFileSet, Woff2FormatOptions,
108 WoffFormatOptions,
109};
110
111type PreparedVariantInput = (
112 svg::types::PreparedVariantFamily,
113 Vec<input::LoadedSvgFile>,
114 Option<svg::VariantGlyphCache>,
115);
116
117fn prepare_variant_family(
118 options: &mut ResolvedGenerateWebfontsOptions,
119 source_files: Vec<Vec<input::LoadedSvgFile>>,
120) -> std::io::Result<PreparedVariantInput> {
121 let mut cache = options.incremental.then(svg::VariantGlyphCache::default);
122 let family = prepare_variant_family_cached(
123 options,
124 source_files.iter().map(Vec::as_slice).collect(),
125 cache.as_mut(),
126 )?;
127 let sources = source_files.into_iter().flatten().collect();
128 Ok((family, sources, cache))
129}
130
131fn prepare_variant_family_cached(
132 options: &mut ResolvedGenerateWebfontsOptions,
133 source_files: Vec<&[input::LoadedSvgFile]>,
134 cache: Option<&mut svg::VariantGlyphCache>,
135) -> std::io::Result<svg::types::PreparedVariantFamily> {
136 let (mut family, codepoints) = build_variant_family_sources(
137 source_files,
138 &options.explicit_codepoints,
139 options.start_codepoint,
140 )?;
141 let variants = options
142 .variants
143 .as_ref()
144 .expect("variant source preparation requires resolved variants");
145 let variant_names = variants
146 .variants
147 .iter()
148 .map(|variant| variant.name.as_str())
149 .collect::<Vec<_>>();
150 let fallback_index = options.missing_glyphs.variant.as_deref().map(|name| {
151 variant_names
152 .iter()
153 .position(|variant_name| *variant_name == name)
154 .expect("validated fallback must name a resolved variant")
155 });
156 resolve_missing_glyphs(
157 &mut family,
158 options.missing_glyphs.behavior,
159 fallback_index,
160 &variant_names,
161 )?;
162 options.codepoints = codepoints;
163 let svg_options = svg::svg_options_from_options(options);
164 let prepared = match cache {
165 Some(cache) => svg::prepare_variant_svg_family_cached(&svg_options, &family, Some(cache))?,
166 None => svg::prepare_variant_svg_family(&svg_options, &family)?,
167 };
168 Ok(prepared)
169}
170
171#[cfg(feature = "napi")]
172fn variant_preparation_join_error(error: tokio::task::JoinError) -> napi::Error {
173 napi::Error::new(
174 Status::GenericFailure,
175 format!("Native variant preparation task failed: {error}"),
176 )
177}
178
179#[cfg(all(test, feature = "napi"))]
180#[unsafe(no_mangle)]
181extern "C" fn napi_call_threadsafe_function(
182 _: napi::sys::napi_threadsafe_function,
183 _: *mut std::ffi::c_void,
184 _: napi::sys::napi_threadsafe_function_call_mode,
185) -> napi::sys::napi_status {
186 0
187}
188
189#[cfg(all(test, feature = "napi"))]
190#[unsafe(no_mangle)]
191extern "C" fn napi_release_threadsafe_function(
192 _: napi::sys::napi_threadsafe_function,
193 _: napi::sys::napi_threadsafe_function_release_mode,
194) -> napi::sys::napi_status {
195 0
196}
197
198#[cfg(all(test, feature = "napi"))]
199#[tokio::test]
200async fn napi_variant_preparation_reports_panicking_worker() {
201 let join_error = tokio::task::spawn_blocking(|| panic!("variant preparation panic"))
202 .await
203 .unwrap_err();
204 let error = variant_preparation_join_error(join_error);
205
206 assert!(
207 error
208 .reason
209 .starts_with("Native variant preparation task failed:")
210 );
211}
212
213#[cfg(all(test, feature = "napi"))]
214#[tokio::test]
215async fn napi_variant_generation_succeeds_after_preparing_sources() {
216 let path = test_helpers::webfont_fixture("add.svg");
217 let variants = vec![
218 FontVariant {
219 name: "small".to_owned(),
220 files: vec![path.clone()],
221 weight: Some(300),
222 default: Some(true),
223 },
224 FontVariant {
225 name: "large".to_owned(),
226 files: vec![path.clone()],
227 weight: Some(700),
228 default: None,
229 },
230 ];
231 let options = GenerateWebfontsOptions {
232 dest: "artifacts".to_owned(),
233 files: vec![],
234 types: Some(vec![FontType::Woff2]),
235 variants: Some(variants.clone()),
236 write_files: Some(false),
237 ..Default::default()
238 };
239
240 let result = generate_webfonts(options, None, None, None)
241 .await
242 .expect("variant generation should succeed");
243 assert!(result.woff2_bytes().is_some());
244 assert!(result.eot_bytes().is_none());
245
246 let mut duplicate = variants;
247 duplicate[0].files.push(path);
248 let error = generate_webfonts(
249 GenerateWebfontsOptions {
250 dest: "artifacts".to_owned(),
251 files: vec![],
252 types: Some(vec![FontType::Woff2]),
253 variants: Some(duplicate),
254 ..Default::default()
255 },
256 None,
257 None,
258 None,
259 )
260 .await
261 .err()
262 .expect("duplicate names within one variant should fail");
263 assert!(error.reason.contains("must be unique"));
264}
265
266#[cfg(all(test, feature = "napi"))]
267#[tokio::test]
268async fn napi_generation_keeps_the_ordinary_source_path() {
269 let result = generate_webfonts(
270 GenerateWebfontsOptions {
271 css: Some(false),
272 dest: "artifacts".to_owned(),
273 files: vec![test_helpers::webfont_fixture("add.svg")],
274 html: Some(false),
275 types: Some(vec![FontType::Svg]),
276 write_files: Some(false),
277 ..Default::default()
278 },
279 None,
280 None,
281 None,
282 )
283 .await
284 .expect("ordinary NAPI generation should succeed");
285
286 assert!(result.svg_string().is_some());
287}
288
289#[cfg(feature = "napi")]
304#[napi]
305#[allow(clippy::type_complexity)] pub async fn generate_webfonts(
307 options: GenerateWebfontsOptions,
308 rename: Option<ThreadsafeFunction<Vec<String>, Vec<String>, Vec<String>, Status, false>>,
309 css_context: Option<
310 ThreadsafeFunction<
311 serde_json::Map<String, serde_json::Value>,
312 serde_json::Map<String, serde_json::Value>,
313 serde_json::Map<String, serde_json::Value>,
314 Status,
315 false,
316 >,
317 >,
318 html_context: Option<
319 ThreadsafeFunction<
320 serde_json::Map<String, serde_json::Value>,
321 serde_json::Map<String, serde_json::Value>,
322 serde_json::Map<String, serde_json::Value>,
323 Status,
324 false,
325 >,
326 >,
327) -> napi::Result<GenerateWebfontsResult> {
328 validate_generate_webfonts_options(&options)?;
329 let mut result = if options.variants.is_some() {
330 let mut resolved_options = resolve_generate_webfonts_options(options)?;
331 let variant_paths = resolved_options
332 .variants
333 .as_ref()
334 .expect("validated variant options must resolve variants")
335 .variants
336 .iter()
337 .map(|variant| variant.files.clone())
338 .collect::<Vec<_>>();
339 let source_files = load_variant_svg_files_napi(&variant_paths, rename.as_ref()).await?;
340 let generation = tokio::task::spawn_blocking(move || {
341 let (family, source_files, cache) =
342 prepare_variant_family(&mut resolved_options, source_files)?;
343 generate_variant_webfonts_sync(resolved_options, source_files, family, cache)
344 });
345 generation.await.map_err(variant_preparation_join_error)??
346 } else {
347 let source_files = load_svg_files_napi(&options.files, rename.as_ref(), true).await?;
348 let mut resolved_options = resolve_generate_webfonts_options(options)?;
349 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
350 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
351 .await
352 .map_err(|error| {
353 napi::Error::new(
354 Status::GenericFailure,
355 format!("Native webfont generation task failed: {error}"),
356 )
357 })??
358 };
359
360 if css_context.is_some() || html_context.is_some() {
364 let shared =
365 SharedTemplateData::new(&result.options, &result.source_files).map_err(to_napi_err)?;
366
367 let mut css_ctx = build_css_context(&result.options, &shared);
368 if css_context.is_some() {
369 css_ctx = apply_context_function(css_ctx, css_context.as_ref())
370 .await
371 .map_err(to_napi_err)?;
372 css_ctx.insert(
373 "__webfontVariantMode".to_owned(),
374 serde_json::Value::Bool(result.options.variants.is_some()),
375 );
376 result.css_context = Some(css_ctx.clone());
377 }
378
379 let mut html_ctx =
380 if result.options.html || html_context.is_some() || result.options.variants.is_some() {
381 if result.options.variants.is_some() && css_context.is_some() {
382 rendering::build_html_context_with_css(
383 &result.options,
384 &shared,
385 &result.source_files,
386 &css_ctx,
387 )
388 .map_err(to_napi_err)?
389 } else {
390 build_html_context(&result.options, &shared, &result.source_files, None)
391 .map_err(to_napi_err)?
392 }
393 } else {
394 serde_json::Map::new()
395 };
396 if html_context.is_some() {
397 html_ctx = apply_context_function(html_ctx, html_context.as_ref())
398 .await
399 .map_err(to_napi_err)?;
400 result.html_context = Some(html_ctx.clone());
401 }
402
403 let (html_registry, html_template_dependencies) =
405 build_html_registry_and_dependencies(&result.options).map_err(to_napi_err)?;
406 let css_hbs_context = handlebars::Context::wraps(&css_ctx).map_err(to_napi_err)?;
407 let html_hbs_context = handlebars::Context::wraps(&html_ctx).map_err(to_napi_err)?;
408 let _ = result.cached.set(Ok(CachedTemplateData {
409 shared,
410 css_context: css_ctx,
411 css_hbs_context: Mutex::new(css_hbs_context),
412 html_context: html_ctx,
413 html_hbs_context: Mutex::new(html_hbs_context),
414 html_template_dependencies,
415 html_registry,
416 render_cache: Mutex::new(Default::default()),
417 }));
418 }
419
420 if result.options.write_files
421 && let Some(written) = write_generate_webfonts_result(&result).await?
422 {
423 result.seed_written_outputs(written);
425 }
426
427 Ok(result)
428}
429
430pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
432
433pub async fn generate(
438 options: GenerateWebfontsOptions,
439 rename: Option<RenameFn>,
440) -> std::io::Result<GenerateWebfontsResult> {
441 validate_generate_webfonts_options(&options)?;
442 let result = if options.variants.is_some() {
443 let mut resolved_options = resolve_generate_webfonts_options(options)?;
444 let variant_paths = resolved_options
445 .variants
446 .as_ref()
447 .expect("validated variant options must resolve variants")
448 .variants
449 .iter()
450 .map(|variant| variant.files.clone())
451 .collect::<Vec<_>>();
452 let source_files = load_variant_svg_files(&variant_paths, rename.as_deref()).await?;
453 let generation = tokio::task::spawn_blocking(move || {
454 let (family, source_files, cache) =
455 prepare_variant_family(&mut resolved_options, source_files)?;
456 generate_variant_webfonts_sync(resolved_options, source_files, family, cache)
457 });
458 generation.await.map_err(std::io::Error::other)??
459 } else {
460 let source_files = load_svg_files(&options.files, rename.as_deref()).await?;
461 let mut resolved_options = resolve_generate_webfonts_options(options)?;
462 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
463 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
464 .await
465 .map_err(std::io::Error::other)??
466 };
467
468 if result.options.write_files
469 && let Some(written) = write_generate_webfonts_result(&result).await?
470 {
471 result.seed_written_outputs(written);
473 }
474
475 Ok(result)
476}
477
478pub fn generate_sync(
481 options: GenerateWebfontsOptions,
482 rename: Option<RenameFn>,
483) -> std::io::Result<GenerateWebfontsResult> {
484 tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
485}