1mod eot;
63mod incremental;
64mod sfnt;
65mod svg;
66mod templates;
67#[cfg(test)]
68mod test_helpers;
69mod ttf;
70mod types;
71mod util;
72mod woff;
73mod write;
74
75#[cfg(feature = "napi")]
76use napi::threadsafe_function::ThreadsafeFunction;
77#[cfg(feature = "napi")]
78use napi::{Error as NapiError, Status};
79#[cfg(feature = "napi")]
80use napi_derive::napi;
81use rayon::join;
82use std::collections::HashSet;
83use std::io::ErrorKind;
84use std::path::Path;
85use std::sync::Arc;
86#[cfg(feature = "napi")]
87use std::sync::Mutex;
88use tokio::task::JoinSet;
89
90use svg::types::{GlyphCache, PreparedSvgFont, SvgOptions};
91use svg::{
92 build_svg_font, prepare_svg_font, prepare_svg_font_incremental, svg_options_from_options,
93};
94#[cfg(feature = "napi")]
95use templates::{
96 SharedTemplateData, apply_context_function, build_css_context, build_html_context,
97 build_html_registry_and_dependencies,
98};
99use ttf::TtfGlyphCache;
100#[cfg(feature = "napi")]
101use util::to_napi_err;
102use write::write_generate_webfonts_result;
103
104pub use types::{
105 CssContext, FontType, FormatOptions, GenerateWebfontsOptions, GenerateWebfontsResult,
106 GlyphChange, GlyphChangeEntry, HtmlContext, RegenerateError, SvgFormatOptions,
107 TtfFormatOptions, Woff2FormatOptions, WoffFormatOptions,
108};
109use types::{
110 DEFAULT_FONT_ORDER, FontOutputs, LoadedSvgFile, RegenerationState,
111 ResolvedGenerateWebfontsOptions, resolved_font_types,
112};
113
114#[cfg(feature = "bench")]
115pub mod bench_support {
116 use std::io;
117
118 use super::{
119 GenerateWebfontsOptions, GenerateWebfontsResult, GlyphCache, LoadedSvgFile,
120 PreparedSvgFont, build_font_outputs, finalize_generate_webfonts_options, prepare_svg_font,
121 prepare_svg_font_incremental, resolve_generate_webfonts_options, svg_options_from_options,
122 };
123 use crate::sfnt::SerializedFontTables;
124 use crate::svg::types::ParsedGlyph;
125 use crate::svg::{finalize_glyphs, parse_glyphs};
126 use crate::ttf::{self, Woff2TransformCache};
127 use write_fonts::FontBuilder;
128 use write_fonts::types::Tag;
129
130 #[derive(Clone)]
132 pub struct BenchSvgSource {
133 pub path: String,
134 pub glyph_name: String,
135 pub contents: String,
136 }
137
138 #[derive(Clone, Default)]
140 pub struct BenchGlyphCache(GlyphCache);
141
142 pub struct BenchSvgPrepareInput {
144 options: super::ResolvedGenerateWebfontsOptions,
145 sources: Vec<LoadedSvgFile>,
146 }
147
148 #[derive(Clone)]
150 pub struct BenchParsedGlyphs(Vec<ParsedGlyph>);
151
152 #[derive(Clone)]
154 pub struct BenchPreparedSvgFont(PreparedSvgFont);
155
156 #[derive(Clone)]
158 pub struct BenchSerializedFontTables(SerializedFontTables);
159
160 #[derive(Clone, Default)]
162 pub struct BenchWoff2TransformCache(Woff2TransformCache);
163
164 pub struct BenchPreparedWoff2(crate::woff::PreparedWoff2);
166
167 fn load_sources(sources: &[BenchSvgSource]) -> Vec<LoadedSvgFile> {
168 sources
169 .iter()
170 .map(|source| LoadedSvgFile {
171 contents: source.contents.clone().into(),
172 glyph_name: source.glyph_name.clone(),
173 path: source.path.clone(),
174 })
175 .collect()
176 }
177
178 fn resolve(
179 options: GenerateWebfontsOptions,
180 sources: &[LoadedSvgFile],
181 ) -> io::Result<super::ResolvedGenerateWebfontsOptions> {
182 let mut options = resolve_generate_webfonts_options(options)?;
183 finalize_generate_webfonts_options(&mut options, sources)?;
184 Ok(options)
185 }
186
187 pub fn svg_prepare_input(
189 options: GenerateWebfontsOptions,
190 sources: &[BenchSvgSource],
191 ) -> io::Result<BenchSvgPrepareInput> {
192 let sources = load_sources(sources);
193 let options = resolve(options, &sources)?;
194 Ok(BenchSvgPrepareInput { options, sources })
195 }
196
197 pub fn prepare_svg_full(input: &BenchSvgPrepareInput) -> io::Result<usize> {
199 let svg_options = svg_options_from_options(&input.options);
200 let prepared = prepare_svg_font(&svg_options, &input.sources)?;
201 Ok(prepared.processed_glyphs.len())
202 }
203
204 pub fn parse_svg_only(
206 options: GenerateWebfontsOptions,
207 sources: &[BenchSvgSource],
208 ) -> io::Result<BenchParsedGlyphs> {
209 let sources = load_sources(sources);
210 let options = resolve(options, &sources)?;
211 let svg_options = svg_options_from_options(&options);
212 parse_glyphs(&svg_options, &sources).map(BenchParsedGlyphs)
213 }
214
215 pub fn finalize_svg_only(
217 options: GenerateWebfontsOptions,
218 sources: &[BenchSvgSource],
219 parsed: BenchParsedGlyphs,
220 ) -> io::Result<BenchPreparedSvgFont> {
221 let sources = load_sources(sources);
222 let options = resolve(options, &sources)?;
223 let svg_options = svg_options_from_options(&options);
224 finalize_glyphs(&svg_options, parsed.0).map(BenchPreparedSvgFont)
225 }
226
227 pub fn build_outputs_only(
229 options: GenerateWebfontsOptions,
230 sources: &[BenchSvgSource],
231 prepared: &BenchPreparedSvgFont,
232 ) -> io::Result<usize> {
233 let sources = load_sources(sources);
234 let options = resolve(options, &sources)?;
235 let svg_options = svg_options_from_options(&options);
236 let fonts = build_font_outputs(&options, &svg_options, &prepared.0, None)?;
237 Ok(fonts.svg_font.as_ref().map_or(0, |v| v.len())
238 + fonts.ttf_font.as_ref().map_or(0, |v| v.len())
239 + fonts.woff_font.as_ref().map_or(0, |v| v.len())
240 + fonts.woff2_font.as_ref().map_or(0, |v| v.len())
241 + fonts.eot_font.as_ref().map_or(0, |v| v.len()))
242 }
243
244 pub fn build_serialized_ttf_tables(
246 options: GenerateWebfontsOptions,
247 sources: &[BenchSvgSource],
248 prepared: &BenchPreparedSvgFont,
249 ) -> io::Result<BenchSerializedFontTables> {
250 let sources = load_sources(sources);
251 let options = resolve(options, &sources)?;
252 let ttf_options = ttf::ttf_options_from_options(&options);
253 ttf::generate_ttf_font_from_glyphs(ttf_options, &prepared.0.processed_glyphs)
254 .map(BenchSerializedFontTables)
255 }
256
257 pub fn rewrap_serialized_ttf_tables(
259 tables: &BenchSerializedFontTables,
260 ) -> io::Result<BenchSerializedFontTables> {
261 SerializedFontTables::new(tables.0.clone_raw_tables()).map(BenchSerializedFontTables)
262 }
263
264 pub fn serialized_ttf_uncached(tables: &BenchSerializedFontTables) -> Vec<u8> {
266 tables.0.uncached_ttf()
267 }
268
269 pub fn internal_woff2(tables: &BenchSerializedFontTables, quality: u8) -> io::Result<Vec<u8>> {
271 crate::woff::tables_to_woff2(&tables.0, quality, None)
272 }
273
274 pub fn prepare_internal_woff2(
276 tables: &BenchSerializedFontTables,
277 cache: &mut BenchWoff2TransformCache,
278 ) -> io::Result<BenchPreparedWoff2> {
279 crate::woff::prepare_woff2(&tables.0, &mut cache.0).map(BenchPreparedWoff2)
280 }
281
282 pub fn compress_prepared_internal_woff2(
284 prepared: &BenchPreparedWoff2,
285 quality: u8,
286 ) -> io::Result<usize> {
287 crate::woff::compress_prepared_woff2(&prepared.0, quality)
288 }
289
290 pub fn fontbuilder_ttf(tables: &BenchSerializedFontTables) -> Vec<u8> {
292 let mut builder = FontBuilder::new();
293 for table in tables.0.tables() {
294 builder.add_raw(Tag::new(&table.tag), table.bytes.as_slice());
295 }
296 builder.build()
297 }
298
299 pub fn clear_woff1_payload_cache(result: &mut GenerateWebfontsResult) {
301 if let Some(state) = result.regeneration_state.lock().unwrap().as_mut()
302 && let Some(cache) = state.ttf_cache.as_mut()
303 {
304 cache.clear_woff1_payloads();
305 }
306 }
307
308 pub fn prepare_svg_incremental(
310 input: &BenchSvgPrepareInput,
311 cache: &mut BenchGlyphCache,
312 ) -> io::Result<usize> {
313 let svg_options = svg_options_from_options(&input.options);
314 let prepared = prepare_svg_font_incremental(&svg_options, &input.sources, &mut cache.0)?;
315 Ok(prepared.processed_glyphs.len())
316 }
317}
318
319#[cfg(all(test, feature = "napi"))]
320#[unsafe(no_mangle)]
321extern "C" fn napi_call_threadsafe_function(
322 _: napi::sys::napi_threadsafe_function,
323 _: *mut std::ffi::c_void,
324 _: napi::sys::napi_threadsafe_function_call_mode,
325) -> napi::sys::napi_status {
326 0
327}
328
329#[cfg(feature = "napi")]
342#[napi]
343#[allow(clippy::type_complexity)] pub async fn generate_webfonts(
345 options: GenerateWebfontsOptions,
346 rename: Option<ThreadsafeFunction<Vec<String>, Vec<String>, Vec<String>, Status, false>>,
347 css_context: Option<
348 ThreadsafeFunction<
349 serde_json::Map<String, serde_json::Value>,
350 serde_json::Map<String, serde_json::Value>,
351 serde_json::Map<String, serde_json::Value>,
352 Status,
353 false,
354 >,
355 >,
356 html_context: Option<
357 ThreadsafeFunction<
358 serde_json::Map<String, serde_json::Value>,
359 serde_json::Map<String, serde_json::Value>,
360 serde_json::Map<String, serde_json::Value>,
361 Status,
362 false,
363 >,
364 >,
365) -> napi::Result<GenerateWebfontsResult> {
366 validate_generate_webfonts_options(&options)?;
367 let source_files = load_svg_files_napi(&options.files, rename.as_ref()).await?;
368 let mut resolved_options = resolve_generate_webfonts_options(options)?;
369 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
370
371 let mut result =
372 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
373 .await
374 .map_err(|error| {
375 NapiError::new(
376 Status::GenericFailure,
377 format!("Native webfont generation task failed: {error}"),
378 )
379 })??;
380
381 if css_context.is_some() || html_context.is_some() {
385 let shared =
386 SharedTemplateData::new(&result.options, &result.source_files).map_err(to_napi_err)?;
387
388 let mut css_ctx = build_css_context(&result.options, &shared);
389 if css_context.is_some() {
390 css_ctx = apply_context_function(css_ctx, css_context.as_ref())
391 .await
392 .map_err(to_napi_err)?;
393 result.css_context = Some(css_ctx.clone());
394 }
395
396 let mut html_ctx = if result.options.html || html_context.is_some() {
397 build_html_context(&result.options, &shared, &result.source_files, None)
398 .map_err(to_napi_err)?
399 } else {
400 serde_json::Map::new()
401 };
402 if html_context.is_some() {
403 html_ctx = apply_context_function(html_ctx, html_context.as_ref())
404 .await
405 .map_err(to_napi_err)?;
406 result.html_context = Some(html_ctx.clone());
407 }
408
409 let (html_registry, html_template_dependencies) =
411 build_html_registry_and_dependencies(&result.options).map_err(to_napi_err)?;
412 let css_hbs_context = handlebars::Context::wraps(&css_ctx).map_err(to_napi_err)?;
413 let html_hbs_context = handlebars::Context::wraps(&html_ctx).map_err(to_napi_err)?;
414 let _ = result.cached.set(Ok(types::CachedTemplateData {
415 shared,
416 css_context: css_ctx,
417 css_hbs_context: Mutex::new(css_hbs_context),
418 html_context: html_ctx,
419 html_hbs_context: Mutex::new(html_hbs_context),
420 html_template_dependencies,
421 html_registry,
422 render_cache: Mutex::new(Default::default()),
423 }));
424 }
425
426 if result.options.write_files
427 && let Some(written) = write_generate_webfonts_result(&result).await?
428 {
429 result.seed_written_outputs(written);
431 }
432
433 Ok(result)
434}
435
436pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
438
439pub async fn generate(
443 options: GenerateWebfontsOptions,
444 rename: Option<RenameFn>,
445) -> std::io::Result<GenerateWebfontsResult> {
446 validate_generate_webfonts_options(&options)?;
447 let source_files = load_svg_files(&options.files, rename.as_deref()).await?;
448 let mut resolved_options = resolve_generate_webfonts_options(options)?;
449 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
450
451 let result =
452 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
453 .await
454 .map_err(std::io::Error::other)??;
455
456 if result.options.write_files
457 && let Some(written) = write_generate_webfonts_result(&result).await?
458 {
459 result.seed_written_outputs(written);
461 }
462
463 Ok(result)
464}
465
466pub fn generate_sync(
468 options: GenerateWebfontsOptions,
469 rename: Option<RenameFn>,
470) -> std::io::Result<GenerateWebfontsResult> {
471 tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
472}
473
474fn validate_generate_webfonts_options(options: &GenerateWebfontsOptions) -> std::io::Result<()> {
475 if options.dest.is_empty() {
476 return Err(std::io::Error::new(
477 ErrorKind::InvalidInput,
478 "\"options.dest\" is empty.".to_owned(),
479 ));
480 }
481
482 if options.files.is_empty() {
483 return Err(std::io::Error::new(
484 ErrorKind::InvalidInput,
485 "\"options.files\" is empty.".to_owned(),
486 ));
487 }
488
489 if options.css.unwrap_or(true)
490 && let Some(ref path) = options.css_template
491 && !Path::new(path).exists()
492 {
493 return Err(std::io::Error::new(
494 ErrorKind::InvalidInput,
495 format!("\"options.cssTemplate\" file not found: {path}"),
496 ));
497 }
498
499 if options.html.unwrap_or(false)
500 && let Some(ref path) = options.html_template
501 && !Path::new(path).exists()
502 {
503 return Err(std::io::Error::new(
504 ErrorKind::InvalidInput,
505 format!("\"options.htmlTemplate\" file not found: {path}"),
506 ));
507 }
508
509 if let Some(quality) = options
510 .format_options
511 .as_ref()
512 .and_then(|value| value.woff2.as_ref())
513 .and_then(|value| value.compression_quality)
514 && quality > 11
515 {
516 return Err(std::io::Error::new(
517 ErrorKind::InvalidInput,
518 format!(
519 "\"options.formatOptions.woff2.compressionQuality\" must be between 0 and 11, got {quality}."
520 ),
521 ));
522 }
523
524 Ok(())
525}
526
527pub(crate) fn resolve_generate_webfonts_options(
528 options: GenerateWebfontsOptions,
529) -> std::io::Result<ResolvedGenerateWebfontsOptions> {
530 let types = resolved_font_types(&options);
531 validate_font_type_order(&options, &types)?;
532 let order = resolve_font_type_order(&options, &types);
533 let css = options.css.unwrap_or(true);
534 let html = options.html.unwrap_or(false);
535 let font_name = options.font_name.unwrap_or_else(|| "iconfont".to_owned());
536 let css_dest = options
537 .css_dest
538 .unwrap_or_else(|| default_output_dest(&options.dest, &font_name, "css"));
539 let html_dest = options
540 .html_dest
541 .unwrap_or_else(|| default_output_dest(&options.dest, &font_name, "html"));
542 let write_files = options.write_files.unwrap_or(true);
543 let explicit_codepoints: std::collections::BTreeMap<String, u32> =
544 options.codepoints.unwrap_or_default().into_iter().collect();
545
546 let svg_format = options
547 .format_options
548 .as_ref()
549 .and_then(|fo| fo.svg.as_ref());
550 let center_vertically = svg_format
551 .and_then(|s| s.center_vertically)
552 .or(options.center_vertically);
553 let optimize_output = svg_format
554 .and_then(|s| s.optimize_output)
555 .or(options.optimize_output);
556 let preserve_aspect_ratio = svg_format
557 .and_then(|s| s.preserve_aspect_ratio)
558 .or(options.preserve_aspect_ratio);
559
560 Ok(ResolvedGenerateWebfontsOptions {
561 ascent: options.ascent,
562 center_horizontally: options.center_horizontally,
563 center_vertically,
564 css,
565 css_dest,
566 css_template: match options.css_template {
567 Some(ref t) if t.is_empty() => {
568 return Err(std::io::Error::new(
569 ErrorKind::InvalidInput,
570 "\"options.cssTemplate\" must not be empty.".to_owned(),
571 ));
572 }
573 other => other,
574 },
575 codepoints: explicit_codepoints.clone(),
576 explicit_codepoints,
577 css_fonts_url: options.css_fonts_url,
578 descent: options.descent,
579 dest: options.dest,
580 files: options.files,
581 fixed_width: options.fixed_width,
582 format_options: options.format_options,
583 html,
584 html_dest,
585 html_template: match options.html_template {
586 Some(ref t) if t.is_empty() => {
587 return Err(std::io::Error::new(
588 ErrorKind::InvalidInput,
589 "\"options.htmlTemplate\" must not be empty.".to_owned(),
590 ));
591 }
592 other => other,
593 },
594 incremental: options.incremental.unwrap_or(false),
595 font_height: options.font_height,
596 font_name,
597 font_style: options.font_style,
598 font_weight: options.font_weight,
599 ligature: options.ligature.unwrap_or(true),
600 normalize: options.normalize.unwrap_or(true),
601 order,
602 optimize_output,
603 preserve_aspect_ratio,
604 round: options.round,
605 start_codepoint: options.start_codepoint.unwrap_or(0xF101),
606 template_options: options.template_options,
607 types,
608 write_files,
609 })
610}
611
612pub(crate) fn finalize_generate_webfonts_options(
613 options: &mut ResolvedGenerateWebfontsOptions,
614 source_files: &[LoadedSvgFile],
615) -> std::io::Result<()> {
616 options.codepoints = resolve_codepoints(
617 source_files,
618 &options.explicit_codepoints,
619 options.start_codepoint,
620 )?;
621
622 Ok(())
623}
624
625fn resolve_font_type_order(options: &GenerateWebfontsOptions, types: &[FontType]) -> Vec<FontType> {
626 match &options.order {
627 Some(order) => order.clone(),
628 None => DEFAULT_FONT_ORDER
629 .iter()
630 .copied()
631 .filter(|font_type| types.contains(font_type))
632 .collect(),
633 }
634}
635
636fn default_output_dest(dest: &str, font_name: &str, extension: &str) -> String {
637 Path::new(dest)
638 .join(format!("{font_name}.{extension}"))
639 .to_string_lossy()
640 .into_owned()
641}
642
643fn generate_webfonts_sync(
644 options: ResolvedGenerateWebfontsOptions,
645 source_files: Vec<LoadedSvgFile>,
646) -> std::io::Result<GenerateWebfontsResult> {
647 let svg_options = svg_options_from_options(&options);
648 let (prepared, glyph_cache, mut ttf_cache) = if options.incremental {
652 let mut cache = GlyphCache::default();
653 let prepared = prepare_svg_font_incremental(&svg_options, &source_files, &mut cache)?;
654 (prepared, Some(cache), Some(TtfGlyphCache::default()))
655 } else {
656 (prepare_svg_font(&svg_options, &source_files)?, None, None)
657 };
658 let fonts = build_font_outputs(&options, &svg_options, &prepared, ttf_cache.as_mut())?;
659 let regeneration_state = glyph_cache.map(|glyph_cache| RegenerationState {
660 caches_dirty: false,
661 glyph_cache,
662 ttf_cache,
663 written_outputs: std::collections::HashMap::new(),
664 });
665
666 Ok(GenerateWebfontsResult {
667 cached: std::sync::OnceLock::new(),
668 carried_render: None,
669 css_context: None,
670 fonts,
671 html_context: None,
672 options: std::sync::Arc::new(options),
673 regeneration_state: std::sync::Arc::new(std::sync::Mutex::new(regeneration_state)),
674 source_files: std::sync::Arc::new(source_files),
675 })
676}
677
678fn build_font_outputs(
680 options: &ResolvedGenerateWebfontsOptions,
681 svg_options: &SvgOptions<'_>,
682 prepared: &PreparedSvgFont,
683 mut ttf_cache: Option<&mut TtfGlyphCache>,
684) -> std::io::Result<FontOutputs> {
685 let wants_svg = options.types.contains(&FontType::Svg);
686 let wants_ttf = options.types.contains(&FontType::Ttf);
687 let wants_woff = options.types.contains(&FontType::Woff);
688 let wants_woff2 = options.types.contains(&FontType::Woff2);
689 let wants_eot = options.types.contains(&FontType::Eot);
690
691 let (svg_font, ttf_tables) = join(
692 || -> std::io::Result<Option<String>> {
693 if wants_svg {
694 Ok(Some(build_svg_font(svg_options, prepared)))
695 } else {
696 Ok(None)
697 }
698 },
699 || -> std::io::Result<Option<sfnt::SerializedFontTables>> {
700 if wants_ttf || wants_woff || wants_woff2 || wants_eot {
701 let ttf_options = ttf::ttf_options_from_options(options);
702 match ttf_cache.as_deref_mut() {
703 Some(cache) => ttf::generate_ttf_font_from_glyphs_cached(
704 ttf_options,
705 &prepared.processed_glyphs,
706 cache,
707 )
708 .map(Some),
709 None => {
710 ttf::generate_ttf_font_from_glyphs(ttf_options, &prepared.processed_glyphs)
711 .map(Some)
712 }
713 }
714 } else {
715 Ok(None)
716 }
717 },
718 );
719
720 let svg_font = svg_font?.map(Arc::new);
721 let ttf_tables = ttf_tables?;
722
723 let (ttf_font, woff_font, woff2_font, eot_font) = if let Some(ttf_tables) = ttf_tables {
724 let woff_metadata = options
725 .format_options
726 .as_ref()
727 .and_then(|value| value.woff.as_ref())
728 .and_then(|value| value.metadata.as_deref());
729 let woff2_quality = options
730 .format_options
731 .as_ref()
732 .and_then(|value| value.woff2.as_ref())
733 .and_then(|value| value.compression_quality)
734 .unwrap_or(11);
735
736 let ttf_tables = Arc::new(ttf_tables);
737 let ttf_font = wants_ttf.then(|| ttf_tables.ttf_arc());
738 let (woff1_cache, woff2_cache) = match ttf_cache {
739 Some(cache) => {
740 let (woff1, woff2) = cache.output_caches();
741 (Some(woff1), Some(woff2))
742 }
743 None => (None, None),
744 };
745 let (woff_font, (woff2_font, eot_font)) = join(
746 || -> std::io::Result<Option<Vec<u8>>> {
747 if wants_woff {
748 match woff1_cache {
749 Some(cache) => {
750 woff::tables_to_woff1_cached(&ttf_tables, woff_metadata, cache)
751 }
752 None => woff::tables_to_woff1(&ttf_tables, woff_metadata),
753 }
754 .map(Some)
755 } else {
756 Ok(None)
757 }
758 },
759 || {
760 join(
761 || -> std::io::Result<Option<Vec<u8>>> {
762 if wants_woff2 {
763 woff::tables_to_woff2(&ttf_tables, woff2_quality, woff2_cache).map(Some)
764 } else {
765 Ok(None)
766 }
767 },
768 || -> std::io::Result<Option<Vec<u8>>> {
769 if wants_eot {
770 eot::tables_to_eot(&ttf_tables).map(Some)
771 } else {
772 Ok(None)
773 }
774 },
775 )
776 },
777 );
778
779 (
780 ttf_font,
781 woff_font?.map(Arc::new),
782 woff2_font?.map(Arc::new),
783 eot_font?.map(Arc::new),
784 )
785 } else {
786 (None, None, None, None)
787 };
788
789 Ok(FontOutputs {
790 svg_font,
791 ttf_font,
792 woff_font,
793 woff2_font,
794 eot_font,
795 })
796}
797
798fn validate_font_type_order(
799 options: &GenerateWebfontsOptions,
800 requested_types: &[FontType],
801) -> std::io::Result<()> {
802 if let Some(order) = &options.order
803 && let Some(invalid_type) = order
804 .iter()
805 .copied()
806 .find(|font_type| !requested_types.contains(font_type))
807 {
808 return Err(std::io::Error::new(
809 ErrorKind::InvalidInput,
810 format!(
811 "Invalid font type order: '{}' is not present in 'types'.",
812 invalid_type.as_extension()
813 ),
814 ));
815 }
816
817 Ok(())
818}
819
820async fn load_svg_contents(paths: &[String]) -> std::io::Result<Vec<(String, String)>> {
822 let mut tasks = JoinSet::new();
823
824 for (index, path) in paths.iter().cloned().enumerate() {
825 tasks.spawn(async move {
826 tokio::fs::read_to_string(&path)
827 .await
828 .map(|contents| (index, (path, contents)))
829 });
830 }
831
832 let mut results = Vec::with_capacity(paths.len());
833 while let Some(result) = tasks.join_next().await {
834 let (index, pair) = result
835 .map_err(|error| std::io::Error::other(format!("SVG loading task failed: {error}")))?
836 .map_err(|error| {
837 std::io::Error::other(format!("Failed to read source SVG file: {error}"))
838 })?;
839 results.push((index, pair));
840 }
841
842 results.sort_by_key(|(index, _)| *index);
843 Ok(results.into_iter().map(|(_, pair)| pair).collect())
844}
845
846async fn load_svg_files(
848 paths: &[String],
849 rename: Option<&(dyn Fn(&str) -> String + Send + Sync)>,
850) -> std::io::Result<Vec<LoadedSvgFile>> {
851 let raw = load_svg_contents(paths).await?;
852 let source_files: Vec<LoadedSvgFile> = raw
853 .into_iter()
854 .map(|(path, contents)| {
855 let glyph_name = util::glyph_name_from_path(&path, rename)?;
856 Ok(LoadedSvgFile {
857 contents: contents.into(),
858 glyph_name,
859 path,
860 })
861 })
862 .collect::<std::io::Result<_>>()?;
863
864 validate_glyph_names(&source_files)?;
865 Ok(source_files)
866}
867
868#[cfg(feature = "napi")]
870#[allow(clippy::type_complexity)]
871async fn load_svg_files_napi(
872 paths: &[String],
873 rename: Option<
874 &napi::threadsafe_function::ThreadsafeFunction<
875 Vec<String>,
876 Vec<String>,
877 Vec<String>,
878 Status,
879 false,
880 >,
881 >,
882) -> napi::Result<Vec<LoadedSvgFile>> {
883 let raw = load_svg_contents(paths).await.map_err(to_napi_err)?;
884 let glyph_names = if let Some(rename) = rename {
885 let glyph_names = rename.call_async_catch(paths.to_vec()).await?;
886 if glyph_names.len() != raw.len() {
887 return Err(NapiError::new(
888 Status::InvalidArg,
889 "rename callback returned an unexpected number of glyph names".to_owned(),
890 ));
891 }
892 glyph_names
893 } else {
894 raw.iter()
895 .map(|(path, _)| util::default_glyph_name_from_path(path).map_err(to_napi_err))
896 .collect::<napi::Result<_>>()?
897 };
898 let source_files = raw
899 .into_iter()
900 .zip(glyph_names)
901 .map(|((path, contents), glyph_name)| LoadedSvgFile {
902 contents: contents.into(),
903 glyph_name,
904 path,
905 })
906 .collect::<Vec<_>>();
907
908 validate_glyph_names(&source_files).map_err(to_napi_err)?;
909 Ok(source_files)
910}
911
912pub(crate) fn validate_glyph_names(source_files: &[LoadedSvgFile]) -> std::io::Result<()> {
913 let mut seen_names = HashSet::with_capacity(source_files.len());
914
915 for source_file in source_files {
916 if !seen_names.insert(source_file.glyph_name.clone()) {
917 return Err(std::io::Error::new(
918 ErrorKind::InvalidInput,
919 format!(
920 "The glyph name \"{}\" must be unique.",
921 source_file.glyph_name
922 ),
923 ));
924 }
925 }
926
927 Ok(())
928}
929
930use util::resolve_codepoints;
932
933#[cfg(test)]
934mod tests {
935 use super::{
936 resolve_generate_webfonts_options, resolved_font_types, validate_font_type_order,
937 validate_generate_webfonts_options, woff,
938 };
939 use crate::{
940 FontType, FormatOptions, GenerateWebfontsOptions, Woff2FormatOptions,
941 ttf::generate_ttf_font_bytes,
942 };
943
944 #[test]
945 fn generates_woff2_font_with_expected_header() {
946 let ttf_result = generate_ttf_font_bytes(GenerateWebfontsOptions {
947 css: Some(false),
948 dest: "artifacts".to_owned(),
949 files: vec![format!(
950 "{}/../vite-svg-2-webfont/src/fixtures/webfont-test/svg/add.svg",
951 env!("CARGO_MANIFEST_DIR")
952 )],
953 html: Some(false),
954 font_name: Some("iconfont".to_owned()),
955 ligature: Some(false),
956 ..Default::default()
957 })
958 .expect("expected ttf generation to succeed");
959
960 let result = woff::ttf_to_woff2(&ttf_result, 10).expect("woff2 generation should succeed");
961
962 assert_eq!(&result[..4], b"wOF2");
963 }
964
965 #[test]
966 fn rejects_order_entries_that_are_not_present_in_types() {
967 let options = GenerateWebfontsOptions {
968 dest: "artifacts".to_owned(),
969 files: vec![],
970 font_name: Some("iconfont".to_owned()),
971 ligature: Some(false),
972 order: Some(vec![FontType::Svg, FontType::Woff]),
973 types: Some(vec![FontType::Svg]),
974 ..Default::default()
975 };
976
977 let error = validate_font_type_order(&options, &resolved_font_types(&options)).unwrap_err();
978
979 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
980 assert!(
981 error
982 .to_string()
983 .contains("Invalid font type order: 'woff' is not present in 'types'.")
984 );
985 }
986
987 #[test]
988 fn rejects_an_empty_dest() {
989 let options = GenerateWebfontsOptions {
990 dest: String::new(),
991 files: vec!["icon.svg".to_owned()],
992 font_name: Some("iconfont".to_owned()),
993 ligature: Some(false),
994 types: Some(vec![FontType::Svg]),
995 ..Default::default()
996 };
997
998 let error = validate_generate_webfonts_options(&options).unwrap_err();
999
1000 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1001 assert!(error.to_string().contains("\"options.dest\" is empty."));
1002 }
1003
1004 #[test]
1005 fn rejects_empty_files() {
1006 let options = GenerateWebfontsOptions {
1007 dest: "artifacts".to_owned(),
1008 files: vec![],
1009 font_name: Some("iconfont".to_owned()),
1010 ligature: Some(false),
1011 types: Some(vec![FontType::Svg]),
1012 ..Default::default()
1013 };
1014
1015 let error = validate_generate_webfonts_options(&options).unwrap_err();
1016
1017 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1018 assert!(error.to_string().contains("\"options.files\" is empty."));
1019 }
1020
1021 fn options_with_woff2_quality(quality: u8) -> GenerateWebfontsOptions {
1022 GenerateWebfontsOptions {
1023 css: Some(false),
1024 dest: "artifacts".to_owned(),
1025 files: vec!["icon.svg".to_owned()],
1026 font_name: Some("iconfont".to_owned()),
1027 format_options: Some(FormatOptions {
1028 woff2: Some(Woff2FormatOptions {
1029 compression_quality: Some(quality),
1030 }),
1031 ..Default::default()
1032 }),
1033 html: Some(false),
1034 ligature: Some(false),
1035 types: Some(vec![FontType::Woff2]),
1036 ..Default::default()
1037 }
1038 }
1039
1040 #[test]
1041 fn rejects_woff2_compression_quality_above_11() {
1042 let error =
1043 validate_generate_webfonts_options(&options_with_woff2_quality(12)).unwrap_err();
1044
1045 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1046 assert!(error.to_string().contains(
1047 "\"options.formatOptions.woff2.compressionQuality\" must be between 0 and 11, got 12."
1048 ));
1049 }
1050
1051 #[test]
1052 fn accepts_woff2_compression_quality_of_11() {
1053 validate_generate_webfonts_options(&options_with_woff2_quality(11))
1054 .expect("compression quality 11 is the upper bound and must be accepted");
1055 }
1056
1057 #[test]
1058 fn rejects_empty_css_template() {
1059 let options = GenerateWebfontsOptions {
1060 css: Some(true),
1061 css_template: Some(String::new()),
1062 dest: "artifacts".to_owned(),
1063 files: vec!["icon.svg".to_owned()],
1064 html: Some(false),
1065 font_name: Some("iconfont".to_owned()),
1066 ligature: Some(false),
1067 types: Some(vec![FontType::Svg]),
1068 ..Default::default()
1069 };
1070
1071 let error = resolve_generate_webfonts_options(options)
1072 .err()
1073 .expect("expected empty css template to fail");
1074
1075 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1076 assert!(
1077 error
1078 .to_string()
1079 .contains("\"options.cssTemplate\" must not be empty.")
1080 );
1081 }
1082
1083 #[test]
1084 fn rejects_empty_html_template() {
1085 let options = GenerateWebfontsOptions {
1086 css: Some(false),
1087 dest: "artifacts".to_owned(),
1088 files: vec!["icon.svg".to_owned()],
1089 html: Some(true),
1090 html_template: Some(String::new()),
1091 font_name: Some("iconfont".to_owned()),
1092 ligature: Some(false),
1093 types: Some(vec![FontType::Svg]),
1094 ..Default::default()
1095 };
1096
1097 let error = resolve_generate_webfonts_options(options)
1098 .err()
1099 .expect("expected empty html template to fail");
1100
1101 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1102 assert!(
1103 error
1104 .to_string()
1105 .contains("\"options.htmlTemplate\" must not be empty.")
1106 );
1107 }
1108
1109 #[test]
1110 fn resolves_write_defaults_from_dest_and_font_name() {
1111 let options = GenerateWebfontsOptions {
1112 css: Some(false),
1113 dest: "artifacts".to_owned(),
1114 files: vec!["icon.svg".to_owned()],
1115 html: Some(false),
1116 font_name: Some("iconfont".to_owned()),
1117 ligature: Some(false),
1118 types: Some(vec![FontType::Svg]),
1119 ..Default::default()
1120 };
1121
1122 let resolved = resolve_generate_webfonts_options(options)
1123 .expect("expected defaults to resolve successfully");
1124
1125 assert!(resolved.write_files);
1126 assert_eq!(resolved.css_dest, "artifacts/iconfont.css");
1127 assert_eq!(resolved.html_dest, "artifacts/iconfont.html");
1128 }
1129
1130 #[test]
1131 fn rejects_nonexistent_css_template_when_css_is_true() {
1132 let error = validate_generate_webfonts_options(&GenerateWebfontsOptions {
1133 css: Some(true),
1134 css_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
1135 dest: "artifacts".to_owned(),
1136 files: vec!["icon.svg".to_owned()],
1137 html: Some(false),
1138 ..Default::default()
1139 })
1140 .unwrap_err();
1141
1142 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1143 assert!(error.to_string().contains("cssTemplate"));
1144 }
1145
1146 #[test]
1147 fn allows_nonexistent_css_template_when_css_is_false() {
1148 validate_generate_webfonts_options(&GenerateWebfontsOptions {
1149 css: Some(false),
1150 css_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
1151 dest: "artifacts".to_owned(),
1152 files: vec!["icon.svg".to_owned()],
1153 html: Some(false),
1154 ..Default::default()
1155 })
1156 .expect("should allow nonexistent css template when css is false");
1157 }
1158
1159 #[test]
1160 fn rejects_nonexistent_html_template_when_html_is_true() {
1161 let error = validate_generate_webfonts_options(&GenerateWebfontsOptions {
1162 css: Some(false),
1163 dest: "artifacts".to_owned(),
1164 files: vec!["icon.svg".to_owned()],
1165 html: Some(true),
1166 html_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
1167 ..Default::default()
1168 })
1169 .unwrap_err();
1170
1171 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
1172 assert!(error.to_string().contains("htmlTemplate"));
1173 }
1174
1175 #[test]
1176 fn allows_nonexistent_html_template_when_html_is_false() {
1177 validate_generate_webfonts_options(&GenerateWebfontsOptions {
1178 css: Some(false),
1179 dest: "artifacts".to_owned(),
1180 files: vec!["icon.svg".to_owned()],
1181 html: Some(false),
1182 html_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
1183 ..Default::default()
1184 })
1185 .expect("should allow nonexistent html template when html is false");
1186 }
1187}