From 8526566f3970c264b27186563d7448df318d6b67 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 28 Feb 2025 22:05:41 +0100 Subject: [PATCH] Add showbits-typst crate --- Cargo.lock | 13 +++ Cargo.toml | 7 +- showbits-typst/Cargo.toml | 16 +++ showbits-typst/src/lib.rs | 211 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 showbits-typst/Cargo.toml create mode 100644 showbits-typst/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6176f0b..89d15e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2518,6 +2518,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "showbits-typst" +version = "0.0.0" +dependencies = [ + "anyhow", + "image", + "serde", + "serde_json", + "typst", + "typst-assets", + "typst-render", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" diff --git a/Cargo.toml b/Cargo.toml index d54abbe..83cd0fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [workspace] resolver = "3" -members = ["showbits-assets", "showbits-common", "showbits-thermal-printer"] +members = [ + "showbits-assets", + "showbits-common", + "showbits-thermal-printer", + "showbits-typst", +] [workspace.package] version = "0.0.0" diff --git a/showbits-typst/Cargo.toml b/showbits-typst/Cargo.toml new file mode 100644 index 0000000..6536790 --- /dev/null +++ b/showbits-typst/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "showbits-typst" +version = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = { workspace = true } +image = { workspace = true } +serde = { workspace = true } +serde_json = "1.0.139" +typst = { workspace = true } +typst-assets = { workspace = true } +typst-render = { workspace = true } + +[lints] +workspace = true diff --git a/showbits-typst/src/lib.rs b/showbits-typst/src/lib.rs new file mode 100644 index 0000000..d4723ea --- /dev/null +++ b/showbits-typst/src/lib.rs @@ -0,0 +1,211 @@ +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}, + text::{Font, FontBook}, + utils::LazyHash, + visualize::Color, +}; + +// 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_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() { + let font_data = Bytes::new(font_file); + 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)), + }); + } + } + } +} + +pub struct Typst { + library: LazyHash, + book: LazyHash, + fonts: Vec, + files: HashMap>, +} + +impl Typst { + const MAIN_PATH: &str = "/main.typ"; + + pub fn new() -> Self { + let mut loader = FontLoader::new(); + loader.load_embedded_fonts(); + + Self { + library: LazyHash::new(Library::default()), + book: LazyHash::new(loader.book), + fonts: loader.fonts, + files: HashMap::new(), + } + } + + 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) + } +} + +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 { + // TODO Remove debug logging + println!("Accessing source {id:?}"); + + // TODO Do we need to handle packages ourselves? + if id.package().is_some() { + Err(FileError::AccessDenied)? + } + + let path = id.vpath().as_rooted_path(); + let bytes = 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 { + // TODO Remove debug logging + println!("Accessing file {id:?}"); + + // TODO Do we need to handle packages ourselves? + if id.package().is_some() { + Err(FileError::AccessDenied)? + } + + let path = id.vpath().as_rooted_path(); + let bytes = self.get_file_bytes(path)?.to_vec(); + Ok(Bytes::new(bytes)) + } + + fn today(&self, _offset: Option) -> Option { + None + } +}