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