301 lines
7.5 KiB
Rust
301 lines
7.5 KiB
Rust
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<u8>,
|
|
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::<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);
|
|
}
|
|
}
|
|
fn bitmap_pack_bits(w: usize, h: usize, gray: &GrayImage) -> Vec<u8> {
|
|
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<u8>) {
|
|
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<u8> = 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<ImageOrientation>,
|
|
) -> () {
|
|
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<u8> = 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<u8>, ecc: Option<QREcc>) -> () {
|
|
let mut buf: Vec<u8> = 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<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 mut buf: Vec<u8> = 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(())
|
|
}
|
|
}
|