webfont_generator/incremental/
mod.rs1#[cfg(all(test, feature = "fs"))]
2mod tests;
3
4#[cfg(feature = "fs")]
5mod files;
6#[cfg(feature = "fs")]
7mod helpers;
8pub(crate) mod sources;
9#[cfg(feature = "fs")]
10mod variants;
11
12#[cfg(feature = "fs")]
13use std::collections::{HashMap, HashSet};
14#[cfg(feature = "fs")]
15use std::io::ErrorKind;
16#[cfg(feature = "fs")]
17use std::sync::{Arc, Mutex};
18
19#[cfg(feature = "fs")]
20use crate::input::{finalize_generate_webfonts_options, validate_glyph_names};
21#[cfg(feature = "fs")]
22use crate::output::write_generate_webfonts_result_sync;
23#[cfg(feature = "fs")]
24use crate::pipeline::{TtfGlyphCache, build_font_outputs};
25#[cfg(feature = "fs")]
26use crate::result::{GenerateWebfontsResult, RegenerateError};
27#[cfg(feature = "fs")]
28use crate::svg::types::GlyphCache;
29#[cfg(feature = "fs")]
30use crate::svg::{prepare_svg_font_incremental, source_content_hash, svg_options_from_options};
31#[cfg(feature = "fs")]
32use crate::types::{GlyphChange, RegenerationFiles};
33#[cfg(feature = "fs")]
34use files::load_source_updates;
35#[cfg(feature = "fs")]
36use sources::{ordered_sources, updated_sources};
37
38#[cfg(feature = "fs")]
39pub(crate) struct RegenerationState {
40 pub(crate) variant_cache: Option<crate::svg::VariantGlyphCache>,
41 pub(crate) variant_write_pending: bool,
42 pub(crate) caches_dirty: bool,
43 pub(crate) glyph_cache: GlyphCache,
44 pub(crate) ttf_cache: Option<TtfGlyphCache>,
45 pub(crate) written_outputs: HashMap<String, [u8; 16]>,
46}
47
48#[cfg(feature = "fs")]
49struct RegenerationStateLease {
50 slot: Arc<Mutex<Option<RegenerationState>>>,
51 state: Option<RegenerationState>,
52 keep_caches: bool,
53}
54
55#[cfg(feature = "fs")]
56impl RegenerationStateLease {
57 fn state_mut(&mut self) -> &mut RegenerationState {
58 self.state.as_mut().unwrap()
59 }
60
61 fn commit(mut self) {
62 self.keep_caches = true;
63 }
64}
65
66#[cfg(feature = "fs")]
67impl Drop for RegenerationStateLease {
68 fn drop(&mut self) {
69 let mut state = self.state.take().unwrap();
70 if !self.keep_caches && state.caches_dirty {
71 state.variant_cache = None;
72 state.glyph_cache = GlyphCache::default();
73 if state.ttf_cache.is_some() {
74 state.ttf_cache = Some(TtfGlyphCache::default());
75 }
76 }
77 state.caches_dirty = false;
78 *self.slot.lock().unwrap() = Some(state);
79 }
80}
81
82#[cfg(feature = "fs")]
83impl GenerateWebfontsResult {
84 fn take_regeneration_state_lease(&self) -> std::io::Result<RegenerationStateLease> {
85 Ok(RegenerationStateLease {
86 slot: Arc::clone(&self.regeneration_state),
87 state: Some(self.take_regeneration_state()?),
88 keep_caches: false,
89 })
90 }
91
92 pub fn regenerate(
147 &mut self,
148 files: &RegenerationFiles,
149 changes: &[(String, GlyphChange)],
150 ) -> std::io::Result<()> {
151 let threads = self.options.threads;
152 crate::threads::install(threads, || self.regenerate_in_pool(files, changes))
153 }
154
155 fn regenerate_in_pool(
156 &mut self,
157 files: &RegenerationFiles,
158 changes: &[(String, GlyphChange)],
159 ) -> std::io::Result<()> {
160 let ordered_paths = match files {
161 RegenerationFiles::Single(files) => files.as_slice(),
162 RegenerationFiles::Variants(sets) => {
163 return self.regenerate_variant_files(sets, Some(changes));
164 }
165 };
166 self.require_ordinary_regeneration()?;
167 if self.css_context.is_some() || self.html_context.is_some() {
168 return Err(std::io::Error::new(
169 ErrorKind::InvalidInput,
170 "regenerate is not supported for results generated with cssContext/htmlContext callbacks.",
171 ));
172 }
173
174 let mut state = self.take_regeneration_state_lease()?;
175 let result = self.regenerate_with_state(ordered_paths, changes, state.state_mut());
176 if result.is_ok() {
177 state.commit();
178 }
179 result
180 }
181
182 pub async fn regenerate_async(
208 self,
209 files: RegenerationFiles,
210 changes: Vec<(String, GlyphChange)>,
211 ) -> Result<Self, RegenerateError> {
212 self.regenerate_in_background(files, Some(changes)).await
213 }
214
215 fn regenerate_with_state(
216 &mut self,
217 ordered_paths: &[String],
218 changes: &[(String, GlyphChange)],
219 state: &mut RegenerationState,
220 ) -> std::io::Result<()> {
221 let mut options = self.options.as_ref().clone();
222
223 let prev_names: Vec<String> = self
226 .source_files
227 .iter()
228 .map(|f| f.glyph_name.clone())
229 .collect();
230 let prev_codepoints = self.options.codepoints.clone();
231
232 let order_changed = self
233 .source_files
234 .iter()
235 .map(|file| file.id.as_str())
236 .ne(ordered_paths.iter().map(String::as_str));
237 let (updates, changed_inputs) = load_source_updates(&self.source_files, changes, state)?;
238 if !order_changed && !changed_inputs {
239 return Ok(());
240 }
241
242 let definitions = updated_sources(&self.source_files, &updates);
246 let source_files = ordered_sources(&definitions, ordered_paths).map_err(|path| {
247 std::io::Error::new(
248 ErrorKind::InvalidInput,
249 format!("regenerate: `{path}` is in the ordered set but was not loaded; declare it as an added change."),
250 )
251 })?;
252 validate_glyph_names(&source_files)?;
253 options.files = ordered_paths.to_vec();
254
255 finalize_generate_webfonts_options(&mut options, &source_files)?;
258
259 let svg_options = svg_options_from_options(&options);
260 state.caches_dirty = true;
261 let prepared =
262 prepare_svg_font_incremental(&svg_options, &source_files, &mut state.glyph_cache)?;
263 let fonts =
264 build_font_outputs(&options, &svg_options, &prepared, state.ttf_cache.as_mut())?;
265
266 let names_unchanged = source_files
270 .iter()
271 .map(|file| file.glyph_name.as_str())
272 .eq(prev_names.iter().map(String::as_str));
273 let codepoints_unchanged = options.codepoints == prev_codepoints;
274 let carried = self.reusable_render_cache(names_unchanged, codepoints_unchanged);
275 let previous_source_files =
276 std::mem::replace(&mut self.source_files, std::sync::Arc::new(source_files));
277 let previous_options = std::mem::replace(&mut self.options, std::sync::Arc::new(options));
278 let previous_fonts = std::mem::replace(&mut self.fonts, fonts);
279 let previous_cached = std::mem::take(&mut self.cached);
280 let previous_carried = std::mem::replace(&mut self.carried_render, carried);
281 let previous_css_context = self.css_context.take();
282 let previous_html_context = self.html_context.take();
283 let previous_ts = self.ts.clone();
284 if !names_unchanged || previous_css_context.is_some() {
285 self.ts = Default::default();
286 }
287
288 if self.options.write_files
290 && let Err(error) =
291 write_generate_webfonts_result_sync(self, &mut state.written_outputs)
292 {
293 self.source_files = previous_source_files;
294 self.options = previous_options;
295 self.fonts = previous_fonts;
296 self.cached = previous_cached;
297 self.carried_render = previous_carried;
298 self.css_context = previous_css_context;
299 self.html_context = previous_html_context;
300 self.ts = previous_ts;
301 return Err(error);
302 }
303 Ok(())
304 }
305
306 pub fn regenerate_all(&mut self, files: &RegenerationFiles) -> std::io::Result<()> {
328 let threads = self.options.threads;
329 crate::threads::install(threads, || self.regenerate_all_in_pool(files))
330 }
331
332 fn regenerate_all_in_pool(&mut self, files: &RegenerationFiles) -> std::io::Result<()> {
333 let ordered_paths = match files {
334 RegenerationFiles::Single(files) => files.as_slice(),
335 RegenerationFiles::Variants(sets) => return self.regenerate_variant_files(sets, None),
336 };
337 self.require_ordinary_regeneration()?;
338 if !self.options.incremental {
339 return Err(std::io::Error::new(
340 ErrorKind::InvalidInput,
341 "regenerate requires the font to be generated with `incremental` enabled.",
342 ));
343 }
344 let state = self.regeneration_state.lock().unwrap();
345 let cache = &state
346 .as_ref()
347 .ok_or_else(|| {
348 std::io::Error::new(
349 ErrorKind::WouldBlock,
350 "This result is already regenerating or has been replaced.",
351 )
352 })?
353 .glyph_cache;
354 let ordered_set: HashSet<&str> = ordered_paths.iter().map(String::as_str).collect();
355 let mut previous_paths = HashSet::with_capacity(self.source_files.len());
356 let mut changes = Vec::new();
357
358 for source_file in self.source_files.iter() {
359 previous_paths.insert(source_file.id.as_str());
360 if !ordered_set.contains(source_file.id.as_str()) {
361 changes.push((source_file.id.clone(), GlyphChange::Removed));
362 }
363 }
364
365 for path in ordered_paths {
366 if !previous_paths.contains(path.as_str()) {
367 changes.push((path.clone(), GlyphChange::Added { name: None }));
368 continue;
369 }
370
371 let contents = std::fs::read_to_string(path)?;
372 let hash = source_content_hash(&contents);
373 if cache.content_hashes.get(path) != Some(&hash) {
374 changes.push((path.clone(), GlyphChange::Changed { name: None }));
375 }
376 }
377
378 drop(state);
379 self.regenerate(files, &changes)
380 }
381
382 pub async fn regenerate_all_async(
403 self,
404 files: RegenerationFiles,
405 ) -> Result<Self, RegenerateError> {
406 self.regenerate_in_background(files, None).await
407 }
408
409 async fn regenerate_in_background(
410 mut self,
411 files: RegenerationFiles,
412 changes: Option<Vec<(String, GlyphChange)>>,
413 ) -> Result<Self, RegenerateError> {
414 let task = tokio::task::spawn_blocking(move || {
415 let outcome = match changes {
416 Some(changes) => self.regenerate(&files, &changes),
417 None => self.regenerate_all(&files),
418 };
419 match outcome {
420 Ok(()) => Ok(self),
421 Err(source) => Err(RegenerateError::new(Some(self), source)),
422 }
423 });
424 match task.await {
425 Ok(result) => result,
426 Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
427 Err(error) => Err(RegenerateError::new(
428 None,
429 std::io::Error::other(format!("asynchronous regeneration task failed: {error}")),
430 )),
431 }
432 }
433}