diff --git a/Cargo.lock b/Cargo.lock index 683e594..4159cfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1174,9 +1174,11 @@ dependencies = [ "axum", "clap", "escpos", + "image", "palette", "serde", "showbits-common", + "taffy", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index fa02f40..5bee944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,11 @@ image = "0.24.9" palette = "0.7.5" showbits-common.path = "./showbits-common" +[workspace.dependencies.taffy] +version = "0.4.0" +default-features = false +features = ["std", "taffy_tree", "flexbox", "grid", "block_layout"] + [workspace.lints] rust.unsafe_code = "forbid" rust.future_incompatible = "warn" diff --git a/showbits-common/Cargo.toml b/showbits-common/Cargo.toml index 76d3752..f5d52f2 100644 --- a/showbits-common/Cargo.toml +++ b/showbits-common/Cargo.toml @@ -8,11 +8,7 @@ anyhow.workspace = true cosmic-text = "0.11.2" image.workspace = true palette.workspace = true - -[dependencies.taffy] -version = "0.4.0" -default-features = false -features = ["std", "taffy_tree", "flexbox", "grid", "block_layout"] +taffy.workspace = true [lints] workspace = true diff --git a/showbits-thermal-printer/Cargo.toml b/showbits-thermal-printer/Cargo.toml index 0523a19..ae09720 100644 --- a/showbits-thermal-printer/Cargo.toml +++ b/showbits-thermal-printer/Cargo.toml @@ -8,9 +8,11 @@ anyhow.workspace = true axum = "0.7.4" clap = { version = "4.5.1", features = ["derive", "deprecated"] } escpos = { version = "0.7.2", features = ["full"] } +image.workspace = true palette.workspace = true serde = { version = "1.0.197", features = ["derive"] } showbits-common.workspace = true +taffy.workspace = true tokio = { version = "1.36.0", features = ["full"] } [lints] diff --git a/showbits-thermal-printer/src/drawer.rs b/showbits-thermal-printer/src/drawer.rs index bc9ece8..4dc5a82 100644 --- a/showbits-thermal-printer/src/drawer.rs +++ b/showbits-thermal-printer/src/drawer.rs @@ -42,31 +42,6 @@ impl Drawer { Ok(()) } - // fn on_test(&mut self) -> anyhow::Result<()> { - // self.printer.init()?; - - // let x = 48; // bytes - // let y = 48; // dots - - // let m = 0; - // let x_l = x as u8; - // let x_h = (x >> 8) as u8; - // let y_l = y as u8; - // let y_h = (y >> 8) as u8; - // let mut command = vec![0x1D, b'v', b'0', m, x_l, x_h, y_l, y_h]; - // for _y in 0..y { - // for _x in 0..x { - // // command.push((x + y) as u8); - // command.push(0b0000_0011); - // } - // } - // self.printer.custom(&command)?; - - // self.printer.print()?; - - // Ok(()) - // } - // fn on_rip(&mut self) -> anyhow::Result<()> { // self.printer.init()?.feeds(6)?.print()?; // Ok(()) diff --git a/showbits-thermal-printer/src/printer.rs b/showbits-thermal-printer/src/printer.rs index f6bdf12..b98f832 100644 --- a/showbits-thermal-printer/src/printer.rs +++ b/showbits-thermal-printer/src/printer.rs @@ -1,10 +1,13 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use escpos::{ driver::FileDriver, printer::Printer as EPrinter, - utils::{PageCode, Protocol}, + utils::{PageCode, Protocol, GS}, }; +use image::{Rgb, RgbImage}; +use showbits_common::Tree; +use taffy::{AvailableSpace, NodeId, Size}; pub struct Printer { printer: Option>, @@ -16,9 +19,18 @@ impl Printer { /// code can't be changed. /// /// https://en.wikipedia.org/wiki/Code_page_437 - /// https://www.epson-biz.com/modules/ref_charcode_en/index.php?content_id=10 + /// 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. + // TODO Figure out actual width + const WIDTH: u32 = 8 * 32; + + /// Images are printed in chunks because a single print command can only + /// print so much data. + // TODO Figure out sensible chunk height + const CHUNK_HEIGHT: u32 = 42; + pub fn new( printer_path: Option, export_path: Option, @@ -44,4 +56,101 @@ impl Printer { Ok(()) } + + pub fn print_tree( + &mut self, + tree: &mut Tree, + ctx: &mut C, + root: NodeId, + ) -> anyhow::Result<()> { + let available = Size { + width: AvailableSpace::Definite(Self::WIDTH as f32), + // TODO Maybe MinContent? If not, why not? + height: AvailableSpace::MaxContent, + }; + + let image = tree.render(ctx, root, available)?; + + if let Some(path) = &self.export_path { + image.save(path)?; + } + + if let Some(printer) = &mut self.printer { + Self::print_image_to_printer(printer, &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, + image: &RgbImage, + ) -> 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: &RgbImage, 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: Rgb) -> bool { + let [r, g, b] = pixel.0; + let sum = (r as u32) + (g as u32) + (b as u32); + sum <= 3 * 255 / 2 // true == black + } }