webfont_generator/incremental/
mod.rs1#[cfg(test)]
2mod tests;
3
4mod variants;
5
6use std::collections::{HashMap, HashSet};
7use std::io::ErrorKind;
8use std::path::Path;
9use std::sync::{Arc, Mutex};
10
11use crate::input::{LoadedSvgFile, finalize_generate_webfonts_options, validate_glyph_names};
12use crate::output::write_generate_webfonts_result_sync;
13use crate::pipeline::{TtfGlyphCache, build_font_outputs};
14use crate::result::{GenerateWebfontsResult, RegenerateError};
15use crate::svg::types::GlyphCache;
16use crate::svg::{prepare_svg_font_incremental, source_content_hash, svg_options_from_options};
17use crate::types::{GlyphChange, RegenerationFiles};
18
19pub(crate) struct RegenerationState {
20 pub(crate) variant_cache: Option<crate::svg::VariantGlyphCache>,
21 pub(crate) variant_write_pending: bool,
22 pub(crate) caches_dirty: bool,
23 pub(crate) glyph_cache: GlyphCache,
24 pub(crate) ttf_cache: Option<TtfGlyphCache>,
25 pub(crate) written_outputs: HashMap<String, [u8; 16]>,
26}
27
28struct RegenerationStateLease {
29 slot: Arc<Mutex<Option<RegenerationState>>>,
30 state: Option<RegenerationState>,
31 keep_caches: bool,
32}
33
34impl RegenerationStateLease {
35 fn state_mut(&mut self) -> &mut RegenerationState {
36 self.state.as_mut().unwrap()
37 }
38
39 fn commit(mut self) {
40 self.keep_caches = true;
41 }
42}
43
44impl Drop for RegenerationStateLease {
45 fn drop(&mut self) {
46 let mut state = self.state.take().unwrap();
47 if !self.keep_caches && state.caches_dirty {
48 state.variant_cache = None;
49 state.glyph_cache = GlyphCache::default();
50 if state.ttf_cache.is_some() {
51 state.ttf_cache = Some(TtfGlyphCache::default());
52 }
53 }
54 state.caches_dirty = false;
55 *self.slot.lock().unwrap() = Some(state);
56 }
57}
58
59impl GenerateWebfontsResult {
60 fn take_regeneration_state_lease(&self) -> std::io::Result<RegenerationStateLease> {
61 Ok(RegenerationStateLease {
62 slot: Arc::clone(&self.regeneration_state),
63 state: Some(self.take_regeneration_state()?),
64 keep_caches: false,
65 })
66 }
67
68 pub fn regenerate(
123 &mut self,
124 files: &RegenerationFiles,
125 changes: &[(String, GlyphChange)],
126 ) -> std::io::Result<()> {
127 let ordered_paths = match files {
128 RegenerationFiles::Single(files) => files.as_slice(),
129 RegenerationFiles::Variants(sets) => {
130 return self.regenerate_variant_files(sets, Some(changes));
131 }
132 };
133 self.require_ordinary_regeneration()?;
134 if self.css_context.is_some() || self.html_context.is_some() {
135 return Err(std::io::Error::new(
136 ErrorKind::InvalidInput,
137 "regenerate is not supported for results generated with cssContext/htmlContext callbacks.",
138 ));
139 }
140
141 let mut state = self.take_regeneration_state_lease()?;
142 let result = self.regenerate_with_state(ordered_paths, changes, state.state_mut());
143 if result.is_ok() {
144 state.commit();
145 }
146 result
147 }
148
149 pub async fn regenerate_async(
175 self,
176 files: RegenerationFiles,
177 changes: Vec<(String, GlyphChange)>,
178 ) -> Result<Self, RegenerateError> {
179 self.regenerate_in_background(files, Some(changes)).await
180 }
181
182 fn regenerate_with_state(
183 &mut self,
184 ordered_paths: &[String],
185 changes: &[(String, GlyphChange)],
186 state: &mut RegenerationState,
187 ) -> std::io::Result<()> {
188 let cache = &mut state.glyph_cache;
189 let mut source_files = self.source_files.as_ref().clone();
190 let mut options = self.options.as_ref().clone();
191
192 let prev_names: Vec<String> = self
195 .source_files
196 .iter()
197 .map(|f| f.glyph_name.clone())
198 .collect();
199 let prev_codepoints = self.options.codepoints.clone();
200
201 let order_changed = self
202 .source_files
203 .iter()
204 .map(|file| file.path.as_str())
205 .ne(ordered_paths.iter().map(String::as_str));
206 let mut changed_inputs = order_changed;
207
208 for (path, change) in changes {
209 match change {
210 GlyphChange::Removed => {
211 state.caches_dirty = true;
212 let before = source_files.len();
213 source_files.retain(|file| &file.path != path);
214 cache.entries.remove(path);
215 cache.content_hashes.remove(path);
216 cache.processed_entries.remove(path);
217 changed_inputs |= source_files.len() != before;
218 }
219 GlyphChange::Added { name } => {
220 let contents = std::fs::read_to_string(path)?;
221 state.caches_dirty = true;
222 let hash = source_content_hash(&contents);
223 upsert_source_file(&mut source_files, path, contents, name.clone());
224 if let Some(cached) = cache.by_content_hash.get(&hash) {
227 cache.entries.insert(path.clone(), cached.clone());
228 cache.content_hashes.insert(path.clone(), hash);
229 } else {
230 cache.entries.remove(path);
231 cache.content_hashes.remove(path);
232 }
233 cache.processed_entries.remove(path);
234 changed_inputs = true;
235 }
236 GlyphChange::Changed { name } => {
237 let contents = std::fs::read_to_string(path)?;
238 let hash = source_content_hash(&contents);
239 let content_changed = cache.content_hashes.get(path) != Some(&hash);
240 let name_changed = name.as_ref().is_some_and(|name| {
241 source_files
242 .iter()
243 .find(|file| &file.path == path)
244 .is_none_or(|file| file.glyph_name != *name)
245 });
246
247 if !content_changed && !name_changed {
248 continue;
249 }
250
251 upsert_source_file(&mut source_files, path, contents, name.clone());
252 if content_changed {
253 state.caches_dirty = true;
254 if let Some(cached) = cache.by_content_hash.get(&hash) {
255 cache.entries.insert(path.clone(), cached.clone());
256 cache.content_hashes.insert(path.clone(), hash);
257 } else {
258 cache.entries.remove(path);
259 cache.content_hashes.remove(path);
260 }
261 cache.processed_entries.remove(path);
262 }
263 changed_inputs = true;
264 }
265 }
266 }
267
268 if !changed_inputs {
269 return Ok(());
270 }
271
272 let mut by_path: std::collections::HashMap<String, LoadedSvgFile> = source_files
276 .into_iter()
277 .map(|file| (file.path.clone(), file))
278 .collect();
279 let mut reordered = Vec::with_capacity(ordered_paths.len());
280 for path in ordered_paths {
281 let file = by_path.remove(path).ok_or_else(|| {
282 std::io::Error::new(
283 ErrorKind::InvalidInput,
284 format!("regenerate: `{path}` is in the ordered set but was not loaded; declare it as an added change."),
285 )
286 })?;
287 reordered.push(file);
288 }
289 source_files = reordered;
290 validate_glyph_names(&source_files)?;
291 options.files = ordered_paths.to_vec();
292
293 finalize_generate_webfonts_options(&mut options, &source_files)?;
296
297 let svg_options = svg_options_from_options(&options);
298 state.caches_dirty = true;
299 let prepared = prepare_svg_font_incremental(&svg_options, &source_files, cache)?;
300 let fonts =
301 build_font_outputs(&options, &svg_options, &prepared, state.ttf_cache.as_mut())?;
302
303 let names_unchanged = source_files
307 .iter()
308 .map(|file| file.glyph_name.as_str())
309 .eq(prev_names.iter().map(String::as_str));
310 let codepoints_unchanged = options.codepoints == prev_codepoints;
311 let carried = self.reusable_render_cache(names_unchanged, codepoints_unchanged);
312 let previous_source_files =
313 std::mem::replace(&mut self.source_files, std::sync::Arc::new(source_files));
314 let previous_options = std::mem::replace(&mut self.options, std::sync::Arc::new(options));
315 let previous_fonts = std::mem::replace(&mut self.fonts, fonts);
316 let previous_cached = std::mem::take(&mut self.cached);
317 let previous_carried = std::mem::replace(&mut self.carried_render, carried);
318 let previous_css_context = self.css_context.take();
319 let previous_html_context = self.html_context.take();
320
321 if self.options.write_files
323 && let Err(error) =
324 write_generate_webfonts_result_sync(self, &mut state.written_outputs)
325 {
326 self.source_files = previous_source_files;
327 self.options = previous_options;
328 self.fonts = previous_fonts;
329 self.cached = previous_cached;
330 self.carried_render = previous_carried;
331 self.css_context = previous_css_context;
332 self.html_context = previous_html_context;
333 return Err(error);
334 }
335 Ok(())
336 }
337
338 pub fn regenerate_all(&mut self, files: &RegenerationFiles) -> std::io::Result<()> {
360 let ordered_paths = match files {
361 RegenerationFiles::Single(files) => files.as_slice(),
362 RegenerationFiles::Variants(sets) => return self.regenerate_variant_files(sets, None),
363 };
364 self.require_ordinary_regeneration()?;
365 if !self.options.incremental {
366 return Err(std::io::Error::new(
367 ErrorKind::InvalidInput,
368 "regenerate requires the font to be generated with `incremental` enabled.",
369 ));
370 }
371 let state = self.regeneration_state.lock().unwrap();
372 let cache = &state
373 .as_ref()
374 .ok_or_else(|| {
375 std::io::Error::new(
376 ErrorKind::WouldBlock,
377 "This result is already regenerating or has been replaced.",
378 )
379 })?
380 .glyph_cache;
381 let ordered_set: HashSet<&str> = ordered_paths.iter().map(String::as_str).collect();
382 let mut previous_paths = HashSet::with_capacity(self.source_files.len());
383 let mut changes = Vec::new();
384
385 for source_file in self.source_files.iter() {
386 previous_paths.insert(source_file.path.as_str());
387 if !ordered_set.contains(source_file.path.as_str()) {
388 changes.push((source_file.path.clone(), GlyphChange::Removed));
389 }
390 }
391
392 for path in ordered_paths {
393 if !previous_paths.contains(path.as_str()) {
394 changes.push((path.clone(), GlyphChange::Added { name: None }));
395 continue;
396 }
397
398 let contents = std::fs::read_to_string(path)?;
399 let hash = source_content_hash(&contents);
400 if cache.content_hashes.get(path) != Some(&hash) {
401 changes.push((path.clone(), GlyphChange::Changed { name: None }));
402 }
403 }
404
405 drop(state);
406 self.regenerate(files, &changes)
407 }
408
409 pub async fn regenerate_all_async(
430 self,
431 files: RegenerationFiles,
432 ) -> Result<Self, RegenerateError> {
433 self.regenerate_in_background(files, None).await
434 }
435
436 async fn regenerate_in_background(
437 mut self,
438 files: RegenerationFiles,
439 changes: Option<Vec<(String, GlyphChange)>>,
440 ) -> Result<Self, RegenerateError> {
441 let task = tokio::task::spawn_blocking(move || {
442 let outcome = match changes {
443 Some(changes) => self.regenerate(&files, &changes),
444 None => self.regenerate_all(&files),
445 };
446 match outcome {
447 Ok(()) => Ok(self),
448 Err(source) => Err(RegenerateError::new(Some(self), source)),
449 }
450 });
451 match task.await {
452 Ok(result) => result,
453 Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
454 Err(error) => Err(RegenerateError::new(
455 None,
456 std::io::Error::other(format!("asynchronous regeneration task failed: {error}")),
457 )),
458 }
459 }
460}
461
462fn upsert_source_file(
465 source_files: &mut Vec<LoadedSvgFile>,
466 path: &str,
467 contents: String,
468 name: Option<String>,
469) {
470 if let Some(file) = source_files.iter_mut().find(|file| file.path == path) {
471 file.contents = contents.into();
472 if let Some(name) = name {
473 file.glyph_name = name;
474 }
475 } else {
476 let glyph_name = name.unwrap_or_else(|| {
477 Path::new(path)
478 .file_stem()
479 .and_then(|stem| stem.to_str())
480 .unwrap_or_default()
481 .to_owned()
482 });
483 source_files.push(LoadedSvgFile {
484 contents: contents.into(),
485 glyph_name,
486 path: path.to_owned(),
487 });
488 }
489}