Add /typst
This commit is contained in:
parent
1464d074bf
commit
9b6865ff50
7 changed files with 1636 additions and 8 deletions
1373
Cargo.lock
generated
1373
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
pub use block::*;
|
||||
pub use image::*;
|
||||
pub use text::*;
|
||||
pub use typst::*;
|
||||
|
||||
mod block;
|
||||
mod image;
|
||||
mod text;
|
||||
mod typst;
|
||||
|
|
|
|||
219
showbits-common/src/widgets/typst.rs
Normal file
219
showbits-common/src/widgets/typst.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
25
showbits-thermal-printer/src/drawer/typst.rs
Normal file
25
showbits-thermal-printer/src/drawer/typst.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue