use std::{
error::Error,
fs,
path::{Path, PathBuf},
time::Duration,
};
use ecow::eco_format;
use fs::write;
use output_template::{format, has_indexable_template};
use typst::{
diag::{At, SourceResult, Warned},
foundations::Smart,
layout::PagedDocument,
};
use typst_pdf::{PdfOptions, PdfStandard as TypstPdfStandard, PdfStandards, pdf};
use typst_render::render;
use typst_syntax::Span;
use crate::world::SystemWorld;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfStandard {
V1_4,
V1_5,
V1_6,
V1_7,
V2_0,
A1b,
A1a,
A2b,
A2u,
A2a,
A3b,
A3u,
A3a,
A4,
A4f,
A4e,
Ua1,
}
impl From<PdfStandard> for TypstPdfStandard {
fn from(standard: PdfStandard) -> Self {
match standard {
PdfStandard::V1_4 => TypstPdfStandard::V_1_4,
PdfStandard::V1_5 => TypstPdfStandard::V_1_5,
PdfStandard::V1_6 => TypstPdfStandard::V_1_6,
PdfStandard::V1_7 => TypstPdfStandard::V_1_7,
PdfStandard::V2_0 => TypstPdfStandard::V_2_0,
PdfStandard::A1b => TypstPdfStandard::A_1b,
PdfStandard::A1a => TypstPdfStandard::A_1a,
PdfStandard::A2b => TypstPdfStandard::A_2b,
PdfStandard::A2u => TypstPdfStandard::A_2u,
PdfStandard::A2a => TypstPdfStandard::A_2a,
PdfStandard::A3b => TypstPdfStandard::A_3b,
PdfStandard::A3u => TypstPdfStandard::A_3u,
PdfStandard::A3a => TypstPdfStandard::A_3a,
PdfStandard::A4 => TypstPdfStandard::A_4,
PdfStandard::A4f => TypstPdfStandard::A_4f,
PdfStandard::A4e => TypstPdfStandard::A_4e,
PdfStandard::Ua1 => TypstPdfStandard::Ua_1,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CompileParams {
pub input: PathBuf,
pub dict: Vec<(String, String)>,
pub output: PathBuf,
pub font_paths: Vec<PathBuf>,
pub ppi: Option<f32>,
pub package_path: Option<PathBuf>,
pub package_cache_path: Option<PathBuf>,
pub pdf_standards: Option<Vec<PdfStandard>>,
}
pub fn compile(params: &CompileParams) -> Result<Duration, Box<dyn Error>> {
let world = SystemWorld::new(
¶ms.input,
¶ms.font_paths,
params.dict.clone(),
¶ms.package_path,
¶ms.package_cache_path,
)
.map_err(|err| err.to_string())?;
let start = std::time::Instant::now();
let Warned { output, warnings } = typst::compile(&world);
let result = output.and_then(|document| export(&document, params));
match result {
Ok(()) => Ok(start.elapsed()),
Err(errors) => Err(warnings
.into_iter()
.chain(errors)
.map(|diagnostic| {
format!(
"{:?}: {}\n{}",
diagnostic.severity,
diagnostic.message.clone(),
diagnostic
.hints
.iter()
.map(|e| format!("hint: {e}"))
.collect::<Vec<String>>()
.join("\n")
)
})
.collect::<Vec<String>>()
.join("\n")
.into()),
}
}
fn export(document: &PagedDocument, params: &CompileParams) -> SourceResult<()> {
match params.output.extension() {
Some(ext) if ext.eq_ignore_ascii_case("png") => export_image(document, params),
_ => export_pdf(document, params),
}
}
fn export_image(document: &PagedDocument, params: &CompileParams) -> SourceResult<()> {
let output = ¶ms.output.to_str().unwrap_or_default();
let can_handle_multiple = has_indexable_template(output);
if !can_handle_multiple && document.pages.len() > 1 {
panic!("{}", "cannot export multiple images without `{{n}}` in output path");
}
document.pages.iter().enumerate().for_each(|(i, page)| {
let storage;
let path = if can_handle_multiple {
storage = format(output, i + 1, document.pages.len());
Path::new(&storage)
} else {
params.output.as_path()
};
let pixmap = render(page, params.ppi.unwrap_or(144.0) / 72.0);
let buf = pixmap.encode_png().unwrap();
write(path, buf).unwrap();
});
Ok(())
}
fn export_pdf(document: &PagedDocument, params: &CompileParams) -> SourceResult<()> {
let standards = match ¶ms.pdf_standards {
Some(list) => {
let typst_standards: Vec<TypstPdfStandard> = list.iter().map(|s| (*s).into()).collect();
PdfStandards::new(&typst_standards)
.map_err(|err| eco_format!("invalid PDF standards: {err}"))
.at(Span::detached())?
}
None => PdfStandards::default(),
};
let options = PdfOptions {
ident: Smart::Auto,
timestamp: None,
page_ranges: None,
standards,
tagged: true,
};
write(¶ms.output, pdf(document, &options)?)
.map_err(|err| eco_format!("failed to write PDF: {err}"))
.at(Span::detached())?;
Ok(())
}
mod output_template {
const INDEXABLE: [&str; 3] = ["{p}", "{0p}", "{n}"];
pub fn has_indexable_template(output: &str) -> bool {
INDEXABLE.iter().any(|template| output.contains(template))
}
pub fn format(output: &str, this_page: usize, total_pages: usize) -> String {
fn width(i: usize) -> usize {
1 + i.checked_ilog10().unwrap_or(0) as usize
}
let other_templates = ["{t}"];
INDEXABLE
.iter()
.chain(other_templates.iter())
.fold(output.to_string(), |out, template| {
let replacement = match *template {
"{p}" => format!("{this_page}"),
"{0p}" | "{n}" => format!("{:01$}", this_page, width(total_pages)),
"{t}" => format!("{total_pages}"),
_ => unreachable!("unhandled template placeholder {template}"),
};
out.replace(template, replacement.as_str())
})
}
}