Implemented basic webserver

This commit is contained in:
Jurn Wubben 2025-09-29 11:05:31 +02:00
parent b2662dadb7
commit e9d0580f7e
4 changed files with 149 additions and 56 deletions

51
Cargo.lock generated
View file

@ -97,6 +97,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"axum-macros",
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"futures-util", "futures-util",
@ -145,6 +146,17 @@ dependencies = [
"tracing", "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]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.76" version = "0.3.76"
@ -413,6 +425,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.31" version = "0.3.31"
@ -470,6 +488,25 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 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]] [[package]]
name = "half" name = "half"
version = "2.6.0" version = "2.6.0"
@ -548,6 +585,7 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
@ -1412,6 +1450,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "toml" name = "toml"
version = "0.8.23" version = "0.8.23"

View file

@ -6,6 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
image = {version = "0.25.8", features = ["png"]} 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"] } tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.6.1", features = ["limit", "trace"] } tower-http = { version = "0.6.1", features = ["limit", "trace"] }

View file

@ -108,30 +108,41 @@ impl Job {
return Err(EscPosError::InvalidBitmapMode); return Err(EscPosError::InvalidBitmapMode);
} }
let (w, h) = (img.width(), img.height()); let (iw, ih) = (img.width(), img.height());
let swap = matches!(orientation, Some(ImageOrientation::Largest)) let mw = self.max_width as u32;
&& h.min(self.max_width as u32) * w > w.min(self.max_width as u32) * h;
let scale = if swap { let rotate = matches!(orientation, Some(ImageOrientation::Largest)) && {
self.max_width as f32 / h as f32 let scale_a = mw as f32 / iw as f32;
} else { let out_h_a = (ih as f32 * scale_a).round() as u32;
self.max_width as f32 / w as f32 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 { let (pw, ph) = if rotate { (ih, iw) } else { (iw, ih) };
((h as f32 * scale) as u32, self.max_width as u32) let target_w = mw;
} else { let target_h = (ph as f32 * (mw as f32 / pw as f32)).round() as u32;
(self.max_width as u32, (h as f32 * scale) as u32)
};
let img = let mut rotated: Option<DynamicImage> = None;
DynamicImage::ImageRgba8(image::imageops::resize(img, w, h, FilterType::Triangle)); 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 = resized.to_luma8();
let mut gray = img.to_luma8();
auto_brighten(&mut gray); auto_brighten(&mut gray);
let mono = dither(&mut gray); let mono = dither(&mut gray);
let w = target_w;
let h = target_h;
let width_bytes = ((w + 7) >> 3) as u16; let width_bytes = ((w + 7) >> 3) as u16;
let buf = &mut self.content; let buf = &mut self.content;
@ -143,10 +154,10 @@ impl Job {
0x76, 0x76,
0x30, 0x30,
mode, mode,
width_bytes as u8, (width_bytes & 0xFF) as u8,
(width_bytes >> 8) as u8, ((width_bytes >> 8) & 0xFF) as u8,
h as u8, (h & 0xFF) as u8,
(h >> 8) as u8, ((h >> 8) & 0xFF) as u8,
]); ]);
buf.extend_from_slice(&mono); buf.extend_from_slice(&mono);

View file

@ -5,60 +5,90 @@ use std::{io::Write, sync::Arc};
use axum::{ use axum::{
Router, Router,
body::{Body, Bytes},
extract::{Extension, Multipart}, extract::{Extension, Multipart},
http::StatusCode, http::{Response, StatusCode, header},
response::IntoResponse, response::IntoResponse,
routing::post, routing::{get, post},
}; };
use image::DynamicImage; use image::DynamicImage;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::escpos::printer::Printer; use crate::escpos::{errors::EscPosError, job::ImageOrientation, printer::Printer};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let printer = Arc::new(Mutex::new(Printer::new(380))); let printer = Arc::new(Mutex::new(Printer::new(380)));
let app = Router::new() let app = Router::new()
.route("/", post(upload)) .route("/upload", post(upload))
.route("/print", get(print))
.layer(Extension(printer)); .layer(Extension(printer));
println!("Listening on http://127.0.0.1:8000"); let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
axum::Server::bind(&"192.168.1.112:8000".parse().unwrap()) axum::serve(listener, app).await.unwrap();
.serve(app.into_make_service())
.await
.unwrap();
} }
#[axum::debug_handler]
async fn print(
Extension(printer): Extension<Arc<Mutex<Printer>>>,
) -> 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( async fn upload(
Extension(printer): Extension<Arc<Mutex<Printer>>>,
mut multipart: Multipart, mut multipart: Multipart,
Extension(state): Extension<Arc<Mutex<Printer>>>, ) -> Result<Response<Body>, StatusCode> {
) -> Result<impl IntoResponse, (StatusCode, &'static str)> { let field = multipart
let mut field = match multipart.next_field().await { .next_field()
Ok(Some(f)) => f, .await
Ok(None) => return (StatusCode::BAD_REQUEST, "no multipart field").into_response(), .map_err(|_| StatusCode::BAD_REQUEST)?
Err(_) => return (StatusCode::BAD_REQUEST, "multipart parse error").into_response(), .ok_or(StatusCode::BAD_REQUEST)?;
}; let data = field
.bytes()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.to_vec();
let data = match field.bytes().await { let img: DynamicImage =
Ok(b) => b, image::load_from_memory(&data).map_err(|_| StatusCode::UNSUPPORTED_MEDIA_TYPE)?;
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "read field bytes error").into_response(); println!("{}x{}", img.width(), img.height());
}
}; let mut printer = printer.lock().await;
let job = printer.new_job().unwrap();
let img: DynamicImage = match image::load_from_memory(data.chunk()) { job.write_bitmap(&img, Some(ImageOrientation::Largest), None)
Ok(i) => i, .unwrap();
Err(_) => { job.ready = true;
return (StatusCode::UNSUPPORTED_MEDIA_TYPE, "image decode error").into_response();
} // let mut file = std::fs::File::create("/dev/usb/lp0").unwrap();
}; // printer.print_job(&mut file).unwrap();
let s = state.lock().await; Ok((StatusCode::OK).into_response())
let mut job = s.new_job().unwrap();
job.write_bitmap(&img, None, None);
let mut file = std::fs::File::create("/dev/usb/lp0").unwrap();
s.print_job(&mut file);
(StatusCode::OK).into_response()
} }