use std::{error::Error, fmt::Display}; use image::{ DynamicImage, GrayImage, imageops::{self, FilterType, colorops}, }; use serde::Deserialize; // TYPES #[derive(Debug)] pub enum EscPosError { InvalidBarcodeLength(String), } #[repr(u8)] #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum QREcc { Low = 48, Medium = 49, Quartile = 50, High = 51, } #[repr(u8)] #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum BARTextPosition { Hidden = 0, Above = 1, Below = 2, Both = 3, } #[repr(u8)] #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum BARType { Code128 = 0x49, Ean13 = 0x43, Upca = 0x41, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ImageOrientation { Preserve, Largest, LargestOrientation, } #[repr(u8)] #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum JustifyOrientation { #[default] Left = 0, Center = 1, Right = 2, } #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] pub enum TextEffect { Bold, DoubleHeight, DoubleWidth, // InvertColor, Justify(JustifyOrientation), } pub struct EscPosBuilder { pub content: Vec, max_width: u16, } fn is_numeric(s: &[u8]) -> bool { s.iter().all(|&b| b.is_ascii_digit()) } fn bitmap_auto_brighten(gray: &mut GrayImage) { let avg = gray.pixels().map(|p| p.0[0] as u32).sum::() / 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); } } fn bitmap_pack_bits(w: usize, h: usize, gray: &GrayImage) -> Vec { let pitch = (w + 7) >> 3; // pixels per row let mut bits = vec![0u8; pitch * h]; for y in 0..h { for x in 0..w { let byte = y * pitch + (x >> 3); // get index of byte that contains bit for the current pixel let bit = 7 - (x & 7); // get bit position in byte if gray[(x as u32, y as u32)].0[0] == 0 { // if the pixel is black, set it to one bits[byte] |= 1 << bit; } } } bits } impl Copy for JustifyOrientation {} impl Clone for JustifyOrientation { fn clone(&self) -> Self { *self } } impl Error for EscPosError {} impl Display for EscPosError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } } impl EscPosBuilder { fn extend(&mut self, slice: &[u8]) { self.content.extend([0x1B, b'@']); self.content.extend(slice); } pub fn new(max_width: u16) -> Self { EscPosBuilder { content: vec![0x1B, b'@'], max_width, } } pub fn write_feed(&mut self, amount: Option) { let amount = amount.unwrap_or(1); self.extend(&[0x1B, b'd', amount]) } pub fn write_text(&mut self, text: &str, text_effect: Option<&[TextEffect]>) -> () { let mut buf: Vec = vec![]; buf.reserve(12 + text.len()); for i in text_effect.unwrap_or(&[]) { match i { TextEffect::Bold => buf.extend(&[0x1B, b'!', 8]), TextEffect::DoubleWidth => buf.extend(&[0x1B, b'!', 0x10]), TextEffect::DoubleHeight => buf.extend(&[0x1B, b'!', 0x20]), TextEffect::Justify(orientation) => buf.extend(&[0x1B, b'a', *orientation as u8]), } } buf.extend(text.as_bytes()); self.extend(&buf); } pub fn write_bitmap( &mut self, img: &DynamicImage, orientation: Option, ) -> () { let orientation = orientation.unwrap_or(ImageOrientation::Preserve); let mw = self.max_width as u32; let mut image = img.clone(); if matches!(orientation, ImageOrientation::LargestOrientation) && image.width() > image.height() { image = image.rotate90(); } let iw = image.width(); let ih = image.height(); let w = if iw > mw || !matches!(orientation, ImageOrientation::Preserve) { mw } else { iw }; let h = (ih * w) / iw; let img = image.resize(w, h, FilterType::Triangle); let mut gray = img.to_luma8(); imageops::dither(&mut gray, &imageops::colorops::BiLevel); bitmap_auto_brighten(&mut gray); let w = img.width(); let h = img.height(); let bits = bitmap_pack_bits(w as usize, h as usize, &gray); let mut buf: Vec = vec![]; let width_bytes = ((w + 7) >> 3) as u16; let height_u16 = h as u16; buf.extend(&[ 0x1D, b'v', b'0', 0, (width_bytes & 0xFF) as u8, ((width_bytes >> 8) & 0xFF) as u8, (height_u16 & 0xFF) as u8, ((height_u16 >> 8) & 0xFF) as u8, ]); buf.extend(bits); self.extend(&buf); } pub fn write_qr(&mut self, text: &str, size: Option, ecc: Option) -> () { let mut buf: Vec = vec![]; 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(&[ // 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! self.extend(&buf); } pub fn write_barcode( &mut self, text: &str, 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 mut buf: Vec = vec![]; buf.extend([ 0x1D, 0x68, height, 0x1D, 0x77, mod_width, 0x1d, 0x48, text_position, 0x1D, 0x6B, bar_type as u8, len, ]); buf.extend(text); self.extend(&buf); Ok(()) } }