typst_bake/
document.rs

1//! Document structure for document rendering
2
3use crate::error::{Error, Result};
4use crate::resolver::EmbeddedResolver;
5use crate::stats::EmbedStats;
6use crate::util::decompress;
7use include_dir::Dir;
8use std::sync::Mutex;
9use typst::foundations::Dict;
10use typst::layout::PagedDocument;
11use typst_as_lib::{TypstAsLibError, TypstEngine};
12
13/// A fully self-contained document ready for rendering.
14///
15/// Created by the [`document!`](crate::document!) macro with embedded templates, fonts,
16/// and packages. All resources are compressed with zstd and decompressed lazily at runtime.
17pub struct Document {
18    templates: &'static Dir<'static>,
19    packages: &'static Dir<'static>,
20    fonts: &'static Dir<'static>,
21    entry: String,
22    inputs: Mutex<Option<Dict>>,
23    stats: EmbedStats,
24    compiled_cache: Mutex<Option<PagedDocument>>,
25}
26
27impl Document {
28    /// Internal constructor used by the macro.
29    /// Do not use directly.
30    #[doc(hidden)]
31    pub fn __new(
32        templates: &'static Dir<'static>,
33        packages: &'static Dir<'static>,
34        fonts: &'static Dir<'static>,
35        entry: &str,
36        stats: EmbedStats,
37    ) -> Self {
38        Self {
39            templates,
40            packages,
41            fonts,
42            entry: entry.to_string(),
43            inputs: Mutex::new(None),
44            stats,
45            compiled_cache: Mutex::new(None),
46        }
47    }
48
49    /// Add input data to the document.
50    ///
51    /// Define your data structs using the derive macros:
52    /// - **Top-level struct**: Use both [`IntoValue`](crate::IntoValue) and [`IntoDict`](crate::IntoDict)
53    /// - **Nested structs**: Use [`IntoValue`](crate::IntoValue) only
54    ///
55    /// In Typst templates, access the data via `sys.inputs`:
56    /// ```typ
57    /// #import sys: inputs
58    /// = #inputs.title
59    /// ```
60    ///
61    /// # Example
62    ///
63    /// ```rust,ignore
64    /// use typst_bake::{IntoValue, IntoDict};
65    ///
66    /// #[derive(IntoValue, IntoDict)]  // Top-level: both macros
67    /// struct Inputs {
68    ///     title: String,
69    ///     products: Vec<Product>,
70    /// }
71    ///
72    /// #[derive(IntoValue)]  // Nested: IntoValue only
73    /// struct Product {
74    ///     name: String,
75    ///     price: f64,
76    /// }
77    ///
78    /// let inputs = Inputs {
79    ///     title: "Catalog".to_string(),
80    ///     products: vec![
81    ///         Product { name: "Apple".to_string(), price: 1.50 },
82    ///     ],
83    /// };
84    ///
85    /// let pdf = typst_bake::document!("main.typ")
86    ///     .with_inputs(inputs)
87    ///     .to_pdf()?;
88    /// ```
89    pub fn with_inputs<T: Into<Dict>>(self, inputs: T) -> Self {
90        *self.inputs.lock().unwrap() = Some(inputs.into());
91        *self.compiled_cache.lock().unwrap() = None;
92        self
93    }
94
95    /// Get compression statistics for embedded content.
96    pub fn stats(&self) -> &EmbedStats {
97        &self.stats
98    }
99
100    /// Internal method to compile the document (with caching).
101    fn compile_cached(&self) -> Result<()> {
102        // Return early if already cached
103        if self.compiled_cache.lock().unwrap().is_some() {
104            return Ok(());
105        }
106
107        // Read main template content (compressed)
108        let main_file = self
109            .templates
110            .get_file(&self.entry)
111            .ok_or_else(|| Error::EntryNotFound(self.entry.clone()))?;
112
113        // Decompress main file
114        let main_bytes = decompress(main_file.contents())?;
115        let main_content = std::str::from_utf8(&main_bytes).map_err(|_| Error::InvalidUtf8)?;
116
117        // Create resolver
118        let resolver = EmbeddedResolver::new(self.templates, self.packages);
119
120        // Collect and decompress fonts from the embedded fonts directory
121        let font_data: Vec<Vec<u8>> = self
122            .fonts
123            .files()
124            .map(|f| decompress(f.contents()).expect("Font decompression failed"))
125            .collect();
126
127        let font_refs: Vec<&[u8]> = font_data.iter().map(|v| v.as_slice()).collect();
128
129        // Build engine with main file, resolver, and fonts
130        let builder = TypstEngine::builder()
131            .main_file(main_content)
132            .add_file_resolver(resolver)
133            .fonts(font_refs);
134
135        let engine = builder.build();
136
137        // Clone inputs (preserve for retry on failure)
138        let inputs = self.inputs.lock().unwrap().clone();
139
140        // Compile (with or without inputs)
141        let warned_result = if let Some(inputs) = inputs {
142            engine.compile_with_input::<_, PagedDocument>(inputs)
143        } else {
144            engine.compile::<PagedDocument>()
145        };
146
147        // Handle the Warned wrapper and extract result
148        let compiled = warned_result.output.map_err(|e| {
149            let msg = match e {
150                TypstAsLibError::TypstSource(diagnostics) => diagnostics
151                    .iter()
152                    .map(|d| d.message.as_str())
153                    .collect::<Vec<_>>()
154                    .join("\n"),
155                other => format!("{other}"),
156            };
157            Error::Compilation(msg)
158        })?;
159
160        // Store in cache
161        *self.compiled_cache.lock().unwrap() = Some(compiled);
162
163        Ok(())
164    }
165
166    /// Compile the document and generate PDF.
167    ///
168    /// # Returns
169    /// PDF data as bytes.
170    ///
171    /// # Errors
172    /// Returns an error if compilation or PDF generation fails.
173    #[cfg(feature = "pdf")]
174    #[cfg_attr(docsrs, doc(cfg(feature = "pdf")))]
175    pub fn to_pdf(&self) -> Result<Vec<u8>> {
176        self.compile_cached()?;
177        let cache = self.compiled_cache.lock().unwrap();
178        let compiled = cache.as_ref().unwrap();
179        let pdf_bytes = typst_pdf::pdf(compiled, &typst_pdf::PdfOptions::default())
180            .map_err(|e| Error::PdfGeneration(format!("{e:?}")))?;
181        Ok(pdf_bytes)
182    }
183
184    /// Compile the document and generate SVG for each page.
185    ///
186    /// # Returns
187    /// A vector of SVG strings, one per page.
188    ///
189    /// # Errors
190    /// Returns an error if compilation fails.
191    #[cfg(feature = "svg")]
192    #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
193    pub fn to_svg(&self) -> Result<Vec<String>> {
194        self.compile_cached()?;
195        let cache = self.compiled_cache.lock().unwrap();
196        let compiled = cache.as_ref().unwrap();
197        let svgs: Vec<String> = compiled.pages.iter().map(typst_svg::svg).collect();
198        Ok(svgs)
199    }
200
201    /// Compile the document and generate PNG for each page.
202    ///
203    /// # Arguments
204    /// * `dpi` - Resolution in dots per inch (e.g., 72 for 1:1, 144 for Retina, 300 for print)
205    ///
206    /// # Returns
207    /// A vector of PNG bytes, one per page.
208    ///
209    /// # Errors
210    /// Returns an error if compilation or PNG encoding fails.
211    #[cfg(feature = "png")]
212    #[cfg_attr(docsrs, doc(cfg(feature = "png")))]
213    pub fn to_png(&self, dpi: f32) -> Result<Vec<Vec<u8>>> {
214        self.compile_cached()?;
215        let cache = self.compiled_cache.lock().unwrap();
216        let compiled = cache.as_ref().unwrap();
217        let pixel_per_pt = dpi / 72.0;
218        let mut pngs = Vec::with_capacity(compiled.pages.len());
219        for page in &compiled.pages {
220            let pixmap = typst_render::render(page, pixel_per_pt);
221            let png = pixmap
222                .encode_png()
223                .map_err(|e| Error::PngEncoding(format!("{e}")))?;
224            pngs.push(png);
225        }
226        Ok(pngs)
227    }
228}