use crate::error::TypstError;
use crate::media::Media;
use image::DynamicImage;
use std::sync::OnceLock;
use tracing::debug;
use typst::diag::FileResult;
use typst::foundations::{Bytes, Datetime, Duration};
use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook};
use typst::utils::{LazyHash, Scalar};
use typst::{Library, LibraryExt};
use typst_kit::datetime::Time;
use typst_kit::fonts::FontStore;
use typst_layout::PagedDocument;
use typst_render::RenderOptions;
static FONT_STORE: OnceLock<FontStore> = OnceLock::new();
struct TypstWrapperWorld {
source: Source,
library: LazyHash<Library>,
fonts: &'static FontStore,
time: Time,
}
impl TypstWrapperWorld {
fn new(source: String) -> Self {
let fonts = FONT_STORE.get_or_init(|| {
debug!("Loading embedded fonts (one-time initialization)...");
let mut fonts = FontStore::new();
fonts.extend(typst_kit::fonts::embedded());
debug!("Found {} fonts:", fonts.book().families().count());
fonts.book().families().for_each(|f| debug!("- {}", f.0));
fonts
});
Self {
library: LazyHash::new(Library::default()),
fonts,
source: Source::detached(source),
time: Time::system(),
}
}
}
impl typst::World for TypstWrapperWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.fonts.book()
}
fn main(&self) -> FileId {
self.source.id()
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.source.id() {
Ok(self.source.clone())
} else {
todo!("Not implemented!")
}
}
fn file(&self, _id: FileId) -> FileResult<Bytes> {
todo!("Not implemented!")
}
fn font(&self, id: usize) -> Option<Font> {
self.fonts.font(id)
}
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
self.time.today(offset)
}
}
pub fn render_test_label(media: Media) -> Result<DynamicImage, TypstError> {
let label_template = include_str!("../typst/label.typ");
let label_call = format!(
r#"
#label(
width: {}pt,
height: {}pt,
name: "{}",
color_support: {}
)
"#,
media.width_dots(),
media.length_dots().unwrap_or(300),
media,
media.supports_color(),
);
debug!("Rendering example label for {media}...");
let world = TypstWrapperWorld::new(format!("{label_template}{label_call}"));
let document: PagedDocument = typst::compile(&world).output.map_err(|err| TypstError {
reason: format!("Typst compilation failed: {err:?}"),
})?;
let page = document.pages().first().ok_or_else(|| TypstError {
reason: "Compiled document has no pages".to_string(),
})?;
let render_options = RenderOptions {
pixel_per_pt: Scalar::new(1.0),
..RenderOptions::default()
};
let pixmap = typst_render::render(page, &render_options);
let buf = pixmap.encode_png().map_err(|err| TypstError {
reason: format!("PNG encoding failed: {err}"),
})?;
image::load_from_memory(&buf).map_err(|err| TypstError {
reason: format!("Failed to load PNG from memory: {err}"),
})
}
#[cfg(test)]
mod tests {
use super::*;
use strum::IntoEnumIterator;
#[test]
fn test_labels_match_media_dimensions() {
for media in Media::iter() {
let image = render_test_label(media).expect("test label should render");
assert_eq!(image.width(), media.width_dots(), "{media}");
assert_eq!(
image.height(),
media.length_dots().unwrap_or(300),
"{media}"
);
}
}
}