Add /typst

This commit is contained in:
Joscha 2024-05-13 12:16:19 +02:00
parent 1464d074bf
commit 9b6865ff50
7 changed files with 1636 additions and 8 deletions

1373
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,18 @@ edition.workspace = true
[dependencies]
anyhow.workspace = true
comemo = "0.4.0"
cosmic-text.workspace = true
fontdb = "0.17.0"
image.workspace = true
mark.git = "https://github.com/Garmelon/mark.git"
palette.workspace = true
paste = "1.0.14"
showbits-assets.workspace = true
taffy.workspace = true
typst = "0.11.0"
typst-assets = { version = "0.11.0", features = ["fonts"] }
typst-render = "0.11.0"
[lints]
workspace = true

View file

@ -1,7 +1,9 @@
pub use block::*;
pub use image::*;
pub use text::*;
pub use typst::*;
mod block;
mod image;
mod text;
mod typst;

View file

@ -0,0 +1,219 @@
use std::{fs, path::PathBuf, sync::OnceLock};
use anyhow::anyhow;
use comemo::Prehashed;
use image::RgbaImage;
use taffy::{
prelude::{AvailableSpace, Size},
Layout,
};
use typst::{
diag::{FileError, FileResult},
eval::Tracer,
foundations::{Bytes, Datetime},
layout::Abs,
syntax::{FileId, Source},
text::{Font, FontBook},
visualize::Color,
Library, World,
};
use crate::{View, Widget};
// 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()?.into();
Font::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::from_static(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)),
});
}
}
}
}
struct DummyWorld {
library: Prehashed<Library>,
book: Prehashed<FontBook>,
main: Source,
fonts: Vec<FontSlot>,
}
impl DummyWorld {
fn new(main: String) -> Self {
let mut loader = FontLoader::new();
loader.load_embedded_fonts();
Self {
library: Prehashed::new(Library::builder().build()),
book: Prehashed::new(loader.book),
main: Source::detached(main),
fonts: loader.fonts,
}
}
}
impl World for DummyWorld {
fn library(&self) -> &Prehashed<Library> {
&self.library
}
fn book(&self) -> &Prehashed<FontBook> {
&self.book
}
fn main(&self) -> Source {
self.main.clone()
}
fn source(&self, _id: FileId) -> FileResult<Source> {
Err(FileError::AccessDenied)
}
fn file(&self, _id: FileId) -> FileResult<Bytes> {
Err(FileError::AccessDenied)
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts[index].get()
}
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
None
}
}
const SCALE: f32 = 3.0;
pub struct Typst {
code: String,
}
impl Typst {
pub fn new(code: String) -> Self {
Self { code }
}
fn render(&self, width: Option<f32>, height: Option<f32>) -> Result<RgbaImage, Vec<String>> {
let width = match width {
Some(width) => format!("{}pt", width / SCALE),
None => "auto".to_string(),
};
let height = match height {
Some(height) => format!("{}pt", height / SCALE),
None => "auto".to_string(),
};
let mut source = String::new();
source.push_str(&format!("#set page(width: {width}, height: {height})\n"));
source.push_str("#set page(margin: (left: 0mm, right: 0mm, top: 1mm, bottom: 2mm))\n");
source.push_str(&self.code);
let world = DummyWorld::new(source);
let mut tracer = Tracer::new();
let document = typst::compile(&world, &mut tracer).map_err(|errs| {
errs.into_iter()
.map(|sd| sd.message.to_string())
.collect::<Vec<_>>()
})?;
let pixmap =
typst_render::render_merged(&document, SCALE, Color::WHITE, Abs::zero(), Color::WHITE);
let buffer = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
Ok(buffer)
}
}
impl<C> Widget<C> for Typst {
fn size(
&mut self,
_ctx: &mut C,
known: Size<Option<f32>>,
available: Size<AvailableSpace>,
) -> Size<f32> {
let width = known.width.or(match available.width {
AvailableSpace::Definite(width) => Some(width),
AvailableSpace::MinContent => None, // auto
AvailableSpace::MaxContent => Some(4096.0),
});
let height = known.height.or(match available.height {
AvailableSpace::Definite(width) => Some(width),
AvailableSpace::MinContent | AvailableSpace::MaxContent => None, // auto
});
let Ok(buffer) = self.render(width, height) else {
return Size {
width: width.unwrap_or(0.0),
height: height.unwrap_or(0.0),
};
};
// Round up so we definitely have enough space.
// let width = (buffer.width() as f32 / SCALE).ceil() * SCALE + 1.0;
// let height = (buffer.height() as f32 / SCALE).ceil() * SCALE + 1.0;
let width = (buffer.width() as f32).ceil() + 1.0;
let height = (buffer.height() as f32).ceil() + 1.0;
Size { width, height }
}
fn draw_below(
&mut self,
_ctx: &mut C,
view: &mut View<'_>,
_layout: &Layout,
) -> anyhow::Result<()> {
let size = view.size();
let buffer = self
.render(Some(size.x as f32), Some(size.y as f32))
.map_err(|errs| anyhow!("{}", errs.join("\n")))?;
view.image(&buffer);
Ok(())
}
}

