Skip to main content

webfont_generator/memory/
mod.rs

1mod options;
2#[cfg(test)]
3mod tests;
4mod updates;
5
6use std::io::{Error, Result};
7use std::sync::{Arc, OnceLock};
8
9use crate::cache::{CacheState, glyph_set, variant_glyph_set};
10use crate::incremental::sources::{SourceUpdate, refresh_source_cache, replace_source_cache};
11use crate::input::finalize_generate_webfonts_options;
12use crate::output::collect::{CollectedOutput, collect_outputs};
13use crate::pipeline::{
14    TtfGlyphCache, build_content_variant_outputs, build_font_outputs, build_variant_with_cache,
15    build_with_cache,
16};
17use crate::rendering::css::{
18    SharedTemplateData, calc_content_hash, make_content_css_context, make_urls,
19    render_css_with_context,
20};
21use crate::rendering::html::{
22    compile_html_template, make_content_html_context, render_html_with_hbs_context,
23};
24use crate::resolve_content_variant_sources;
25use crate::result::FontOutputs;
26use crate::svg::types::GlyphCache;
27use crate::svg::{
28    VariantGlyphCache, prepare_svg_font, prepare_svg_font_incremental,
29    prepare_variant_svg_family_cached, source_content_hash, svg_options_from_options,
30};
31pub use options::{GenerateWebfontsFromSourcesOptions, SourceFontVariant, SvgSource};
32use options::{ResolvedSourceOptions, resolve};
33#[cfg(feature = "async")]
34pub use updates::SourceRegenerateError;
35pub use updates::{SourceChange, SourceMembership, VariantSourceSet};
36
37/// Persistent cache identity and glyph data retained so `cache_bytes` can encode on request.
38struct PersistentCache {
39    state: CacheState,
40    /// Glyph data for results without incremental caches (which hold it otherwise).
41    glyphs: Option<SourceCaches>,
42    /// Set when a failed update reset the incremental caches: until a build succeeds and refills
43    /// them, exporting would drop every saved glyph, so `cache_bytes` returns `None` instead.
44    caches_reset: bool,
45}
46
47#[derive(Default)]
48struct SourceCaches {
49    glyphs: GlyphCache,
50    ttf: TtfGlyphCache,
51    variants: VariantGlyphCache,
52}
53
54/// Font buffers and independently lazy companion renders from SVG text.
55/// No method on this result reads or writes files. Settings and templates stay fixed.
56pub struct GenerateWebfontsFromSourcesResult {
57    options: ResolvedSourceOptions,
58    fonts: FontOutputs,
59    caches: Option<SourceCaches>,
60    persistent: Option<PersistentCache>,
61    shared: OnceLock<SharedTemplateData>,
62    css: OnceLock<std::result::Result<Arc<String>, String>>,
63    html: OnceLock<std::result::Result<Arc<String>, String>>,
64    ts: OnceLock<Arc<String>>,
65    #[cfg(test)]
66    html_render_count: std::sync::atomic::AtomicUsize,
67}
68
69/// Generate fonts directly from named SVG text, without a runtime or filesystem access.
70/// Both ordinary and variant builds retain the engine's Rayon parallelism.
71///
72/// ```
73/// use webfont_generator::{generate_from_sources_sync, GenerateWebfontsFromSourcesOptions,
74///     SvgSource, SourceChange, SourceMembership, FontType};
75/// let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 2H22V22H2Z"/></svg>"#;
76/// let mut result = generate_from_sources_sync(GenerateWebfontsFromSourcesOptions {
77///     sources: vec![SvgSource { id: "42".into(), name: "square".into(), svg: svg.into() }],
78///     types: Some(vec![FontType::Ttf]),
79///     incremental: Some(true),
80///     ..Default::default()
81/// })?;
82/// assert!(result.ttf_bytes().is_some());
83/// result.regenerate_sync(SourceMembership::Sources(vec!["42".into()]), vec![
84///     SourceChange::Changed { id: "42".into(), svg: svg.into(), name: Some("renamed".into()) },
85/// ])?;
86/// let outputs = result.collect_outputs()?;
87/// assert_eq!(outputs.len(), 2); // TTF and CSS; both shared with the result.
88/// # Ok::<(), std::io::Error>(())
89/// ```
90pub fn generate_from_sources_sync(
91    options: GenerateWebfontsFromSourcesOptions,
92) -> Result<GenerateWebfontsFromSourcesResult> {
93    let threads = options.threads;
94    crate::threads::install(threads, || generate_in_pool(options))
95}
96
97fn generate_in_pool(
98    mut options: GenerateWebfontsFromSourcesOptions,
99) -> Result<GenerateWebfontsFromSourcesResult> {
100    let cache = options.cache.take();
101    let mut options = resolve(options)?;
102    let mut caches = options.common.incremental.then(SourceCaches::default);
103    let (fonts, persistent) = match cache {
104        Some(bytes) => build_cached(&mut options, caches.as_mut(), bytes)?,
105        None => (build(&mut options, caches.as_mut(), &[])?, None),
106    };
107    Ok(GenerateWebfontsFromSourcesResult {
108        options,
109        fonts,
110        caches,
111        persistent,
112        shared: OnceLock::new(),
113        css: OnceLock::new(),
114        html: OnceLock::new(),
115        ts: OnceLock::new(),
116        #[cfg(test)]
117        html_render_count: Default::default(),
118    })
119}
120
121/// Run [`generate_from_sources_sync`] on Tokio's blocking pool.
122/// Requires an active Tokio runtime; scheduling/join failures are returned as errors.
123#[cfg(feature = "async")]
124pub async fn generate_from_sources(
125    options: GenerateWebfontsFromSourcesOptions,
126) -> Result<GenerateWebfontsFromSourcesResult> {
127    let runtime = tokio::runtime::Handle::try_current().map_err(|error| {
128        Error::other(format!(
129            "Source generation requires a Tokio runtime: {error}"
130        ))
131    })?;
132    runtime
133        .spawn_blocking(move || generate_from_sources_sync(options))
134        .await
135        .map_err(generation_join_error)?
136}
137
138#[cfg(feature = "async")]
139fn generation_join_error(error: tokio::task::JoinError) -> Error {
140    Error::other(format!("Source generation task failed: {error}"))
141}
142
143/// Initial build seeded from persistent cache bytes. The persistent state is dropped when nothing
144/// could ever be exported: a full hit without incremental updates.
145fn build_cached(
146    options: &mut ResolvedSourceOptions,
147    caches: Option<&mut SourceCaches>,
148    bytes: Vec<u8>,
149) -> Result<(FontOutputs, Option<PersistentCache>)> {
150    if let Some(variants) = &options.variants {
151        let family = resolve_content_variant_sources(
152            &mut options.common,
153            variants,
154            options.sources.iter().map(Vec::as_slice).collect(),
155        )?;
156        let mut state = CacheState::new_variant(&options.common, variants, &family);
157        if !bytes.is_empty() {
158            state.load(bytes);
159        }
160        let svg_options = svg_options_from_options(&options.common);
161        let built =
162            build_variant_with_cache(&options.common, variants, &svg_options, &family, &state);
163        let (fonts, glyphs, _) = built?;
164        let glyphs = match (caches, glyphs) {
165            (Some(cache), Some(glyphs)) => {
166                cache.variants = glyphs;
167                None
168            }
169            (None, None) => return Ok((fonts, None)),
170            (_, glyphs) => glyphs.map(|variants| SourceCaches {
171                variants,
172                ..Default::default()
173            }),
174        };
175        state.release_glyphs();
176        let persistent = PersistentCache {
177            state,
178            glyphs,
179            caches_reset: false,
180        };
181        return Ok((fonts, Some(persistent)));
182    }
183    finalize_generate_webfonts_options(&mut options.common, &options.sources[0])?;
184    let sources = &options.sources[0];
185    let mut state = CacheState::new(&options.common, sources);
186    if !bytes.is_empty() {
187        state.load(bytes);
188    }
189    let svg_options = svg_options_from_options(&options.common);
190    let mut caches = caches;
191    let ttf = caches.as_deref_mut().map(|cache| &mut cache.ttf);
192    let built = build_with_cache(&options.common, &svg_options, sources, &state, ttf);
193    let (fonts, glyphs, _) = built?;
194    // Incremental results keep glyph data in their caches; others retain it for export.
195    let glyphs = match (caches, glyphs) {
196        (Some(cache), Some(glyphs)) => {
197            cache.glyphs = glyphs;
198            None
199        }
200        (None, None) => return Ok((fonts, None)),
201        (_, glyphs) => glyphs.map(|glyphs| SourceCaches {
202            glyphs,
203            ..Default::default()
204        }),
205    };
206    state.release_glyphs();
207    let persistent = PersistentCache {
208        state,
209        glyphs,
210        caches_reset: false,
211    };
212    Ok((fonts, Some(persistent)))
213}
214
215fn build(
216    options: &mut ResolvedSourceOptions,
217    mut caches: Option<&mut SourceCaches>,
218    updates: &[SourceUpdate],
219) -> Result<FontOutputs> {
220    // Finish all membership, codepoint, color and missing-glyph validation before touching caches.
221    let family = if let Some(variants) = &options.variants {
222        Some(resolve_content_variant_sources(
223            &mut options.common,
224            variants,
225            options.sources.iter().map(Vec::as_slice).collect(),
226        )?)
227    } else {
228        finalize_generate_webfonts_options(&mut options.common, &options.sources[0])?;
229        None
230    };
231    if family.is_none()
232        && let Some(cache) = caches.as_mut()
233    {
234        for update in updates {
235            match update {
236                SourceUpdate::Set(source) => {
237                    refresh_source_cache(
238                        &mut cache.glyphs,
239                        &source.id,
240                        source_content_hash(&source.contents),
241                    );
242                }
243                SourceUpdate::Remove(id) => replace_source_cache(&mut cache.glyphs, id, None),
244            }
245        }
246    }
247    let outputs = (|| {
248        let svg_options = svg_options_from_options(&options.common);
249        if let Some(family) = family {
250            let prepared = prepare_variant_svg_family_cached(
251                &svg_options,
252                &family,
253                caches.as_deref_mut().map(|cache| &mut cache.variants),
254            )?;
255            return build_content_variant_outputs(
256                &options.common,
257                options.variants.as_ref().unwrap(),
258                &prepared,
259            );
260        }
261        let sources = &options.sources[0];
262        match caches.as_deref_mut() {
263            Some(cache) => {
264                let prepared =
265                    prepare_svg_font_incremental(&svg_options, sources, &mut cache.glyphs)?;
266                build_font_outputs(
267                    &options.common,
268                    &svg_options,
269                    &prepared,
270                    Some(&mut cache.ttf),
271                )
272            }
273            None => {
274                let prepared = prepare_svg_font(&svg_options, sources)?;
275                build_font_outputs(&options.common, &svg_options, &prepared, None)
276            }
277        }
278    })();
279    if outputs.is_err()
280        && let Some(cache) = caches
281    {
282        *cache = SourceCaches::default();
283    }
284    outputs
285}
286
287impl GenerateWebfontsFromSourcesResult {
288    /// Borrow the requested SVG font.
289    pub fn svg_string(&self) -> Option<&str> {
290        self.fonts.svg_font.as_deref().map(String::as_str)
291    }
292    /// Borrow the requested TTF font.
293    pub fn ttf_bytes(&self) -> Option<&[u8]> {
294        self.fonts.ttf_font.as_deref().map(Vec::as_slice)
295    }
296    /// Borrow the requested EOT font.
297    pub fn eot_bytes(&self) -> Option<&[u8]> {
298        self.fonts.eot_font.as_deref().map(Vec::as_slice)
299    }
300    /// Borrow the requested WOFF font.
301    pub fn woff_bytes(&self) -> Option<&[u8]> {
302        self.fonts.woff_font.as_deref().map(Vec::as_slice)
303    }
304    /// Borrow the requested WOFF2 font.
305    pub fn woff2_bytes(&self) -> Option<&[u8]> {
306        self.fonts.woff2_font.as_deref().map(Vec::as_slice)
307    }
308
309    fn shared(&self) -> &SharedTemplateData {
310        self.shared.get_or_init(|| {
311            let sources = self
312                .options
313                .sources
314                .iter()
315                .flatten()
316                .cloned()
317                .collect::<Vec<_>>();
318            SharedTemplateData::new(
319                &self.options.common,
320                self.options.css_template.clone(),
321                calc_content_hash(
322                    &self.options.common,
323                    self.options.variants.as_ref(),
324                    &sources,
325                    self.options.css_template.as_deref(),
326                    self.options.html_template.as_deref(),
327                ),
328            )
329        })
330    }
331
332    fn css_context(&self) -> serde_json::Map<String, serde_json::Value> {
333        let options = &self.options.common;
334        let shared = self.shared();
335        make_content_css_context(
336            options,
337            self.options.variants.as_ref(),
338            &make_urls(options, &shared.hash, options.css_fonts_url.as_deref()),
339            shared,
340        )
341    }
342
343    /// Render CSS on first request and share the cached string thereafter.
344    /// Explicit requests render even when CSS collection is disabled.
345    pub fn generate_css(&self) -> Result<Arc<String>> {
346        self.css
347            .get_or_init(|| {
348                render_css_with_context(self.shared(), &self.css_context())
349                    .map(Arc::new)
350                    .map_err(|error| error.to_string())
351            })
352            .clone()
353            .map_err(Error::other)
354    }
355
356    /// Render an HTML preview on first request. Its styles reuse the CSS render cache.
357    /// Requesting CSS alone never compiles the HTML template.
358    pub fn generate_html(&self) -> Result<Arc<String>> {
359        self.html
360            .get_or_init(|| {
361                self.render_html()
362                    .map(Arc::new)
363                    .map_err(|error| error.to_string())
364            })
365            .clone()
366            .map_err(Error::other)
367    }
368
369    fn render_html(&self) -> Result<String> {
370        #[cfg(test)]
371        self.html_render_count
372            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
373        let template = compile_html_template(self.options.html_template.clone())?;
374        let css = self.generate_css()?;
375        let sources = self
376            .options
377            .sources
378            .iter()
379            .flatten()
380            .cloned()
381            .collect::<Vec<_>>();
382        let css_ctx = self.options.variants.as_ref().map(|_| self.css_context());
383        let context = make_content_html_context(
384            &self.options.common,
385            self.shared(),
386            &sources,
387            css.as_ref().clone(),
388            css_ctx.as_ref(),
389        );
390        let hbs_context = handlebars::Context::wraps(&context).map_err(Error::other)?;
391        render_html_with_hbs_context(template.registry.as_ref(), &hbs_context, &context)
392    }
393
394    /// Render a fixed TypeScript source module with icon names and selector settings.
395    /// Explicit requests work even when TS collection is disabled and never render templates.
396    pub fn generate_ts(&self) -> Result<Arc<String>> {
397        Ok(self
398            .ts
399            .get_or_init(|| {
400                Arc::new(crate::rendering::ts::render_ts(
401                    &self.options.common,
402                    self.options
403                        .sources
404                        .iter()
405                        .flatten()
406                        .map(|source| source.glyph_name.as_str()),
407                    self.options.variants.is_some(),
408                    None,
409                ))
410            })
411            .clone())
412    }
413
414    /// Encode the persistent cache for storage, reflecting the current fonts and sources
415    /// (including incremental updates). Returns `None` when the result was generated without
416    /// the [`cache`](GenerateWebfontsFromSourcesOptions::cache) option, or when the bytes equal
417    /// the ones passed in or last returned, so callers can skip rewriting their store.
418    pub fn cache_bytes(&mut self) -> Option<Vec<u8>> {
419        let PersistentCache {
420            state,
421            glyphs,
422            caches_reset,
423        } = self.persistent.as_mut()?;
424        if *caches_reset {
425            return None;
426        }
427        let caches = glyphs.as_ref().or(self.caches.as_ref())?;
428        let options = &self.options.common;
429        if let Some(variants) = &self.options.variants {
430            let sources = &self.options.sources;
431            let family = resolve_content_variant_sources(
432                &mut options.clone(),
433                variants,
434                sources.iter().map(Vec::as_slice).collect(),
435            )
436            .expect("committed variant sources resolve");
437            state.refresh_glyph_set(variant_glyph_set(&family));
438            return state.update_variants(sources, &caches.variants, &self.fonts, &options.types);
439        }
440        let sources = &self.options.sources[0];
441        state.refresh_glyph_set(glyph_set(options, sources));
442        state.update(sources, &caches.glyphs, &self.fonts, &options.types)
443    }
444
445    /// Collect fonts and enabled companions, sharing all payloads with this result.
446    pub fn collect_outputs(&self) -> Result<Vec<CollectedOutput>> {
447        let css = self
448            .options
449            .common
450            .css
451            .then(|| self.generate_css())
452            .transpose()?;
453        let html = self
454            .options
455            .common
456            .html
457            .then(|| self.generate_html())
458            .transpose()?;
459        let ts = self
460            .options
461            .common
462            .ts
463            .then(|| self.generate_ts())
464            .transpose()?;
465        Ok(collect_outputs(&self.fonts, css, html, ts))
466    }
467}