use std::{ collections::HashMap, fs, path::{Path, PathBuf}, sync::OnceLock, }; use anyhow::anyhow; use image::RgbaImage; use serde::Serialize; use typst::{ Library, World, diag::{FileError, FileResult}, foundations::{Bytes, Datetime}, layout::{Abs, PagedDocument}, syntax::{FileId, Source, VirtualPath, package::PackageSpec}, text::{Font, FontBook}, utils::LazyHash, visualize::Color, }; use typst_kit::{ download::{Downloader, ProgressSink}, package::PackageStorage, }; // The logic for detecting and loading fonts was ripped straight from: // https://github.com/typst/typst/blob/69dcc89d84176838c293b2d59747cd65e28843ad/crates/typst-cli/src/fonts.rs // https://github.com/typst/typst/blob/69dcc89d84176838c293b2d59747cd65e28843ad/crates/typst-cli/src/world.rs#L193-L195 struct FontSlot { path: PathBuf, index: u32, font: OnceLock>, } impl FontSlot { pub fn get(&self) -> Option { self.font .get_or_init(|| { let data = fs::read(&self.path).ok()?; Font::new(Bytes::new(data), self.index) }) .clone() } } struct FontLoader { book: FontBook, fonts: Vec, } impl FontLoader { fn new() -> Self { Self { book: FontBook::new(), fonts: vec![], } } fn load_font_file(&mut self, data: &'static [u8]) { // https://github.com/typst/typst/blob/be12762d942e978ddf2e0ac5c34125264ab483b7/crates/typst-cli/src/fonts.rs#L107-L121 let font_data = Bytes::new(data); for (i, font) in Font::iter(font_data).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { path: PathBuf::new(), index: i as u32, font: OnceLock::from(Some(font)), }); } } fn load_embedded_fonts(&mut self) { // https://github.com/typst/typst/blob/be12762d942e978ddf2e0ac5c34125264ab483b7/crates/typst-cli/src/fonts.rs#L107-L121 for font_file in typst_assets::fonts() { self.load_font_file(font_file); } } fn load_unifonts(&mut self) { self.load_font_file(showbits_assets::UNIFONT); self.load_font_file(showbits_assets::UNIFONT_JP); self.load_font_file(showbits_assets::UNIFONT_UPPER); } } pub struct Typst { library: LazyHash, book: LazyHash, fonts: Vec, files: HashMap>, packages: PackageStorage, } impl Typst { const MAIN_PATH: &str = "/main.typ"; pub fn new() -> Self { let mut loader = FontLoader::new(); loader.load_embedded_fonts(); loader.load_unifonts(); Self { library: LazyHash::new(Library::default()), book: LazyHash::new(loader.book), fonts: loader.fonts, files: HashMap::new(), packages: PackageStorage::new( None, None, Downloader::new(format!( "showbits-thermal-printer/{}", env!("CARGO_PKG_VERSION") )), ), } } pub fn add_file(&mut self, path: impl ToString, data: impl Into>) { let path = path.to_string(); let data = data.into(); self.files.insert(path, data); } pub fn with_file(mut self, path: impl ToString, data: impl Into>) -> Self { self.add_file(path, data); self } pub fn add_json(&mut self, path: impl ToString, data: &T) { let data = serde_json::to_vec(data).expect("data should serialize to json"); self.add_file(path, data); } pub fn with_json(mut self, path: impl ToString, data: &T) -> Self { self.add_json(path, data); self } pub fn add_main_file(&mut self, data: impl Into>) { self.add_file(Self::MAIN_PATH, data); } pub fn with_main_file(mut self, data: impl Into>) -> Self { self.add_main_file(data); self } pub fn render(&self) -> anyhow::Result { let document = typst::compile::(self) .output .map_err(|err| { let msg = err .into_iter() .map(|it| it.message.to_string()) .collect::>() .join("\n"); anyhow!("{msg}") })?; let pixmap = typst_render::render_merged(&document, 1.0, Abs::zero(), Some(Color::WHITE)); RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) .ok_or(anyhow!("Failed to create image from raw pixel data")) } fn get_file_bytes(&self, path: &Path) -> FileResult<&[u8]> { let path_str = path .to_str() .ok_or_else(|| FileError::NotFound(path.to_path_buf()))?; let bytes = self .files .get(path_str) .ok_or_else(|| FileError::NotFound(path.to_path_buf()))?; Ok(bytes) } fn get_package_file_bytes( &self, spec: &PackageSpec, vpath: &'static VirtualPath, ) -> FileResult> { let dir = self.packages.prepare_package(spec, &mut ProgressSink)?; let path = vpath.resolve(&dir).ok_or(FileError::AccessDenied)?; if path.is_dir() { Err(FileError::IsDirectory)?; } fs::read(&path).map_err(|it| FileError::from_io(it, &path)) } } impl Default for Typst { fn default() -> Self { Self::new() } } impl World for Typst { fn library(&self) -> &LazyHash { &self.library } fn book(&self) -> &LazyHash { &self.book } fn font(&self, index: usize) -> Option { self.fonts.get(index)?.get() } fn main(&self) -> FileId { FileId::new(None, VirtualPath::new(Self::MAIN_PATH)) } fn source(&self, id: FileId) -> FileResult { println!("Accessing source {id:?}"); let bytes = if let Some(spec) = id.package() { self.get_package_file_bytes(spec, id.vpath())? } else { let path = id.vpath().as_rooted_path(); self.get_file_bytes(path)?.to_vec() }; let text = String::from_utf8(bytes).map_err(|_| FileError::InvalidUtf8)?; Ok(Source::new(id, text)) } fn file(&self, id: FileId) -> FileResult { println!("Accessing file {id:?}"); let bytes = if let Some(spec) = id.package() { self.get_package_file_bytes(spec, id.vpath())? } else { let path = id.vpath().as_rooted_path(); self.get_file_bytes(path)?.to_vec() }; Ok(Bytes::new(bytes)) } fn today(&self, _offset: Option) -> Option { None } }