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