showbits/showbits-typst/src/lib.rs
2025-03-04 03:23:38 +01:00

247 lines
6.8 KiB
Rust

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<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_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<Library>,
book: LazyHash<FontBook>,
fonts: Vec<FontSlot>,
files: HashMap<String, Vec<u8>>,
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<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)
}
fn get_package_file_bytes(
&self,
spec: &PackageSpec,
vpath: &'static VirtualPath,
) -> FileResult<Vec<u8>> {
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<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> {
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<Bytes> {
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<i64>) -> Option<Datetime> {
None
}
}