custom_printer/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    fs::File,
5    io::{self, Write},
6};
7
8// List of supported commands
9const BIT_IMAGE: &[u8] = &[0x1B, 0x2A];
10const TOTAL_CUT: &[u8] = &[0x1B, 0x69];
11const PARTIAL_CUT: &[u8] = &[0x1B, 0x6D];
12
13/// Modes supported by [`CustomPrinter::bit_image()`] function.
14pub enum BitImageMode {
15    /// 8 dot single density
16    Dots8SingleDensity,
17    /// 8 dot double density
18    Dots8DoubleDensity,
19    /// 24 dot single density
20    Dots24SingleDensity,
21    /// 24 dot double density
22    Dots24DoubleDensity,
23}
24
25/// Cut types supported by [`CustomPrinter::cut_paper()`] function.
26pub enum CutType {
27    /// Total cut
28    TotalCut,
29    /// Partial cut, only valid for TL60 and TL80 printers.
30    PartialCut,
31}
32
33/// The main struct to construct printing commands and accomplish actual printing.
34///
35/// The APIs are designed to be able to concatenate one after the other.
36/// # Examples
37///
38/// ```no_run
39/// # use custom_printer::{BitImageMode, CustomPrinter, CutType};
40/// let mut printer = CustomPrinter::new("/dev/usb/lp0").unwrap();
41/// printer
42///     .bit_image(
43///         "logo.bmp",
44///         BitImageMode::Dots24DoubleDensity
45///     )
46///     .unwrap()
47///     .cut_paper(CutType::PartialCut)
48///     .run()
49///     .unwrap()
50///     .bit_image(
51///         "greeting.bmp",
52///         BitImageMode::Dots24DoubleDensity
53///     )
54///     .unwrap()
55///     .cut_paper(CutType::TotalCut)
56///     .run()
57///     .unwrap();
58/// ```
59pub struct CustomPrinter {
60    file: File,
61    cmd: Vec<u8>,
62}
63
64impl CustomPrinter {
65    /// Create a new [`CustomPrinter`] with the device node `dev`.
66    ///
67    /// **NOTE:** Device node `dev` must be readable and writable by current user.
68    ///
69    /// # Examples
70    ///
71    /// ```rust
72    /// # use custom_printer::CustomPrinter;
73    /// CustomPrinter::new("/dev/usb/lp0")
74    /// # ;
75    /// ```
76    pub fn new(dev: &str) -> Result<Self, io::Error> {
77        let file = File::options().read(true).write(true).open(dev)?;
78        Ok(Self {
79            file,
80            cmd: Vec::new(),
81        })
82    }
83
84    pub(crate) fn convert_bitmap_to_bitimage(
85        width: usize,
86        height: usize,
87        bitmap: &[u8],
88        mode: &BitImageMode,
89    ) -> Vec<u8> {
90        // number of lines in a bank
91        let bank = match mode {
92            BitImageMode::Dots8SingleDensity | BitImageMode::Dots8DoubleDensity => 8,
93            BitImageMode::Dots24SingleDensity | BitImageMode::Dots24DoubleDensity => 24,
94        };
95        // number of banks in bit image (might have padding lines in the last bank)
96        let banks = (height + bank - 1) / bank;
97        // number of bytes in bit image
98        let size = banks * (bank / 8) * width;
99        let mut bitimage = vec![0; size];
100        // number of bytes in a line
101        let step = width / 8;
102
103        for i in 0..banks {
104            for j in 0..width {
105                for k in 0..bank {
106                    let src = i * step * bank + k * step + j / 8;
107                    let dst = i * width * (bank / 8) + j * (bank / 8) + k / 8;
108                    if src < bitmap.len() && bitmap[src] & (0x80 >> (j % 8)) != 0 {
109                        bitimage[dst] |= 0x80 >> (k % 8);
110                    }
111                }
112            }
113        }
114
115        bitimage
116    }
117
118    /// Append commands for printing a bit image from `path` in `mode`. See [`BitImageMode`] for supported modes.
119    ///
120    /// **NOTE:** Because opening and reading the image file may fail, so the return Self is wrapped in a [`Result`]
121    /// and needs to be unwrapped before concatenating with other constructing functions.
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// # use custom_printer::{BitImageMode, CustomPrinter};
127    /// # let mut printer = CustomPrinter::new("/dev/null").unwrap();
128    /// printer
129    ///     .bit_image(
130    ///         "tests/data/Thermal_Test_Image.png",
131    ///         BitImageMode::Dots24DoubleDensity
132    ///     )
133    ///     .unwrap();
134    /// ```
135    pub fn bit_image(&mut self, path: &str, mode: BitImageMode) -> Result<&mut Self, io::Error> {
136        // Open image and convert to grayscale
137        let img = image::open(path)
138            .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?
139            .grayscale();
140
141        let width = img.width() as usize;
142        let height = img.height() as usize;
143
144        // convert 8bpp grayscaled image to 1 bpp bitmap
145        let mut bitmap: Vec<u8> = vec![0; img.as_bytes().len() / 8];
146        for (i, byte) in img.as_bytes().iter().enumerate() {
147            // invert the bits
148            if *byte == 0x00 {
149                bitmap[i / 8] |= 0x80 >> (i % 8);
150            }
151        }
152
153        // for (i, byte) in bitmap.iter().enumerate() {
154        //     for j in 0..8 {
155        //         print!("{}", if byte & (0x80 >> j) != 0 { 1 } else { 0 });
156        //     }
157        //     if i % (width / 8) == ((width / 8) - 1) {
158        //         println!();
159        //     }
160        // }
161
162        let bitimage = Self::convert_bitmap_to_bitimage(width, height, &bitmap, &mode);
163
164        let (m, k) = match mode {
165            BitImageMode::Dots8SingleDensity => (0x00, width),
166            BitImageMode::Dots8DoubleDensity => (0x01, width),
167            BitImageMode::Dots24SingleDensity => (0x20, width * 3),
168            BitImageMode::Dots24DoubleDensity => (0x21, width * 3),
169        };
170
171        for i in 0..bitimage.len() / k {
172            self.cmd.extend_from_slice(BIT_IMAGE);
173            self.cmd
174                .extend_from_slice(&[m, (width % 256) as u8, (width / 256) as u8]);
175            self.cmd.extend_from_slice(&bitimage[i * k..(i + 1) * k]);
176            // for j in 0..k {
177            //     print!("{:02x} ", bitimage[i * k + j]);
178            // }
179            // println!();
180        }
181
182        Ok(self)
183    }
184
185    /// Append a command for cutting the paper totally ([`CutType::TotalCut`]) or partially ([`CutType::PartialCut`]).
186    ///
187    /// # Examples
188    ///
189    /// ```rust
190    /// # use custom_printer::{CustomPrinter, CutType};
191    /// # let mut printer = CustomPrinter::new("/dev/null").unwrap();
192    /// printer.cut_paper(CutType::TotalCut);
193    /// ```
194    pub fn cut_paper(&mut self, cut_type: CutType) -> &mut Self {
195        match cut_type {
196            CutType::TotalCut => {
197                self.cmd.extend_from_slice(TOTAL_CUT);
198            }
199            CutType::PartialCut => {
200                self.cmd.extend_from_slice(PARTIAL_CUT);
201            }
202        }
203
204        self
205    }
206
207    /// Run the constructed commands in the [`CustomPrinter`].
208    ///
209    /// The constructed commands will be cleared if the printing succeeds.
210    ///
211    /// **NOTE:** Because writing to the device node may fail, so the return Self is wrapped in a [`Result`]
212    /// and needs to be unwrapped before concatenating with other constructing functions.
213    ///
214    /// # Examples
215    ///
216    /// ```rust
217    /// # use custom_printer::{BitImageMode, CustomPrinter, CutType};
218    /// # let mut printer = CustomPrinter::new("/dev/null").unwrap();
219    /// printer
220    ///     .bit_image(
221    ///         "tests/data/Thermal_Test_Image.png",
222    ///         BitImageMode::Dots24DoubleDensity
223    ///     )
224    ///     .unwrap()
225    ///     .cut_paper(CutType::TotalCut)
226    ///     .run()
227    ///     .unwrap();
228    /// ```
229    pub fn run(&mut self) -> Result<&mut Self, io::Error> {
230        self.file.write_all(&self.cmd)?;
231
232        self.cmd.clear();
233        Ok(self)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    const THERMAL_WIDTH: usize = 384;
242    const THERMAL_HEIGHT: usize = 288;
243    const THERMAL_TXT: &str = include_str!("../tests/data/thermal.txt");
244    const THERMAL_8DOTS: &[u8] = include_bytes!("../tests/data/thermal.b8");
245    const THERMAL_24DOTS: &[u8] = include_bytes!("../tests/data/thermal.b24");
246    const THERMAL_PNG_PATH: &str = "tests/data/Thermal_Test_Image.png";
247    const DEV_NULL: &str = "/dev/null";
248
249    #[test]
250    fn test_cut_paper() {
251        let mut printer = CustomPrinter::new(DEV_NULL).unwrap();
252        assert_eq!(printer.cut_paper(CutType::TotalCut).cmd, TOTAL_CUT);
253
254        let mut printer = CustomPrinter::new(DEV_NULL).unwrap();
255        assert_eq!(printer.cut_paper(CutType::PartialCut).cmd, PARTIAL_CUT);
256    }
257
258    #[test]
259    #[ignore]
260    fn helper_prepare_bitimage() {
261        let converter = |text: &str, inverted: bool, output: &mut File, bank: usize| {
262            let lines: Vec<&str> = text.trim().split('\n').collect();
263            let width = lines[0].len();
264            let banks = (lines.len() + (bank - 1)) / bank;
265
266            for i in 0..banks {
267                for j in 0..width {
268                    let mut byte: u8 = 0;
269                    for k in 0..bank {
270                        let line_no = i * bank + k;
271                        // padding lines are always 0
272                        if line_no < lines.len() {
273                            let b = lines[line_no].chars().nth(j).unwrap();
274                            if inverted {
275                                if b == '0' {
276                                    byte |= 0x80 >> (k % 8);
277                                }
278                            } else {
279                                if b == '1' {
280                                    byte |= 0x80 >> (k % 8);
281                                }
282                            }
283                        }
284                        if k % 8 == 7 {
285                            output.write(&[byte]).ok();
286                            byte = 0;
287                        }
288                    }
289                }
290            }
291        };
292
293        let mut output = File::options()
294            .create(true)
295            .write(true)
296            .truncate(true)
297            .open("tests/data/thermal.b8")
298            .unwrap();
299        converter(THERMAL_TXT, true, &mut output, 8);
300
301        let mut output = File::options()
302            .create(true)
303            .write(true)
304            .truncate(true)
305            .open("tests/data/thermal.b24")
306            .unwrap();
307        converter(THERMAL_TXT, true, &mut output, 24);
308    }
309
310    fn convert_text_to_bitmap(text: &str, inverted: bool) -> Vec<u8> {
311        let mut data = Vec::new();
312
313        text.trim().split('\n').for_each(|line| {
314            if line.len() % 8 != 0 {
315                eprintln!("Length of each line of text must be dividable by 8");
316                return;
317            }
318            for i in (0..line.len()).step_by(8) {
319                if let Ok(byte) = u8::from_str_radix(&line[i..i + 8], 2) {
320                    data.extend_from_slice(&[if inverted { !byte } else { byte }]);
321                } else {
322                    eprintln!("text contains characters neither 0 nor 1");
323                    return;
324                }
325            }
326        });
327
328        data
329    }
330
331    #[test]
332    fn test_convert_bitmap_to_bitimage_8dots() {
333        let bitmap = convert_text_to_bitmap(THERMAL_TXT, true);
334        assert_eq!(
335            &CustomPrinter::convert_bitmap_to_bitimage(
336                THERMAL_WIDTH,
337                THERMAL_HEIGHT,
338                &bitmap,
339                &BitImageMode::Dots8SingleDensity
340            ),
341            THERMAL_8DOTS
342        );
343        assert_eq!(
344            &CustomPrinter::convert_bitmap_to_bitimage(
345                THERMAL_WIDTH,
346                THERMAL_HEIGHT,
347                &bitmap,
348                &BitImageMode::Dots8DoubleDensity
349            ),
350            THERMAL_8DOTS
351        );
352    }
353
354    #[test]
355    fn test_convert_bitmap_to_bitimage_24dots() {
356        let bitmap = convert_text_to_bitmap(THERMAL_TXT, true);
357        assert_eq!(
358            &CustomPrinter::convert_bitmap_to_bitimage(
359                THERMAL_WIDTH,
360                THERMAL_HEIGHT,
361                &bitmap,
362                &BitImageMode::Dots24SingleDensity
363            ),
364            THERMAL_24DOTS
365        );
366        assert_eq!(
367            &CustomPrinter::convert_bitmap_to_bitimage(
368                THERMAL_WIDTH,
369                THERMAL_HEIGHT,
370                &bitmap,
371                &BitImageMode::Dots24DoubleDensity
372            ),
373            THERMAL_24DOTS
374        );
375    }
376
377    #[test]
378    fn test_bit_image() {
379        let mut printer = CustomPrinter::new(DEV_NULL).unwrap();
380        printer
381            .bit_image(THERMAL_PNG_PATH, BitImageMode::Dots8SingleDensity)
382            .unwrap();
383
384        let mut printer = CustomPrinter::new(DEV_NULL).unwrap();
385        printer
386            .bit_image(THERMAL_PNG_PATH, BitImageMode::Dots8DoubleDensity)
387            .unwrap();
388
389        let mut printer = CustomPrinter::new(DEV_NULL).unwrap();
390        printer
391            .bit_image(THERMAL_PNG_PATH, BitImageMode::Dots24SingleDensity)
392            .unwrap();
393
394        let mut printer = CustomPrinter::new(DEV_NULL).unwrap();
395        printer
396            .bit_image(THERMAL_PNG_PATH, BitImageMode::Dots24DoubleDensity)
397            .unwrap();
398    }
399
400    #[test]
401    fn test_multiple_run() {}
402}