webfont_generator/incremental/mod.rs
1#[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 /// Rebuild after a batch of file changes, reusing cached glyph geometry for files whose
93 /// contents are unchanged. Requires the result to have been generated with `incremental`
94 /// enabled. `files` selects the matching ordinary or variant input mode. Each contained list
95 /// is the complete file set after the changes, in the order a fresh
96 /// build would use (e.g. the glob result); the rebuilt glyphs are ordered to match it, so
97 /// auto-assigned codepoints and glyph order — and therefore the output bytes — are identical
98 /// to a fresh `generate` of that set, including for additions that sort before existing
99 /// glyphs. `changes` describes what to do per affected file: added/changed files are read from
100 /// disk and re-parsed; any file absent from its list is dropped (an explicit `Removed`
101 /// is optional but harmless). Every requested format is rebuilt in memory, and — matching
102 /// `generate` — when the result was built with `write_files` enabled the refreshed fonts are
103 /// written to disk too, while CSS/HTML companion files are skipped if their rendered bytes are
104 /// unchanged from the previous write. Rendered CSS/HTML is reused when the glyph names and
105 /// codepoints the templates read are unchanged (a content edit), and re-rendered otherwise.
106 ///
107 /// For families, supply every configured variant exactly once. Each design's file order is
108 /// independent. Hints describe family-wide paths: `Added` is new to the family, `Changed`
109 /// applies to every final consumer, and `Removed` must be absent from every final list.
110 /// Membership-only changes need no hint; newly assigned paths are loaded automatically.
111 /// Invalid batches and font-build failures preserve the previous result. Variant writes
112 /// occur after the new state commits, so write errors leave retryable new in-memory output.
113 ///
114 /// ```rust,no_run
115 /// use webfont_generator::{
116 /// generate_sync, FontType, GenerateWebfontsOptions, GlyphChange,
117 /// };
118 ///
119 /// # fn main() -> std::io::Result<()> {
120 /// let files = vec![
121 /// "icons/add.svg".to_owned(),
122 /// "icons/remove.svg".to_owned(),
123 /// ];
124 /// let mut result = generate_sync(
125 /// GenerateWebfontsOptions {
126 /// dest: "dist".to_owned(),
127 /// files: files.clone(),
128 /// incremental: Some(true),
129 /// types: Some(vec![FontType::Woff2]),
130 /// write_files: Some(false),
131 /// ..Default::default()
132 /// },
133 /// None,
134 /// )?;
135 ///
136 /// result.regenerate(
137 /// &webfont_generator::RegenerationFiles::Single(files),
138 /// &[(
139 /// "icons/add.svg".to_owned(),
140 /// GlyphChange::Changed { name: None },
141 /// )],
142 /// )?;
143 /// # Ok(())
144 /// # }
145 /// ```
146 pub fn regenerate(
147 &mut self,
148 files: &RegenerationFiles,
149 changes: &[(String, GlyphChange)],
150 ) -> std::io::Result<()> {
151 let ordered_paths = match files {
152 RegenerationFiles::Single(files) => files.as_slice(),
153 RegenerationFiles::Variants(sets) => {
154 return self.regenerate_variant_files(sets, Some(changes));
155 }
156 };
157 self.require_ordinary_regeneration()?;
158 if self.css_context.is_some() || self.html_context.is_some() {
159 return Err(std::io::Error::new(
160 ErrorKind::InvalidInput,
161 "regenerate is not supported for results generated with cssContext/htmlContext callbacks.",
162 ));
163 }
164
165 let mut state = self.take_regeneration_state_lease()?;
166 let result = self.regenerate_with_state(ordered_paths, changes, state.state_mut());
167 if result.is_ok() {
168 state.commit();
169 }
170 result
171 }
172
173 /// Asynchronously rebuild on Tokio's blocking pool and return the next result generation.
174 /// This consumes `self`, so Rust prevents reuse of a stale result after success. On ordinary
175 /// failure, [`RegenerateError::into_result`] returns the input result for retry. This future is
176 /// not cancellation-safe: dropping it lets already-started blocking work finish and drops the
177 /// consumed result; filesystem writes may continue.
178 ///
179 /// ```rust,no_run
180 /// use webfont_generator::{generate_sync, GenerateWebfontsOptions, GlyphChange};
181 ///
182 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
183 /// let files = vec!["icons/add.svg".to_owned()];
184 /// let result = generate_sync(GenerateWebfontsOptions {
185 /// files: files.clone(),
186 /// dest: "dist".to_owned(),
187 /// incremental: Some(true),
188 /// ..Default::default()
189 /// }, None)?;
190 /// let result = result.regenerate_async(
191 /// webfont_generator::RegenerationFiles::Single(files.clone()),
192 /// vec![(files[0].clone(), GlyphChange::Changed { name: None })],
193 /// ).await?;
194 /// # let _ = result;
195 /// # Ok(())
196 /// # }
197 /// ```
198 pub async fn regenerate_async(
199 self,
200 files: RegenerationFiles,
201 changes: Vec<(String, GlyphChange)>,
202 ) -> Result<Self, RegenerateError> {
203 self.regenerate_in_background(files, Some(changes)).await
204 }
205
206 fn regenerate_with_state(
207 &mut self,
208 ordered_paths: &[String],
209 changes: &[(String, GlyphChange)],
210 state: &mut RegenerationState,
211 ) -> std::io::Result<()> {
212 let mut options = self.options.as_ref().clone();
213
214 // Snapshot the inputs the CSS/HTML templates depend on (glyph names + codepoints) so we
215 // can tell, after rebuilding, whether the rendered output could have changed.
216 let prev_names: Vec<String> = self
217 .source_files
218 .iter()
219 .map(|f| f.glyph_name.clone())
220 .collect();
221 let prev_codepoints = self.options.codepoints.clone();
222
223 let order_changed = self
224 .source_files
225 .iter()
226 .map(|file| file.id.as_str())
227 .ne(ordered_paths.iter().map(String::as_str));
228 let (updates, changed_inputs) = load_source_updates(&self.source_files, changes, state)?;
229 if !order_changed && !changed_inputs {
230 return Ok(());
231 }
232
233 // Reorder to the caller's order so additions land in their fresh-build position rather than
234 // at the tail. `ordered_paths` is authoritative for membership: any file not listed is
235 // dropped here (handling removals), and every listed path must resolve to a known file.
236 let definitions = updated_sources(&self.source_files, &updates);
237 let source_files = ordered_sources(&definitions, ordered_paths).map_err(|path| {
238 std::io::Error::new(
239 ErrorKind::InvalidInput,
240 format!("regenerate: `{path}` is in the ordered set but was not loaded; declare it as an added change."),
241 )
242 })?;
243 validate_glyph_names(&source_files)?;
244 options.files = ordered_paths.to_vec();
245
246 // Re-resolve codepoints for the (possibly changed) set from the stable explicit base, so
247 // auto-assigned codepoints match what a fresh build of the new set would produce.
248 finalize_generate_webfonts_options(&mut options, &source_files)?;
249
250 let svg_options = svg_options_from_options(&options);
251 state.caches_dirty = true;
252 let prepared =
253 prepare_svg_font_incremental(&svg_options, &source_files, &mut state.glyph_cache)?;
254 let fonts =
255 build_font_outputs(&options, &svg_options, &prepared, state.ttf_cache.as_mut())?;
256
257 // Rebuild the template data fresh (the font hash changed), but keep the rendered CSS/HTML
258 // that can't have changed: only when the glyph names and codepoints the templates read are
259 // unchanged (e.g. a pure content edit). Carry those safe entries into the new result.
260 let names_unchanged = source_files
261 .iter()
262 .map(|file| file.glyph_name.as_str())
263 .eq(prev_names.iter().map(String::as_str));
264 let codepoints_unchanged = options.codepoints == prev_codepoints;
265 let carried = self.reusable_render_cache(names_unchanged, codepoints_unchanged);
266 let previous_source_files =
267 std::mem::replace(&mut self.source_files, std::sync::Arc::new(source_files));
268 let previous_options = std::mem::replace(&mut self.options, std::sync::Arc::new(options));
269 let previous_fonts = std::mem::replace(&mut self.fonts, fonts);
270 let previous_cached = std::mem::take(&mut self.cached);
271 let previous_carried = std::mem::replace(&mut self.carried_render, carried);
272 let previous_css_context = self.css_context.take();
273 let previous_html_context = self.html_context.take();
274
275 // Match `generate`'s write behavior: refresh font files, and skip unchanged CSS/HTML.
276 if self.options.write_files
277 && let Err(error) =
278 write_generate_webfonts_result_sync(self, &mut state.written_outputs)
279 {
280 self.source_files = previous_source_files;
281 self.options = previous_options;
282 self.fonts = previous_fonts;
283 self.cached = previous_cached;
284 self.carried_render = previous_carried;
285 self.css_context = previous_css_context;
286 self.html_context = previous_html_context;
287 return Err(error);
288 }
289 Ok(())
290 }
291
292 /// Rebuild after re-diffing the complete ordered file set against the retained incremental
293 /// cache. This is useful when the caller has a fresh file list but not a reliable watcher
294 /// change batch: every current path is re-read and hashed, missing prior paths are treated as
295 /// removed, new paths as added, and paths whose content hash changed as changed. Existing glyph
296 /// names are preserved; added paths derive their glyph name from the file stem.
297 ///
298 /// ```rust,no_run
299 /// use webfont_generator::{generate_sync, GenerateWebfontsOptions};
300 ///
301 /// # fn run() -> std::io::Result<()> {
302 /// let files = vec!["icons/add.svg".to_owned()];
303 /// let mut result = generate_sync(GenerateWebfontsOptions {
304 /// files: files.clone(),
305 /// dest: "dist".to_owned(),
306 /// incremental: Some(true),
307 /// ..Default::default()
308 /// }, None)?;
309 /// result.regenerate_all(&webfont_generator::RegenerationFiles::Single(files))?;
310 /// # Ok(())
311 /// # }
312 /// ```
313 pub fn regenerate_all(&mut self, files: &RegenerationFiles) -> std::io::Result<()> {
314 let ordered_paths = match files {
315 RegenerationFiles::Single(files) => files.as_slice(),
316 RegenerationFiles::Variants(sets) => return self.regenerate_variant_files(sets, None),
317 };
318 self.require_ordinary_regeneration()?;
319 if !self.options.incremental {
320 return Err(std::io::Error::new(
321 ErrorKind::InvalidInput,
322 "regenerate requires the font to be generated with `incremental` enabled.",
323 ));
324 }
325 let state = self.regeneration_state.lock().unwrap();
326 let cache = &state
327 .as_ref()
328 .ok_or_else(|| {
329 std::io::Error::new(
330 ErrorKind::WouldBlock,
331 "This result is already regenerating or has been replaced.",
332 )
333 })?
334 .glyph_cache;
335 let ordered_set: HashSet<&str> = ordered_paths.iter().map(String::as_str).collect();
336 let mut previous_paths = HashSet::with_capacity(self.source_files.len());
337 let mut changes = Vec::new();
338
339 for source_file in self.source_files.iter() {
340 previous_paths.insert(source_file.id.as_str());
341 if !ordered_set.contains(source_file.id.as_str()) {
342 changes.push((source_file.id.clone(), GlyphChange::Removed));
343 }
344 }
345
346 for path in ordered_paths {
347 if !previous_paths.contains(path.as_str()) {
348 changes.push((path.clone(), GlyphChange::Added { name: None }));
349 continue;
350 }
351
352 let contents = std::fs::read_to_string(path)?;
353 let hash = source_content_hash(&contents);
354 if cache.content_hashes.get(path) != Some(&hash) {
355 changes.push((path.clone(), GlyphChange::Changed { name: None }));
356 }
357 }
358
359 drop(state);
360 self.regenerate(files, &changes)
361 }
362
363 /// Asynchronously re-diff and rebuild on Tokio's blocking pool, consuming the old result and
364 /// returning the next result generation. This future is not cancellation-safe: dropping it
365 /// lets already-started blocking work finish and drops the consumed result.
366 ///
367 /// ```rust,no_run
368 /// use webfont_generator::{generate_sync, GenerateWebfontsOptions};
369 ///
370 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
371 /// let files = vec!["icons/add.svg".to_owned()];
372 /// let result = generate_sync(GenerateWebfontsOptions {
373 /// files: files.clone(),
374 /// dest: "dist".to_owned(),
375 /// incremental: Some(true),
376 /// ..Default::default()
377 /// }, None)?;
378 /// let result = result.regenerate_all_async(webfont_generator::RegenerationFiles::Single(files)).await?;
379 /// # let _ = result;
380 /// # Ok(())
381 /// # }
382 /// ```
383 pub async fn regenerate_all_async(
384 self,
385 files: RegenerationFiles,
386 ) -> Result<Self, RegenerateError> {
387 self.regenerate_in_background(files, None).await
388 }
389
390 async fn regenerate_in_background(
391 mut self,
392 files: RegenerationFiles,
393 changes: Option<Vec<(String, GlyphChange)>>,
394 ) -> Result<Self, RegenerateError> {
395 let task = tokio::task::spawn_blocking(move || {
396 let outcome = match changes {
397 Some(changes) => self.regenerate(&files, &changes),
398 None => self.regenerate_all(&files),
399 };
400 match outcome {
401 Ok(()) => Ok(self),
402 Err(source) => Err(RegenerateError::new(Some(self), source)),
403 }
404 });
405 match task.await {
406 Ok(result) => result,
407 Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
408 Err(error) => Err(RegenerateError::new(
409 None,
410 std::io::Error::other(format!("asynchronous regeneration task failed: {error}")),
411 )),
412 }
413 }
414}