151 lines
5.5 KiB
Rust
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
|
|
}
|
|
}
|