use std::path::PathBuf; use anyhow::Context; use escpos::{ driver::FileDriver, printer::Printer as EPrinter, printer_options::PrinterOptions, utils::{GS, PageCode, Protocol}, }; use image::{Rgba, RgbaImage}; use crate::color; pub struct Printer { printer: Option>, export_path: Option, } impl Printer { /// Experimentation has determined that the printer uses PC437 and the page /// code can't be changed. /// /// /// const PAGE_CODE: PageCode = PageCode::PC437; /// Width of the printable area in pixels. /// /// Assumed to be a multiple of 8, then measured to that precision. pub const WIDTH: u32 = 8 * 48; /// Images are printed in chunks because a single print command can only /// print so much data. /// /// Looking at the [epson docs][0], most printers seem to support a max /// height of 2303, though some go up to 4095. Because I don't want to waste /// a bunch of paper trying various different heights, I'll go with 1023 /// because it's nice and round and slightly conservative. /// /// [0]: https://download4.epson.biz/sec_pubs/pos/reference_en/escpos/gs_lv_0.html const CHUNK_HEIGHT: u32 = 0b0000_0011_1111_1111; pub fn new( printer_path: Option, export_path: Option, ) -> anyhow::Result { let printer = if let Some(path) = printer_path { let driver = FileDriver::open(&path) .with_context(|| format!("At {}", path.display())) .context("Failed to open printer driver")?; let protocol = Protocol::default(); let mut options = PrinterOptions::default(); options.page_code(Some(Self::PAGE_CODE)); Some(EPrinter::new(driver, protocol, Some(options))) } else { None }; Ok(Self { printer, export_path, }) } pub fn print_image(&mut self, image: &RgbaImage) -> anyhow::Result<()> { if let Some(path) = &self.export_path { image .save(path) .with_context(|| format!("At {}", path.display())) .context("Failed to export to-be-printed image")?; } if let Some(printer) = &mut self.printer { Self::print_image_to_printer(printer, image).context("Failed to print image")?; } Ok(()) } /// Uses the obsolete `GS v 0` command to print an image. /// /// The image is printed in chunks because the command used has a maximum /// amount of data it can handle. In-between chunks, the paper is not moved, /// meaning that chunks connect to each other seamlessly. /// /// fn print_image_to_printer( printer: &mut EPrinter, image: &RgbaImage, ) -> anyhow::Result<()> { assert_eq!(Self::WIDTH % 8, 0); assert_eq!(image.width(), Self::WIDTH); printer.init()?; for y_offset in (0..image.height()).step_by(Self::CHUNK_HEIGHT as usize) { // The command takes the width in bytes (groups of 8 pixels) and the // height in pixels. Both are then split into two bytes and sent. let chunk_width = Self::WIDTH / 8; let chunk_height = Self::CHUNK_HEIGHT.min(image.height() - y_offset); let m = 0; // Normal resolution let [_, _, x_h, x_l] = chunk_width.to_be_bytes(); let [_, _, y_h, y_l] = chunk_height.to_be_bytes(); let mut command = vec![GS, b'v', b'0', m, x_l, x_h, y_l, y_h]; for y in y_offset..y_offset + chunk_height { for x in (0..Self::WIDTH).step_by(8) { command.push(Self::get_horizontal_byte_starting_at(image, x, y)); } } printer.custom(&command)?; } printer.print()?; Ok(()) } fn get_horizontal_byte_starting_at(image: &RgbaImage, x: u32, y: u32) -> u8 { let p7 = Self::pixel_to_bit(*image.get_pixel(x, y)); let p6 = Self::pixel_to_bit(*image.get_pixel(x + 1, y)); let p5 = Self::pixel_to_bit(*image.get_pixel(x + 2, y)); let p4 = Self::pixel_to_bit(*image.get_pixel(x + 3, y)); let p3 = Self::pixel_to_bit(*image.get_pixel(x + 4, y)); let p2 = Self::pixel_to_bit(*image.get_pixel(x + 5, y)); let p1 = Self::pixel_to_bit(*image.get_pixel(x + 6, y)); let p0 = Self::pixel_to_bit(*image.get_pixel(x + 7, y)); let b7 = if p7 { 0b1000_0000 } else { 0 }; let b6 = if p6 { 0b0100_0000 } else { 0 }; let b5 = if p5 { 0b0010_0000 } else { 0 }; let b4 = if p4 { 0b0001_0000 } else { 0 }; let b3 = if p3 { 0b0000_1000 } else { 0 }; let b2 = if p2 { 0b0000_0100 } else { 0 }; let b1 = if p1 { 0b0000_0010 } else { 0 }; let b0 = if p0 { 0b0000_0001 } else { 0 }; b7 + b6 + b5 + b4 + b3 + b2 + b1 + b0 } /// Convert pixel to bit, `true` is black and `false` is white. /// /// Instead of doing the physically accurate thing, I do what makes the most /// sense visually. fn pixel_to_bit(pixel: Rgba) -> bool { let color = color::image_to_palette(pixel); let avg = (color.red + color.green + color.blue) / 3.0; avg < 0.5 // true == black } }