use std::num::NonZeroUsize;
use ecow::EcoVec;
use typst::World;
use typst::diag::{SourceDiagnostic, Warned};
use typst_html::HtmlDocument;
use typst_layout::PagedDocument;
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Pdf,
Png,
Svg,
Html,
}
impl OutputFormat {
pub fn extension(self) -> &'static str {
match self {
Self::Pdf => "pdf",
Self::Png => "png",
Self::Svg => "svg",
Self::Html => "html",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CompileOptions {
pub pages: Vec<PageRange>,
pub ppi: Option<f32>,
pub pdf_standards: PdfStandards,
pub creation_timestamp: Option<Timestamp>,
}
pub type PageRange = std::ops::RangeInclusive<Option<NonZeroUsize>>;
pub fn parse_pages(text: &str) -> Result<Vec<PageRange>, String> {
text.split(',')
.map(|part| {
let part = part.trim();
let parse = |s: &str| -> Result<Option<NonZeroUsize>, String> {
if s.is_empty() {
return Ok(None);
}
s.parse::<NonZeroUsize>()
.map(Some)
.map_err(|_| format!("invalid page number `{s}`"))
};
match part.split_once('-') {
Some((start, end)) => Ok(parse(start)?..=parse(end)?),
None => {
let page = parse(part)?.ok_or_else(|| "empty page range".to_owned())?;
Ok(Some(page)..=Some(page))
}
}
})
.collect()
}
#[derive(Debug, Clone)]
pub struct CompileOutput {
pub format: OutputFormat,
pub outputs: Vec<Vec<u8>>,
pub warnings: EcoVec<SourceDiagnostic>,
}
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
#[error("compilation failed with {} error(s)", errors.len())]
Diagnostics {
errors: EcoVec<SourceDiagnostic>,
warnings: EcoVec<SourceDiagnostic>,
},
#[error("PNG encoding failed: {0}")]
PngEncoding(String),
}
pub fn compile(
world: &dyn World,
format: OutputFormat,
options: &CompileOptions,
) -> Result<CompileOutput, CompileError> {
if format == OutputFormat::Html {
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
let document = output.map_err(|errors| CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
})?;
let html =
typst_html::html(&document, &typst_html::HtmlOptions::default()).map_err(|errors| {
CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
}
})?;
return Ok(CompileOutput {
format,
outputs: vec![html.into_bytes()],
warnings,
});
}
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
let document = output.map_err(|errors| CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
})?;
let outputs = match format {
OutputFormat::Pdf => {
let timestamp = options
.creation_timestamp
.or_else(|| world.today(None).map(Timestamp::new_utc));
let pdf_options = PdfOptions {
timestamp,
page_ranges: page_ranges(options),
standards: options.pdf_standards.clone(),
..Default::default()
};
let pdf = typst_pdf::pdf(&document, &pdf_options).map_err(|errors| {
CompileError::Diagnostics {
errors,
warnings: warnings.clone(),
}
})?;
vec![pdf]
}
OutputFormat::Png => {
let ppi = options.ppi.unwrap_or(144.0);
let render_options = typst_render::RenderOptions {
pixel_per_pt: (f64::from(ppi) / 72.0).into(),
..Default::default()
};
selected_pages(&document, options)
.map(|page| {
typst_render::render(page, &render_options)
.encode_png()
.map_err(|err| CompileError::PngEncoding(err.to_string()))
})
.collect::<Result<Vec<_>, _>>()?
}
OutputFormat::Svg => {
let svg_options = typst_svg::SvgOptions::default();
selected_pages(&document, options)
.map(|page| typst_svg::svg(page, &svg_options).into_bytes())
.collect()
}
OutputFormat::Html => unreachable!("handled above"),
};
Ok(CompileOutput {
format,
outputs,
warnings,
})
}
fn page_ranges(options: &CompileOptions) -> Option<typst::layout::PageRanges> {
(!options.pages.is_empty()).then(|| typst::layout::PageRanges::new(options.pages.clone()))
}
fn selected_pages<'a>(
document: &'a PagedDocument,
options: &'a CompileOptions,
) -> impl Iterator<Item = &'a typst_layout::Page> {
let ranges = page_ranges(options);
document
.pages()
.iter()
.enumerate()
.filter(move |(index, _)| {
ranges.as_ref().is_none_or(|ranges| {
NonZeroUsize::new(index + 1).is_some_and(|number| ranges.includes_page(number))
})
})
.map(|(_, page)| page)
}