mod commands;
pub mod nfce;
pub use nfce::EscPosNFCeBuilder;
use image::{io::Reader as ImageReader, DynamicImage, GenericImageView, GrayImage, Luma};
use std::io::Cursor;
pub struct EscPosBuilder {
buffer: Vec<u8>,
paper_width: u8,
paper_dots: u32,
}
impl EscPosBuilder {
pub fn new() -> Self {
let mut s = Self {
buffer: Vec::new(),
paper_width: 80,
paper_dots: 576,
};
s.buffer.extend_from_slice(commands::INIT);
s.buffer.extend_from_slice(&[0x1B, 0x74, 0x02]); s
}
pub fn paper_width(mut self, mm: u8) -> Self {
self.paper_width = mm;
self.paper_dots = if mm >= 80 { 576 } else { 384 };
self
}
pub fn printer_dpi(mut self, dpi: u32) -> Self {
let printable_mm: f32 = if self.paper_width >= 80 { 72.0 } else { 48.0 };
self.paper_dots = (printable_mm * dpi as f32 / 25.4).round() as u32;
self
}
pub fn printable_dots(mut self, dots: u32) -> Self {
self.paper_dots = dots;
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 font_height(mut self, size: u8) -> Self {
let h = size.saturating_sub(1).min(7);
let byte = h << 4; self.buffer.extend_from_slice(&[0x1D, 0x21, byte]);
self
}
pub fn line_spacing(mut self, dots: u8) -> Self {
self.buffer.extend_from_slice(&[0x1B, 0x33, dots]);
self
}
pub fn line_spacing_default(mut self) -> Self {
self.buffer.extend_from_slice(&[0x1B, 0x32]);
self
}
pub fn text(mut self, s: impl AsRef<str>) -> Self {
self.buffer.extend_from_slice(&encode_cp850(s.as_ref()));
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 font_b(mut self, on: bool) -> Self {
self.buffer.extend_from_slice(&[0x1B, 0x4D, if on { 1 } else { 0 }]);
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 {
use barcoders::sym::code128::Code128;
let is_numeric_even = data.chars().all(|c| c.is_ascii_digit()) && data.len() % 2 == 0;
let code_data = if is_numeric_even {
format!("\u{0106}{data}")
} else {
format!("\u{0105}{data}")
};
let encoded = match Code128::new(&code_data) {
Ok(b) => b.encode(),
Err(_) => return self,
};
let module_width: u32 = 2;
let bar_height: u32 = 80;
let total_width: u32 = encoded.len() as u32 * module_width;
let mut img = GrayImage::new(total_width, bar_height);
for (idx, &bar) in encoded.iter().enumerate() {
let luma = if bar == 1 { 0u8 } else { 255u8 };
for dx in 0..module_width {
let x = idx as u32 * module_width + dx;
for y in 0..bar_height {
img.put_pixel(x, y, Luma([luma]));
}
}
}
let raster = rasterize(&DynamicImage::ImageLuma8(img), self.paper_width);
self.buffer.extend_from_slice(&raster);
self.buffer.push(b'\n');
self
}
pub fn qr_code(mut self, data: &str, size: u8) -> Self {
let model: u8 = 50; 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 open_drawer(mut self, pin: u8) -> Self {
let cmd = if pin == 5 { commands::CASH_DRAWER_PIN5 } else { commands::CASH_DRAWER_PIN2 };
self.buffer.extend_from_slice(cmd);
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
}
pub fn qr_with_text_right(mut self, qr_data: &str, lines: &[(String, bool)]) -> Self {
use font8x8::UnicodeFonts;
use qrcodegen::{QrCode, QrCodeEcc};
let qr = match QrCode::encode_text(qr_data, QrCodeEcc::Medium) {
Ok(q) => q,
Err(_) => return self,
};
let qr_modules = qr.size() as u32;
let quiet = 4u32;
let total_mod = qr_modules + quiet * 2;
let paper_dots = self.paper_dots;
let max_qr_w = paper_dots * 55 / 100;
let scale = (max_qr_w / total_mod).max(2);
let qr_px = total_mod * scale;
const S: u32 = 2; const FONT_W: u32 = 8 * S; const FONT_H: u32 = 8 * S; const LINE_H: u32 = FONT_H + 4;
const GAP: u32 = 8;
let text_x = qr_px + GAP;
let img_h = qr_px.max(lines.len() as u32 * LINE_H).max(1);
let mut img = GrayImage::from_pixel(paper_dots, img_h, Luma([255u8]));
for my in 0..qr_modules {
for mx in 0..qr_modules {
if qr.get_module(mx as i32, my as i32) {
let px0 = (quiet + mx) * scale;
let py0 = (quiet + my) * scale;
for dy in 0..scale {
for dx in 0..scale {
let x = px0 + dx;
let y = py0 + dy;
if x < paper_dots && y < img_h {
img.put_pixel(x, y, Luma([0u8]));
}
}
}
}
}
}
let draw_glyph = |img: &mut GrayImage, cx: u32, y0: u32, glyph: [u8; 8], offset_x: u32| {
for (row, &byte) in glyph.iter().enumerate() {
for sy in 0..S {
let y = y0 + row as u32 * S + sy;
if y >= img_h { break; }
for bit in 0..8u32 {
if byte & (1u8 << bit) != 0 {
for sx in 0..S {
let x = cx + offset_x + bit * S + sx;
if x < paper_dots {
img.put_pixel(x, y, Luma([0u8]));
}
}
}
}
}
}
};
for (li, (line, bold)) in lines.iter().enumerate() {
let y0 = li as u32 * LINE_H;
let mut cx = text_x;
for ch in line.chars() {
if cx + FONT_W > paper_dots { break; }
let glyph = font8x8::BASIC_FONTS
.get(ch)
.or_else(|| font8x8::LATIN_FONTS.get(ch))
.unwrap_or([0u8; 8]);
draw_glyph(&mut img, cx, y0, glyph, 0);
if *bold {
draw_glyph(&mut img, cx, y0, glyph, 1);
}
cx += FONT_W;
}
}
let raster = rasterize(&DynamicImage::ImageLuma8(img), self.paper_width);
self.buffer.extend_from_slice(&raster);
self
}
}
impl Default for EscPosBuilder {
fn default() -> Self {
Self::new()
}
}
fn encode_cp850(s: &str) -> Vec<u8> {
s.chars().map(|c| {
if c.is_ascii() { return c as u8; }
match c {
'Ç' => 0x80, 'ü' => 0x81, 'é' => 0x82, 'â' => 0x83,
'ä' => 0x84, 'à' => 0x85, 'å' => 0x86, 'ç' => 0x87,
'ê' => 0x88, 'ë' => 0x89, 'è' => 0x8A, 'ï' => 0x8B,
'î' => 0x8C, 'ì' => 0x8D, 'Ä' => 0x8E, 'Å' => 0x8F,
'É' => 0x90, 'æ' => 0x91, 'Æ' => 0x92, 'ô' => 0x93,
'ö' => 0x94, 'ò' => 0x95, 'û' => 0x96, 'ù' => 0x97,
'ÿ' => 0x98, 'Ö' => 0x99, 'Ü' => 0x9A, 'ø' => 0x9B,
'£' => 0x9C, 'Ø' => 0x9D, 'á' => 0xA0, 'í' => 0xA1,
'ó' => 0xA2, 'ú' => 0xA3, 'ñ' => 0xA4, 'Ñ' => 0xA5,
'ª' => 0xA6, 'º' => 0xA7, '¿' => 0xA8, '®' => 0xA9,
'½' => 0xAB, '¼' => 0xAC, '«' => 0xAE, '»' => 0xAF,
'Á' => 0xB5, 'Â' => 0xB6, 'À' => 0xB7, '©' => 0xB8,
'¢' => 0xBD, 'ã' => 0xC6, 'Ã' => 0xC7, 'ð' => 0xD0,
'Ð' => 0xD1, 'Ê' => 0xD2, 'Ë' => 0xD3, 'È' => 0xD4,
'Í' => 0xD6, 'Î' => 0xD7, 'Ï' => 0xD8, 'Ì' => 0xDE,
'Ó' => 0xE0, 'ß' => 0xE1, 'Ô' => 0xE2, 'Ò' => 0xE3,
'õ' => 0xE4, 'Õ' => 0xE5, 'µ' => 0xE6, 'Ú' => 0xE9,
'Û' => 0xEA, 'Ù' => 0xEB, 'ý' => 0xEC, 'Ý' => 0xED,
'°' => 0xF8, '±' => 0xF1, '¶' => 0xF4, '§' => 0xF5,
'÷' => 0xF6, '¸' => 0xF7, '¨' => 0xF9, '²' => 0xFD,
'³' => 0xFC, '¹' => 0xFB, '·' => 0xFA,
_ => b'?',
}
}).collect()
}
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(4).position(|w| w == [0x1D, 0x76, 0x30, 0x00]).unwrap();
assert!(bytes.len() > pos + 8);
}
}