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"
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"

View file

@ -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"] }

View file

@ -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<DynamicImage> = 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);

View file

@ -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<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(
Extension(printer): Extension<Arc<Mutex<Printer>>>,
mut multipart: Multipart,
Extension(state): Extension<Arc<Mutex<Printer>>>,
) -> Result<impl IntoResponse, (StatusCode, &'static str)> {
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<Response<Body>, 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())
}