Compare commits

..

2 commits

10 changed files with 93 additions and 1122 deletions

737
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,3 @@ edition = "2024"
[dependencies]
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"] }

BIN
out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

52
src/escpos.rs Normal file
View file

@ -0,0 +1,52 @@
use crate::dithering::{auto_brighten, dither};
use image::{
DynamicImage,
imageops::{FilterType, colorops},
};
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 mut gray = img.to_luma8();
auto_brighten(&mut gray);
let mono = dither(&mut gray);
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
}

View file

@ -1,8 +0,0 @@
#[derive(Debug)]
pub enum EscPosError {
EmptyQueue,
InvalidQueueIndex,
InvalidBitmapMode,
InvalidBarcodeLength(String),
InvalidTextSize,
}

View file

@ -1,266 +0,0 @@
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(())
}
}

View file

@ -1,3 +0,0 @@
pub mod errors;
pub mod job;
pub mod printer;

View file

@ -1,43 +0,0 @@
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)
}
}

View file

@ -1,94 +1,27 @@
mod dithering;
mod escpos;
use std::{io::Write, sync::Arc};
use image::{ImageError, ImageReader};
use std::{env, fs, io, io::Write, process};
use axum::{
Router,
body::{Body, Bytes},
extract::{Extension, Multipart},
http::{Response, StatusCode, header},
response::IntoResponse,
routing::{get, post},
};
use image::DynamicImage;
use tokio::sync::Mutex;
fn main() {
let args: Vec<String> = env::args().collect();
use crate::escpos::{errors::EscPosError, job::ImageOrientation, printer::Printer};
let len = args.len();
if len < 2 || len > 2 {
println!("Please provide a path to the image.");
process::exit(1);
}
#[tokio::main]
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();
}
#[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,
) -> 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)
let img = ImageReader::open(&args[1])
.map_err(|err| ImageError::IoError(err))
.and_then(|v| v.decode())
.unwrap();
job.ready = true;
// let mut file = std::fs::File::create("/dev/usb/lp0").unwrap();
// printer.print_job(&mut file).unwrap();
let mut escpos = escpos::escpos_raster(&img, Some(escpos::ImageOrientation::Largest), Some(0));
for _ in 0..2 {
escpos.push('\n' as u8);
}
Ok((StatusCode::OK).into_response())
io::stdout().write(&escpos).unwrap();
}