1use 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
13pub 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 #[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 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 pub fn stats(&self) -> &EmbedStats {
97 &self.stats
98 }
99
100 fn compile_cached(&self) -> Result<()> {
102 if self.compiled_cache.lock().unwrap().is_some() {
104 return Ok(());
105 }
106
107 let main_file = self
109 .templates
110 .get_file(&self.entry)
111 .ok_or_else(|| Error::EntryNotFound(self.entry.clone()))?;
112
113 let main_bytes = decompress(main_file.contents())?;
115 let main_content = std::str::from_utf8(&main_bytes).map_err(|_| Error::InvalidUtf8)?;
116
117 let resolver = EmbeddedResolver::new(self.templates, self.packages);
119
120 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 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 let inputs = self.inputs.lock().unwrap().clone();
139
140 let warned_result = if let Some(inputs) = inputs {
142 engine.compile_with_input::<_, PagedDocument>(inputs)
143 } else {
144 engine.compile::<PagedDocument>()
145 };
146
147 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 *self.compiled_cache.lock().unwrap() = Some(compiled);
162
163 Ok(())
164 }
165
166 #[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 #[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 #[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}