mod commands;
pub mod nfce;
pub use nfce::EscPosNFCeBuilder;
use image::{io::Reader as ImageReader, DynamicImage, GenericImageView};
use std::io::Cursor;
pub struct EscPosBuilder {
buffer: Vec<u8>,
paper_width: u8,
}
impl EscPosBuilder {
pub fn new() -> Self {
let mut s = Self {
buffer: Vec::new(),
paper_width: 80,
};
s.buffer.extend_from_slice(commands::INIT);
s
}
pub fn paper_width(mut self, mm: u8) -> Self {
self.paper_width = mm;
self
}
pub fn align_left(mut self) -> Self {
self.buffer.extend_from_slice(commands::ALIGN_LEFT);
self
}
pub fn align_center(mut self) -> Self {
self.buffer.extend_from_slice(commands::ALIGN_CENTER);
self
}
pub fn align_right(mut self) -> Self {
self.buffer.extend_from_slice(commands::ALIGN_RIGHT);
self
}
pub fn bold(mut self, on: bool) -> Self {
let cmd = if on { commands::BOLD_ON } else { commands::BOLD_OFF };
self.buffer.extend_from_slice(cmd);
self
}
pub fn underline(mut self, on: bool) -> Self {
let cmd = if on { commands::UNDERLINE_ON } else { commands::UNDERLINE_OFF };
self.buffer.extend_from_slice(cmd);
self
}
pub fn font_size(mut self, size: u8) -> Self {
let n = size.saturating_sub(1).min(7);
let byte = (n << 4) | n; self.buffer.extend_from_slice(&[0x1D, 0x21, byte]);
self
}
pub fn text(mut self, s: impl AsRef<str>) -> Self {
self.buffer.extend_from_slice(s.as_ref().as_bytes());
self
}
pub fn divider(mut self) -> Self {
let cols: usize = if self.paper_width >= 80 { 48 } else { 32 };
let mut line = "-".repeat(cols);
line.push('\n');
self.buffer.extend_from_slice(line.as_bytes());
self
}
pub fn feed(mut self, lines: u8) -> Self {
self.buffer.extend_from_slice(&[0x1B, 0x64, lines]);
self
}
pub fn barcode_128(mut self, data: &str) -> Self {
let bytes = data.as_bytes();
self.buffer.extend_from_slice(&[0x1D, 0x6B, 0x49]);
self.buffer.push(bytes.len() as u8);
self.buffer.extend_from_slice(bytes);
self
}
pub fn qr_code(mut self, data: &str, size: u8) -> Self {
let model: u8 = 2; let size = size.clamp(1, 16);
let data_bytes = data.as_bytes();
let data_len = data_bytes.len() as u16 + 3;
let pl = (data_len & 0xFF) as u8;
let ph = ((data_len >> 8) & 0xFF) as u8;
self.buffer.extend_from_slice(&[0x1D, 0x28, 0x6B, 0x04, 0x00, 0x31, 0x41, model, 0x00]);
self.buffer.extend_from_slice(&[0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, size]);
self.buffer.extend_from_slice(&[0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x45, 0x31]);
self.buffer.extend_from_slice(&[0x1D, 0x28, 0x6B, pl, ph, 0x31, 0x50, 0x30]);
self.buffer.extend_from_slice(data_bytes);
self.buffer.extend_from_slice(&[0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30]);
self
}
pub fn image(mut self, img_bytes: &[u8]) -> Self {
let img = match ImageReader::new(Cursor::new(img_bytes))
.with_guessed_format()
.ok()
.and_then(|r| r.decode().ok())
{
Some(i) => i,
None => return self,
};
let raster = rasterize(&img, self.paper_width);
self.buffer.extend_from_slice(&raster);
self
}
pub fn cut(mut self) -> Self {
self.buffer.extend_from_slice(commands::CUT_FULL);
self
}
pub fn partial_cut(mut self) -> Self {
self.buffer.extend_from_slice(commands::CUT_PARTIAL);
self
}
pub fn build(self) -> Vec<u8> {
self.buffer
}
}
impl Default for EscPosBuilder {
fn default() -> Self {
Self::new()
}
}
fn rasterize(img: &DynamicImage, paper_width_mm: u8) -> Vec<u8> {
let max_dots: u32 = if paper_width_mm >= 80 { 576 } else { 384 };
let (orig_w, orig_h) = img.dimensions();
let (w, h) = if orig_w > max_dots {
let scale = max_dots as f32 / orig_w as f32;
(max_dots, (orig_h as f32 * scale) as u32)
} else {
(orig_w, orig_h)
};
let img = img.resize_exact(w, h, image::imageops::FilterType::Lanczos3);
let gray = img.to_luma8();
let bytes_per_row = ((w + 7) / 8) as u16;
let xl = (bytes_per_row & 0xFF) as u8;
let xh = ((bytes_per_row >> 8) & 0xFF) as u8;
let yl = (h & 0xFF) as u8;
let yh = ((h >> 8) & 0xFF) as u8;
let mut out = vec![0x1D, 0x76, 0x30, 0x00, xl, xh, yl, yh];
for row in gray.rows() {
let pixels: Vec<u8> = row.map(|p| p.0[0]).collect();
for chunk in pixels.chunks(8) {
let mut byte = 0u8;
for (i, &luma) in chunk.iter().enumerate() {
if luma < 128 {
byte |= 0x80 >> i; }
}
out.push(byte);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_returns_nonempty_bytes() {
let bytes = EscPosBuilder::new()
.paper_width(80)
.align_center()
.bold(true)
.text("EMPRESA LTDA\n")
.bold(false)
.align_left()
.text("CNPJ: 11.222.333/0001-81\n")
.divider()
.text(format!("{:<20} {:>10}\n", "PRODUTO EXEMPLO", "R$ 50,00"))
.divider()
.align_right()
.bold(true)
.text("TOTAL R$ 50,00\n")
.bold(false)
.cut()
.build();
assert!(!bytes.is_empty());
assert_eq!(&bytes[0..2], &[0x1B, 0x40]); assert_eq!(&bytes[bytes.len() - 3..], &[0x1D, 0x56, 0x00]); }
#[test]
fn divider_58mm_is_shorter() {
let b80 = EscPosBuilder::new().paper_width(80).divider().build();
let b58 = EscPosBuilder::new().paper_width(58).divider().build();
assert!(b80.len() > b58.len());
}
#[test]
fn qr_code_produces_bytes() {
let bytes = EscPosBuilder::new().qr_code("https://example.com", 4).build();
assert!(bytes.len() > 2);
}
#[test]
fn barcode_128_encodes_data() {
let data = "12345678";
let bytes = EscPosBuilder::new().barcode_128(data).build();
let pos = bytes.windows(3).position(|w| w == [0x1D, 0x6B, 0x49]).unwrap();
assert_eq!(bytes[pos + 3], data.len() as u8);
}
}