Compare commits
8 commits
master
...
printserve
| Author | SHA1 | Date | |
|---|---|---|---|
| f726a374ca | |||
| e9d0580f7e | |||
| b2662dadb7 | |||
| 7d42fe7211 | |||
| 998e5926dc | |||
| 9710e81ac1 | |||
| be3fd25fcf | |||
| 280d2fcbab |
9 changed files with 1146 additions and 124 deletions
741
Cargo.lock
generated
741
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,3 +5,8 @@ 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", "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"] }
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,31 @@
|
||||||
use image::DynamicImage;
|
use image::{
|
||||||
|
DynamicImage, GrayImage,
|
||||||
|
imageops::{self, colorops},
|
||||||
|
};
|
||||||
|
pub fn auto_brighten(gray: &mut GrayImage) -> () {
|
||||||
|
let avg = gray.pixels().map(|p| p.0[0] as u32).sum::<u32>() / gray.len() as u32;
|
||||||
|
|
||||||
|
if avg < 100 {
|
||||||
|
colorops::brighten_in_place(gray, 40);
|
||||||
|
colorops::contrast_in_place(gray, 25.0);
|
||||||
|
} else if avg > 180 {
|
||||||
|
colorops::brighten_in_place(gray, -15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dither(gray: &mut GrayImage) -> Vec<u8> {
|
||||||
|
imageops::dither(gray, &imageops::colorops::BiLevel);
|
||||||
|
|
||||||
pub fn atkinson_mono(img: &DynamicImage) -> Vec<u8> {
|
|
||||||
let mut gray = img.to_luma8();
|
|
||||||
let (w, h) = (gray.width() as usize, gray.height() as usize);
|
let (w, h) = (gray.width() as usize, gray.height() as usize);
|
||||||
let pitch = (w + 7) >> 3;
|
let pitch = (w + 7) >> 3;
|
||||||
let mut bits = vec![0u8; pitch * h];
|
let mut bits = vec![0u8; pitch * h];
|
||||||
let buf = gray.as_mut();
|
|
||||||
|
|
||||||
for y in 0..h {
|
for y in 0..h {
|
||||||
let row = y * w;
|
|
||||||
for x in 0..w {
|
for x in 0..w {
|
||||||
let i = row + x;
|
|
||||||
let old = buf[i];
|
|
||||||
let new = if old < 128 { 0 } else { 255 };
|
|
||||||
buf[i] = new;
|
|
||||||
let bit = new == 0;
|
|
||||||
if bit {
|
|
||||||
let byte = y * pitch + (x >> 3);
|
let byte = y * pitch + (x >> 3);
|
||||||
bits[byte] |= 128 >> (x & 7);
|
let bit = 7 - (x & 7);
|
||||||
}
|
if gray[(x as u32, y as u32)].0[0] == 0 {
|
||||||
let err = ((old as i16) - (new as i16)) / 8;
|
bits[byte] |= 1 << bit;
|
||||||
|
|
||||||
if x + 1 < w {
|
|
||||||
buf[i + 1] = (buf[i + 1] as i16 + err).clamp(0, 255) as u8;
|
|
||||||
}
|
|
||||||
if x + 2 < w {
|
|
||||||
buf[i + 2] = (buf[i + 2] as i16 + err).clamp(0, 255) as u8;
|
|
||||||
}
|
|
||||||
if y + 1 < h {
|
|
||||||
let down = i + w;
|
|
||||||
if x > 0 {
|
|
||||||
buf[down - 1] = (buf[down - 1] as i16 + err).clamp(0, 255) as u8;
|
|
||||||
}
|
|
||||||
buf[down] = (buf[down] as i16 + err).clamp(0, 255) as u8;
|
|
||||||
if x + 1 < w {
|
|
||||||
buf[down + 1] = (buf[down + 1] as i16 + err).clamp(0, 255) as u8;
|
|
||||||
}
|
|
||||||
if y + 2 < h {
|
|
||||||
buf[down + w] = (buf[down + w] as i16 + err).clamp(0, 255) as u8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
use crate::dithering::atkinson_mono;
|
|
||||||
use image::{DynamicImage, imageops::FilterType};
|
|
||||||
|
|
||||||
const MAX_WIDTH: u32 = 384;
|
|
||||||
|
|
||||||
pub enum ImageOrientation {
|
|
||||||
Preserve,
|
|
||||||
Largest,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn escpos_raster(
|
|
||||||
img: &DynamicImage,
|
|
||||||
orientation: Option<ImageOrientation>,
|
|
||||||
mode: Option<u8>,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
let mode = mode.unwrap_or(0);
|
|
||||||
assert!(mode <= 3 || (48..=51).contains(&mode));
|
|
||||||
|
|
||||||
let (w, h) = (img.width(), img.height());
|
|
||||||
let swap = matches!(orientation, Some(ImageOrientation::Largest))
|
|
||||||
&& h.min(MAX_WIDTH) * w > w.min(MAX_WIDTH) * h;
|
|
||||||
|
|
||||||
let (w, h) = if swap { (h, w) } else { (w, h) };
|
|
||||||
let (w, h) = (MAX_WIDTH, h.saturating_mul(MAX_WIDTH).div_ceil(w));
|
|
||||||
|
|
||||||
let img = if swap { &img.rotate90() } else { &img };
|
|
||||||
let img = img.resize(w, h, FilterType::Triangle);
|
|
||||||
|
|
||||||
let mono = atkinson_mono(&img);
|
|
||||||
let (width_px, height_px) = (img.width() as u16, img.height() as u16);
|
|
||||||
let width_bytes = ((width_px + 7) >> 3) as u16;
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(8 + mono.len());
|
|
||||||
out.extend_from_slice(&[
|
|
||||||
0x1D,
|
|
||||||
0x76,
|
|
||||||
0x30,
|
|
||||||
mode,
|
|
||||||
width_bytes as u8,
|
|
||||||
(width_bytes >> 8) as u8,
|
|
||||||
height_px as u8,
|
|
||||||
(height_px >> 8) as u8,
|
|
||||||
]);
|
|
||||||
out.extend_from_slice(&mono);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
8
src/escpos/errors.rs
Normal file
8
src/escpos/errors.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EscPosError {
|
||||||
|
EmptyQueue,
|
||||||
|
InvalidQueueIndex,
|
||||||
|
InvalidBitmapMode,
|
||||||
|
InvalidBarcodeLength(String),
|
||||||
|
InvalidTextSize,
|
||||||
|
}
|
||||||
266
src/escpos/job.rs
Normal file
266
src/escpos/job.rs
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
use crate::{
|
||||||
|
dithering::{auto_brighten, dither},
|
||||||
|
escpos::errors::EscPosError,
|
||||||
|
};
|
||||||
|
use image::{DynamicImage, imageops::FilterType};
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum QREcc {
|
||||||
|
Low = 48,
|
||||||
|
Medium = 49,
|
||||||
|
Quartile = 50,
|
||||||
|
High = 51,
|
||||||
|
}
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum BARTextPosition {
|
||||||
|
Hidden = 0,
|
||||||
|
Above = 1,
|
||||||
|
Below = 2,
|
||||||
|
Both = 3,
|
||||||
|
}
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum BARType {
|
||||||
|
CODE128 = 0x49,
|
||||||
|
EAN13 = 0x43,
|
||||||
|
UPCA = 0x41,
|
||||||
|
}
|
||||||
|
pub enum ImageOrientation {
|
||||||
|
Preserve,
|
||||||
|
Largest,
|
||||||
|
}
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum JustifyOrientation {
|
||||||
|
Left = 0,
|
||||||
|
Center = 1,
|
||||||
|
Right = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TextEffect {
|
||||||
|
Bold,
|
||||||
|
DoubleHeight,
|
||||||
|
DoubleWidth,
|
||||||
|
InvertColor,
|
||||||
|
Justify(JustifyOrientation),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Job {
|
||||||
|
pub ready: bool,
|
||||||
|
pub content: Vec<u8>,
|
||||||
|
max_width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_numeric(s: &[u8]) -> bool {
|
||||||
|
s.iter().all(|&b| b.is_ascii_digit())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Copy for JustifyOrientation {}
|
||||||
|
impl Clone for JustifyOrientation {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Job {
|
||||||
|
pub fn new(max_width: u16) -> Self {
|
||||||
|
Job {
|
||||||
|
content: vec![0x1B, b'@'],
|
||||||
|
ready: false,
|
||||||
|
max_width: max_width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_feed(&mut self, amount: Option<u8>) -> () {
|
||||||
|
let amount = amount.unwrap_or(1);
|
||||||
|
self.content.extend_from_slice(&[0x1B, b'd', amount])
|
||||||
|
}
|
||||||
|
pub fn write_text(
|
||||||
|
&mut self,
|
||||||
|
text: &String,
|
||||||
|
text_effect: &[TextEffect],
|
||||||
|
) -> Result<(), EscPosError> {
|
||||||
|
let buf = &mut self.content;
|
||||||
|
buf.extend_from_slice(&[0x1B, b'@']);
|
||||||
|
|
||||||
|
for i in text_effect {
|
||||||
|
match i {
|
||||||
|
TextEffect::Bold => buf.extend_from_slice(&[0x1B, b'E', 1]),
|
||||||
|
TextEffect::DoubleWidth => buf.extend_from_slice(&[0x1B, b'E', 1]),
|
||||||
|
TextEffect::DoubleHeight => buf.extend_from_slice(&[0x1B, b'E', 1]),
|
||||||
|
TextEffect::InvertColor => buf.extend_from_slice(&[0x1D, 0x42, 1]),
|
||||||
|
TextEffect::Justify(orientation) => {
|
||||||
|
buf.extend_from_slice(&[0x1B, b'a', orientation.clone() as u8])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_from_slice(text.as_bytes());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_bitmap(
|
||||||
|
&mut self,
|
||||||
|
img: &DynamicImage,
|
||||||
|
orientation: Option<ImageOrientation>,
|
||||||
|
mode: Option<u8>,
|
||||||
|
) -> Result<(), EscPosError> {
|
||||||
|
let mode = mode.unwrap_or(0);
|
||||||
|
if !(mode <= 3 || (48..=51).contains(&mode)) {
|
||||||
|
return Err(EscPosError::InvalidBitmapMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (iw, ih) = (img.width(), img.height());
|
||||||
|
let mw = self.max_width as u32;
|
||||||
|
|
||||||
|
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 (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 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 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;
|
||||||
|
buf.reserve(8 + mono.len());
|
||||||
|
buf.extend_from_slice(&[
|
||||||
|
0x1B,
|
||||||
|
b'@',
|
||||||
|
0x1D,
|
||||||
|
0x76,
|
||||||
|
0x30,
|
||||||
|
mode,
|
||||||
|
(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);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn write_qr(
|
||||||
|
&mut self,
|
||||||
|
text: String,
|
||||||
|
size: Option<u8>,
|
||||||
|
ecc: Option<QREcc>,
|
||||||
|
) -> Result<(), EscPosError> {
|
||||||
|
let buf = &mut self.content;
|
||||||
|
let ecc = ecc.unwrap_or(QREcc::Medium);
|
||||||
|
let size = match size {
|
||||||
|
Some(v) => v.clamp(6, 15),
|
||||||
|
None => 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = text.as_bytes();
|
||||||
|
let len = text.len() + 3;
|
||||||
|
buf.extend_from_slice(&[
|
||||||
|
0x1B,
|
||||||
|
b'@',
|
||||||
|
// Size
|
||||||
|
0x1D,
|
||||||
|
0x28,
|
||||||
|
0x6B,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x31,
|
||||||
|
0x43,
|
||||||
|
size,
|
||||||
|
// Error correction level
|
||||||
|
0x1D,
|
||||||
|
0x28,
|
||||||
|
0x6B,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x31,
|
||||||
|
0x45,
|
||||||
|
ecc as u8,
|
||||||
|
// Storing data...
|
||||||
|
0x1D,
|
||||||
|
0x28,
|
||||||
|
0x6B,
|
||||||
|
(len & 0xFF) as u8,
|
||||||
|
((len >> 8) & 0xFF) as u8,
|
||||||
|
0x31,
|
||||||
|
0x50,
|
||||||
|
0x30,
|
||||||
|
]);
|
||||||
|
buf.extend(text);
|
||||||
|
buf.extend(&[0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30, 0x0A]); // Print!
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
pub fn write_barcode(
|
||||||
|
&mut self,
|
||||||
|
text: String,
|
||||||
|
height: Option<u8>,
|
||||||
|
mod_width: Option<u8>,
|
||||||
|
text_position: Option<BARTextPosition>,
|
||||||
|
bar_type: Option<BARType>,
|
||||||
|
) -> Result<(), EscPosError> {
|
||||||
|
let height: u8 = height.unwrap_or(80);
|
||||||
|
let mod_width: u8 = mod_width.unwrap_or(2);
|
||||||
|
let text_position = text_position.unwrap_or(BARTextPosition::Below) as u8;
|
||||||
|
let bar_type = bar_type.unwrap_or(BARType::CODE128);
|
||||||
|
|
||||||
|
let text = text.as_bytes();
|
||||||
|
let len = text.len() as u8;
|
||||||
|
let is_num = is_numeric(text);
|
||||||
|
match bar_type {
|
||||||
|
BARType::UPCA if len != 11 || !is_num => Err(EscPosError::InvalidBarcodeLength(
|
||||||
|
"UCPA Requires 11 numbers.".to_string(),
|
||||||
|
)),
|
||||||
|
|
||||||
|
BARType::EAN13 if len != 12 || !is_num => Err(EscPosError::InvalidBarcodeLength(
|
||||||
|
"EAN13 Requires 12 numbers.".to_string(),
|
||||||
|
)),
|
||||||
|
_ => Ok(()),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let buf = &mut self.content;
|
||||||
|
buf.extend([
|
||||||
|
0x1B,
|
||||||
|
b'@',
|
||||||
|
0x1D,
|
||||||
|
0x68,
|
||||||
|
height,
|
||||||
|
0x1D,
|
||||||
|
0x77,
|
||||||
|
mod_width,
|
||||||
|
0x1d,
|
||||||
|
0x48,
|
||||||
|
text_position,
|
||||||
|
0x1D,
|
||||||
|
0x6B,
|
||||||
|
bar_type as u8,
|
||||||
|
len,
|
||||||
|
]);
|
||||||
|
buf.extend(text);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/escpos/mod.rs
Normal file
3
src/escpos/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod errors;
|
||||||
|
pub mod job;
|
||||||
|
pub mod printer;
|
||||||
43
src/escpos/printer.rs
Normal file
43
src/escpos/printer.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::escpos::{errors::EscPosError, job::Job};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub struct Printer {
|
||||||
|
pub queue: Vec<Job>,
|
||||||
|
pub max_width: u16,
|
||||||
|
}
|
||||||
|
impl Printer {
|
||||||
|
pub fn new(max_width: u16) -> Self {
|
||||||
|
Printer {
|
||||||
|
queue: Vec::new(),
|
||||||
|
max_width: max_width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_job(&mut self) -> Result<&mut Job, EscPosError> {
|
||||||
|
self.queue.push(Job::new(self.max_width));
|
||||||
|
self.queue.last_mut().ok_or(EscPosError::InvalidQueueIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_job(&mut self) -> Result<Job, EscPosError> {
|
||||||
|
self.queue
|
||||||
|
.extract_if(.., |j| j.ready)
|
||||||
|
.next()
|
||||||
|
.ok_or(EscPosError::EmptyQueue)
|
||||||
|
}
|
||||||
|
pub fn print_job(&mut self, writer: &mut impl Write) -> Result<(), EscPosError> {
|
||||||
|
let mut job = self.extract_job()?;
|
||||||
|
job.write_feed(Some(2));
|
||||||
|
|
||||||
|
writer.write(&job.content).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn export_job(&mut self) -> Result<Vec<u8>, EscPosError> {
|
||||||
|
let mut job = self.extract_job()?;
|
||||||
|
job.write_feed(Some(2));
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(2 + job.content.len());
|
||||||
|
out.extend(job.content);
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/main.rs
99
src/main.rs
|
|
@ -1,27 +1,94 @@
|
||||||
mod dithering;
|
mod dithering;
|
||||||
mod escpos;
|
mod escpos;
|
||||||
|
|
||||||
use image::{ImageError, ImageReader};
|
use std::{io::Write, sync::Arc};
|
||||||
use std::{env, fs, io, io::Write, process};
|
|
||||||
|
|
||||||
fn main() {
|
use axum::{
|
||||||
let args: Vec<String> = env::args().collect();
|
Router,
|
||||||
|
body::{Body, Bytes},
|
||||||
|
extract::{Extension, Multipart},
|
||||||
|
http::{Response, StatusCode, header},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
use image::DynamicImage;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
let len = args.len();
|
use crate::escpos::{errors::EscPosError, job::ImageOrientation, printer::Printer};
|
||||||
if len < 2 || len > 2 {
|
|
||||||
println!("Please provide a path to the image.");
|
#[tokio::main]
|
||||||
process::exit(1);
|
async fn main() {
|
||||||
|
let printer = Arc::new(Mutex::new(Printer::new(380)));
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/upload", post(upload))
|
||||||
|
.route("/print", get(print))
|
||||||
|
.layer(Extension(printer));
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let img = ImageReader::open(&args[1])
|
#[axum::debug_handler]
|
||||||
.map_err(|err| ImageError::IoError(err))
|
async fn print(
|
||||||
.and_then(|v| v.decode())
|
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,
|
||||||
|
) -> 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 img: DynamicImage =
|
||||||
|
image::load_from_memory(&data).map_err(|_| StatusCode::UNSUPPORTED_MEDIA_TYPE)?;
|
||||||
|
|
||||||
|
println!("{}x{}", img.width(), img.height());
|
||||||
|
|
||||||
|
let mut printer = printer.lock().await;
|
||||||
|
let job = printer.new_job().unwrap();
|
||||||
|
job.write_bitmap(&img, Some(ImageOrientation::Largest), None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
job.ready = true;
|
||||||
|
|
||||||
let mut escpos = escpos::escpos_raster(&img, Some(escpos::ImageOrientation::Largest), Some(0));
|
// let mut file = std::fs::File::create("/dev/usb/lp0").unwrap();
|
||||||
for _ in 0..2 {
|
// printer.print_job(&mut file).unwrap();
|
||||||
escpos.push('\n' as u8);
|
|
||||||
}
|
|
||||||
|
|
||||||
io::stdout().write(&escpos).unwrap();
|
Ok((StatusCode::OK).into_response())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue