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 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}