Add option to export original images

This commit is contained in:
Joscha 2025-04-06 11:46:33 +02:00
parent c8fb228a24
commit dad0f282c6
5 changed files with 51 additions and 20 deletions

View file

@ -1,14 +1,15 @@
#!/usr/bin/env fish #!/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 and not set -ql _flag_help
or begin or begin
echo "Usage:" (status filename) "[OPTIONS]" echo "Usage:" (status filename) "[OPTIONS]"
echo echo
echo "Options:" echo "Options:"
echo " -h, --help Show this help" echo " -h, --help Show this help"
echo " -p, --print Attach to printer at /dev/usb/lp0" echo " -p, --print Attach to printer at /dev/usb/lp0"
echo " -r, --release Use 'cargo run --release'" echo " -o, --originals Export original images"
echo " -r, --release Use 'cargo run --release'"
return return
end end
@ -22,6 +23,11 @@ if set -ql _flag_print
set arg_print -p /dev/usb/lp0 set arg_print -p /dev/usb/lp0
end end
set -l arg_originals
if set -ql _flag_originals
set arg_originals -o target/originals
end
cargo run $arg_release \ cargo run $arg_release \
--package showbits-thermal-printer \ --package showbits-thermal-printer \
-- target/queue -e target/image.png $arg_print -- target/queue -e target/image.png $arg_print $arg_originals

View file

@ -4,6 +4,7 @@ Description=Showbits Thermal Printer
[Service] [Service]
Type=simple 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
# ExecStart=/home/bondrucker/showbits-thermal-printer queue -a 0.0.0.0:8001 -p /dev/usb/lp0 -o originals
Restart=on-failure Restart=on-failure
User=bondrucker User=bondrucker

View file

@ -1,4 +1,4 @@
use std::io::Cursor; use std::{fs, io::Cursor};
use anyhow::{Context, anyhow, bail}; use anyhow::{Context, anyhow, bail};
use axum::{ use axum::{
@ -10,6 +10,7 @@ use image::{
DynamicImage, EncodableLayout, ImageDecoder, ImageFormat, ImageReader, Luma, Pixel, RgbaImage, DynamicImage, EncodableLayout, ImageDecoder, ImageFormat, ImageReader, Luma, Pixel, RgbaImage,
imageops, imageops,
}; };
use jiff::Timestamp;
use mark::dither::{AlgoFloydSteinberg, AlgoStucki, Algorithm, DiffEuclid, Palette}; use mark::dither::{AlgoFloydSteinberg, AlgoStucki, Algorithm, DiffEuclid, Palette};
use palette::LinSrgb; use palette::LinSrgb;
use serde::Serialize; use serde::Serialize;
@ -101,17 +102,7 @@ pub async fn post(server: State<Server>, mut multipart: Multipart) -> somehow::R
while let Some(field) = multipart.next_field().await? { while let Some(field) = multipart.next_field().await? {
match field.name() { match field.name() {
Some("image") => { Some("image") => {
let data = field.bytes().await?; image = Some(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());
} }
Some("title") => { Some("title") => {
data.title = Some(field.text().await?).filter(|it| !it.is_empty()); data.title = Some(field.text().await?).filter(|it| !it.is_empty());
@ -139,10 +130,31 @@ pub async fn post(server: State<Server>, mut multipart: Multipart) -> somehow::R
return Ok(status_code(StatusCode::UNPROCESSABLE_ENTITY)); 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_width = Some(384);
let max_height = Some(1024); let max_height = Some(1024);
let image = dither(image, max_width, max_height, bright, &algo).map_err(somehow::Error)?; let image = dither(image, max_width, max_height, bright, &algo).map_err(somehow::Error)?;
// Encode dithered image for typst
let mut bytes: Vec<u8> = Vec::new(); let mut bytes: Vec<u8> = Vec::new();
image image
.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png) .write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)

View file

@ -33,6 +33,11 @@ struct Args {
/// Export an image of whatever is printed here. /// Export an image of whatever is printed here.
#[arg(long, short)] #[arg(long, short)]
export: Option<PathBuf>, export: Option<PathBuf>,
/// Export the original images printed by the image document, before
/// dithering or other manipulation.
#[arg(long, short)]
originals: Option<PathBuf>,
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@ -44,7 +49,7 @@ fn main() -> anyhow::Result<()> {
let mut drawer = Drawer::new(rx, printer); let mut drawer = Drawer::new(rx, printer);
let runtime = Runtime::new()?; 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 { runtime.spawn(async move {
loop { loop {
let _ = tx.send(Command::Backlog).await; let _ = tx.send(Command::Backlog).await;

View file

@ -2,6 +2,8 @@ pub mod somehow;
mod r#static; mod r#static;
pub mod statuscode; pub mod statuscode;
use std::path::PathBuf;
use axum::{ use axum::{
Router, Router,
extract::DefaultBodyLimit, extract::DefaultBodyLimit,
@ -18,6 +20,7 @@ use crate::{documents, drawer::Command};
#[derive(Clone)] #[derive(Clone)]
pub struct Server { pub struct Server {
tx: mpsc::Sender<Command>, tx: mpsc::Sender<Command>,
pub originals: Option<PathBuf>,
} }
impl Server { impl Server {
@ -28,7 +31,11 @@ impl Server {
} }
} }
pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()> { pub async fn run(
tx: mpsc::Sender<Command>,
addr: String,
originals: Option<PathBuf>,
) -> anyhow::Result<()> {
let app = Router::new() let app = Router::new()
// Files // Files
.route("/", get(r#static::get_index)) .route("/", get(r#static::get_index))
@ -47,7 +54,7 @@ pub async fn run(tx: mpsc::Sender<Command>, addr: String) -> anyhow::Result<()>
.route("/api/xkcd", post(documents::xkcd::post)) .route("/api/xkcd", post(documents::xkcd::post))
// Rest // Rest
.layer(DefaultBodyLimit::max(32 * 1024 * 1024)) // 32 MiB .layer(DefaultBodyLimit::max(32 * 1024 * 1024)) // 32 MiB
.with_state(Server { tx }); .with_state(Server { tx, originals });
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app).await?; axum::serve(listener, app).await?;