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