From 280d2fcbabdbf839b9f3f8c92f037a0824488566 Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Wed, 24 Sep 2025 17:32:07 +0200 Subject: [PATCH] Implemented: Queues, QR Codes, BARCodes Implemented QR and barcode --- src/escpos.rs | 270 +++++++++++++++++++++++++++++++++++++++++++------- src/main.rs | 26 +++-- 2 files changed, 255 insertions(+), 41 deletions(-) diff --git a/src/escpos.rs b/src/escpos.rs index d5c1b67..13d33f8 100644 --- a/src/escpos.rs +++ b/src/escpos.rs @@ -1,46 +1,246 @@ use crate::dithering::atkinson_mono; use image::{DynamicImage, imageops::FilterType}; +use std::{collections::HashMap, io::Write, ops::AddAssign}; const MAX_WIDTH: u32 = 384; +#[derive(Debug)] +pub enum EscPosError { + EmptyQueue, + InvalidQueueIndex, + InvalidBitmapMode, + InvalidBarcodeLength(String), +} + +#[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, } -pub fn escpos_raster( - img: &DynamicImage, - orientation: Option, - mode: Option, -) -> Vec { - 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 +pub struct Job { + content: Vec, + pub ready: bool, +} +pub struct Printer { + pub queue: Vec, +} + +fn is_numeric(s: &[u8]) -> bool { + s.iter().all(|&b| b.is_ascii_digit()) +} + +impl Job { + pub fn new() -> Self { + Job { + content: vec![0x1B, b'@'], + ready: false, + } + } + + pub fn write_bitmap( + &mut self, + img: &DynamicImage, + orientation: Option, + mode: Option, + ) -> Result<(), EscPosError> { + let mode = mode.unwrap_or(0); + if !(mode <= 3 || (48..=51).contains(&mode)) { + return Err(EscPosError::InvalidBitmapMode); + } + + 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 scale = if swap { + MAX_WIDTH as f32 / h as f32 + } else { + MAX_WIDTH as f32 / w as f32 + }; + + let (w, h) = if swap { + ((h as f32 * scale) as u32, MAX_WIDTH) + } else { + (MAX_WIDTH, (h as f32 * scale) as u32) + }; + + let img = + DynamicImage::ImageRgba8(image::imageops::resize(img, w, h, FilterType::Triangle)); + + let img = if swap { img.rotate90() } else { img }; + let mono = atkinson_mono(&img); + let width_bytes = ((w + 7) >> 3) as u16; + + let buf = &mut self.content; + buf.reserve(8 + mono.len()); + buf.extend_from_slice(&[ + 0x1D, + 0x76, + 0x30, + mode, + width_bytes as u8, + (width_bytes >> 8) as u8, + h as u8, + (h >> 8) as u8, + ]); + buf.extend_from_slice(&mono); + + Ok(()) + } + pub fn write_qr( + &mut self, + text: String, + size: Option, + ecc: Option, + ) -> 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(&[ + // 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, + mod_width: Option, + text_position: Option, + bar_type: Option, + ) -> 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([ + 0x1D, + 0x68, + height, + 0x1D, + 0x77, + mod_width, + 0x1d, + 0x48, + text_position, + 0x1D, + 0x6B, + bar_type as u8, + len, + ]); + buf.extend(text); + + Ok(()) + } +} +impl Printer { + pub fn new() -> Self { + Printer { queue: Vec::new() } + } + + pub fn new_job(&mut self) -> Result<&mut Job, EscPosError> { + self.queue.push(Job::new()); + self.queue.last_mut().ok_or(EscPosError::InvalidQueueIndex) + } + pub fn print_job(&mut self, writer: &mut impl Write) -> Result<(), EscPosError> { + let page_feed: u8 = 0x0A; + let job = self + .queue + .extract_if(.., |j| j.ready) + .next() + .ok_or(EscPosError::EmptyQueue)?; + + // writer.write(&[page_feed]).unwrap(); // FIXME: remove unwraps + writer.write(&job.content).unwrap(); + // writer.write(&[page_feed]).unwrap(); + + Ok(()) + } + pub fn export_job(&mut self) -> Result, EscPosError> { + let page_feed: u8 = 0x0A; + let job = self + .queue + .extract_if(.., |j| j.ready) + .next() + .ok_or(EscPosError::EmptyQueue)?; + + let mut out = Vec::with_capacity(2 + job.content.len()); + out.push(page_feed); + out.extend(job.content); + out.push(page_feed); + + Ok(out) + } } diff --git a/src/main.rs b/src/main.rs index 9de8881..740f049 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,9 @@ mod dithering; mod escpos; use image::{ImageError, ImageReader}; -use std::{env, fs, io, io::Write, process}; +use std::{env, process}; + +use crate::escpos::Printer; fn main() { let args: Vec = env::args().collect(); @@ -18,10 +20,22 @@ fn main() { .and_then(|v| v.decode()) .unwrap(); - let mut escpos = escpos::escpos_raster(&img, Some(escpos::ImageOrientation::Largest), Some(0)); - for _ in 0..2 { - escpos.push('\n' as u8); - } + let mut printer = Printer::new(); + let job = printer.new_job().unwrap(); + // job.write_qr("hi".to_string(), None, None).unwrap(); + job.write_barcode( + "hhhhhhhhhhh".to_string(), + None, + None, + None, + Some(escpos::BARType::UPCA), + ) + .unwrap(); + // job.write_bitmap(&img, None, None).unwrap(); + job.ready = true; - io::stdout().write(&escpos).unwrap(); + println!("{}", printer.queue.len()); + + let mut out = std::fs::File::create("/dev/usb/lp0").unwrap(); + printer.print_job(&mut out).unwrap(); }