Typstify /cells endpoint
This commit is contained in:
parent
6bb431cf8f
commit
ca4e807c9f
9 changed files with 133 additions and 120 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
use showbits_typst::Typst;
|
use showbits_typst::Typst;
|
||||||
|
|
||||||
|
pub mod cells;
|
||||||
pub mod egg;
|
pub mod egg;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
|
||||||
3
showbits-thermal-printer/src/documents/cells/data.json
Normal file
3
showbits-thermal-printer/src/documents/cells/data.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"feed": true
|
||||||
|
}
|
||||||
BIN
showbits-thermal-printer/src/documents/cells/image.png
Normal file
BIN
showbits-thermal-printer/src/documents/cells/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
1
showbits-thermal-printer/src/documents/cells/lib
Symbolic link
1
showbits-thermal-printer/src/documents/cells/lib
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../lib
|
||||||
10
showbits-thermal-printer/src/documents/cells/main.typ
Normal file
10
showbits-thermal-printer/src/documents/cells/main.typ
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#import "lib/main.typ" as lib;
|
||||||
|
#show: it => lib.init(it)
|
||||||
|
|
||||||
|
#let data = json("data.json")
|
||||||
|
|
||||||
|
#image("image.png")
|
||||||
|
|
||||||
|
#if data.feed {
|
||||||
|
lib.feed
|
||||||
|
}
|
||||||
110
showbits-thermal-printer/src/documents/cells/mod.rs
Normal file
110
showbits-thermal-printer/src/documents/cells/mod.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::{Form, extract::State};
|
||||||
|
use image::{ImageFormat, Rgba, RgbaImage, imageops};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
drawer::{Command, NewTypstDrawing},
|
||||||
|
printer::Printer,
|
||||||
|
server::{Server, somehow},
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLACK: Rgba<u8> = Rgba([0, 0, 0, 255]);
|
||||||
|
const WHITE: Rgba<u8> = Rgba([255, 255, 255, 255]);
|
||||||
|
|
||||||
|
fn b2c(bool: bool) -> Rgba<u8> {
|
||||||
|
match bool {
|
||||||
|
true => BLACK,
|
||||||
|
false => WHITE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn c2b(color: Rgba<u8>) -> bool {
|
||||||
|
color == BLACK
|
||||||
|
}
|
||||||
|
|
||||||
|
fn neighbors_at(image: &RgbaImage, x: u32, y: u32) -> [bool; 3] {
|
||||||
|
let left = x
|
||||||
|
.checked_sub(1)
|
||||||
|
.map(|x| *image.get_pixel(x, y))
|
||||||
|
.unwrap_or(WHITE);
|
||||||
|
|
||||||
|
let mid = *image.get_pixel(x, y);
|
||||||
|
|
||||||
|
let right = image.get_pixel_checked(x + 1, y).copied().unwrap_or(WHITE);
|
||||||
|
|
||||||
|
[c2b(left), c2b(mid), c2b(right)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_rule(rule: u8, neighbors: [bool; 3]) -> bool {
|
||||||
|
let [left, mid, right] = neighbors.map(|n| n as u8);
|
||||||
|
let index = (left << 2) | (mid << 1) | right;
|
||||||
|
rule & (1 << index) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Data {
|
||||||
|
feed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct FormData {
|
||||||
|
pub rule: Option<u8>,
|
||||||
|
pub rows: Option<u32>,
|
||||||
|
pub scale: Option<u32>,
|
||||||
|
pub feed: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post(server: State<Server>, Form(form): Form<FormData>) -> somehow::Result<()> {
|
||||||
|
let data = Data {
|
||||||
|
feed: form.feed.unwrap_or(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rule = form.rule.unwrap_or_else(rand::random);
|
||||||
|
let scale = form.scale.unwrap_or(4).clamp(1, 16);
|
||||||
|
let rows = form.rows.unwrap_or(128 * 4 / scale).clamp(1, 1024 / scale);
|
||||||
|
let cols = Printer::WIDTH / scale;
|
||||||
|
|
||||||
|
let mut image: image::ImageBuffer<Rgba<u8>, Vec<u8>> = RgbaImage::new(cols, rows);
|
||||||
|
|
||||||
|
// Initialize first line randomly
|
||||||
|
for x in 0..image.width() {
|
||||||
|
image.put_pixel(x, 0, b2c(rand::random()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate next rows
|
||||||
|
for y in 1..image.height() {
|
||||||
|
for x in 0..image.width() {
|
||||||
|
let neighbors = neighbors_at(&image, x, y - 1);
|
||||||
|
let state = apply_rule(rule, neighbors);
|
||||||
|
image.put_pixel(x, y, b2c(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = imageops::resize(
|
||||||
|
&image,
|
||||||
|
image.width() * scale,
|
||||||
|
image.height() * scale,
|
||||||
|
imageops::Nearest,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
|
image
|
||||||
|
.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)
|
||||||
|
.context("failed to encode image as png")
|
||||||
|
.map_err(somehow::Error)?;
|
||||||
|
|
||||||
|
let typst = super::typst_with_lib()
|
||||||
|
.with_json("/data.json", &data)
|
||||||
|
.with_file("/image.png", bytes)
|
||||||
|
.with_main_file(include_str!("main.typ"));
|
||||||
|
|
||||||
|
let _ = server
|
||||||
|
.tx
|
||||||
|
.send(Command::draw(NewTypstDrawing::new(typst)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
mod backlog;
|
mod backlog;
|
||||||
mod calendar;
|
mod calendar;
|
||||||
mod cells;
|
|
||||||
mod chat_message;
|
mod chat_message;
|
||||||
mod new_typst;
|
mod new_typst;
|
||||||
mod photo;
|
mod photo;
|
||||||
|
|
@ -13,9 +12,9 @@ use tokio::sync::mpsc;
|
||||||
use crate::persistent_printer::PersistentPrinter;
|
use crate::persistent_printer::PersistentPrinter;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
backlog::BacklogDrawing, calendar::CalendarDrawing, cells::CellsDrawing,
|
backlog::BacklogDrawing, calendar::CalendarDrawing, chat_message::ChatMessageDrawing,
|
||||||
chat_message::ChatMessageDrawing, new_typst::NewTypstDrawing, photo::PhotoDrawing,
|
new_typst::NewTypstDrawing, photo::PhotoDrawing, tictactoe::TicTacToeDrawing,
|
||||||
tictactoe::TicTacToeDrawing, typst::TypstDrawing,
|
typst::TypstDrawing,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const FEED: f32 = 96.0;
|
pub const FEED: f32 = 96.0;
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
use image::{
|
|
||||||
Rgba, RgbaImage,
|
|
||||||
imageops::{self, FilterType},
|
|
||||||
};
|
|
||||||
use showbits_common::{Node, Tree, WidgetExt, color, widgets::Image};
|
|
||||||
use taffy::{AlignItems, Display, FlexDirection, prelude::length, style_helpers::percent};
|
|
||||||
|
|
||||||
use crate::{persistent_printer::PersistentPrinter, printer::Printer};
|
|
||||||
|
|
||||||
use super::{Context, Drawing, FEED};
|
|
||||||
|
|
||||||
const BLACK: Rgba<u8> = Rgba([0, 0, 0, 255]);
|
|
||||||
const WHITE: Rgba<u8> = Rgba([255, 255, 255, 255]);
|
|
||||||
|
|
||||||
fn b2c(bool: bool) -> Rgba<u8> {
|
|
||||||
match bool {
|
|
||||||
true => BLACK,
|
|
||||||
false => WHITE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn c2b(color: Rgba<u8>) -> bool {
|
|
||||||
color == BLACK
|
|
||||||
}
|
|
||||||
|
|
||||||
fn neighbors_at(image: &RgbaImage, x: u32, y: u32) -> [bool; 3] {
|
|
||||||
let left = x
|
|
||||||
.checked_sub(1)
|
|
||||||
.map(|x| *image.get_pixel(x, y))
|
|
||||||
.unwrap_or(WHITE);
|
|
||||||
|
|
||||||
let mid = *image.get_pixel(x, y);
|
|
||||||
|
|
||||||
let right = image.get_pixel_checked(x + 1, y).copied().unwrap_or(WHITE);
|
|
||||||
|
|
||||||
[c2b(left), c2b(mid), c2b(right)]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_rule(rule: u8, neighbors: [bool; 3]) -> bool {
|
|
||||||
let [left, mid, right] = neighbors.map(|n| n as u8);
|
|
||||||
let index = (left << 2) | (mid << 1) | right;
|
|
||||||
rule & (1 << index) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CellsDrawing {
|
|
||||||
pub rule: u8,
|
|
||||||
pub rows: u32,
|
|
||||||
pub scale: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drawing for CellsDrawing {
|
|
||||||
fn draw(&self, printer: &mut PersistentPrinter, ctx: &mut Context) -> anyhow::Result<()> {
|
|
||||||
let mut image: image::ImageBuffer<Rgba<u8>, Vec<u8>> =
|
|
||||||
RgbaImage::new(Printer::WIDTH / self.scale, self.rows);
|
|
||||||
|
|
||||||
// Initialize first line randomly
|
|
||||||
for x in 0..image.width() {
|
|
||||||
image.put_pixel(x, 0, b2c(rand::random()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate next rows
|
|
||||||
for y in 1..self.rows {
|
|
||||||
for x in 0..image.width() {
|
|
||||||
let neighbors = neighbors_at(&image, x, y - 1);
|
|
||||||
let state = apply_rule(self.rule, neighbors);
|
|
||||||
image.put_pixel(x, y, b2c(state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = imageops::resize(
|
|
||||||
&image,
|
|
||||||
image.width() * self.scale,
|
|
||||||
image.height() * self.scale,
|
|
||||||
FilterType::Nearest,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut tree = Tree::<Context>::new(color::WHITE);
|
|
||||||
|
|
||||||
let image = Image::new(image).node().register(&mut tree)?;
|
|
||||||
|
|
||||||
let root = Node::empty()
|
|
||||||
.with_size_width(percent(1.0))
|
|
||||||
.with_padding_bottom(length(FEED))
|
|
||||||
.with_display(Display::Flex)
|
|
||||||
.with_flex_direction(FlexDirection::Column)
|
|
||||||
.with_align_items(Some(AlignItems::Center))
|
|
||||||
.and_child(image)
|
|
||||||
.register(&mut tree)?;
|
|
||||||
|
|
||||||
printer.print_tree(&mut tree, ctx, root)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,8 +15,7 @@ use tokio::{net::TcpListener, sync::mpsc};
|
||||||
use crate::{
|
use crate::{
|
||||||
documents,
|
documents,
|
||||||
drawer::{
|
drawer::{
|
||||||
CalendarDrawing, CellsDrawing, ChatMessageDrawing, Command, PhotoDrawing, TicTacToeDrawing,
|
CalendarDrawing, ChatMessageDrawing, Command, PhotoDrawing, TicTacToeDrawing, TypstDrawing,
|
||||||
TypstDrawing,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -30,7 +29,10 @@ pub struct Server {
|
||||||
pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()> {
|
pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()> {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/calendar", post(post_calendar))
|
.route("/calendar", post(post_calendar))
|
||||||
.route("/cells", post(post_cells))
|
.route(
|
||||||
|
"/cells",
|
||||||
|
post(documents::cells::post).fallback(get_static_file),
|
||||||
|
)
|
||||||
.route("/chat_message", post(post_chat_message))
|
.route("/chat_message", post(post_chat_message))
|
||||||
.route("/egg", post(documents::egg::post).fallback(get_static_file))
|
.route("/egg", post(documents::egg::post).fallback(get_static_file))
|
||||||
.route(
|
.route(
|
||||||
|
|
@ -71,26 +73,6 @@ async fn post_calendar(server: State<Server>, request: Form<PostCalendarForm>) {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// /cells
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PostCellsForm {
|
|
||||||
rule: u8,
|
|
||||||
rows: Option<u32>,
|
|
||||||
scale: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn post_cells(server: State<Server>, request: Form<PostCellsForm>) {
|
|
||||||
let _ = server
|
|
||||||
.tx
|
|
||||||
.send(Command::draw(CellsDrawing {
|
|
||||||
rule: request.0.rule,
|
|
||||||
rows: request.0.rows.unwrap_or(32).min(512),
|
|
||||||
scale: request.0.scale.unwrap_or(4),
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// /chat_message
|
// /chat_message
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue