Add showbits-typst crate
This commit is contained in:
parent
949b2683a0
commit
8526566f39
4 changed files with 246 additions and 1 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -2518,6 +2518,19 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "showbits-typst"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"image",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"typst",
|
||||||
|
"typst-assets",
|
||||||
|
"typst-render",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["showbits-assets", "showbits-common", "showbits-thermal-printer"]
|
members = [
|
||||||
|
"showbits-assets",
|
||||||
|
"showbits-common",
|
||||||
|
"showbits-thermal-printer",
|
||||||
|
"showbits-typst",
|
||||||
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
|
||||||
16
showbits-typst/Cargo.toml
Normal file
16
showbits-typst/Cargo.toml
Normal 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
211
showbits-typst/src/lib.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue