use anyhow::{Context, anyhow, bail};
use common::parser_tools::ExportImages;
use crate::typst_markup::typst_image_paths;
use typst::diag::{SourceDiagnostic, Warned};
use typst::ecow::EcoVec;
use typst::foundations::Bytes;
use typst::text::Font;
use typst_as_lib::TypstEngine;
use typst_layout::PagedDocument;
use typst_pdf::PdfOptions;
pub fn compile_typst_pdf(
markup: &str,
fonts: Vec<Vec<u8>>,
images: &ExportImages,
) -> anyhow::Result<(Vec<u8>, usize)> {
if fonts.is_empty() {
bail!("no fonts supplied: at least one embedded font is required");
}
let mut faces: Vec<Font> = Vec::new();
for (i, bytes) in fonts.into_iter().enumerate() {
let len = bytes.len();
let parsed: Vec<Font> = Font::iter(Bytes::new(bytes)).collect();
if parsed.is_empty() {
bail!(
"font #{i} ({len} bytes) could not be parsed as a font (corrupt or unsupported format)"
);
}
faces.extend(parsed);
}
let resolved_images: Vec<(String, Vec<u8>)> = typst_image_paths(images)
.into_iter()
.filter_map(|(src, path)| Some((path, images.get(&src)?.bytes.clone())))
.collect();
let engine = TypstEngine::builder()
.main_file(markup.to_string())
.fonts(faces)
.with_static_file_resolver(
resolved_images
.iter()
.map(|(path, bytes)| (path.as_str(), bytes.clone()))
.collect::<Vec<_>>(),
)
.build();
let Warned { output, warnings } = engine.compile::<PagedDocument>();
let doc = output.map_err(|err| anyhow!(render_compile_error(err, &warnings)))?;
if !warnings.is_empty() {
eprintln!("typst compile warnings:\n{}", render_diagnostics(&warnings));
}
let page_count = doc.pages().len();
let options = PdfOptions::default();
let pdf = typst_pdf::pdf(&doc, &options)
.map_err(|diags| anyhow!(render_diagnostics(&diags)))
.context("typst_pdf::pdf failed")?;
Ok((pdf, page_count))
}
fn render_compile_error(
err: typst_as_lib::TypstAsLibError,
warnings: &EcoVec<SourceDiagnostic>,
) -> String {
use typst_as_lib::TypstAsLibError as E;
match err {
E::TypstSource(diags) => {
let mut s = render_diagnostics(&diags);
if !warnings.is_empty() {
s.push_str("\n--- warnings ---\n");
s.push_str(&render_diagnostics(warnings));
}
s
}
other => format!("{other}"),
}
}
fn render_diagnostics(diags: &EcoVec<SourceDiagnostic>) -> String {
diags
.iter()
.map(|d| {
let mut s = format!("{:?}: {}", d.severity, d.message);
for hint in &d.hints {
s.push_str(&format!("\n hint: {}", hint.v));
}
s
})
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_FONT: &[u8] = include_bytes!("../tests/assets/DejaVuSerif.ttf");
#[test]
fn compiles_trivial_markup_to_a_pdf() {
let markup = "#set page(width: 10cm, height: 10cm)\nHello, world.";
let (pdf, pages) = compile_typst_pdf(markup, vec![TEST_FONT.to_vec()], &Default::default())
.expect("compiles");
assert!(pdf.starts_with(b"%PDF-"), "output must be a PDF");
assert!(pdf.len() > 100, "a real PDF is not a handful of bytes");
assert_eq!(pages, 1, "one short line lays out on a single page");
}
#[test]
fn empty_font_list_is_rejected() {
let err = compile_typst_pdf("Hello.", vec![], &Default::default()).unwrap_err();
assert!(err.to_string().contains("no fonts supplied"));
}
#[test]
fn corrupt_font_bytes_are_rejected_not_silently_dropped() {
let err =
compile_typst_pdf("Hello.", vec![vec![0u8; 16]], &Default::default()).unwrap_err();
assert!(
err.to_string().contains("could not be parsed as a font"),
"got: {err}"
);
}
#[test]
fn bad_markup_reports_a_readable_diagnostic() {
let err = compile_typst_pdf(
"#foo_bar_does_not_exist()",
vec![TEST_FONT.to_vec()],
&Default::default(),
)
.unwrap_err();
assert!(err.to_string().contains("unknown variable"), "got: {err}");
}
#[test]
fn missing_font_family_is_a_warning_not_an_error() {
let markup = "#set text(font: \"Totally Not A Real Font\")\nHello.";
let (pdf, _) = compile_typst_pdf(markup, vec![TEST_FONT.to_vec()], &Default::default())
.expect("still compiles");
assert!(pdf.starts_with(b"%PDF-"));
}
}