From e9d0580f7eba57005f5318b7568aa2ddab9aa1da Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Mon, 29 Sep 2025 11:05:31 +0200 Subject: [PATCH] Implemented basic webserver --- Cargo.lock | 51 ++++++++++++++++++++++++ Cargo.toml | 3 +- src/escpos/job.rs | 53 +++++++++++++++---------- src/main.rs | 98 +++++++++++++++++++++++++++++++---------------- 4 files changed, 149 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 456fc8a..9b365db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -145,6 +146,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -413,6 +425,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -470,6 +488,25 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.6.0" @@ -548,6 +585,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1412,6 +1450,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" diff --git a/Cargo.toml b/Cargo.toml index c9654db..96e58bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] image = {version = "0.25.8", features = ["png"]} -axum = { version = "0.8.4", features = ["multipart"] } +# axum = { version = "0.8.4", features = ["multipart", "debug"] } +axum = { version = "0.8.4", features = ["multipart", "http2", "macros"] } tokio = { version = "1.0", features = ["full"] } tower-http = { version = "0.6.1", features = ["limit", "trace"] } diff --git a/src/escpos/job.rs b/src/escpos/job.rs index 4c9fcd3..2677737 100644 --- a/src/escpos/job.rs +++ b/src/escpos/job.rs @@ -108,30 +108,41 @@ impl Job { return Err(EscPosError::InvalidBitmapMode); } - let (w, h) = (img.width(), img.height()); - let swap = matches!(orientation, Some(ImageOrientation::Largest)) - && h.min(self.max_width as u32) * w > w.min(self.max_width as u32) * h; + let (iw, ih) = (img.width(), img.height()); + let mw = self.max_width as u32; - let scale = if swap { - self.max_width as f32 / h as f32 - } else { - self.max_width as f32 / w as f32 + let rotate = matches!(orientation, Some(ImageOrientation::Largest)) && { + let scale_a = mw as f32 / iw as f32; + let out_h_a = (ih as f32 * scale_a).round() as u32; + let area_a = (mw as u64) * (out_h_a as u64); + let scale_b = mw as f32 / ih as f32; + let out_h_b = (iw as f32 * scale_b).round() as u32; + let area_b = (mw as u64) * (out_h_b as u64); + area_b > area_a }; - let (w, h) = if swap { - ((h as f32 * scale) as u32, self.max_width as u32) - } else { - (self.max_width as u32, (h as f32 * scale) as u32) - }; + let (pw, ph) = if rotate { (ih, iw) } else { (iw, ih) }; + let target_w = mw; + let target_h = (ph as f32 * (mw as f32 / pw as f32)).round() as u32; - let img = - DynamicImage::ImageRgba8(image::imageops::resize(img, w, h, FilterType::Triangle)); + let mut rotated: Option = None; + if rotate { + rotated = Some(img.rotate90()); + } + let src: &DynamicImage = rotated.as_ref().unwrap_or(img); + let resized = DynamicImage::ImageRgba8(image::imageops::resize( + src, + target_w, + target_h, + FilterType::Triangle, + )); - let img = if swap { img.rotate90() } else { img }; - let mut gray = img.to_luma8(); + let mut gray = resized.to_luma8(); auto_brighten(&mut gray); - let mono = dither(&mut gray); + + let w = target_w; + let h = target_h; let width_bytes = ((w + 7) >> 3) as u16; let buf = &mut self.content; @@ -143,10 +154,10 @@ impl Job { 0x76, 0x30, mode, - width_bytes as u8, - (width_bytes >> 8) as u8, - h as u8, - (h >> 8) as u8, + (width_bytes & 0xFF) as u8, + ((width_bytes >> 8) & 0xFF) as u8, + (h & 0xFF) as u8, + ((h >> 8) & 0xFF) as u8, ]); buf.extend_from_slice(&mono); diff --git a/src/main.rs b/src/main.rs index 4861c41..d48ba02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,60 +5,90 @@ use std::{io::Write, sync::Arc}; use axum::{ Router, + body::{Body, Bytes}, extract::{Extension, Multipart}, - http::StatusCode, + http::{Response, StatusCode, header}, response::IntoResponse, - routing::post, + routing::{get, post}, }; use image::DynamicImage; use tokio::sync::Mutex; -use crate::escpos::printer::Printer; +use crate::escpos::{errors::EscPosError, job::ImageOrientation, printer::Printer}; #[tokio::main] async fn main() { let printer = Arc::new(Mutex::new(Printer::new(380))); let app = Router::new() - .route("/", post(upload)) + .route("/upload", post(upload)) + .route("/print", get(print)) .layer(Extension(printer)); - println!("Listening on http://127.0.0.1:8000"); - axum::Server::bind(&"192.168.1.112:8000".parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); } +#[axum::debug_handler] +async fn print( + Extension(printer): Extension>>, +) -> impl axum::response::IntoResponse { + let mut printer = printer.lock().await; + let bytes = match printer.export_job() { + Ok(data) => data, + Err(EscPosError::EmptyQueue) => { + println!("requested but no queue"); + return ( + StatusCode::NO_CONTENT, + [(header::CONTENT_TYPE, "application/octet-stream")], + Vec::new(), + ); + } + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(header::CONTENT_TYPE, "application/octet-stream")], + Vec::new(), + ); + } + }; + + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ) +} + +#[axum::debug_handler] async fn upload( + Extension(printer): Extension>>, mut multipart: Multipart, - Extension(state): Extension>>, -) -> Result { - let mut field = match multipart.next_field().await { - Ok(Some(f)) => f, - Ok(None) => return (StatusCode::BAD_REQUEST, "no multipart field").into_response(), - Err(_) => return (StatusCode::BAD_REQUEST, "multipart parse error").into_response(), - }; +) -> Result, StatusCode> { + let field = multipart + .next_field() + .await + .map_err(|_| StatusCode::BAD_REQUEST)? + .ok_or(StatusCode::BAD_REQUEST)?; + let data = field + .bytes() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .to_vec(); - let data = match field.bytes().await { - Ok(b) => b, - Err(_) => { - return (StatusCode::INTERNAL_SERVER_ERROR, "read field bytes error").into_response(); - } - }; + let img: DynamicImage = + image::load_from_memory(&data).map_err(|_| StatusCode::UNSUPPORTED_MEDIA_TYPE)?; - let img: DynamicImage = match image::load_from_memory(data.chunk()) { - Ok(i) => i, - Err(_) => { - return (StatusCode::UNSUPPORTED_MEDIA_TYPE, "image decode error").into_response(); - } - }; + println!("{}x{}", img.width(), img.height()); - let s = state.lock().await; - let mut job = s.new_job().unwrap(); - job.write_bitmap(&img, None, None); + let mut printer = printer.lock().await; + let job = printer.new_job().unwrap(); + job.write_bitmap(&img, Some(ImageOrientation::Largest), None) + .unwrap(); + job.ready = true; - let mut file = std::fs::File::create("/dev/usb/lp0").unwrap(); - s.print_job(&mut file); - (StatusCode::OK).into_response() + // let mut file = std::fs::File::create("/dev/usb/lp0").unwrap(); + // printer.print_job(&mut file).unwrap(); + + Ok((StatusCode::OK).into_response()) }