Skip to main content

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 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    /// Asynchronously rebuild on Tokio's blocking pool and return the next result generation.
183    /// This consumes `self`, so Rust prevents reuse of a stale result after success. On ordinary
184    /// failure, [`RegenerateError::into_result`] returns the input result for retry. This future is
185    /// not cancellation-safe: dropping it lets already-started blocking work finish and drops the
186    /// consumed result; filesystem writes may continue.
187    ///
188    /// ```rust,no_run
189    /// use webfont_generator::{generate_sync, GenerateWebfontsOptions, GlyphChange};
190    ///
191    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
192    /// let files = vec!["icons/add.svg".to_owned()];
193    /// let result = generate_sync(GenerateWebfontsOptions {
194    ///     files: files.clone(),
195    ///     dest: "dist".to_owned(),
196    ///     incremental: Some(true),
197    ///     ..Default::default()
198    /// }, None)?;
199    /// let result = result.regenerate_async(
200    ///     webfont_generator::RegenerationFiles::Single(files.clone()),
201    ///     vec![(files[0].clone(), GlyphChange::Changed { name: None })],
202    /// ).await?;
203    /// # let _ = result;
204    /// # Ok(())
205    /// # }
206    /// ```
207    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        // Snapshot the inputs the CSS/HTML templates depend on (glyph names + codepoints) so we
224        // can tell, after rebuilding, whether the rendered output could have changed.
225        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        // Reorder to the caller's order so additions land in their fresh-build position rather than
243        // at the tail. `ordered_paths` is authoritative for membership: any file not listed is
244        // dropped here (handling removals), and every listed path must resolve to a known file.
245        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        // Re-resolve codepoints for the (possibly changed) set from the stable explicit base, so
256        // auto-assigned codepoints match what a fresh build of the new set would produce.
257        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        // Rebuild the template data fresh (the font hash changed), but keep the rendered CSS/HTML
267        // that can't have changed: only when the glyph names and codepoints the templates read are
268        // unchanged (e.g. a pure content edit). Carry those safe entries into the new result.
269        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        // Match `generate`'s write behavior: refresh font files, and skip unchanged CSS/HTML.
289        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    /// Rebuild after re-diffing the complete ordered file set against the retained incremental
307    /// cache. This is useful when the caller has a fresh file list but not a reliable watcher
308    /// change batch: every current path is re-read and hashed, missing prior paths are treated as
309    /// removed, new paths as added, and paths whose content hash changed as changed. Existing glyph
310    /// names are preserved; added paths derive their glyph name from the file stem.
311    ///
312    /// ```rust,no_run
313    /// use webfont_generator::{generate_sync, GenerateWebfontsOptions};
314    ///
315    /// # fn run() -> std::io::Result<()> {
316    /// let files = vec!["icons/add.svg".to_owned()];
317    /// let mut result = generate_sync(GenerateWebfontsOptions {
318    ///     files: files.clone(),
319    ///     dest: "dist".to_owned(),
320    ///     incremental: Some(true),
321    ///     ..Default::default()
322    /// }, None)?;
323    /// result.regenerate_all(&webfont_generator::RegenerationFiles::Single(files))?;
324    /// # Ok(())
325    /// # }
326    /// ```
327    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    /// Asynchronously re-diff and rebuild on Tokio's blocking pool, consuming the old result and
383    /// returning the next result generation. This future is not cancellation-safe: dropping it
384    /// lets already-started blocking work finish and drops the consumed result.
385    ///
386    /// ```rust,no_run
387    /// use webfont_generator::{generate_sync, GenerateWebfontsOptions};
388    ///
389    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
390    /// let files = vec!["icons/add.svg".to_owned()];
391    /// let result = generate_sync(GenerateWebfontsOptions {
392    ///     files: files.clone(),
393    ///     dest: "dist".to_owned(),
394    ///     incremental: Some(true),
395    ///     ..Default::default()
396    /// }, None)?;
397    /// let result = result.regenerate_all_async(webfont_generator::RegenerationFiles::Single(files)).await?;
398    /// # let _ = result;
399    /// # Ok(())
400    /// # }
401    /// ```
402    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}