1use crate::error::{Error, Result};
4use crate::resolver::EmbeddedResolver;
5use crate::stats::EmbedStats;
6use crate::util::decompress;
7use include_dir::{Dir, File};
8use std::sync::{Mutex, MutexGuard};
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: &'static str,
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: &'static str,
36 stats: EmbedStats,
37 ) -> Self {
38 Self {
39 templates,
40 packages,
41 fonts,
42 entry,
43 inputs: Mutex::new(None),
44 stats,
45 compiled_cache: Mutex::new(None),
46 }
47 }
48
49 fn lock_inputs(&self) -> MutexGuard<'_, Option<Dict>> {
50 self.inputs.lock().expect("lock poisoned")
51 }
52
53 fn lock_cache(&self) -> MutexGuard<'_, Option<PagedDocument>> {
54 self.compiled_cache.lock().expect("lock poisoned")
55 }
56
57 pub fn with_inputs<T: Into<Dict>>(self, inputs: T) -> Self {
98 *self.lock_inputs() = Some(inputs.into());
99 *self.lock_cache() = None;
100 self
101 }
102
103 pub fn stats(&self) -> &EmbedStats {
105 &self.stats
106 }
107
108 fn compile_cached(&self) -> Result<()> {
110 if self.lock_cache().is_some() {
111 return Ok(());
112 }
113
114 let main_file =
116 find_entry(self.templates, self.entry).ok_or(Error::EntryNotFound(self.entry))?;
117
118 let main_bytes = decompress(main_file.contents())?;
119 let main_content = std::str::from_utf8(&main_bytes).map_err(|_| Error::InvalidUtf8)?;
120
121 let resolver = EmbeddedResolver::new(self.templates, self.packages);
122
123 let font_data: Vec<Vec<u8>> = self
125 .fonts
126 .files()
127 .map(|f| decompress(f.contents()).map_err(Error::from))
128 .collect::<Result<Vec<_>>>()?;
129
130 let font_refs: Vec<&[u8]> = font_data.iter().map(Vec::as_slice).collect();
131
132 let engine = TypstEngine::builder()
133 .main_file((self.entry, main_content))
134 .add_file_resolver(resolver)
135 .fonts(font_refs)
136 .build();
137
138 let inputs = self.lock_inputs().clone();
140
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 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 => other.to_string(),
156 };
157 Error::Compilation(msg)
158 })?;
159
160 *self.lock_cache() = Some(compiled);
161
162 Ok(())
163 }
164
165 fn with_compiled<F, T>(&self, f: F) -> Result<T>
167 where
168 F: FnOnce(&PagedDocument) -> Result<T>,
169 {
170 self.compile_cached()?;
171 let cache = self.lock_cache();
172 let compiled = cache
173 .as_ref()
174 .expect("compiled_cache must be Some after successful compile_cached()");
175 f(compiled)
176 }
177
178 #[cfg(feature = "pdf")]
186 #[cfg_attr(docsrs, doc(cfg(feature = "pdf")))]
187 pub fn to_pdf(&self) -> Result<Vec<u8>> {
188 self.with_compiled(|compiled| {
189 typst_pdf::pdf(compiled, &typst_pdf::PdfOptions::default())
190 .map_err(|e| Error::PdfGeneration(format!("{e:?}")))
191 })
192 }
193
194 #[cfg(feature = "svg")]
202 #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
203 pub fn to_svg(&self) -> Result<Vec<String>> {
204 self.with_compiled(|compiled| Ok(compiled.pages.iter().map(typst_svg::svg).collect()))
205 }
206
207 #[cfg(feature = "png")]
218 #[cfg_attr(docsrs, doc(cfg(feature = "png")))]
219 pub fn to_png(&self, dpi: f32) -> Result<Vec<Vec<u8>>> {
220 self.with_compiled(|compiled| {
221 let pixel_per_pt = dpi / 72.0;
222 compiled
223 .pages
224 .iter()
225 .map(|page| {
226 typst_render::render(page, pixel_per_pt)
227 .encode_png()
228 .map_err(|e| Error::PngEncoding(e.to_string()))
229 })
230 .collect()
231 })
232 }
233}
234
235fn find_entry<'a>(dir: &'a Dir<'a>, path: &str) -> Option<&'a File<'a>> {
237 let normalized = path.trim_start_matches("./").replace('\\', "/");
238 let (dir_path, file_name) = match normalized.rsplit_once('/') {
239 Some((d, f)) => (Some(d), f),
240 None => (None, normalized.as_str()),
241 };
242
243 let target_dir = match dir_path {
244 Some(dir_path) => {
245 let mut current = dir;
246 for segment in dir_path.split('/') {
247 current = current
248 .dirs()
249 .find(|d| d.path().file_name().and_then(|n| n.to_str()) == Some(segment))?;
250 }
251 current
252 }
253 None => dir,
254 };
255
256 target_dir
257 .files()
258 .find(|f| f.path().file_name().and_then(|n| n.to_str()) == Some(file_name))
259}