Implemented: Queues, QR Codes, BARCodes

Implemented QR and barcode
This commit is contained in:
Jurn Wubben 2025-09-24 17:32:07 +02:00
parent 410e203f9e
commit 280d2fcbab
2 changed files with 255 additions and 41 deletions

View file

@ -1,46 +1,246 @@
use crate::dithering::atkinson_mono; use crate::dithering::atkinson_mono;
use image::{DynamicImage, imageops::FilterType}; use image::{DynamicImage, imageops::FilterType};
use std::{collections::HashMap, io::Write, ops::AddAssign};
const MAX_WIDTH: u32 = 384; 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 { pub enum ImageOrientation {
Preserve, Preserve,
Largest, Largest,
} }
pub fn escpos_raster( pub struct Job {
img: &DynamicImage, content: Vec<u8>,
orientation: Option<ImageOrientation>, pub ready: bool,
mode: Option<u8>, }
) -> Vec<u8> { pub struct Printer {
let mode = mode.unwrap_or(0); pub queue: Vec<Job>,
assert!(mode <= 3 || (48..=51).contains(&mode)); }
let (w, h) = (img.width(), img.height()); fn is_numeric(s: &[u8]) -> bool {
let swap = matches!(orientation, Some(ImageOrientation::Largest)) s.iter().all(|&b| b.is_ascii_digit())
&& h.min(MAX_WIDTH) * w > w.min(MAX_WIDTH) * h; }
let (w, h) = if swap { (h, w) } else { (w, h) }; impl Job {
let (w, h) = (MAX_WIDTH, h.saturating_mul(MAX_WIDTH).div_ceil(w)); pub fn new() -> Self {
Job {
let img = if swap { &img.rotate90() } else { &img }; content: vec![0x1B, b'@'],
let img = img.resize(w, h, FilterType::Triangle); ready: false,
}
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; pub fn write_bitmap(
&mut self,
let mut out = Vec::with_capacity(8 + mono.len()); img: &DynamicImage,
out.extend_from_slice(&[ orientation: Option<ImageOrientation>,
0x1D, mode: Option<u8>,
0x76, ) -> Result<(), EscPosError> {
0x30, let mode = mode.unwrap_or(0);
mode, if !(mode <= 3 || (48..=51).contains(&mode)) {
width_bytes as u8, return Err(EscPosError::InvalidBitmapMode);
(width_bytes >> 8) as u8, }
height_px as u8,
(height_px >> 8) as u8, let (w, h) = (img.width(), img.height());
]); let swap = matches!(orientation, Some(ImageOrientation::Largest))
out.extend_from_slice(&mono); && h.min(MAX_WIDTH) * w > w.min(MAX_WIDTH) * h;
out
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<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(&[
// 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([
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<Vec<u8>, 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)
}
} }

View file

@ -2,7 +2,9 @@ mod dithering;
mod escpos; mod escpos;
use image::{ImageError, ImageReader}; use image::{ImageError, ImageReader};
use std::{env, fs, io, io::Write, process}; use std::{env, process};
use crate::escpos::Printer;
fn main() { fn main() {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
@ -18,10 +20,22 @@ fn main() {
.and_then(|v| v.decode()) .and_then(|v| v.decode())
.unwrap(); .unwrap();
let mut escpos = escpos::escpos_raster(&img, Some(escpos::ImageOrientation::Largest), Some(0)); let mut printer = Printer::new();
for _ in 0..2 { let job = printer.new_job().unwrap();
escpos.push('\n' as u8); // 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();
} }