use std::path::Path;
use chrono::{Datelike, Timelike, Utc};
use typst::foundations::Smart;
use typst::layout::PageRanges;
use typst_layout::PagedDocument;
use typst_pdf::{PdfStandard, PdfStandards, Timestamp};
use crate::clock::Clock;
use crate::data::DataSet;
use crate::diagnostics::to_diagnostics;
use crate::error::{PublishError, Result};
use crate::fonts::SharedFonts;
use crate::template::{PreparedTemplate, TemplateSource};
use crate::world::PublishWorld;
pub fn render_pdf(
fonts: &SharedFonts,
template: TemplateSource,
data: DataSet,
config: &PdfConfig,
) -> Result<Vec<u8>> {
let prepared = PreparedTemplate::new(template)?;
render_pdf_prepared(fonts, &prepared, data, config)
}
pub fn render_pdf_prepared(
fonts: &SharedFonts,
template: &PreparedTemplate,
data: DataSet,
config: &PdfConfig,
) -> Result<Vec<u8>> {
let world = PublishWorld::new(
template.root.clone(),
template.source.clone(),
fonts,
data,
config.clock,
typst::Features::default(),
)?;
let warned = typst::compile::<PagedDocument>(&world);
let doc = warned
.output
.map_err(|diags| PublishError::Compilation(to_diagnostics(&world, &diags)))?;
let options = build_pdf_options(config)?;
let pdf_bytes = typst_pdf::pdf(&doc, &options)
.map_err(|diags| PublishError::Export(to_diagnostics(&world, &diags)))?;
Ok(pdf_bytes)
}
pub fn render_pdf_to_file(
fonts: &SharedFonts,
template: TemplateSource,
data: DataSet,
config: &PdfConfig,
output: &Path,
) -> Result<()> {
let bytes = render_pdf(fonts, template, data, config)?;
std::fs::write(output, &bytes)?;
Ok(())
}
#[derive(Clone)]
pub struct PdfConfig {
ident: Smart<String>,
creator: Smart<Option<String>>,
timestamp: Option<Timestamp>,
page_ranges: Option<PageRanges>,
standards: Vec<PdfStandard>,
tagged: bool,
pretty: bool,
pub(crate) clock: Clock,
}
impl PdfConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn ident(mut self, ident: impl Into<String>) -> Self {
self.ident = Smart::Custom(ident.into());
self
}
#[must_use]
pub fn creator(mut self, creator: impl Into<String>) -> Self {
self.creator = Smart::Custom(Some(creator.into()));
self
}
#[must_use]
pub fn pretty(mut self, pretty: bool) -> Self {
self.pretty = pretty;
self
}
#[must_use]
pub fn timestamp(mut self, ts: Timestamp) -> Self {
self.timestamp = Some(ts);
self
}
#[must_use]
pub fn timestamp_now_utc(self) -> Self {
let now = Utc::now();
if let Some(dt) = typst::foundations::Datetime::from_ymd_hms(
now.year(),
now.month().try_into().unwrap_or(1),
now.day().try_into().unwrap_or(1),
now.hour().try_into().unwrap_or(0),
now.minute().try_into().unwrap_or(0),
now.second().try_into().unwrap_or(0),
) {
Self {
timestamp: Some(Timestamp::new_utc(dt)),
..self
}
} else {
self
}
}
#[must_use]
pub fn page_ranges(mut self, ranges: PageRanges) -> Self {
self.page_ranges = Some(ranges);
self
}
#[must_use]
pub fn standards(mut self, standards: &[PdfStandard]) -> Self {
self.standards = standards.to_vec();
self
}
#[must_use]
pub fn archival_accessible(self) -> Self {
self.standards(&[PdfStandard::Ua_1, PdfStandard::A_2a])
}
#[must_use]
pub fn tagged(mut self, tagged: bool) -> Self {
self.tagged = tagged;
self
}
#[must_use]
pub fn clock(mut self, clock: Clock) -> Self {
self.clock = clock;
self
}
}
impl Default for PdfConfig {
fn default() -> Self {
Self {
ident: Smart::Auto,
creator: Smart::Auto,
timestamp: None,
page_ranges: None,
standards: Vec::new(),
tagged: true,
pretty: false,
clock: Clock::SystemLocal,
}
}
}
fn build_pdf_options(config: &PdfConfig) -> Result<typst_pdf::PdfOptions> {
let ident = config.ident.clone();
let standards = if config.standards.is_empty() {
PdfStandards::default()
} else {
PdfStandards::new(&config.standards).map_err(|e| {
let mut msg = e.message().to_string();
for hint in e.hints() {
msg.push_str("\n hint: ");
msg.push_str(hint);
}
PublishError::InvalidStandards(msg)
})?
};
Ok(typst_pdf::PdfOptions {
ident,
creator: config.creator.clone(),
timestamp: config.timestamp,
page_ranges: config.page_ranges.clone(),
standards,
tagged: config.tagged,
pretty: config.pretty,
})
}