From dad0f282c64ef970a72202ecfbd0605412fcd3a3 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 6 Apr 2025 11:46:33 +0200 Subject: [PATCH] Add option to export original images --- meta/dev-thermal-printer | 16 ++++++--- meta/spinch/showbits-thermal-printer.service | 1 + .../src/documents/image/mod.rs | 36 ++++++++++++------- showbits-thermal-printer/src/main.rs | 7 +++- showbits-thermal-printer/src/server.rs | 11 ++++-- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/meta/dev-thermal-printer b/meta/dev-thermal-printer index bbf6139..e9b791b 100755 --- a/meta/dev-thermal-printer +++ b/meta/dev-thermal-printer @@ -1,14 +1,15 @@ #!/usr/bin/env fish -argparse h/help p/print r/release -- $argv +argparse h/help p/print o/originals r/release -- $argv and not set -ql _flag_help or begin echo "Usage:" (status filename) "[OPTIONS]" echo echo "Options:" - echo " -h, --help Show this help" - echo " -p, --print Attach to printer at /dev/usb/lp0" - echo " -r, --release Use 'cargo run --release'" + echo " -h, --help Show this help" + echo " -p, --print Attach to printer at /dev/usb/lp0" + echo " -o, --originals Export original images" + echo " -r, --release Use 'cargo run --release'" return end @@ -22,6 +23,11 @@ if set -ql _flag_print set arg_print -p /dev/usb/lp0 end +set -l arg_originals +if set -ql _flag_originals + set arg_originals -o target/originals +end + cargo run $arg_release \ --package showbits-thermal-printer \ - -- target/queue -e target/image.png $arg_print + -- target/queue -e target/image.png $arg_print $arg_originals diff --git a/meta/spinch/showbits-thermal-printer.service b/meta/spinch/showbits-thermal-printer.service index 579aca8..bef835d 100644 --- a/meta/spinch/showbits-thermal-printer.service +++ b/meta/spinch/showbits-thermal-printer.service @@ -4,6 +4,7 @@ Description=Showbits Thermal Printer [Service] Type=simple ExecStart=/home/bondrucker/showbits-thermal-printer queue -a 0.0.0.0:8001 -p /dev/usb/lp0 +# ExecStart=/home/bondrucker/showbits-thermal-printer queue -a 0.0.0.0:8001 -p /dev/usb/lp0 -o originals Restart=on-failure User=bondrucker diff --git a/showbits-thermal-printer/src/documents/image/mod.rs b/showbits-thermal-printer/src/documents/image/mod.rs index 9260cb4..197bbe9 100644 --- a/showbits-thermal-printer/src/documents/image/mod.rs +++ b/showbits-thermal-printer/src/documents/image/mod.rs @@ -1,4 +1,4 @@ -use std::io::Cursor; +use std::{fs, io::Cursor}; use anyhow::{Context, anyhow, bail}; use axum::{ @@ -10,6 +10,7 @@ use image::{ DynamicImage, EncodableLayout, ImageDecoder, ImageFormat, ImageReader, Luma, Pixel, RgbaImage, imageops, }; +use jiff::Timestamp; use mark::dither::{AlgoFloydSteinberg, AlgoStucki, Algorithm, DiffEuclid, Palette}; use palette::LinSrgb; use serde::Serialize; @@ -101,17 +102,7 @@ pub async fn post(server: State, mut multipart: Multipart) -> somehow::R while let Some(field) = multipart.next_field().await? { match field.name() { Some("image") => { - let data = field.bytes().await?; - - // https://github.com/image-rs/image/issues/2392#issuecomment-2547393362 - let mut decoder = ImageReader::new(Cursor::new(data.as_bytes())) - .with_guessed_format()? - .into_decoder()?; - let orientation = decoder.orientation()?; - let mut decoded = DynamicImage::from_decoder(decoder)?; - decoded.apply_orientation(orientation); - - image = Some(decoded.to_rgba8()); + image = Some(field.bytes().await?); } Some("title") => { data.title = Some(field.text().await?).filter(|it| !it.is_empty()); @@ -139,10 +130,31 @@ pub async fn post(server: State, mut multipart: Multipart) -> somehow::R return Ok(status_code(StatusCode::UNPROCESSABLE_ENTITY)); }; + // Export original image if requested + if let Some(dir) = &server.originals { + fs::create_dir_all(dir)?; + let path = dir.join(Timestamp::now().as_millisecond().to_string()); + fs::write(path, &image)?; + } + + // Decode image data + let image = { + // https://github.com/image-rs/image/issues/2392#issuecomment-2547393362 + let mut decoder = ImageReader::new(Cursor::new(image.as_bytes())) + .with_guessed_format()? + .into_decoder()?; + let orientation = decoder.orientation()?; + let mut decoded = DynamicImage::from_decoder(decoder)?; + decoded.apply_orientation(orientation); + decoded.to_rgba8() + }; + + // Dither image let max_width = Some(384); let max_height = Some(1024); let image = dither(image, max_width, max_height, bright, &algo).map_err(somehow::Error)?; + // Encode dithered image for typst let mut bytes: Vec = Vec::new(); image .write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png) diff --git a/showbits-thermal-printer/src/main.rs b/showbits-thermal-printer/src/main.rs index a4d9935..0c658ec 100644 --- a/showbits-thermal-printer/src/main.rs +++ b/showbits-thermal-printer/src/main.rs @@ -33,6 +33,11 @@ struct Args { /// Export an image of whatever is printed here. #[arg(long, short)] export: Option, + + /// Export the original images printed by the image document, before + /// dithering or other manipulation. + #[arg(long, short)] + originals: Option, } fn main() -> anyhow::Result<()> { @@ -44,7 +49,7 @@ fn main() -> anyhow::Result<()> { let mut drawer = Drawer::new(rx, printer); let runtime = Runtime::new()?; - runtime.spawn(server::run(tx.clone(), args.address)); + runtime.spawn(server::run(tx.clone(), args.address, args.originals)); runtime.spawn(async move { loop { let _ = tx.send(Command::Backlog).await; diff --git a/showbits-thermal-printer/src/server.rs b/showbits-thermal-printer/src/server.rs index 14f2c9c..f0a7953 100644 --- a/showbits-thermal-printer/src/server.rs +++ b/showbits-thermal-printer/src/server.rs @@ -2,6 +2,8 @@ pub mod somehow; mod r#static; pub mod statuscode; +use std::path::PathBuf; + use axum::{ Router, extract::DefaultBodyLimit, @@ -18,6 +20,7 @@ use crate::{documents, drawer::Command}; #[derive(Clone)] pub struct Server { tx: mpsc::Sender, + pub originals: Option, } impl Server { @@ -28,7 +31,11 @@ impl Server { } } -pub async fn run(tx: mpsc::Sender, addr: String) -> anyhow::Result<()> { +pub async fn run( + tx: mpsc::Sender, + addr: String, + originals: Option, +) -> anyhow::Result<()> { let app = Router::new() // Files .route("/", get(r#static::get_index)) @@ -47,7 +54,7 @@ pub async fn run(tx: mpsc::Sender, addr: String) -> anyhow::Result<()> .route("/api/xkcd", post(documents::xkcd::post)) // Rest .layer(DefaultBodyLimit::max(32 * 1024 * 1024)) // 32 MiB - .with_state(Server { tx }); + .with_state(Server { tx, originals }); let listener = TcpListener::bind(addr).await?; axum::serve(listener, app).await?;