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