Skip to main content

smart_package_tracker/render/
png.rs

1//! PNG rendering.
2//!
3//! Output is 8-bit RGBA with a `pHYs` chunk describing the physical
4//! resolution, so a 300 DPI label prints at its intended physical size rather
5//! than at whatever the consuming application assumes.
6
7use alloc::vec;
8use alloc::vec::Vec;
9
10use super::{hri, Layout, RenderOptions, Renderer};
11use crate::error::{Error, Result};
12use crate::symbology::Symbol;
13
14/// Inches per metre, for the `pHYs` chunk.
15const INCHES_PER_METRE: f64 = 39.370_078_740_157_48;
16
17/// The PNG renderer.
18///
19/// # Examples
20///
21/// ```
22/// use smart_package_tracker::{RenderOptions, symbology::{Code128, Symbology}, render::{Png, Renderer}};
23///
24/// let symbol = Code128.encode("PKG-9ED9285C")?;
25/// let bytes = Png.render(&symbol, &RenderOptions::default())?;
26/// assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n");
27/// # Ok::<(), smart_package_tracker::Error>(())
28/// ```
29#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
30pub struct Png;
31
32impl Renderer for Png {
33    type Output = Vec<u8>;
34
35    fn render(&self, symbol: &Symbol, options: &RenderOptions) -> Result<Vec<u8>> {
36        let layout = options.layout(symbol)?;
37        let pixels = rasterize(symbol, options, &layout);
38        encode(&pixels, &layout, options)
39    }
40}
41
42/// Paint the symbol and its HRI line into an RGBA buffer.
43fn rasterize(symbol: &Symbol, options: &RenderOptions, layout: &Layout) -> Vec<u8> {
44    let width = layout.width_px as usize;
45    let height = layout.height_px as usize;
46    let fg = options.foreground();
47    let bg = options.background();
48
49    let mut buf = vec![0u8; width * height * 4];
50    for px in buf.chunks_exact_mut(4) {
51        px.copy_from_slice(&[bg.r, bg.g, bg.b, bg.a]);
52    }
53
54    let put = |x: u32, y: u32, buf: &mut Vec<u8>| {
55        if x >= layout.width_px || y >= layout.height_px {
56            return;
57        }
58        let i = ((y as usize) * width + (x as usize)) * 4;
59        buf[i..i + 4].copy_from_slice(&[fg.r, fg.g, fg.b, fg.a]);
60    };
61
62    // Modules.
63    let modules = symbol.modules();
64    for my in 0..modules.height() {
65        // A linear symbol has one module row stretched over the full bar
66        // height; a matrix symbol has one module row per grid row.
67        let (y0, y1) = if symbol.is_linear() {
68            (layout.symbol_y_px, layout.symbol_y_px + layout.symbol_h_px)
69        } else {
70            let top = layout.symbol_y_px + my * layout.module_px;
71            (top, top + layout.module_px)
72        };
73
74        for mx in 0..modules.width() {
75            if !modules.get(mx, my) {
76                continue;
77            }
78            let x0 = layout.symbol_x_px + mx * layout.module_px;
79            for y in y0..y1 {
80                for x in x0..x0 + layout.module_px {
81                    put(x, y, &mut buf);
82                }
83            }
84        }
85    }
86
87    // Human-readable text.
88    if layout.hri_scale > 0 {
89        let scale = layout.hri_scale;
90        for (index, ch) in symbol.payload().chars().enumerate() {
91            let glyph = hri::glyph(ch);
92            let origin_x = layout.hri_x_px + (index as u32) * hri::ADVANCE * scale;
93            for gy in 0..hri::GLYPH_H {
94                for gx in 0..hri::GLYPH_W {
95                    if !hri::pixel(glyph, gx, gy) {
96                        continue;
97                    }
98                    let x0 = origin_x + gx * scale;
99                    let y0 = layout.hri_y_px + gy * scale;
100                    for y in y0..y0 + scale {
101                        for x in x0..x0 + scale {
102                            put(x, y, &mut buf);
103                        }
104                    }
105                }
106            }
107        }
108    }
109
110    buf
111}
112
113/// Wrap the RGBA buffer in a PNG container.
114fn encode(pixels: &[u8], layout: &Layout, options: &RenderOptions) -> Result<Vec<u8>> {
115    let mut out = Vec::new();
116    {
117        let mut encoder = ::png::Encoder::new(&mut out, layout.width_px, layout.height_px);
118        encoder.set_color(::png::ColorType::Rgba);
119        encoder.set_depth(::png::BitDepth::Eight);
120
121        let ppu = (f64::from(options.dpi()) * INCHES_PER_METRE).round() as u32;
122        encoder.set_pixel_dims(Some(::png::PixelDimensions {
123            xppu: ppu,
124            yppu: ppu,
125            unit: ::png::Unit::Meter,
126        }));
127
128        let mut writer = encoder
129            .write_header()
130            .map_err(|e| Error::Render(alloc::format!("png header: {e}")))?;
131        writer
132            .write_image_data(pixels)
133            .map_err(|e| Error::Render(alloc::format!("png image data: {e}")))?;
134        writer
135            .finish()
136            .map_err(|e| Error::Render(alloc::format!("png finish: {e}")))?;
137    }
138    Ok(out)
139}
140
141#[cfg(all(test, feature = "code128"))]
142mod tests {
143    use super::*;
144    use crate::render::{Color, Length, QuietZone};
145    use crate::symbology::{Code128, Symbology};
146
147    const PNG_MAGIC: &[u8] = b"\x89PNG\r\n\x1a\n";
148
149    fn symbol() -> Symbol {
150        Code128.encode("PKG-9ED9285C").unwrap()
151    }
152
153    /// Decode our own output so the assertions are about pixels, not bytes.
154    fn decode(bytes: &[u8]) -> (u32, u32, Vec<u8>) {
155        let decoder = ::png::Decoder::new(std::io::Cursor::new(bytes));
156        let mut reader = decoder.read_info().expect("valid png");
157        let mut buf = vec![0; reader.output_buffer_size().unwrap()];
158        let info = reader.next_frame(&mut buf).expect("valid frame");
159        buf.truncate(info.buffer_size());
160        (info.width, info.height, buf)
161    }
162
163    fn pixel_at(w: u32, buf: &[u8], x: u32, y: u32) -> [u8; 4] {
164        let i = ((y as usize) * (w as usize) + (x as usize)) * 4;
165        [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
166    }
167
168    #[test]
169    fn produces_a_valid_png_of_the_expected_size() {
170        let s = symbol();
171        let opts = RenderOptions::default();
172        let layout = opts.layout(&s).unwrap();
173        let bytes = Png.render(&s, &opts).unwrap();
174
175        assert_eq!(&bytes[..8], PNG_MAGIC);
176        let (w, h, _) = decode(&bytes);
177        assert_eq!((w, h), (layout.width_px, layout.height_px));
178    }
179
180    #[test]
181    fn quiet_zone_is_actually_blank() {
182        let s = symbol();
183        let opts = RenderOptions::default();
184        let layout = opts.layout(&s).unwrap();
185        let (w, _, buf) = decode(&Png.render(&s, &opts).unwrap());
186
187        let white = [255, 255, 255, 255];
188        for x in 0..layout.quiet_x_px {
189            assert_eq!(pixel_at(w, &buf, x, 0), white, "left quiet zone not blank");
190            let right = layout.width_px - 1 - x;
191            assert_eq!(
192                pixel_at(w, &buf, right, 0),
193                white,
194                "right quiet zone not blank"
195            );
196        }
197    }
198
199    #[test]
200    fn first_and_last_module_are_dark() {
201        // Code 128 starts and ends with a bar; those must land exactly at the
202        // inner edges of the quiet zone.
203        let s = symbol();
204        let opts = RenderOptions::default();
205        let layout = opts.layout(&s).unwrap();
206        let (w, _, buf) = decode(&Png.render(&s, &opts).unwrap());
207
208        let black = [0, 0, 0, 255];
209        assert_eq!(pixel_at(w, &buf, layout.symbol_x_px, 0), black);
210        let last = layout.symbol_x_px + layout.symbol_w_px - 1;
211        assert_eq!(pixel_at(w, &buf, last, 0), black);
212    }
213
214    #[test]
215    fn every_module_column_is_uniform() {
216        // If modules did not snap to whole pixels, columns would show partial
217        // coverage. Compare each rendered column against the module grid.
218        let s = symbol();
219        let opts = RenderOptions::builder()
220            .human_readable(false)
221            .build()
222            .unwrap();
223        let layout = opts.layout(&s).unwrap();
224        let (w, _, buf) = decode(&Png.render(&s, &opts).unwrap());
225
226        for mx in 0..s.modules().width() {
227            let expected = if s.modules().get(mx, 0) {
228                [0, 0, 0, 255]
229            } else {
230                [255, 255, 255, 255]
231            };
232            for i in 0..layout.module_px {
233                let x = layout.symbol_x_px + mx * layout.module_px + i;
234                assert_eq!(
235                    pixel_at(w, &buf, x, 0),
236                    expected,
237                    "column {x} (module {mx})"
238                );
239            }
240        }
241    }
242
243    #[test]
244    fn custom_colors_are_applied() {
245        let s = symbol();
246        let opts = RenderOptions::builder()
247            .colors(Color::rgb(10, 20, 30), Color::rgb(200, 210, 220))
248            .build()
249            .unwrap();
250        let layout = opts.layout(&s).unwrap();
251        let (w, _, buf) = decode(&Png.render(&s, &opts).unwrap());
252
253        assert_eq!(pixel_at(w, &buf, 0, 0), [200, 210, 220, 255]);
254        assert_eq!(pixel_at(w, &buf, layout.symbol_x_px, 0), [10, 20, 30, 255]);
255    }
256
257    #[test]
258    fn transparent_background_is_preserved() {
259        let s = symbol();
260        let opts = RenderOptions::builder()
261            .colors(Color::BLACK, Color::TRANSPARENT)
262            .build()
263            .unwrap();
264        let (w, _, buf) = decode(&Png.render(&s, &opts).unwrap());
265        assert_eq!(
266            pixel_at(w, &buf, 0, 0)[3],
267            0,
268            "background should be transparent"
269        );
270    }
271
272    #[test]
273    fn physical_resolution_is_recorded() {
274        let s = symbol();
275        let opts = RenderOptions::builder().dpi(300).build().unwrap();
276        let bytes = Png.render(&s, &opts).unwrap();
277
278        let decoder = ::png::Decoder::new(std::io::Cursor::new(&bytes[..]));
279        let reader = decoder.read_info().unwrap();
280        let dims = reader
281            .info()
282            .pixel_dims
283            .expect("pHYs chunk should be present");
284        assert!(matches!(dims.unit, ::png::Unit::Meter));
285        // 300 dpi is 11811 pixels per metre.
286        assert_eq!(dims.xppu, 11811);
287        assert_eq!(dims.yppu, dims.xppu);
288    }
289
290    #[test]
291    fn output_is_byte_for_byte_reproducible() {
292        let s = symbol();
293        let opts = RenderOptions::default();
294        assert_eq!(
295            Png.render(&s, &opts).unwrap(),
296            Png.render(&s, &opts).unwrap()
297        );
298    }
299
300    #[test]
301    fn hri_draws_dark_pixels_below_the_bars() {
302        let s = symbol();
303        let opts = RenderOptions::default();
304        let layout = opts.layout(&s).unwrap();
305        let (w, _, buf) = decode(&Png.render(&s, &opts).unwrap());
306
307        let dark = (layout.hri_y_px..layout.height_px)
308            .flat_map(|y| (0..layout.width_px).map(move |x| (x, y)))
309            .filter(|(x, y)| pixel_at(w, &buf, *x, *y) == [0, 0, 0, 255])
310            .count();
311        assert!(dark > 0, "no HRI pixels were drawn");
312    }
313
314    #[test]
315    fn no_quiet_zone_yields_a_symbol_width_image() {
316        let s = symbol();
317        let opts = RenderOptions::builder()
318            .quiet_zone(QuietZone::None)
319            .human_readable(false)
320            .build()
321            .unwrap();
322        let (w, h, _) = decode(&Png.render(&s, &opts).unwrap());
323        let layout = opts.layout(&s).unwrap();
324        assert_eq!(w, layout.symbol_w_px);
325        assert_eq!(h, layout.symbol_h_px);
326    }
327
328    #[test]
329    fn a_one_pixel_module_still_renders() {
330        let s = symbol();
331        let opts = RenderOptions::builder()
332            .module_width(Length::Px(1.0))
333            .height(Length::Px(20.0))
334            .build()
335            .unwrap();
336        let (w, _, _) = decode(&Png.render(&s, &opts).unwrap());
337        assert_eq!(w, s.modules().width() + 20);
338    }
339}