View file

@ -6,6 +6,7 @@ mod image;
mod photo;
mod text;
mod tictactoe;
mod typst;
use showbits_common::widgets::{FontStuff, HasFontStuff};
use tokio::sync::mpsc;
@ -15,7 +16,7 @@ use crate::printer::Printer;
pub use self::{
calendar::CalendarDrawing, cells::CellsDrawing, chat_message::ChatMessageDrawing,
egg::EggDrawing, image::ImageDrawing, photo::PhotoDrawing, text::TextDrawing,
tictactoe::TicTacToeDrawing,
tictactoe::TicTacToeDrawing, typst::TypstDrawing,
};
#[derive(Default)]

View file

@ -0,0 +1,25 @@
use showbits_common::{color::WHITE, widgets::Typst, Node, Tree, WidgetExt};
use taffy::style_helpers::percent;
use crate::printer::Printer;
use super::{Context, Drawing};
pub struct TypstDrawing(pub String);
impl Drawing for TypstDrawing {
fn draw(&self, printer: &mut Printer, ctx: &mut Context) -> anyhow::Result<()> {
let mut tree = Tree::<Context>::new(WHITE);
let typst = Typst::new(self.0.clone()).node().register(&mut tree)?;
let root = Node::empty()
.with_size_width(percent(1.0))
.and_child(typst)
.register(&mut tree)?;
printer.print_tree(&mut tree, ctx, root)?;
printer.feed()?;
Ok(())
}
}

View file

@ -14,7 +14,7 @@ use tokio::{net::TcpListener, sync::mpsc};
use crate::drawer::{
CalendarDrawing, CellsDrawing, ChatMessageDrawing, Command, EggDrawing, ImageDrawing,
PhotoDrawing, TextDrawing, TicTacToeDrawing,
PhotoDrawing, TextDrawing, TicTacToeDrawing, TypstDrawing,
};
use self::{r#static::get_static_file, statuscode::status_code};
@ -34,6 +34,7 @@ pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()>
.route("/photo", post(post_photo).fallback(get_static_file))
.route("/text", post(post_text))
.route("/tictactoe", post(post_tictactoe))
.route("/typst", post(post_typst))
.fallback(get(get_static_file))
.layer(DefaultBodyLimit::max(32 * 1024 * 1024)) // 32 MiB
.with_state(Server { tx });
@ -191,3 +192,17 @@ async fn post_text(server: State<Server>, request: Form<PostTextForm>) {
async fn post_tictactoe(server: State<Server>) {
let _ = server.tx.send(Command::draw(TicTacToeDrawing)).await;
}
// /typst
#[derive(Deserialize)]
struct PostTypstForm {
source: String,
}
async fn post_typst(server: State<Server>, request: Form<PostTypstForm>) {
let _ = server
.tx
.send(Command::draw(TypstDrawing(request.0.source)))
.await;
}