escpos_rust/instruction/
escpos_image.rs

1extern crate serde;
2extern crate base64;
3extern crate image;
4extern crate log;
5
6use log::warn;
7use super::{Justification};
8use crate::{Error, command::{Command}};
9use image::{DynamicImage, GenericImageView, Pixel};
10use serde::{Serialize, Deserialize, ser::Serializer, de::Deserializer};
11
12use std::collections::{HashMap, HashSet};
13use serde::ser::SerializeTuple;
14
15/// Image adapted to the printer.
16///
17/// The EscposImage structure keeps the original image, and contains a cache for constructed images for specific printer widths
18#[derive(Clone, Debug)]
19pub struct EscposImage {
20    source: String,
21    /// Source image, usefull for scaling
22    dynamic_image: DynamicImage,
23    ///
24    cached_widths: HashSet<u16>,
25    /// Cache that holds the picture scaled for specific widths
26    pub(crate) cache: HashMap<u16, Vec<u8>>
27}
28
29impl EscposImage {
30    /// Pub fn creates a new EscposImage from a [DynamicImage](https://docs.rs/image/0.23.14/image/enum.DynamicImage.html)
31    ///
32    /// The scale parameters goes from 0 to 255, controlling which percentage of the width should the image hold. The justification allows for a bit more specific image alignment.
33    pub fn new(mut dynamic_image: DynamicImage, scale: u8, justification: Justification) -> Result<EscposImage, Error> {
34        // We extract geometrical data.
35        let (im_width, im_height) = dynamic_image.dimensions();
36        let aspect_ratio = (im_width as f64)/(im_height as f64);
37
38        // Notice that the width will stay untouched on these steps
39
40        // We compute the scaled width and height, multiplying height by the ratio
41        let sc_width = (im_width as f64) * (scale as f64)/255.0;
42        // With the aspect ratio, we determine the hight.
43        let sc_height = ((sc_width)/aspect_ratio).floor() as u32;
44        // We force floor the width, and also cast it as a u32
45        let sc_width = sc_width.floor() as u32;
46
47        // We create the new image width
48        let mut back = DynamicImage::new_rgba8(im_width, sc_height);
49
50        // We compute the offset for the inner rendering
51        let x_offset = match justification {
52            Justification::Left => 0,
53            Justification::Center => (im_width - sc_width)/2,
54            Justification::Right => im_width - sc_width
55        };
56
57        // We overlay it in the back image
58        image::imageops::overlay(
59            &mut back,
60            &image::imageops::resize(&dynamic_image, sc_width, sc_height, image::imageops::FilterType::Nearest),
61            x_offset, 0 // x and y from the corner
62        );
63
64        // We have to create a new cropped image
65        dynamic_image = DynamicImage::ImageRgba8(image::imageops::crop(&mut back, 0, 0, im_width, sc_height).to_image());
66
67        let mut encoded = Vec::new();
68        // Weird clippy suggestion, the variant acts as a function in the map_err method...
69        dynamic_image.write_to(&mut encoded, image::ImageFormat::Png).map_err(Error::ImageError)?;
70
71        let source = base64::encode(&encoded);
72        
73        Ok(EscposImage {
74            source,
75            dynamic_image,
76            cached_widths: HashSet::new(),
77            cache: HashMap::new()
78        })
79    }
80
81    fn build_scaled(&self, printer_width: u16) -> Vec<u8> {
82        let mut feed = Vec::new();
83        feed.extend_from_slice(&Command::NoLine.as_bytes());
84        
85        let (im_width, im_height) = self.dynamic_image.dimensions();
86        // We redefine the aspect ratio
87        let aspect_ratio = (im_width as f64)/(im_height as f64);
88        
89        // Each row will contain the information of 8 rows from the picture
90        //const printer_width: usize = 384;
91        //const printer_width: usize = 576;
92        //let mut printer_rows: Vec<[u8; printer_width]> = Vec::new();
93        let mut printer_rows: Vec<Vec<u8>> = Vec::new();
94
95        // El *3 es por la baja densidad de impresión vertical (1 byte en lugar de 3)
96        let new_height = ((printer_width as f64)/(aspect_ratio*3.0)).floor() as u32;
97        
98        let b = image::imageops::resize(&self.dynamic_image, printer_width as u32, new_height, image::imageops::FilterType::Nearest);
99
100        // We will turn the image into a grayscale boolean matrix
101        for (y, pixel_row) in b.enumerate_rows() {
102            // Here we iterate over each row of the image.
103            if y%8 == 0 {
104                printer_rows.push(vec![0; printer_width as usize]);
105            }
106            let row = printer_rows.get_mut((y/8) as usize).unwrap();
107            // Here, we iterate horizontally this time
108            for (x, y, pixel) in pixel_row {
109                let ps = pixel.channels();
110                // We get the color as a boolean
111                let mut color = if ps.len() == 3 || ps[3] > 64 {
112                    let grayscale = 0.2126*(ps[0] as f64) + 0.7152*(ps[1] as f64) + 0.0722*(ps[2] as f64);
113                    if grayscale < 78.0 {
114                        0x01
115                    } else {
116                        0x00
117                    }
118                } else {
119                    // It is transparent, so no color
120                    0x00
121                };
122                // We shift the boolean by 7 - y%8 positions in the register
123                color <<= 7 - y%8;
124                // An or operation preserves the previous pixels in the rows
125                row[x as usize] |= color;
126            }
127        }
128
129        // Finally, we push each row to the feed vector
130        for (_idx, printer_row) in printer_rows.iter().enumerate() {
131            // We first, declare a bitmap mode
132            feed.extend_from_slice(&Command::Bitmap.as_bytes());
133            // Now, we pass m
134            let m = 0x01;
135            feed.push(m);
136            // The formula on how many pixels we will do, is nL + nH * 256
137            feed.push((printer_width % 256) as u8); // nL
138            feed.push((printer_width / 256) as u8); // nH
139            // feed.push(0x80); // nL
140            // feed.push(0x01); // nH
141            feed.extend_from_slice(printer_row);
142            feed.push(b'\n'); // Line feed and print
143        }
144        feed.extend_from_slice(&Command::ResetLine.as_bytes());
145        feed.extend_from_slice(&Command::Reset.as_bytes());
146
147        feed
148    }
149
150    /// Creates a cached image for the specified width
151    ///
152    /// Useful method to decrease the number of operations done per printing, by skipping the scaling step for a specific printer.
153    pub fn cache_for(&mut self, width: u16) {
154        self.cache.insert(width, self.build_scaled(width));
155        self.cached_widths.insert(width);
156    }
157
158    pub fn feed(&self, width: u16) -> Vec<u8> {
159        if let Some(feed) = self.cache.get(&width) {
160            feed.clone()
161        } else {
162            // We have to create the picture... might be costly
163            warn!("Building an image on the fly in non-mutable mode. Consider caching the width.");
164            self.build_scaled(width)
165        }
166    }
167}
168
169// Manual implementation of serialization
170impl Serialize for EscposImage {
171    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172    where S: Serializer {
173        let mut tup = serializer.serialize_tuple(2)?;
174        tup.serialize_element(&self.source)?;
175        tup.serialize_element(&self.cached_widths)?;
176        tup.end()
177    }
178}
179
180struct EscposImageVisitor;
181
182impl<'de> serde::de::Visitor<'de> for EscposImageVisitor {
183    type Value = EscposImage;
184
185    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
186        formatter.write_str("a tuple containing as first element a base64 encoded image, as second a list of cached widths")
187    }
188
189    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where A: serde::de::SeqAccess<'de> {
190        let value: Option<&[u8]> = seq.next_element()?;
191        let value = value.ok_or_else(|| serde::de::Error::custom("first element of tuple missing"))?;
192        let content = match base64::decode(value) {
193            Ok(v) => v,
194            Err(_) => return Err(serde::de::Error::custom("string is not a valid base64 sequence"))
195        };
196        let dynamic_image = image::load_from_memory(&content).map_err(|_| serde::de::Error::custom("first element of tuple not an image"))?;
197        // We will serialize it already
198        let mut escpos_image = EscposImage::new(dynamic_image, 255, Justification::Left).map_err(|e| serde::de::Error::custom(format!("failed to create the image, {}", e)))?;
199        let cached_widths: HashSet<u16> = seq.next_element()?.ok_or_else(|| serde::de::Error::custom("second element of tuple missing"))?;
200
201        for width in cached_widths {
202            escpos_image.cache_for(width);
203        }
204
205        Ok(escpos_image)
206    }
207}
208
209// Manual implementation of deserialization
210impl<'de> Deserialize<'de> for EscposImage {
211    fn deserialize<D>(deserializer: D) -> Result<EscposImage, D::Error>
212    where D: Deserializer<'de> {
213        deserializer.deserialize_seq(EscposImageVisitor)
214    }
215}