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::incremental::sources::{SourceUpdate, refresh_source_cache, replace_source_cache};
10use crate::input::finalize_generate_webfonts_options;
11use crate::output::collect::{CollectedOutput, collect_outputs};
12use crate::pipeline::{TtfGlyphCache, build_content_variant_outputs, build_font_outputs};
13use crate::rendering::css::{
14    SharedTemplateData, calc_content_hash, make_content_css_context, make_urls,
15    render_css_with_context,
16};
17use crate::rendering::html::{
18    compile_html_template, make_content_html_context, render_html_with_hbs_context,
19};
20use crate::resolve_content_variant_sources;
21use crate::result::FontOutputs;
22use crate::svg::types::GlyphCache;
23use crate::svg::{
24    VariantGlyphCache, prepare_svg_font, prepare_svg_font_incremental,
25    prepare_variant_svg_family_cached, source_content_hash, svg_options_from_options,
26};
27pub use options::{GenerateWebfontsFromSourcesOptions, SourceFontVariant, SvgSource};
28use options::{ResolvedSourceOptions, resolve};
29#[cfg(feature = "async")]
30pub use updates::SourceRegenerateError;
31pub use updates::{SourceChange, SourceMembership, VariantSourceSet};
32
33#[derive(Default)]
34struct SourceCaches {
35    glyphs: GlyphCache,
36    ttf: TtfGlyphCache,
37    variants: VariantGlyphCache,
38}
39
40/// Font buffers and independently lazy companion renders from SVG text.
41/// No method on this result reads or writes files. Settings and templates stay fixed.
42pub struct GenerateWebfontsFromSourcesResult {
43    options: ResolvedSourceOptions,
44    fonts: FontOutputs,
45    caches: Option<SourceCaches>,
46    shared: OnceLock<SharedTemplateData>,
47    css: OnceLock<std::result::Result<Arc<String>, String>>,
48    html: OnceLock<std::result::Result<Arc<String>, String>>,
49    #[cfg(test)]
50    html_render_count: std::sync::atomic::AtomicUsize,
51}
52
53/// Generate fonts directly from named SVG text, without a runtime or filesystem access.
54/// Both ordinary and variant builds retain the engine's Rayon parallelism.
55///
56/// ```
57/// use webfont_generator::{generate_from_sources_sync, GenerateWebfontsFromSourcesOptions,
58///     SvgSource, SourceChange, SourceMembership, FontType};
59/// let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 2H22V22H2Z"/></svg>"#;
60/// let mut result = generate_from_sources_sync(GenerateWebfontsFromSourcesOptions {
61///     sources: vec![SvgSource { id: "42".into(), name: "square".into(), svg: svg.into() }],
62///     types: Some(vec![FontType::Ttf]),
63///     incremental: Some(true),
64///     ..Default::default()
65/// })?;
66/// assert!(result.ttf_bytes().is_some());
67/// result.regenerate_sync(SourceMembership::Sources(vec!["42".into()]), vec![
68///     SourceChange::Changed { id: "42".into(), svg: svg.into(), name: Some("renamed".into()) },
69/// ])?;
70/// let outputs = result.collect_outputs()?;
71/// assert_eq!(outputs.len(), 2); // TTF and CSS; both shared with the result.
72/// # Ok::<(), std::io::Error>(())
73/// ```
74pub fn generate_from_sources_sync(
75    options: GenerateWebfontsFromSourcesOptions,
76) -> Result<GenerateWebfontsFromSourcesResult> {
77    let mut options = resolve(options)?;
78    let mut caches = options.common.incremental.then(SourceCaches::default);
79    let fonts = build(&mut options, caches.as_mut(), &[])?;
80    Ok(GenerateWebfontsFromSourcesResult {
81        options,
82        fonts,
83        caches,
84        shared: OnceLock::new(),
85        css: OnceLock::new(),
86        html: OnceLock::new(),
87        #[cfg(test)]
88        html_render_count: Default::default(),
89    })
90}
91
92/// Run [`generate_from_sources_sync`] on Tokio's blocking pool.
93/// Requires an active Tokio runtime; scheduling/join failures are returned as errors.
94#[cfg(feature = "async")]
95pub async fn generate_from_sources(
96    options: GenerateWebfontsFromSourcesOptions,
97) -> Result<GenerateWebfontsFromSourcesResult> {
98    let runtime = tokio::runtime::Handle::try_current().map_err(|error| {
99        Error::other(format!(
100            "Source generation requires a Tokio runtime: {error}"
101        ))
102    })?;
103    runtime
104        .spawn_blocking(move || generate_from_sources_sync(options))
105        .await
106        .map_err(generation_join_error)?
107}
108
109#[cfg(feature = "async")]
110fn generation_join_error(error: tokio::task::JoinError) -> Error {
111    Error::other(format!("Source generation task failed: {error}"))
112}
113
114fn build(
115    options: &mut ResolvedSourceOptions,
116    mut caches: Option<&mut SourceCaches>,
117    updates: &[SourceUpdate],
118) -> Result<FontOutputs> {
119    // Finish all membership, codepoint, color and missing-glyph validation before touching caches.
120    let family = if let Some(variants) = &options.variants {
121        Some(resolve_content_variant_sources(
122            &mut options.common,
123            variants,
124            options.sources.iter().map(Vec::as_slice).collect(),
125        )?)
126    } else {
127        finalize_generate_webfonts_options(&mut options.common, &options.sources[0])?;
128        None
129    };
130    if family.is_none()
131        && let Some(cache) = caches.as_mut()
132    {
133        for update in updates {
134            match update {
135                SourceUpdate::Set(source) => {
136                    refresh_source_cache(
137                        &mut cache.glyphs,
138                        &source.id,
139                        source_content_hash(&source.contents),
140                    );
141                }
142                SourceUpdate::Remove(id) => replace_source_cache(&mut cache.glyphs, id, None),
143            }
144        }
145    }
146    let outputs = (|| {
147        let svg_options = svg_options_from_options(&options.common);
148        if let Some(family) = family {
149            let prepared = prepare_variant_svg_family_cached(
150                &svg_options,
151                &family,
152                caches.as_deref_mut().map(|cache| &mut cache.variants),
153            )?;
154            return build_content_variant_outputs(
155                &options.common,
156                options.variants.as_ref().unwrap(),
157                &prepared,
158            );
159        }
160        let sources = &options.sources[0];
161        match caches.as_deref_mut() {
162            Some(cache) => {
163                let prepared =
164                    prepare_svg_font_incremental(&svg_options, sources, &mut cache.glyphs)?;
165                build_font_outputs(
166                    &options.common,
167                    &svg_options,
168                    &prepared,
169                    Some(&mut cache.ttf),
170                )
171            }
172            None => {
173                let prepared = prepare_svg_font(&svg_options, sources)?;
174                build_font_outputs(&options.common, &svg_options, &prepared, None)
175            }
176        }
177    })();
178    if outputs.is_err()
179        && let Some(cache) = caches
180    {
181        *cache = SourceCaches::default();
182    }
183    outputs
184}
185
186impl GenerateWebfontsFromSourcesResult {
187    /// Borrow the requested SVG font.
188    pub fn svg_string(&self) -> Option<&str> {
189        self.fonts.svg_font.as_deref().map(String::as_str)
190    }
191    /// Borrow the requested TTF font.
192    pub fn ttf_bytes(&self) -> Option<&[u8]> {
193        self.fonts.ttf_font.as_deref().map(Vec::as_slice)
194    }
195    /// Borrow the requested EOT font.
196    pub fn eot_bytes(&self) -> Option<&[u8]> {
197        self.fonts.eot_font.as_deref().map(Vec::as_slice)
198    }
199    /// Borrow the requested WOFF font.
200    pub fn woff_bytes(&self) -> Option<&[u8]> {
201        self.fonts.woff_font.as_deref().map(Vec::as_slice)
202    }
203    /// Borrow the requested WOFF2 font.
204    pub fn woff2_bytes(&self) -> Option<&[u8]> {
205        self.fonts.woff2_font.as_deref().map(Vec::as_slice)
206    }
207
208    fn shared(&self) -> &SharedTemplateData {
209        self.shared.get_or_init(|| {
210            let sources = self
211                .options
212                .sources
213                .iter()
214                .flatten()
215                .cloned()
216                .collect::<Vec<_>>();
217            SharedTemplateData::new(
218                &self.options.common,
219                self.options.css_template.clone(),
220                calc_content_hash(
221                    &self.options.common,
222                    self.options.variants.as_ref(),
223                    &sources,
224                    self.options.css_template.as_deref(),
225                    self.options.html_template.as_deref(),
226                ),
227            )
228        })
229    }
230
231    fn css_context(&self) -> serde_json::Map<String, serde_json::Value> {
232        let options = &self.options.common;
233        let shared = self.shared();
234        make_content_css_context(
235            options,
236            self.options.variants.as_ref(),
237            &make_urls(options, &shared.hash, options.css_fonts_url.as_deref()),
238            shared,
239        )
240    }
241
242    /// Render CSS on first request and share the cached string thereafter.
243    /// Explicit requests render even when CSS collection is disabled.
244    pub fn generate_css(&self) -> Result<Arc<String>> {
245        self.css
246            .get_or_init(|| {
247                render_css_with_context(self.shared(), &self.css_context())
248                    .map(Arc::new)
249                    .map_err(|error| error.to_string())
250            })
251            .clone()
252            .map_err(Error::other)
253    }
254
255    /// Render an HTML preview on first request. Its styles reuse the CSS render cache.
256    /// Requesting CSS alone never compiles the HTML template.
257    pub fn generate_html(&self) -> Result<Arc<String>> {
258        self.html
259            .get_or_init(|| {
260                self.render_html()
261                    .map(Arc::new)
262                    .map_err(|error| error.to_string())
263            })
264            .clone()
265            .map_err(Error::other)
266    }
267
268    fn render_html(&self) -> Result<String> {
269        #[cfg(test)]
270        self.html_render_count
271            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
272        let template = compile_html_template(self.options.html_template.clone())?;
273        let css = self.generate_css()?;
274        let sources = self
275            .options
276            .sources
277            .iter()
278            .flatten()
279            .cloned()
280            .collect::<Vec<_>>();
281        let css_ctx = self.options.variants.as_ref().map(|_| self.css_context());
282        let context = make_content_html_context(
283            &self.options.common,
284            self.shared(),
285            &sources,
286            css.as_ref().clone(),
287            css_ctx.as_ref(),
288        );
289        let hbs_context = handlebars::Context::wraps(&context).map_err(Error::other)?;
290        render_html_with_hbs_context(template.registry.as_ref(), &hbs_context, &context)
291    }
292
293    /// Collect fonts and enabled companions, sharing all payloads with this result.
294    pub fn collect_outputs(&self) -> Result<Vec<CollectedOutput>> {
295        let css = self
296            .options
297            .common
298            .css
299            .then(|| self.generate_css())
300            .transpose()?;
301        let html = self
302            .options
303            .common
304            .html
305            .then(|| self.generate_html())
306            .transpose()?;
307        Ok(collect_outputs(&self.fonts, css, html))
308    }
309}