webfont_generator/incremental/
mod.rs1#[cfg(test)]
2mod tests;
3
4use std::collections::HashSet;
5use std::io::ErrorKind;
6use std::path::Path;
7
8use crate::svg::{prepare_svg_font_incremental, source_content_hash, svg_options_from_options};
9use crate::types::{GenerateWebfontsResult, GlyphChange, LoadedSvgFile};
10use crate::write::write_generate_webfonts_result_sync;
11use crate::{build_font_outputs, finalize_generate_webfonts_options, validate_glyph_names};
12
13impl GenerateWebfontsResult {
14 pub fn regenerate(
61 &mut self,
62 ordered_paths: &[String],
63 changes: &[(String, GlyphChange)],
64 ) -> std::io::Result<()> {
65 if self.css_context.is_some() || self.html_context.is_some() {
66 return Err(std::io::Error::new(
67 ErrorKind::InvalidInput,
68 "regenerate is not supported for results generated with cssContext/htmlContext callbacks.",
69 ));
70 }
71
72 let mut cache = self.glyph_cache.clone().ok_or_else(|| {
73 std::io::Error::new(
74 ErrorKind::InvalidInput,
75 "regenerate requires the font to be generated with `incremental` enabled.",
76 )
77 })?;
78 let mut source_files = self.source_files.clone();
79 let mut options = self.options.clone();
80
81 let prev_names: Vec<String> = self
84 .source_files
85 .iter()
86 .map(|f| f.glyph_name.clone())
87 .collect();
88 let prev_codepoints = self.options.codepoints.clone();
89
90 let order_changed = self
91 .source_files
92 .iter()
93 .map(|file| file.path.as_str())
94 .ne(ordered_paths.iter().map(String::as_str));
95 let mut changed_inputs = order_changed;
96
97 for (path, change) in changes {
98 match change {
99 GlyphChange::Removed => {
100 let before = source_files.len();
101 source_files.retain(|file| &file.path != path);
102 cache.entries.remove(path);
103 cache.content_hashes.remove(path);
104 cache.processed_entries.remove(path);
105 changed_inputs |= source_files.len() != before;
106 }
107 GlyphChange::Added { name } => {
108 let contents = std::fs::read_to_string(path)?;
109 let hash = source_content_hash(&contents);
110 upsert_source_file(&mut source_files, path, contents, name.clone());
111 if let Some(cached) = cache.by_content_hash.get(&hash) {
114 cache.entries.insert(path.clone(), cached.clone());
115 cache.content_hashes.insert(path.clone(), hash);
116 } else {
117 cache.entries.remove(path);
118 cache.content_hashes.remove(path);
119 }
120 cache.processed_entries.remove(path);
121 changed_inputs = true;
122 }
123 GlyphChange::Changed { name } => {
124 let contents = std::fs::read_to_string(path)?;
125 let hash = source_content_hash(&contents);
126 let content_changed = cache.content_hashes.get(path) != Some(&hash);
127 let name_changed = name.as_ref().is_some_and(|name| {
128 source_files
129 .iter()
130 .find(|file| &file.path == path)
131 .is_none_or(|file| file.glyph_name != *name)
132 });
133
134 if !content_changed && !name_changed {
135 continue;
136 }
137
138 upsert_source_file(&mut source_files, path, contents, name.clone());
139 if content_changed {
140 if let Some(cached) = cache.by_content_hash.get(&hash) {
141 cache.entries.insert(path.clone(), cached.clone());
142 cache.content_hashes.insert(path.clone(), hash);
143 } else {
144 cache.entries.remove(path);
145 cache.content_hashes.remove(path);
146 }
147 cache.processed_entries.remove(path);
148 }
149 changed_inputs = true;
150 }
151 }
152 }
153
154 if !changed_inputs {
155 return Ok(());
156 }
157
158 let mut by_path: std::collections::HashMap<String, LoadedSvgFile> = source_files
162 .into_iter()
163 .map(|file| (file.path.clone(), file))
164 .collect();
165 let mut reordered = Vec::with_capacity(ordered_paths.len());
166 for path in ordered_paths {
167 let file = by_path.remove(path).ok_or_else(|| {
168 std::io::Error::new(
169 ErrorKind::InvalidInput,
170 format!("regenerate: `{path}` is in the ordered set but was not loaded; declare it as an added change."),
171 )
172 })?;
173 reordered.push(file);
174 }
175 source_files = reordered;
176 validate_glyph_names(&source_files)?;
177 options.files = ordered_paths.to_vec();
178
179 finalize_generate_webfonts_options(&mut options, &source_files)?;
182
183 let svg_options = svg_options_from_options(&options);
184 let prepared = prepare_svg_font_incremental(&svg_options, &source_files, &mut cache)?;
185 let fonts = build_font_outputs(&options, &svg_options, &prepared, self.ttf_cache.as_mut())?;
186
187 let names_unchanged = source_files
191 .iter()
192 .map(|file| file.glyph_name.as_str())
193 .eq(prev_names.iter().map(String::as_str));
194 let codepoints_unchanged = options.codepoints == prev_codepoints;
195 self.source_files = source_files;
196 self.options = options;
197 self.fonts = fonts;
198 self.glyph_cache = Some(cache);
199 self.reset_render_cache(names_unchanged, codepoints_unchanged);
200
201 if self.options.write_files {
203 write_generate_webfonts_result_sync(self)?;
204 }
205 Ok(())
206 }
207
208 pub fn regenerate_all(&mut self, ordered_paths: &[String]) -> std::io::Result<()> {
214 let cache = self.glyph_cache.as_ref().ok_or_else(|| {
215 std::io::Error::new(
216 ErrorKind::InvalidInput,
217 "regenerate requires the font to be generated with `incremental` enabled.",
218 )
219 })?;
220 let ordered_set: HashSet<&str> = ordered_paths.iter().map(String::as_str).collect();
221 let mut previous_paths = HashSet::with_capacity(self.source_files.len());
222 let mut changes = Vec::new();
223
224 for source_file in &self.source_files {
225 previous_paths.insert(source_file.path.as_str());
226 if !ordered_set.contains(source_file.path.as_str()) {
227 changes.push((source_file.path.clone(), GlyphChange::Removed));
228 }
229 }
230
231 for path in ordered_paths {
232 if !previous_paths.contains(path.as_str()) {
233 changes.push((path.clone(), GlyphChange::Added { name: None }));
234 continue;
235 }
236
237 let contents = std::fs::read_to_string(path)?;
238 let hash = source_content_hash(&contents);
239 if cache.content_hashes.get(path) != Some(&hash) {
240 changes.push((path.clone(), GlyphChange::Changed { name: None }));
241 }
242 }
243
244 self.regenerate(ordered_paths, &changes)
245 }
246}
247
248fn upsert_source_file(
251 source_files: &mut Vec<LoadedSvgFile>,
252 path: &str,
253 contents: String,
254 name: Option<String>,
255) {
256 if let Some(file) = source_files.iter_mut().find(|file| file.path == path) {
257 file.contents = contents;
258 if let Some(name) = name {
259 file.glyph_name = name;
260 }
261 } else {
262 let glyph_name = name.unwrap_or_else(|| {
263 Path::new(path)
264 .file_stem()
265 .and_then(|stem| stem.to_str())
266 .unwrap_or_default()
267 .to_owned()
268 });
269 source_files.push(LoadedSvgFile {
270 contents,
271 glyph_name,
272 path: path.to_owned(),
273 });
274 }
275}