Add showbits-typst crate

This commit is contained in:
Joscha 2025-02-28 22:05:41 +01:00
parent 949b2683a0
commit 8526566f39
4 changed files with 246 additions and 1 deletions

13
Cargo.lock generated
View file

@ -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"

View file

@ -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"

16
showbits-typst/Cargo.toml Normal file
View file

@ -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

211
showbits-typst/src/lib.rs Normal file
View file

@ -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<Option<Font>>,
}
impl FontSlot {
pub fn get(&self) -> Option<Font> {
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<FontSlot>,
}
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<Library>,
book: LazyHash<FontBook>,
fonts: Vec<FontSlot>,
files: HashMap<String, Vec<u8>>,
}
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<Vec<u8>>) {
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<Vec<u8>>) -> Self {
self.add_file(path, data);
self
}
pub fn add_json<T: Serialize>(&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<T: Serialize>(mut self, path: impl ToString, data: &T) -> Self {
self.add_json(path, data);
self
}
pub fn add_main_file(&mut self, data: impl Into<Vec<u8>>) {
self.add_file(Self::MAIN_PATH, data);
}
pub fn with_main_file(mut self, data: impl Into<Vec<u8>>) -> Self {
self.add_main_file(data);
self
}
pub fn render(&self) -> anyhow::Result<RgbaImage> {
let document = typst::compile::<PagedDocument>(self)
.output
.map_err(|err| {
let msg = err
.into_iter()
.map(|it| it.message.to_string())
.collect::<Vec<_>>()
.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<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index)?.get()
}
fn main(&self) -> FileId {
FileId::new(None, VirtualPath::new(Self::MAIN_PATH))
}
fn source(&self, id: FileId) -> FileResult<Source> {
// 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<Bytes> {
// 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<i64>) -> Option<Datetime> {
None
}
}