1use std::num::NonZeroUsize;
4
5use ecow::EcoVec;
6use typst::World;
7use typst::diag::{SourceDiagnostic, Warned};
8use typst_html::HtmlDocument;
9use typst_layout::PagedDocument;
10use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OutputFormat {
15 Pdf,
16 Png,
17 Svg,
18 Html,
23}
24
25impl OutputFormat {
26 pub fn extension(self) -> &'static str {
28 match self {
29 Self::Pdf => "pdf",
30 Self::Png => "png",
31 Self::Svg => "svg",
32 Self::Html => "html",
33 }
34 }
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct CompileOptions {
40 pub pages: Vec<PageRange>,
45 pub ppi: Option<f32>,
47 pub pdf_standards: PdfStandards,
49 pub creation_timestamp: Option<Timestamp>,
52}
53
54pub type PageRange = std::ops::RangeInclusive<Option<NonZeroUsize>>;
56
57pub fn parse_pages(text: &str) -> Result<Vec<PageRange>, String> {
59 text.split(',')
60 .map(|part| {
61 let part = part.trim();
62 let parse = |s: &str| -> Result<Option<NonZeroUsize>, String> {
63 if s.is_empty() {
64 return Ok(None);
65 }
66 s.parse::<NonZeroUsize>()
67 .map(Some)
68 .map_err(|_| format!("invalid page number `{s}`"))
69 };
70 match part.split_once('-') {
71 Some((start, end)) => Ok(parse(start)?..=parse(end)?),
72 None => {
73 let page = parse(part)?.ok_or_else(|| "empty page range".to_owned())?;
74 Ok(Some(page)..=Some(page))
75 }
76 }
77 })
78 .collect()
79}
80
81#[derive(Debug, Clone)]
83pub struct CompileOutput {
84 pub format: OutputFormat,
86 pub outputs: Vec<Vec<u8>>,
89 pub warnings: EcoVec<SourceDiagnostic>,
91}
92
93#[derive(Debug, thiserror::Error)]
95pub enum CompileError {
96 #[error("compilation failed with {} error(s)", errors.len())]
99 Diagnostics {
100 errors: EcoVec<SourceDiagnostic>,
101 warnings: EcoVec<SourceDiagnostic>,
102 },
103 #[error("PNG encoding failed: {0}")]
104 PngEncoding(String),
105}
106
107pub fn compile(
112 world: &dyn World,
113 format: OutputFormat,
114 options: &CompileOptions,
115) -> Result<CompileOutput, CompileError> {
116 if format == OutputFormat::Html {
117 let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
118 let document = output.map_err(|errors| CompileError::Diagnostics {
119 errors,
120 warnings: warnings.clone(),
121 })?;
122 let html =
123 typst_html::html(&document, &typst_html::HtmlOptions::default()).map_err(|errors| {
124 CompileError::Diagnostics {
125 errors,
126 warnings: warnings.clone(),
127 }
128 })?;
129 return Ok(CompileOutput {
130 format,
131 outputs: vec![html.into_bytes()],
132 warnings,
133 });
134 }
135
136 let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
137 let document = output.map_err(|errors| CompileError::Diagnostics {
138 errors,
139 warnings: warnings.clone(),
140 })?;
141
142 let outputs = match format {
143 OutputFormat::Pdf => {
144 let timestamp = options
145 .creation_timestamp
146 .or_else(|| world.today(None).map(Timestamp::new_utc));
147 let pdf_options = PdfOptions {
148 timestamp,
149 page_ranges: page_ranges(options),
150 standards: options.pdf_standards.clone(),
151 ..Default::default()
152 };
153 let pdf = typst_pdf::pdf(&document, &pdf_options).map_err(|errors| {
154 CompileError::Diagnostics {
155 errors,
156 warnings: warnings.clone(),
157 }
158 })?;
159 vec![pdf]
160 }
161 OutputFormat::Png => {
162 let ppi = options.ppi.unwrap_or(144.0);
163 let render_options = typst_render::RenderOptions {
164 pixel_per_pt: (f64::from(ppi) / 72.0).into(),
165 ..Default::default()
166 };
167 selected_pages(&document, options)
168 .map(|page| {
169 typst_render::render(page, &render_options)
170 .encode_png()
171 .map_err(|err| CompileError::PngEncoding(err.to_string()))
172 })
173 .collect::<Result<Vec<_>, _>>()?
174 }
175 OutputFormat::Svg => {
176 let svg_options = typst_svg::SvgOptions::default();
177 selected_pages(&document, options)
178 .map(|page| typst_svg::svg(page, &svg_options).into_bytes())
179 .collect()
180 }
181 OutputFormat::Html => unreachable!("handled above"),
182 };
183
184 Ok(CompileOutput {
185 format,
186 outputs,
187 warnings,
188 })
189}
190
191fn page_ranges(options: &CompileOptions) -> Option<typst::layout::PageRanges> {
192 (!options.pages.is_empty()).then(|| typst::layout::PageRanges::new(options.pages.clone()))
193}
194
195fn selected_pages<'a>(
196 document: &'a PagedDocument,
197 options: &'a CompileOptions,
198) -> impl Iterator<Item = &'a typst_layout::Page> {
199 let ranges = page_ranges(options);
200 document
201 .pages()
202 .iter()
203 .enumerate()
204 .filter(move |(index, _)| {
205 ranges.as_ref().is_none_or(|ranges| {
206 NonZeroUsize::new(index + 1).is_some_and(|number| ranges.includes_page(number))
207 })
208 })
209 .map(|(_, page)| page)
210}