typst_bake/
document.rs

1//! Document structure for PDF generation
2
3use crate::resolver::EmbeddedResolver;
4use crate::stats::EmbedStats;
5use include_dir::Dir;
6use std::io::Cursor;
7use typst::foundations::Dict;
8use typst_as_lib::TypstEngine;
9
10/// A document ready for PDF generation.
11///
12/// Created by the `document!()` macro with embedded templates, fonts, and packages.
13pub struct Document {
14    templates: &'static Dir<'static>,
15    packages: &'static Dir<'static>,
16    fonts: &'static Dir<'static>,
17    entry: String,
18    inputs: Option<Dict>,
19    stats: EmbedStats,
20}
21
22impl Document {
23    /// Internal constructor used by the macro.
24    /// Do not use directly.
25    #[doc(hidden)]
26    pub fn __new(
27        templates: &'static Dir<'static>,
28        packages: &'static Dir<'static>,
29        fonts: &'static Dir<'static>,
30        entry: &str,
31        stats: EmbedStats,
32    ) -> Self {
33        Self {
34            templates,
35            packages,
36            fonts,
37            entry: entry.to_string(),
38            inputs: None,
39            stats,
40        }
41    }
42
43    /// Add input data to the document.
44    ///
45    /// The data must implement `IntoDict` from `derive_typst_intoval`.
46    ///
47    /// # Example
48    /// ```rust,ignore
49    /// use derive_typst_intoval::{IntoValue, IntoDict};
50    ///
51    /// #[derive(IntoValue, IntoDict)]
52    /// struct Inputs {
53    ///     title: String,
54    /// }
55    ///
56    /// typst_bake::document!("main.typ")
57    ///     .with_inputs(Inputs { title: "Hello".into() })
58    /// ```
59    pub fn with_inputs<T: Into<Dict>>(mut self, inputs: T) -> Self {
60        self.inputs = Some(inputs.into());
61        self
62    }
63
64    /// Get compression statistics for embedded content.
65    pub fn stats(&self) -> &EmbedStats {
66        &self.stats
67    }
68
69    /// Compile the document and generate PDF.
70    ///
71    /// # Returns
72    /// PDF data as bytes.
73    ///
74    /// # Errors
75    /// Returns an error if compilation or PDF generation fails.
76    pub fn to_pdf(self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
77        // Read main template content (compressed)
78        let main_file = self
79            .templates
80            .get_file(&self.entry)
81            .ok_or_else(|| format!("Entry file not found: {}", self.entry))?;
82
83        // Decompress main file
84        let main_bytes = decompress(main_file.contents())?;
85        let main_content =
86            std::str::from_utf8(&main_bytes).map_err(|_| "Entry file is not valid UTF-8")?;
87
88        // Create resolver
89        let resolver = EmbeddedResolver::new(self.templates, self.packages);
90
91        // Collect and decompress fonts from the embedded fonts directory
92        let font_data: Vec<Vec<u8>> = self
93            .fonts
94            .files()
95            .map(|f| decompress(f.contents()).expect("Font decompression failed"))
96            .collect();
97
98        let font_refs: Vec<&[u8]> = font_data.iter().map(|v| v.as_slice()).collect();
99
100        // Build engine with main file, resolver, and fonts
101        let builder = TypstEngine::builder()
102            .main_file(main_content)
103            .add_file_resolver(resolver)
104            .fonts(font_refs.into_iter());
105
106        let engine = builder.build();
107
108        // Compile (with or without inputs)
109        // Use PagedDocument as the concrete document type
110        use typst::layout::PagedDocument;
111        let warned_result = if let Some(inputs) = self.inputs {
112            engine.compile_with_input::<_, PagedDocument>(inputs)
113        } else {
114            engine.compile::<PagedDocument>()
115        };
116
117        // Handle the Warned wrapper and extract result
118        let compiled =
119            warned_result
120                .output
121                .map_err(|e| format!("Compilation failed: {:?}", e))?;
122
123        // Generate PDF
124        let pdf_bytes = typst_pdf::pdf(&compiled, &typst_pdf::PdfOptions::default())
125            .map_err(|e| format!("PDF generation failed: {:?}", e))?;
126
127        Ok(pdf_bytes)
128    }
129}
130
131/// Decompress zstd compressed data
132fn decompress(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
133    let decompressed = zstd::decode_all(Cursor::new(data))?;
134    Ok(decompressed)
135}