Skip to main content

zpl_forge/forge/
png.rs

1//! PNG rendering backend for ZPL label output.
2//!
3//! This module provides [`PngBackend`], which rasterizes ZPL commands into
4//! RGB PNG images using the `image` and `imageproc` crates.
5
6use std::cmp::max;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use ab_glyph::{Font, ScaleFont};
11use base64::{Engine as _, engine::general_purpose};
12use image::{
13    ImageBuffer, Rgb, RgbImage, Rgba, RgbaImage,
14    imageops::{overlay, rotate90, rotate180, rotate270},
15};
16use imageproc::drawing::{
17    draw_filled_circle_mut, draw_filled_ellipse_mut, draw_filled_rect_mut, draw_polygon_mut,
18    draw_text_mut,
19};
20use imageproc::point::Point;
21use imageproc::rect::Rect;
22use rxing::common::BitMatrix;
23use rxing::{BarcodeFormat, EncodeHintType, EncodeHintValue, EncodeHints};
24
25use super::{barcode_1d_format, barcode_cache};
26use crate::engine::{Barcode1DKind, FontManager, ZplForgeBackend};
27use crate::{ZplError, ZplResult};
28
29/// A rendering backend that produces PNG images.
30///
31/// This backend uses the `image` and `imageproc` crates to draw ZPL instructions
32/// onto an RGB canvas.
33pub struct PngBackend {
34    canvas: RgbImage,
35    font_manager: Option<Arc<FontManager>>,
36}
37
38impl Default for PngBackend {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl PngBackend {
45    /// Creates a new `PngBackend` instance with an empty canvas.
46    pub fn new() -> Self {
47        Self {
48            canvas: ImageBuffer::new(0, 0),
49            font_manager: None,
50        }
51    }
52
53    /// Performs an XOR overlay of a source image onto the canvas at (x, y).
54    fn xor_overlay(&mut self, src: &RgbImage, x: i64, y: i64) {
55        let (sw, sh) = src.dimensions();
56        let (cw, ch) = self.canvas.dimensions();
57
58        for sy in 0..sh {
59            let dy = y + sy as i64;
60            if dy < 0 || dy >= ch as i64 {
61                continue;
62            }
63
64            for sx in 0..sw {
65                let dx = x + sx as i64;
66                if dx < 0 || dx >= cw as i64 {
67                    continue;
68                }
69
70                let src_pixel = src[(sx, sy)];
71                if src_pixel.0 != [255, 255, 255] {
72                    let dest_pixel = &mut self.canvas[(dx as u32, dy as u32)];
73                    dest_pixel.0[0] ^= 255;
74                    dest_pixel.0[1] ^= 255;
75                    dest_pixel.0[2] ^= 255;
76                }
77            }
78        }
79    }
80
81    /// Inverts the colors within a specified rectangular area.
82    fn invert_rect(&mut self, rect: Rect) {
83        let (cw, ch) = self.canvas.dimensions();
84        let x_start = rect.left().max(0) as u32;
85        let y_start = rect.top().max(0) as u32;
86        let x_end = (rect.right() as u32).min(cw);
87        let y_end = (rect.bottom() as u32).min(ch);
88
89        for py in y_start..y_end {
90            for px in x_start..x_end {
91                let pixel = &mut self.canvas[(px, py)];
92                pixel.0[0] ^= 255;
93                pixel.0[1] ^= 255;
94                pixel.0[2] ^= 255;
95            }
96        }
97    }
98
99    /// Helper to execute a drawing operation.
100    fn draw_wrapper<F>(
101        &mut self,
102        x: u32,
103        y: u32,
104        width: u32,
105        height: u32,
106        reverse_print: bool,
107        draw_op: F,
108    ) -> ZplResult<()>
109    where
110        F: FnOnce(&mut RgbImage, i32, i32),
111    {
112        if reverse_print {
113            let mut temp_buf = ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255]));
114            draw_op(&mut temp_buf, 0, 0);
115            self.xor_overlay(&temp_buf, x as i64, y as i64);
116        } else {
117            draw_op(&mut self.canvas, x as i32, y as i32);
118        }
119        Ok(())
120    }
121
122    fn parse_hex_color(&self, color: &Option<String>) -> Rgb<u8> {
123        if let Some(hex) = color {
124            let hex = hex.trim_start_matches('#');
125            if hex.len() == 6 {
126                if let (Ok(r), Ok(g), Ok(b)) = (
127                    u8::from_str_radix(&hex[0..2], 16),
128                    u8::from_str_radix(&hex[2..4], 16),
129                    u8::from_str_radix(&hex[4..6], 16),
130                ) {
131                    return Rgb([r, g, b]);
132                }
133            } else if hex.len() == 3
134                && let (Ok(r), Ok(g), Ok(b)) = (
135                    u8::from_str_radix(&hex[0..1], 16),
136                    u8::from_str_radix(&hex[1..2], 16),
137                    u8::from_str_radix(&hex[2..3], 16),
138                )
139            {
140                return Rgb([r * 17, g * 17, b * 17]);
141            }
142        }
143        Rgb([0, 0, 0])
144    }
145
146    fn get_text_width(
147        &self,
148        text: &str,
149        font_char: char,
150        height: Option<u32>,
151        width: Option<u32>,
152    ) -> u32 {
153        match self.font_manager.as_ref() {
154            Some(fm) => fm.measure_text(font_char, height, width, text),
155            None => 0,
156        }
157    }
158}
159
160impl ZplForgeBackend for PngBackend {
161    fn setup_page(&mut self, width: f64, height: f64, _resolution: f32) {
162        // Safety limit to avoid OOM: 8192x8192 is enough for most labels
163        const MAX_DIM: u32 = 8192;
164        let w = (width as u32).min(MAX_DIM);
165        let h = (height as u32).min(MAX_DIM);
166        self.canvas = ImageBuffer::from_pixel(w, h, Rgb([255, 255, 255]));
167    }
168
169    fn setup_font_manager(&mut self, font_manager: &FontManager) {
170        self.font_manager = Some(Arc::new(font_manager.clone()));
171    }
172
173    fn draw_text(
174        &mut self,
175        x: u32,
176        y: u32,
177        font: char,
178        height: Option<u32>,
179        width: Option<u32>,
180        orientation: char,
181        text: &str,
182        _reverse_print: bool,
183        color: Option<String>,
184    ) -> ZplResult<()> {
185        if text.is_empty() {
186            return Ok(());
187        }
188
189        let fm = self
190            .font_manager
191            .as_ref()
192            .ok_or_else(|| ZplError::FontError("Font manager not initialized".into()))?;
193        let (font_arc, layout) = fm
194            .text_layout(font, height, width)
195            .ok_or_else(|| ZplError::FontError(format!("Font not found: {}", font)))?;
196        let font_data = font_arc.clone();
197        let scale = layout.px;
198
199        // imageproc places the baseline at `y + ascent`; shift so capital
200        // letters start exactly at the ZPL cell top (Zebra behavior).
201        let ascent = font_data.as_scaled(scale).ascent();
202        let y_offset = (layout.baseline - ascent).round() as i32;
203
204        let text_color = self.parse_hex_color(&color);
205
206        if !matches!(orientation, 'R' | 'I' | 'B') {
207            draw_text_mut(
208                &mut self.canvas,
209                text_color,
210                x as i32,
211                y as i32 + y_offset,
212                scale,
213                &font_data,
214                text,
215            );
216            return Ok(());
217        }
218
219        // Rotated text: render on a temporary transparent surface, rotate it, and blit
220        // non-transparent pixels so the background stays transparent.
221        //
222        // Ink can overflow the character cell on both sides: ascenders and
223        // accents rise above the cap line (`y_offset` is negative because the
224        // font ascent exceeds the cap height) and descenders can drop below
225        // `cell_h`. Pad the surface so nothing is clipped, then shift the blit
226        // anchor so the cell's top-left corner still lands exactly on (x, y).
227        let text_w = self.get_text_width(text, font, height, width).max(1);
228        let font_h = (layout.cell_h.ceil() as u32).max(1);
229        let top_pad = (-y_offset).max(0) as u32;
230        // `descent()` is negative: ink below the baseline reaches `baseline - descent`.
231        let descent = font_data.as_scaled(scale).descent();
232        let ink_bottom = (layout.baseline - descent).ceil() as i32;
233        let bottom_pad = (ink_bottom - font_h as i32).max(0) as u32;
234        let mut tmp =
235            RgbaImage::from_pixel(text_w, font_h + top_pad + bottom_pad, Rgba([0, 0, 0, 0]));
236        let text_rgba = Rgba([text_color.0[0], text_color.0[1], text_color.0[2], 255]);
237        draw_text_mut(
238            &mut tmp,
239            text_rgba,
240            0,
241            y_offset + top_pad as i32,
242            scale,
243            &font_data,
244            text,
245        );
246
247        // Rotation moves each pad to a different edge; only pads landing on
248        // the low-index side displace the cell content and must be subtracted
249        // from the anchor.
250        let (rotated, pad_x, pad_y) = match orientation {
251            // 90° cw: top pad → right edge, bottom pad → left edge.
252            'R' => (rotate90(&tmp), bottom_pad, 0),
253            // 180°: top pad → bottom edge, bottom pad → top edge.
254            'I' => (rotate180(&tmp), 0, bottom_pad),
255            // 270° cw: top pad → left edge, bottom pad → right edge.
256            _ => (rotate270(&tmp), top_pad, 0),
257        };
258
259        let (cw, ch) = self.canvas.dimensions();
260        for (sx, sy, p) in rotated.enumerate_pixels() {
261            if p.0[3] > 0 {
262                let dx = x as i64 + sx as i64 - pad_x as i64;
263                let dy = y as i64 + sy as i64 - pad_y as i64;
264                if (0..cw as i64).contains(&dx) && (0..ch as i64).contains(&dy) {
265                    self.canvas[(dx as u32, dy as u32)] = Rgb([p.0[0], p.0[1], p.0[2]]);
266                }
267            }
268        }
269        Ok(())
270    }
271
272    fn draw_graphic_box(
273        &mut self,
274        x: u32,
275        y: u32,
276        width: u32,
277        height: u32,
278        thickness: u32,
279        color: char,
280        custom_color: Option<String>,
281        rounding: u32,
282        reverse_print: bool,
283    ) -> ZplResult<()> {
284        let w = max(width, 1);
285        let h = max(height, 1);
286        let t = thickness;
287        let r = (rounding as f64 * 8.0) as i32;
288
289        let (draw_color, clear_color) = if let Some(custom) = custom_color {
290            (self.parse_hex_color(&Some(custom)), Rgb([255, 255, 255]))
291        } else if color == 'B' {
292            (Rgb([0, 0, 0]), Rgb([255, 255, 255]))
293        } else {
294            (Rgb([255, 255, 255]), Rgb([0, 0, 0]))
295        };
296
297        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
298            let draw_rounded_fill =
299                |img: &mut RgbImage, px: i32, py: i32, pw: u32, ph: u32, pr: i32, pc: Rgb<u8>| {
300                    if pw == 0 || ph == 0 {
301                        return;
302                    }
303                    if pr <= 0 {
304                        draw_filled_rect_mut(img, Rect::at(px, py).of_size(pw, ph), pc);
305                    } else {
306                        let pr = pr.max(0).min((pw / 2) as i32).min((ph / 2) as i32);
307                        let inner_w = pw.saturating_sub(2 * pr as u32).max(1);
308                        let inner_h = ph.saturating_sub(2 * pr as u32).max(1);
309                        draw_filled_rect_mut(img, Rect::at(px + pr, py).of_size(inner_w, ph), pc);
310                        draw_filled_rect_mut(img, Rect::at(px, py + pr).of_size(pw, inner_h), pc);
311                        draw_filled_circle_mut(img, (px + pr, py + pr), pr, pc);
312                        draw_filled_circle_mut(img, (px + pw as i32 - pr - 1, py + pr), pr, pc);
313                        draw_filled_circle_mut(img, (px + pr, py + ph as i32 - pr - 1), pr, pc);
314                        draw_filled_circle_mut(
315                            img,
316                            (px + pw as i32 - pr - 1, py + ph as i32 - pr - 1),
317                            pr,
318                            pc,
319                        );
320                    }
321                };
322
323            draw_rounded_fill(img, px, py, w, h, r, draw_color);
324            if t * 2 < w && t * 2 < h {
325                draw_rounded_fill(
326                    img,
327                    px + t as i32,
328                    py + t as i32,
329                    w - t * 2,
330                    h - t * 2,
331                    (r - t as i32).max(0),
332                    clear_color,
333                );
334            }
335        };
336
337        self.draw_wrapper(x, y, w, h, reverse_print, draw_op)
338    }
339
340    fn draw_graphic_circle(
341        &mut self,
342        x: u32,
343        y: u32,
344        radius: u32,
345        thickness: u32,
346        _color: char,
347        custom_color: Option<String>,
348        reverse_print: bool,
349    ) -> ZplResult<()> {
350        let color = self.parse_hex_color(&custom_color);
351        let clear_color = Rgb([255, 255, 255]);
352
353        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
354            let center_x = px + radius as i32;
355            let center_y = py + radius as i32;
356            draw_filled_circle_mut(img, (center_x, center_y), radius as i32, color);
357
358            if radius > thickness {
359                draw_filled_circle_mut(
360                    img,
361                    (center_x, center_y),
362                    (radius - thickness) as i32,
363                    clear_color,
364                );
365            }
366        };
367
368        self.draw_wrapper(x, y, radius * 2, radius * 2, reverse_print, draw_op)
369    }
370
371    fn draw_graphic_ellipse(
372        &mut self,
373        x: u32,
374        y: u32,
375        width: u32,
376        height: u32,
377        thickness: u32,
378        _color: char,
379        custom_color: Option<String>,
380        reverse_print: bool,
381    ) -> ZplResult<()> {
382        let color = self.parse_hex_color(&custom_color);
383        let clear_color = Rgb([255, 255, 255]);
384
385        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
386            let rx = (width / 2) as i32;
387            let ry = (height / 2) as i32;
388            let center_x = px + rx;
389            let center_y = py + ry;
390            draw_filled_ellipse_mut(img, (center_x, center_y), rx, ry, color);
391
392            let t = thickness as i32;
393            if rx > t && ry > t {
394                draw_filled_ellipse_mut(img, (center_x, center_y), rx - t, ry - t, clear_color);
395            }
396        };
397
398        self.draw_wrapper(x, y, width, height, reverse_print, draw_op)
399    }
400
401    fn draw_graphic_field(
402        &mut self,
403        x: u32,
404        y: u32,
405        width: u32,
406        height: u32,
407        data: &[u8],
408        reverse_print: bool,
409    ) -> ZplResult<()> {
410        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
411            let row_bytes = width.div_ceil(8);
412            let (img_w, img_h) = (img.width() as i32, img.height() as i32);
413
414            for (row_idx, row_data) in data.chunks(row_bytes as usize).enumerate() {
415                let dy = py + row_idx as i32;
416                if dy < 0 || dy >= img_h || row_idx as u32 >= height {
417                    continue;
418                }
419
420                for (byte_idx, &byte) in row_data.iter().enumerate() {
421                    if byte == 0 {
422                        continue;
423                    }
424                    let base_x = px + (byte_idx as i32 * 8);
425                    for bit_idx in 0..8 {
426                        let col_idx = byte_idx as u32 * 8 + bit_idx;
427                        if col_idx >= width {
428                            break;
429                        }
430
431                        if (byte & (0x80 >> bit_idx)) != 0 {
432                            let dx = base_x + bit_idx as i32;
433                            if dx >= 0 && dx < img_w {
434                                img[(dx as u32, dy as u32)] = Rgb([0, 0, 0]);
435                            }
436                        }
437                    }
438                }
439            }
440        };
441
442        self.draw_wrapper(x, y, width, height, reverse_print, draw_op)
443    }
444
445    fn draw_graphic_image_custom(
446        &mut self,
447        x: u32,
448        y: u32,
449        width: u32,
450        height: u32,
451        data: &str,
452    ) -> ZplResult<()> {
453        let image_data = general_purpose::STANDARD
454            .decode(data.trim())
455            .map_err(|e| ZplError::ImageError(format!("Failed to decode base64: {}", e)))?;
456
457        let img = image::load_from_memory(&image_data)
458            .map_err(|e| ZplError::ImageError(format!("Failed to load image: {}", e)))?
459            .to_rgb8();
460
461        let (orig_w, orig_h) = img.dimensions();
462        let (target_w, target_h) = match (width, height) {
463            (0, 0) => (orig_w, orig_h),
464            (w, 0) => {
465                let h = (orig_h as f32 * (w as f32 / orig_w as f32)).round() as u32;
466                (w, h)
467            }
468            (0, h) => {
469                let w = (orig_w as f32 * (h as f32 / orig_h as f32)).round() as u32;
470                (w, h)
471            }
472            (w, h) => (w, h),
473        };
474
475        let resized_img = if target_w != orig_w || target_h != orig_h {
476            image::imageops::resize(
477                &img,
478                target_w,
479                target_h,
480                image::imageops::FilterType::Lanczos3,
481            )
482        } else {
483            img
484        };
485
486        overlay(&mut self.canvas, &resized_img, x as i64, y as i64);
487        Ok(())
488    }
489
490    fn draw_code128(
491        &mut self,
492        x: u32,
493        y: u32,
494        orientation: char,
495        height: u32,
496        module_width: u32,
497        interpretation_line: char,
498        interpretation_line_above: char,
499        _check_digit: char,
500        _mode: char,
501        data: &str,
502        reverse_print: bool,
503    ) -> ZplResult<()> {
504        let (clean_data, hint_val) = if let Some(stripped) = data.strip_prefix(">:") {
505            (stripped, Some("B"))
506        } else if let Some(stripped) = data.strip_prefix(">;") {
507            (stripped, Some("C"))
508        } else if let Some(stripped) = data.strip_prefix(">9") {
509            (stripped, Some("A"))
510        } else {
511            (data, Some("B")) // Standard default is Code Set B
512        };
513
514        let hints = hint_val.map(|v| {
515            let mut h = HashMap::new();
516            h.insert(
517                EncodeHintType::FORCE_CODE_SET,
518                EncodeHintValue::ForceCodeSet(v.to_string()),
519            );
520            EncodeHints::from(h)
521        });
522
523        self.draw_1d_barcode(
524            x,
525            y,
526            orientation,
527            height,
528            module_width,
529            clean_data,
530            BarcodeFormat::CODE_128,
531            reverse_print,
532            interpretation_line,
533            interpretation_line_above,
534            hints,
535            hint_val.unwrap_or(""),
536        )
537    }
538
539    fn draw_qr_code(
540        &mut self,
541        x: u32,
542        y: u32,
543        orientation: char,
544        _model: u32,
545        magnification: u32,
546        error_correction: char,
547        _mask: u32,
548        data: &str,
549        reverse_print: bool,
550    ) -> ZplResult<()> {
551        let level = match error_correction {
552            'L' => "L",
553            'M' => "M",
554            'Q' => "Q",
555            'H' => "H",
556            _ => "M",
557        };
558
559        let mut hints = HashMap::new();
560        hints.insert(
561            EncodeHintType::ERROR_CORRECTION,
562            EncodeHintValue::ErrorCorrection(level.to_string()),
563        );
564        hints.insert(
565            EncodeHintType::MARGIN,
566            EncodeHintValue::Margin("0".to_owned()),
567        );
568        let hints: EncodeHints = hints.into();
569
570        let bit_matrix = barcode_cache::encode_cached(
571            BarcodeFormat::QR_CODE,
572            data,
573            &format!("ec:{}", level),
574            Some(&hints),
575        )?;
576
577        let mag = max(magnification, 1);
578        self.fill_matrix_cells(x, y, orientation, mag, mag, &bit_matrix, reverse_print);
579        Ok(())
580    }
581
582    fn draw_datamatrix(
583        &mut self,
584        x: u32,
585        y: u32,
586        orientation: char,
587        module_size: u32,
588        data: &str,
589        reverse_print: bool,
590    ) -> ZplResult<()> {
591        let bit_matrix = barcode_cache::encode_cached(BarcodeFormat::DATA_MATRIX, data, "", None)?;
592
593        let m = max(module_size, 1);
594        self.fill_matrix_cells(x, y, orientation, m, m, &bit_matrix, reverse_print);
595        Ok(())
596    }
597
598    fn draw_pdf417(
599        &mut self,
600        x: u32,
601        y: u32,
602        orientation: char,
603        row_height: u32,
604        module_width: u32,
605        security_level: u32,
606        data: &str,
607        reverse_print: bool,
608    ) -> ZplResult<()> {
609        let mut hints = HashMap::new();
610        hints.insert(
611            EncodeHintType::ERROR_CORRECTION,
612            EncodeHintValue::ErrorCorrection(security_level.min(8).to_string()),
613        );
614        hints.insert(
615            EncodeHintType::MARGIN,
616            EncodeHintValue::Margin("0".to_owned()),
617        );
618        let hints: EncodeHints = hints.into();
619
620        let bit_matrix = barcode_cache::encode_cached(
621            BarcodeFormat::PDF_417,
622            data,
623            &format!("ec:{}", security_level.min(8)),
624            Some(&hints),
625        )?;
626
627        let cw = max(module_width, 1);
628        let ch = max(row_height, 1);
629        self.fill_matrix_cells(x, y, orientation, cw, ch, &bit_matrix, reverse_print);
630        Ok(())
631    }
632
633    fn draw_code39(
634        &mut self,
635        x: u32,
636        y: u32,
637        orientation: char,
638        _check_digit: char,
639        height: u32,
640        module_width: u32,
641        interpretation_line: char,
642        interpretation_line_above: char,
643        data: &str,
644        reverse_print: bool,
645    ) -> ZplResult<()> {
646        self.draw_1d_barcode(
647            x,
648            y,
649            orientation,
650            height,
651            module_width,
652            data,
653            BarcodeFormat::CODE_39,
654            reverse_print,
655            interpretation_line,
656            interpretation_line_above,
657            None,
658            "",
659        )
660    }
661
662    fn draw_barcode_1d(
663        &mut self,
664        kind: Barcode1DKind,
665        x: u32,
666        y: u32,
667        orientation: char,
668        height: u32,
669        module_width: u32,
670        interpretation_line: char,
671        interpretation_line_above: char,
672        data: &str,
673        reverse_print: bool,
674    ) -> ZplResult<()> {
675        self.draw_1d_barcode(
676            x,
677            y,
678            orientation,
679            height,
680            module_width,
681            data,
682            barcode_1d_format(kind),
683            reverse_print,
684            interpretation_line,
685            interpretation_line_above,
686            None,
687            "",
688        )
689    }
690
691    fn draw_graphic_diagonal(
692        &mut self,
693        x: u32,
694        y: u32,
695        width: u32,
696        height: u32,
697        thickness: u32,
698        color: char,
699        custom_color: Option<String>,
700        diagonal_orientation: char,
701        reverse_print: bool,
702    ) -> ZplResult<()> {
703        let draw_color = if custom_color.is_some() {
704            self.parse_hex_color(&custom_color)
705        } else if color == 'W' {
706            Rgb([255, 255, 255])
707        } else {
708            Rgb([0, 0, 0])
709        };
710
711        let w = max(width, 1) as i32;
712        let h = max(height, 1) as i32;
713        let t = (max(thickness, 1) as i32).min(w);
714
715        let draw_op = move |img: &mut RgbImage, px: i32, py: i32| {
716            // Filled parallelogram with horizontal thickness `t`.
717            let pts = if diagonal_orientation == 'L' {
718                // '\' top-left → bottom-right
719                [
720                    Point::new(px, py),
721                    Point::new(px + t, py),
722                    Point::new(px + w, py + h),
723                    Point::new(px + w - t, py + h),
724                ]
725            } else {
726                // '/' bottom-left → top-right
727                [
728                    Point::new(px, py + h),
729                    Point::new(px + t, py + h),
730                    Point::new(px + w, py),
731                    Point::new(px + w - t, py),
732                ]
733            };
734            draw_polygon_mut(img, &pts, draw_color);
735        };
736
737        self.draw_wrapper(x, y, w as u32, h as u32, reverse_print, draw_op)
738    }
739
740    fn finalize(&mut self) -> ZplResult<Vec<u8>> {
741        let mut bytes = Vec::new();
742        let mut cursor = std::io::Cursor::new(&mut bytes);
743        self.canvas
744            .write_to(&mut cursor, image::ImageFormat::Png)
745            .map_err(|e| ZplError::BackendError(format!("Failed to write PNG: {}", e)))?;
746        Ok(bytes)
747    }
748}
749
750impl PngBackend {
751    /// Paints every set cell of a 2-D bit matrix as a filled rectangle,
752    /// scaling each cell to `cell_w` × `cell_h` dots and applying the
753    /// requested orientation.
754    #[allow(clippy::too_many_arguments)]
755    fn fill_matrix_cells(
756        &mut self,
757        x: u32,
758        y: u32,
759        orientation: char,
760        cell_w: u32,
761        cell_h: u32,
762        bit_matrix: &BitMatrix,
763        reverse_print: bool,
764    ) {
765        let bw = bit_matrix.getWidth();
766        let bh = bit_matrix.getHeight();
767        let full_w = bw * cell_w;
768        let full_h = bh * cell_h;
769
770        for gy in 0..bh {
771            for gx in 0..bw {
772                if !bit_matrix.get(gx, gy) {
773                    continue;
774                }
775                let lx = (gx * cell_w) as i32;
776                let ly = (gy * cell_h) as i32;
777                let (w, h) = (cell_w, cell_h);
778                let rect = match orientation {
779                    'R' => {
780                        let nx = full_h as i32 - (ly + h as i32);
781                        Rect::at(x as i32 + nx, y as i32 + lx).of_size(h, w)
782                    }
783                    'I' => {
784                        let nx = full_w as i32 - (lx + w as i32);
785                        let ny = full_h as i32 - (ly + h as i32);
786                        Rect::at(x as i32 + nx, y as i32 + ny).of_size(w, h)
787                    }
788                    'B' => {
789                        let ny = full_w as i32 - (lx + w as i32);
790                        Rect::at(x as i32 + ly, y as i32 + ny).of_size(h, w)
791                    }
792                    _ => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
793                };
794                if reverse_print {
795                    self.invert_rect(rect);
796                } else {
797                    draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
798                }
799            }
800        }
801    }
802
803    #[allow(clippy::too_many_arguments)]
804    fn draw_1d_barcode(
805        &mut self,
806        x: u32,
807        y: u32,
808        orientation: char,
809        height: u32,
810        module_width: u32,
811        data: &str,
812        format: BarcodeFormat,
813        reverse_print: bool,
814        interpretation_line: char,
815        interpretation_line_above: char,
816        hints: Option<EncodeHints>,
817        hints_key: &str,
818    ) -> ZplResult<()> {
819        let bit_matrix = barcode_cache::encode_cached(format, data, hints_key, hints.as_ref())?;
820
821        let mw = max(module_width, 1);
822        let bh = height;
823        let bw = bit_matrix.getWidth() * mw;
824
825        let (full_w, full_h) = match orientation {
826            'N' | 'I' => (bw, bh),
827            'R' | 'B' => (bh, bw),
828            _ => (bw, bh),
829        };
830
831        let transform_rect = |lx: i32, ly: i32, w: u32, h: u32| -> Rect {
832            match orientation {
833                'N' => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
834                'R' => {
835                    let new_x = bh as i32 - (ly + h as i32);
836                    let new_y = lx;
837                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
838                }
839                'I' => {
840                    let new_x = bw as i32 - (lx + w as i32);
841                    let new_y = bh as i32 - (ly + h as i32);
842                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(w, h)
843                }
844                'B' => {
845                    let new_x = ly;
846                    let new_y = bw as i32 - (lx + w as i32);
847                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
848                }
849                _ => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
850            }
851        };
852
853        for gx in 0..bit_matrix.getWidth() {
854            if bit_matrix.get(gx, 0) {
855                let rect = transform_rect((gx * mw) as i32, 0, mw, bh);
856                if reverse_print {
857                    self.invert_rect(rect);
858                } else {
859                    draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
860                }
861            }
862        }
863
864        if interpretation_line == 'Y' {
865            let font_char = '0';
866            let (text_h, gap) = crate::engine::font::interpretation_metrics(module_width);
867            let text_y = if interpretation_line_above == 'Y' {
868                y.saturating_sub(text_h + gap)
869            } else {
870                y + full_h + gap
871            };
872
873            let text_width = self.get_text_width(data, font_char, Some(text_h), None);
874            let text_x = if full_w > text_width {
875                x + (full_w - text_width) / 2
876            } else {
877                x
878            };
879
880            self.draw_text(
881                text_x,
882                text_y,
883                font_char,
884                Some(text_h),
885                None,
886                'N',
887                data,
888                false,
889                None,
890            )?;
891        }
892
893        Ok(())
894    }
895}