showbits/showbits-thermal-printer/src/printer.rs

151 lines
5.5 KiB
Rust

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<EPrinter<FileDriver>>,
export_path: Option<PathBuf>,
}
impl Printer {
/// Experimentation has determined that the printer uses PC437 and the page
/// code can't be changed.
///
/// <https://en.wikipedia.org/wiki/Code_page_437>
/// <https://download4.epson.biz/sec_pubs/pos/reference_en/charcode/page_00.html>
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<PathBuf>,
export_path: Option<PathBuf>,
) -> anyhow::Result<Self> {
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.
///
/// <https://download4.epson.biz/sec_pubs/pos/reference_en/escpos/gs_lv_0.html>
fn print_image_to_printer(
printer: &mut EPrinter<FileDriver>,
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<u8>) -> bool {
let color = color::image_to_palette(pixel);
let avg = (color.red + color.green + color.blue) / 3.0;
avg < 0.5 // true == black
}
}