1#[cfg(feature = "bench")]
64pub mod bench_support;
65mod byte_helpers;
66mod formats;
67mod hooks;
68mod incremental;
69mod input;
70mod output;
71mod pipeline;
72mod rendering;
73mod result;
74mod sfnt;
75mod svg;
76#[cfg(test)]
77mod test_helpers;
78mod types;
79
80pub use hooks::GenerationHooks;
81use std::sync::Mutex;
82
83use input::{
84 ResolvedGenerateWebfontsOptions, build_variant_family_sources,
85 finalize_generate_webfonts_options, resolve_generate_webfonts_options, resolve_missing_glyphs,
86 validate_generate_webfonts_options,
87};
88use input::{load_svg_files_with_hooks, load_variant_svg_files_with_hooks};
89use output::write_generate_webfonts_result;
90use pipeline::{generate_variant_webfonts_sync, generate_webfonts_sync};
91use rendering::{
92 CachedTemplateData, SharedTemplateData, build_css_context, build_html_context,
93 build_html_registry_and_dependencies,
94};
95pub use result::{GenerateWebfontsResult, RegenerateError};
96pub use types::{
97 CssContext, FontType, FontVariant, FormatOptions, GenerateWebfontsOptions, GlyphChange,
98 GlyphChangeEntry, HtmlContext, MissingGlyphBehavior, MissingGlyphOptions, RegenerationFiles,
99 SvgFormatOptions, TemplateVariant, TtfFormatOptions, VariantFileSet, Woff2FormatOptions,
100 WoffFormatOptions,
101};
102
103type PreparedVariantInput = (
104 svg::types::PreparedVariantFamily,
105 Vec<input::LoadedSvgFile>,
106 Option<svg::VariantGlyphCache>,
107);
108
109fn prepare_variant_family(
110 options: &mut ResolvedGenerateWebfontsOptions,
111 source_files: Vec<Vec<input::LoadedSvgFile>>,
112) -> std::io::Result<PreparedVariantInput> {
113 let mut cache = options.incremental.then(svg::VariantGlyphCache::default);
114 let family = prepare_variant_family_cached(
115 options,
116 source_files.iter().map(Vec::as_slice).collect(),
117 cache.as_mut(),
118 )?;
119 let sources = source_files.into_iter().flatten().collect();
120 Ok((family, sources, cache))
121}
122
123fn prepare_variant_family_cached(
124 options: &mut ResolvedGenerateWebfontsOptions,
125 source_files: Vec<&[input::LoadedSvgFile]>,
126 cache: Option<&mut svg::VariantGlyphCache>,
127) -> std::io::Result<svg::types::PreparedVariantFamily> {
128 let (mut family, codepoints) = build_variant_family_sources(
129 source_files,
130 &options.explicit_codepoints,
131 options.start_codepoint,
132 )?;
133 let variants = options
134 .variants
135 .as_ref()
136 .expect("variant source preparation requires resolved variants");
137 let variant_names = variants
138 .variants
139 .iter()
140 .map(|variant| variant.name.as_str())
141 .collect::<Vec<_>>();
142 let fallback_index = options.missing_glyphs.variant.as_deref().map(|name| {
143 variant_names
144 .iter()
145 .position(|variant_name| *variant_name == name)
146 .expect("validated fallback must name a resolved variant")
147 });
148 resolve_missing_glyphs(
149 &mut family,
150 options.missing_glyphs.behavior,
151 fallback_index,
152 &variant_names,
153 )?;
154 options.codepoints = codepoints;
155 let svg_options = svg::svg_options_from_options(options);
156 let prepared = match cache {
157 Some(cache) => svg::prepare_variant_svg_family_cached(&svg_options, &family, Some(cache))?,
158 None => svg::prepare_variant_svg_family(&svg_options, &family)?,
159 };
160 Ok(prepared)
161}
162
163fn variant_preparation_join_error(error: tokio::task::JoinError) -> std::io::Error {
164 std::io::Error::other(format!("Native variant preparation task failed: {error}"))
165}
166
167#[cfg(test)]
168#[tokio::test]
169async fn variant_preparation_reports_panicking_worker() {
170 let join_error = tokio::task::spawn_blocking(|| panic!("variant preparation panic"))
171 .await
172 .unwrap_err();
173 let error = variant_preparation_join_error(join_error);
174
175 assert!(
176 error
177 .to_string()
178 .starts_with("Native variant preparation task failed:")
179 );
180}
181
182#[cfg(test)]
183#[tokio::test]
184async fn hooked_variant_generation_succeeds_after_preparing_sources() {
185 let path = test_helpers::webfont_fixture("add.svg");
186 let variants = vec![
187 FontVariant {
188 name: "small".to_owned(),
189 files: vec![path.clone()],
190 weight: Some(300),
191 default: Some(true),
192 },
193 FontVariant {
194 name: "large".to_owned(),
195 files: vec![path.clone()],
196 weight: Some(700),
197 default: None,
198 },
199 ];
200 let options = GenerateWebfontsOptions {
201 dest: "artifacts".to_owned(),
202 files: vec![],
203 types: Some(vec![FontType::Woff2]),
204 variants: Some(variants.clone()),
205 write_files: Some(false),
206 ..Default::default()
207 };
208
209 let result = generate_with_hooks(options, &())
210 .await
211 .expect("variant generation should succeed");
212 assert!(result.woff2_bytes().is_some());
213 assert!(result.eot_bytes().is_none());
214
215 let mut duplicate = variants;
216 duplicate[0].files.push(path);
217 let error = generate_with_hooks(
218 GenerateWebfontsOptions {
219 dest: "artifacts".to_owned(),
220 files: vec![],
221 types: Some(vec![FontType::Woff2]),
222 variants: Some(duplicate),
223 ..Default::default()
224 },
225 &(),
226 )
227 .await
228 .err()
229 .expect("duplicate names within one variant should fail");
230 assert!(error.to_string().contains("must be unique"));
231}
232
233#[cfg(test)]
234#[tokio::test]
235async fn hooked_generation_keeps_the_ordinary_source_path() {
236 let result = generate_with_hooks(
237 GenerateWebfontsOptions {
238 css: Some(false),
239 dest: "artifacts".to_owned(),
240 files: vec![test_helpers::webfont_fixture("add.svg")],
241 html: Some(false),
242 types: Some(vec![FontType::Svg]),
243 write_files: Some(false),
244 ..Default::default()
245 },
246 &(),
247 )
248 .await
249 .expect("ordinary generation with hooks should succeed");
250
251 assert!(result.svg_string().is_some());
252}
253
254pub async fn generate_with_hooks<H: GenerationHooks>(
269 options: GenerateWebfontsOptions,
270 hooks: &H,
271) -> Result<GenerateWebfontsResult, H::Error> {
272 validate_generate_webfonts_options(&options)?;
273 let mut result = if options.variants.is_some() {
274 let mut resolved_options = resolve_generate_webfonts_options(options)?;
275 let variant_paths = resolved_options
276 .variants
277 .as_ref()
278 .expect("validated variant options must resolve variants")
279 .variants
280 .iter()
281 .map(|variant| variant.files.clone())
282 .collect::<Vec<_>>();
283 let source_files = load_variant_svg_files_with_hooks(&variant_paths, hooks).await?;
284 let generation = tokio::task::spawn_blocking(move || {
285 let (family, source_files, cache) =
286 prepare_variant_family(&mut resolved_options, source_files)?;
287 generate_variant_webfonts_sync(resolved_options, source_files, family, cache)
288 });
289 generation.await.map_err(variant_preparation_join_error)??
290 } else {
291 let source_files = load_svg_files_with_hooks(&options.files, hooks, true).await?;
292 let mut resolved_options = resolve_generate_webfonts_options(options)?;
293 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
294 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
295 .await
296 .map_err(|error| {
297 std::io::Error::other(format!("Native webfont generation task failed: {error}"))
298 })??
299 };
300
301 if hooks.has_css_context() || hooks.has_html_context() {
305 let shared = SharedTemplateData::new(&result.options, &result.source_files)?;
306
307 let mut css_ctx = build_css_context(&result.options, &shared);
308 if hooks.has_css_context() {
309 css_ctx = hooks.css_context(css_ctx).await?;
310 css_ctx.insert(
311 "__webfontVariantMode".to_owned(),
312 serde_json::Value::Bool(result.options.variants.is_some()),
313 );
314 result.css_context = Some(css_ctx.clone());
315 }
316
317 let mut html_ctx =
318 if result.options.html || hooks.has_html_context() || result.options.variants.is_some()
319 {
320 if result.options.variants.is_some() && hooks.has_css_context() {
321 rendering::build_html_context_with_css(
322 &result.options,
323 &shared,
324 &result.source_files,
325 &css_ctx,
326 )?
327 } else {
328 build_html_context(&result.options, &shared, &result.source_files, None)?
329 }
330 } else {
331 serde_json::Map::new()
332 };
333 if hooks.has_html_context() {
334 html_ctx = hooks.html_context(html_ctx).await?;
335 result.html_context = Some(html_ctx.clone());
336 }
337
338 let (html_registry, html_template_dependencies) =
340 build_html_registry_and_dependencies(&result.options)?;
341 let css_hbs_context =
342 handlebars::Context::wraps(&css_ctx).map_err(std::io::Error::other)?;
343 let html_hbs_context =
344 handlebars::Context::wraps(&html_ctx).map_err(std::io::Error::other)?;
345 let _ = result.cached.set(Ok(CachedTemplateData {
346 shared,
347 css_context: css_ctx,
348 css_hbs_context: Mutex::new(css_hbs_context),
349 html_context: html_ctx,
350 html_hbs_context: Mutex::new(html_hbs_context),
351 html_template_dependencies,
352 html_registry,
353 render_cache: Mutex::new(Default::default()),
354 }));
355 }
356
357 if result.options.write_files
358 && let Some(written) = write_generate_webfonts_result(&result).await?
359 {
360 result.seed_written_outputs(written);
362 }
363
364 Ok(result)
365}
366
367pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
369
370pub async fn generate(
375 options: GenerateWebfontsOptions,
376 rename: Option<RenameFn>,
377) -> std::io::Result<GenerateWebfontsResult> {
378 generate_with_hooks(options, &hooks::RenameHooks(rename.as_deref())).await
379}
380
381pub fn generate_sync(
384 options: GenerateWebfontsOptions,
385 rename: Option<RenameFn>,
386) -> std::io::Result<GenerateWebfontsResult> {
387 tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
388}