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::datamatrix::encoder::SymbolShapeHint;
24use rxing::{BarcodeFormat, EncodeHintType, EncodeHintValue, EncodeHints};
25
26use super::{barcode_1d_format, barcode_cache, symbology};
27use crate::engine::{Barcode1DKind, FontManager, ZplForgeBackend};
28use crate::{ZplError, ZplResult};
29
30/// `rxing`'s numeric code for PDF417 text compaction.
31const PDF417_TEXT_COMPACTION: u32 = 1;
32
33/// A rendering backend that produces PNG images.
34///
35/// This backend uses the `image` and `imageproc` crates to draw ZPL instructions
36/// onto an RGB canvas.
37pub struct PngBackend {
38    canvas: RgbImage,
39    font_manager: Option<Arc<FontManager>>,
40}
41
42impl Default for PngBackend {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl PngBackend {
49    /// Creates a new `PngBackend` instance with an empty canvas.
50    pub fn new() -> Self {
51        Self {
52            canvas: ImageBuffer::new(0, 0),
53            font_manager: None,
54        }
55    }
56
57    /// Performs an XOR overlay of a source image onto the canvas at (x, y).
58    fn xor_overlay(&mut self, src: &RgbImage, x: i64, y: i64) {
59        let (sw, sh) = src.dimensions();
60        let (cw, ch) = self.canvas.dimensions();
61
62        for sy in 0..sh {
63            let dy = y + sy as i64;
64            if dy < 0 || dy >= ch as i64 {
65                continue;
66            }
67
68            for sx in 0..sw {
69                let dx = x + sx as i64;
70                if dx < 0 || dx >= cw as i64 {
71                    continue;
72                }
73
74                let src_pixel = src[(sx, sy)];
75                if src_pixel.0 != [255, 255, 255] {
76                    let dest_pixel = &mut self.canvas[(dx as u32, dy as u32)];
77                    dest_pixel.0[0] ^= 255;
78                    dest_pixel.0[1] ^= 255;
79                    dest_pixel.0[2] ^= 255;
80                }
81            }
82        }
83    }
84
85    /// Inverts the colors within a specified rectangular area.
86    fn invert_rect(&mut self, rect: Rect) {
87        let (cw, ch) = self.canvas.dimensions();
88        let x_start = rect.left().max(0) as u32;
89        let y_start = rect.top().max(0) as u32;
90        let x_end = (rect.right() as u32).min(cw);
91        let y_end = (rect.bottom() as u32).min(ch);
92
93        for py in y_start..y_end {
94            for px in x_start..x_end {
95                let pixel = &mut self.canvas[(px, py)];
96                pixel.0[0] ^= 255;
97                pixel.0[1] ^= 255;
98                pixel.0[2] ^= 255;
99            }
100        }
101    }
102
103    /// Helper to execute a drawing operation.
104    fn draw_wrapper<F>(
105        &mut self,
106        x: u32,
107        y: u32,
108        width: u32,
109        height: u32,
110        reverse_print: bool,
111        draw_op: F,
112    ) -> ZplResult<()>
113    where
114        F: FnOnce(&mut RgbImage, i32, i32),
115    {
116        if reverse_print {
117            let mut temp_buf = ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255]));
118            draw_op(&mut temp_buf, 0, 0);
119            self.xor_overlay(&temp_buf, x as i64, y as i64);
120        } else {
121            draw_op(&mut self.canvas, x as i32, y as i32);
122        }
123        Ok(())
124    }
125
126    fn parse_hex_color(&self, color: &Option<String>) -> Rgb<u8> {
127        if let Some(hex) = color {
128            let hex = hex.trim_start_matches('#');
129            if hex.len() == 6 {
130                if let (Ok(r), Ok(g), Ok(b)) = (
131                    u8::from_str_radix(&hex[0..2], 16),
132                    u8::from_str_radix(&hex[2..4], 16),
133                    u8::from_str_radix(&hex[4..6], 16),
134                ) {
135                    return Rgb([r, g, b]);
136                }
137            } else if hex.len() == 3
138                && let (Ok(r), Ok(g), Ok(b)) = (
139                    u8::from_str_radix(&hex[0..1], 16),
140                    u8::from_str_radix(&hex[1..2], 16),
141                    u8::from_str_radix(&hex[2..3], 16),
142                )
143            {
144                return Rgb([r * 17, g * 17, b * 17]);
145            }
146        }
147        Rgb([0, 0, 0])
148    }
149
150    fn get_text_width(
151        &self,
152        text: &str,
153        font_char: char,
154        height: Option<u32>,
155        width: Option<u32>,
156    ) -> u32 {
157        match self.font_manager.as_ref() {
158            Some(fm) => fm.measure_text(font_char, height, width, text),
159            None => 0,
160        }
161    }
162}
163
164impl ZplForgeBackend for PngBackend {
165    fn setup_page(&mut self, width: f64, height: f64, _resolution: f32) {
166        // Safety limit to avoid OOM: 8192x8192 is enough for most labels
167        const MAX_DIM: u32 = 8192;
168        let w = (width as u32).min(MAX_DIM);
169        let h = (height as u32).min(MAX_DIM);
170        self.canvas = ImageBuffer::from_pixel(w, h, Rgb([255, 255, 255]));
171    }
172
173    fn setup_font_manager(&mut self, font_manager: &FontManager) {
174        self.font_manager = Some(Arc::new(font_manager.clone()));
175    }
176
177    fn draw_text(
178        &mut self,
179        x: u32,
180        y: u32,
181        font: char,
182        height: Option<u32>,
183        width: Option<u32>,
184        orientation: char,
185        text: &str,
186        _reverse_print: bool,
187        color: Option<String>,
188    ) -> ZplResult<()> {
189        if text.is_empty() {
190            return Ok(());
191        }
192
193        let fm = self
194            .font_manager
195            .as_ref()
196            .ok_or_else(|| ZplError::FontError("Font manager not initialized".into()))?;
197        let (font_arc, layout) = fm
198            .text_layout(font, height, width)
199            .ok_or_else(|| ZplError::FontError(format!("Font not found: {}", font)))?;
200        let font_data = font_arc.clone();
201        let scale = layout.px;
202
203        // imageproc places the baseline at `y + ascent`; shift so capital
204        // letters start exactly at the ZPL cell top (Zebra behavior).
205        let ascent = font_data.as_scaled(scale).ascent();
206        let y_offset = (layout.baseline - ascent).round() as i32;
207
208        let text_color = self.parse_hex_color(&color);
209
210        if !matches!(orientation, 'R' | 'I' | 'B') {
211            draw_text_mut(
212                &mut self.canvas,
213                text_color,
214                x as i32,
215                y as i32 + y_offset,
216                scale,
217                &font_data,
218                text,
219            );
220            return Ok(());
221        }
222
223        // Rotated text: render on a temporary transparent surface, rotate it, and blit
224        // non-transparent pixels so the background stays transparent.
225        //
226        // Ink can overflow the character cell on both sides: ascenders and
227        // accents rise above the cap line (`y_offset` is negative because the
228        // font ascent exceeds the cap height) and descenders can drop below
229        // `cell_h`. Pad the surface so nothing is clipped, then shift the blit
230        // anchor so the cell's top-left corner still lands exactly on (x, y).
231        let text_w = self.get_text_width(text, font, height, width).max(1);
232        let font_h = (layout.cell_h.ceil() as u32).max(1);
233        let top_pad = (-y_offset).max(0) as u32;
234        // `descent()` is negative: ink below the baseline reaches `baseline - descent`.
235        let descent = font_data.as_scaled(scale).descent();
236        let ink_bottom = (layout.baseline - descent).ceil() as i32;
237        let bottom_pad = (ink_bottom - font_h as i32).max(0) as u32;
238        let mut tmp =
239            RgbaImage::from_pixel(text_w, font_h + top_pad + bottom_pad, Rgba([0, 0, 0, 0]));
240        let text_rgba = Rgba([text_color.0[0], text_color.0[1], text_color.0[2], 255]);
241        draw_text_mut(
242            &mut tmp,
243            text_rgba,
244            0,
245            y_offset + top_pad as i32,
246            scale,
247            &font_data,
248            text,
249        );
250
251        // Rotation moves each pad to a different edge; only pads landing on
252        // the low-index side displace the cell content and must be subtracted
253        // from the anchor.
254        let (rotated, pad_x, pad_y) = match orientation {
255            // 90° cw: top pad → right edge, bottom pad → left edge.
256            'R' => (rotate90(&tmp), bottom_pad, 0),
257            // 180°: top pad → bottom edge, bottom pad → top edge.
258            'I' => (rotate180(&tmp), 0, bottom_pad),
259            // 270° cw: top pad → left edge, bottom pad → right edge.
260            _ => (rotate270(&tmp), top_pad, 0),
261        };
262
263        let (cw, ch) = self.canvas.dimensions();
264        for (sx, sy, p) in rotated.enumerate_pixels() {
265            if p.0[3] > 0 {
266                let dx = x as i64 + sx as i64 - pad_x as i64;
267                let dy = y as i64 + sy as i64 - pad_y as i64;
268                if (0..cw as i64).contains(&dx) && (0..ch as i64).contains(&dy) {
269                    self.canvas[(dx as u32, dy as u32)] = Rgb([p.0[0], p.0[1], p.0[2]]);
270                }
271            }
272        }
273        Ok(())
274    }
275
276    fn draw_graphic_box(
277        &mut self,
278        x: u32,
279        y: u32,
280        width: u32,
281        height: u32,
282        thickness: u32,
283        color: char,
284        custom_color: Option<String>,
285        rounding: u32,
286        reverse_print: bool,
287    ) -> ZplResult<()> {
288        let w = max(width, 1);
289        let h = max(height, 1);
290        let t = thickness;
291        let r = (rounding as f64 * 8.0) as i32;
292
293        let (draw_color, clear_color) = if let Some(custom) = custom_color {
294            (self.parse_hex_color(&Some(custom)), Rgb([255, 255, 255]))
295        } else if color == 'B' {
296            (Rgb([0, 0, 0]), Rgb([255, 255, 255]))
297        } else {
298            (Rgb([255, 255, 255]), Rgb([0, 0, 0]))
299        };
300
301        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
302            let draw_rounded_fill =
303                |img: &mut RgbImage, px: i32, py: i32, pw: u32, ph: u32, pr: i32, pc: Rgb<u8>| {
304                    if pw == 0 || ph == 0 {
305                        return;
306                    }
307                    if pr <= 0 {
308                        draw_filled_rect_mut(img, Rect::at(px, py).of_size(pw, ph), pc);
309                    } else {
310                        let pr = pr.max(0).min((pw / 2) as i32).min((ph / 2) as i32);
311                        let inner_w = pw.saturating_sub(2 * pr as u32).max(1);
312                        let inner_h = ph.saturating_sub(2 * pr as u32).max(1);
313                        draw_filled_rect_mut(img, Rect::at(px + pr, py).of_size(inner_w, ph), pc);
314                        draw_filled_rect_mut(img, Rect::at(px, py + pr).of_size(pw, inner_h), pc);
315                        draw_filled_circle_mut(img, (px + pr, py + pr), pr, pc);
316                        draw_filled_circle_mut(img, (px + pw as i32 - pr - 1, py + pr), pr, pc);
317                        draw_filled_circle_mut(img, (px + pr, py + ph as i32 - pr - 1), pr, pc);
318                        draw_filled_circle_mut(
319                            img,
320                            (px + pw as i32 - pr - 1, py + ph as i32 - pr - 1),
321                            pr,
322                            pc,
323                        );
324                    }
325                };
326
327            draw_rounded_fill(img, px, py, w, h, r, draw_color);
328            if t * 2 < w && t * 2 < h {
329                draw_rounded_fill(
330                    img,
331                    px + t as i32,
332                    py + t as i32,
333                    w - t * 2,
334                    h - t * 2,
335                    (r - t as i32).max(0),
336                    clear_color,
337                );
338            }
339        };
340
341        self.draw_wrapper(x, y, w, h, reverse_print, draw_op)
342    }
343
344    fn draw_graphic_circle(
345        &mut self,
346        x: u32,
347        y: u32,
348        radius: u32,
349        thickness: u32,
350        _color: char,
351        custom_color: Option<String>,
352        reverse_print: bool,
353    ) -> ZplResult<()> {
354        let color = self.parse_hex_color(&custom_color);
355        let clear_color = Rgb([255, 255, 255]);
356
357        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
358            let center_x = px + radius as i32;
359            let center_y = py + radius as i32;
360            draw_filled_circle_mut(img, (center_x, center_y), radius as i32, color);
361
362            if radius > thickness {
363                draw_filled_circle_mut(
364                    img,
365                    (center_x, center_y),
366                    (radius - thickness) as i32,
367                    clear_color,
368                );
369            }
370        };
371
372        self.draw_wrapper(x, y, radius * 2, radius * 2, reverse_print, draw_op)
373    }
374
375    fn draw_graphic_ellipse(
376        &mut self,
377        x: u32,
378        y: u32,
379        width: u32,
380        height: u32,
381        thickness: u32,
382        _color: char,
383        custom_color: Option<String>,
384        reverse_print: bool,
385    ) -> ZplResult<()> {
386        let color = self.parse_hex_color(&custom_color);
387        let clear_color = Rgb([255, 255, 255]);
388
389        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
390            let rx = (width / 2) as i32;
391            let ry = (height / 2) as i32;
392            let center_x = px + rx;
393            let center_y = py + ry;
394            draw_filled_ellipse_mut(img, (center_x, center_y), rx, ry, color);
395
396            let t = thickness as i32;
397            if rx > t && ry > t {
398                draw_filled_ellipse_mut(img, (center_x, center_y), rx - t, ry - t, clear_color);
399            }
400        };
401
402        self.draw_wrapper(x, y, width, height, reverse_print, draw_op)
403    }
404
405    fn draw_graphic_field(
406        &mut self,
407        x: u32,
408        y: u32,
409        width: u32,
410        height: u32,
411        data: &[u8],
412        reverse_print: bool,
413    ) -> ZplResult<()> {
414        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
415            let row_bytes = width.div_ceil(8);
416            let (img_w, img_h) = (img.width() as i32, img.height() as i32);
417
418            for (row_idx, row_data) in data.chunks(row_bytes as usize).enumerate() {
419                let dy = py + row_idx as i32;
420                if dy < 0 || dy >= img_h || row_idx as u32 >= height {
421                    continue;
422                }
423
424                for (byte_idx, &byte) in row_data.iter().enumerate() {
425                    if byte == 0 {
426                        continue;
427                    }
428                    let base_x = px + (byte_idx as i32 * 8);
429                    for bit_idx in 0..8 {
430                        let col_idx = byte_idx as u32 * 8 + bit_idx;
431                        if col_idx >= width {
432                            break;
433                        }
434
435                        if (byte & (0x80 >> bit_idx)) != 0 {
436                            let dx = base_x + bit_idx as i32;
437                            if dx >= 0 && dx < img_w {
438                                img[(dx as u32, dy as u32)] = Rgb([0, 0, 0]);
439                            }
440                        }
441                    }
442                }
443            }
444        };
445
446        self.draw_wrapper(x, y, width, height, reverse_print, draw_op)
447    }
448
449    fn draw_graphic_image_custom(
450        &mut self,
451        x: u32,
452        y: u32,
453        width: u32,
454        height: u32,
455        data: &str,
456    ) -> ZplResult<()> {
457        let image_data = general_purpose::STANDARD
458            .decode(data.trim())
459            .map_err(|e| ZplError::ImageError(format!("Failed to decode base64: {}", e)))?;
460
461        let img = image::load_from_memory(&image_data)
462            .map_err(|e| ZplError::ImageError(format!("Failed to load image: {}", e)))?
463            .to_rgb8();
464
465        let (orig_w, orig_h) = img.dimensions();
466        let (target_w, target_h) = match (width, height) {
467            (0, 0) => (orig_w, orig_h),
468            (w, 0) => {
469                let h = (orig_h as f32 * (w as f32 / orig_w as f32)).round() as u32;
470                (w, h)
471            }
472            (0, h) => {
473                let w = (orig_w as f32 * (h as f32 / orig_h as f32)).round() as u32;
474                (w, h)
475            }
476            (w, h) => (w, h),
477        };
478
479        let resized_img = if target_w != orig_w || target_h != orig_h {
480            image::imageops::resize(
481                &img,
482                target_w,
483                target_h,
484                image::imageops::FilterType::Lanczos3,
485            )
486        } else {
487            img
488        };
489
490        overlay(&mut self.canvas, &resized_img, x as i64, y as i64);
491        Ok(())
492    }
493
494    fn draw_code128(
495        &mut self,
496        x: u32,
497        y: u32,
498        orientation: char,
499        height: u32,
500        module_width: u32,
501        interpretation_line: char,
502        interpretation_line_above: char,
503        _check_digit: char,
504        _mode: char,
505        data: &str,
506        reverse_print: bool,
507    ) -> ZplResult<()> {
508        let (clean_data, code_set) = super::code128_code_set(data);
509
510        let mut h = HashMap::new();
511        h.insert(
512            EncodeHintType::FORCE_CODE_SET,
513            EncodeHintValue::ForceCodeSet(code_set.to_string()),
514        );
515        let hints = Some(EncodeHints::from(h));
516
517        self.draw_1d_barcode(
518            x,
519            y,
520            orientation,
521            height,
522            module_width,
523            // Code 128 is a four-width symbology; ^BY's ratio does not apply.
524            0.0,
525            clean_data,
526            BarcodeFormat::CODE_128,
527            reverse_print,
528            interpretation_line,
529            interpretation_line_above,
530            hints,
531            code_set,
532        )
533    }
534
535    fn draw_qr_code(
536        &mut self,
537        x: u32,
538        y: u32,
539        orientation: char,
540        _model: u32,
541        magnification: u32,
542        error_correction: char,
543        _mask: u32,
544        data: &str,
545        reverse_print: bool,
546    ) -> ZplResult<()> {
547        let (ec, payload) = super::qr_field_data(data, error_correction);
548        let level = match ec {
549            'L' => "L",
550            'Q' => "Q",
551            'H' => "H",
552            _ => "M",
553        };
554
555        let mut hints = HashMap::new();
556        hints.insert(
557            EncodeHintType::ERROR_CORRECTION,
558            EncodeHintValue::ErrorCorrection(level.to_string()),
559        );
560        let hints: EncodeHints = hints.into();
561
562        let bit_matrix = barcode_cache::encode_cached(
563            BarcodeFormat::QR_CODE,
564            payload,
565            &format!("ec:{}", level),
566            Some(&hints),
567        )?;
568
569        let mag = max(magnification, 1);
570        self.fill_matrix_cells(
571            x,
572            y + super::QR_ORIGIN_Y_OFFSET,
573            orientation,
574            mag,
575            mag,
576            &bit_matrix,
577            reverse_print,
578        );
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        // Zebra emits a square Data Matrix unless explicit rows/columns ask for
592        // a rectangle; `rxing` defaults to the smallest fitting symbol, which is
593        // often rectangular (26x12 instead of 18x18 for a 20-byte payload).
594        let hints = EncodeHints {
595            DataMatrixShape: Some(SymbolShapeHint::FORCE_SQUARE),
596            ..Default::default()
597        };
598
599        let bit_matrix =
600            barcode_cache::encode_cached(BarcodeFormat::DATA_MATRIX, data, "sq", Some(&hints))?;
601
602        let m = max(module_size, 1);
603        self.fill_matrix_cells(x, y, orientation, m, m, &bit_matrix, reverse_print);
604        Ok(())
605    }
606
607    fn draw_pdf417(
608        &mut self,
609        x: u32,
610        y: u32,
611        orientation: char,
612        row_height: u32,
613        module_width: u32,
614        security_level: u32,
615        data: &str,
616        reverse_print: bool,
617    ) -> ZplResult<()> {
618        let ec = security_level.min(8);
619        let hints = EncodeHints {
620            ErrorCorrection: Some(ec.to_string()),
621            // Text compaction matches Zebra's choice for alphanumeric payloads.
622            Pdf417Compaction: Some(PDF417_TEXT_COMPACTION.to_string()),
623            Pdf417Dimensions: Some(super::pdf417_dimensions(None, None)),
624            ..Default::default()
625        };
626
627        let bit_matrix = barcode_cache::encode_cached(
628            BarcodeFormat::PDF_417,
629            data,
630            &format!("ec:{ec}:c1:t"),
631            Some(&hints),
632        )?;
633        let bit_matrix = super::pdf417_descale(&bit_matrix)?;
634
635        let cw = max(module_width, 1);
636        let ch = max(row_height, 1);
637        self.fill_matrix_cells(x, y, orientation, cw, ch, &bit_matrix, reverse_print);
638        Ok(())
639    }
640
641    fn draw_micropdf417(
642        &mut self,
643        x: u32,
644        y: u32,
645        orientation: char,
646        height: u32,
647        _mode: u32,
648        data: &str,
649        reverse_print: bool,
650    ) -> ZplResult<()> {
651        let bit_matrix = barcode_cache::encode_cached(BarcodeFormat::PDF_417, data, "", None)?;
652        let ch = max(height, 1);
653        self.fill_matrix_cells(x, y, orientation, 1, ch, &bit_matrix, reverse_print);
654        Ok(())
655    }
656
657    fn draw_aztec_code(
658        &mut self,
659        x: u32,
660        y: u32,
661        orientation: char,
662        magnification: u32,
663        data: &str,
664        reverse_print: bool,
665    ) -> ZplResult<()> {
666        let bit_matrix = barcode_cache::encode_cached(BarcodeFormat::AZTEC, data, "", None)?;
667        let m = max(magnification, 1);
668        self.fill_matrix_cells(x, y, orientation, m, m, &bit_matrix, reverse_print);
669        Ok(())
670    }
671
672    fn draw_code39(
673        &mut self,
674        x: u32,
675        y: u32,
676        orientation: char,
677        _check_digit: char,
678        height: u32,
679        module_width: u32,
680        ratio: f32,
681        interpretation_line: char,
682        interpretation_line_above: char,
683        data: &str,
684        reverse_print: bool,
685    ) -> ZplResult<()> {
686        self.draw_1d_barcode(
687            x,
688            y,
689            orientation,
690            height,
691            module_width,
692            ratio,
693            data,
694            BarcodeFormat::CODE_39,
695            reverse_print,
696            interpretation_line,
697            interpretation_line_above,
698            None,
699            "",
700        )
701    }
702
703    fn draw_barcode_1d(
704        &mut self,
705        kind: Barcode1DKind,
706        x: u32,
707        y: u32,
708        orientation: char,
709        height: u32,
710        module_width: u32,
711        ratio: f32,
712        check_digit: char,
713        interpretation_line: char,
714        interpretation_line_above: char,
715        data: &str,
716        reverse_print: bool,
717    ) -> ZplResult<()> {
718        // MSI and POSTNET have no rxing writer; both are encoded natively.
719        match kind {
720            Barcode1DKind::Msi => {
721                let check = symbology::MsiCheck::from_zpl(check_digit);
722                let elements = symbology::msi_elements(data, check, module_width, ratio);
723                let text = symbology::msi_text(data, check);
724                return self.draw_elements(
725                    x,
726                    y,
727                    orientation,
728                    height,
729                    module_width,
730                    &elements,
731                    reverse_print,
732                    interpretation_line,
733                    interpretation_line_above,
734                    &text,
735                );
736            }
737            Barcode1DKind::Postnet => {
738                return self.draw_postnet(
739                    x,
740                    y,
741                    orientation,
742                    height,
743                    module_width,
744                    data,
745                    reverse_print,
746                    interpretation_line,
747                    interpretation_line_above,
748                );
749            }
750            _ => {}
751        }
752
753        self.draw_1d_barcode(
754            x,
755            y,
756            orientation,
757            height,
758            module_width,
759            ratio,
760            data,
761            barcode_1d_format(kind),
762            reverse_print,
763            interpretation_line,
764            interpretation_line_above,
765            None,
766            "",
767        )
768    }
769
770    fn draw_graphic_diagonal(
771        &mut self,
772        x: u32,
773        y: u32,
774        width: u32,
775        height: u32,
776        thickness: u32,
777        color: char,
778        custom_color: Option<String>,
779        diagonal_orientation: char,
780        reverse_print: bool,
781    ) -> ZplResult<()> {
782        let draw_color = if custom_color.is_some() {
783            self.parse_hex_color(&custom_color)
784        } else if color == 'W' {
785            Rgb([255, 255, 255])
786        } else {
787            Rgb([0, 0, 0])
788        };
789
790        let w = max(width, 1) as i32;
791        let h = max(height, 1) as i32;
792        let t = (max(thickness, 1) as i32).min(w);
793
794        let draw_op = move |img: &mut RgbImage, px: i32, py: i32| {
795            // Filled parallelogram with horizontal thickness `t`.
796            let pts = if diagonal_orientation == 'L' {
797                // '\' top-left → bottom-right
798                [
799                    Point::new(px, py),
800                    Point::new(px + t, py),
801                    Point::new(px + w, py + h),
802                    Point::new(px + w - t, py + h),
803                ]
804            } else {
805                // '/' bottom-left → top-right
806                [
807                    Point::new(px, py + h),
808                    Point::new(px + t, py + h),
809                    Point::new(px + w, py),
810                    Point::new(px + w - t, py),
811                ]
812            };
813            draw_polygon_mut(img, &pts, draw_color);
814        };
815
816        self.draw_wrapper(x, y, w as u32, h as u32, reverse_print, draw_op)
817    }
818
819    fn finalize(&mut self) -> ZplResult<Vec<u8>> {
820        let mut bytes = Vec::new();
821        let mut cursor = std::io::Cursor::new(&mut bytes);
822        self.canvas
823            .write_to(&mut cursor, image::ImageFormat::Png)
824            .map_err(|e| ZplError::BackendError(format!("Failed to write PNG: {}", e)))?;
825        Ok(bytes)
826    }
827}
828
829impl PngBackend {
830    /// Paints every set cell of a 2-D bit matrix as a filled rectangle,
831    /// scaling each cell to `cell_w` × `cell_h` dots and applying the
832    /// requested orientation.
833    #[allow(clippy::too_many_arguments)]
834    fn fill_matrix_cells(
835        &mut self,
836        x: u32,
837        y: u32,
838        orientation: char,
839        cell_w: u32,
840        cell_h: u32,
841        bit_matrix: &BitMatrix,
842        reverse_print: bool,
843    ) {
844        let bw = bit_matrix.getWidth();
845        let bh = bit_matrix.getHeight();
846        let full_w = bw * cell_w;
847        let full_h = bh * cell_h;
848
849        for gy in 0..bh {
850            for gx in 0..bw {
851                if !bit_matrix.get(gx, gy) {
852                    continue;
853                }
854                let lx = (gx * cell_w) as i32;
855                let ly = (gy * cell_h) as i32;
856                let (w, h) = (cell_w, cell_h);
857                let rect = match orientation {
858                    'R' => {
859                        let nx = full_h as i32 - (ly + h as i32);
860                        Rect::at(x as i32 + nx, y as i32 + lx).of_size(h, w)
861                    }
862                    'I' => {
863                        let nx = full_w as i32 - (lx + w as i32);
864                        let ny = full_h as i32 - (ly + h as i32);
865                        Rect::at(x as i32 + nx, y as i32 + ny).of_size(w, h)
866                    }
867                    'B' => {
868                        let ny = full_w as i32 - (lx + w as i32);
869                        Rect::at(x as i32 + ly, y as i32 + ny).of_size(h, w)
870                    }
871                    _ => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
872                };
873                if reverse_print {
874                    self.invert_rect(rect);
875                } else {
876                    draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
877                }
878            }
879        }
880    }
881
882    #[allow(clippy::too_many_arguments)]
883    fn draw_1d_barcode(
884        &mut self,
885        x: u32,
886        y: u32,
887        orientation: char,
888        height: u32,
889        module_width: u32,
890        ratio: f32,
891        data: &str,
892        format: BarcodeFormat,
893        reverse_print: bool,
894        interpretation_line: char,
895        interpretation_line_above: char,
896        hints: Option<EncodeHints>,
897        hints_key: &str,
898    ) -> ZplResult<()> {
899        let numeric_data;
900        let data = match format {
901            BarcodeFormat::EAN_13
902            | BarcodeFormat::EAN_8
903            | BarcodeFormat::UPC_A
904            | BarcodeFormat::UPC_E
905            | BarcodeFormat::ITF => {
906                numeric_data = data
907                    .chars()
908                    .filter(|c| c.is_ascii_digit())
909                    .collect::<String>();
910                &numeric_data
911            }
912            _ => data,
913        };
914
915        let padded_data;
916        let data = if format == BarcodeFormat::ITF && data.len() % 2 != 0 {
917            padded_data = format!("0{}", data);
918            &padded_data
919        } else {
920            data
921        };
922
923        let bit_matrix = barcode_cache::encode_cached(format, data, hints_key, hints.as_ref())?;
924
925        let mw = max(module_width, 1);
926        // Two-width symbologies are re-laid-out at the ^BY ratio; every other
927        // symbology keeps the encoder's module widths.
928        let elements = symbology::matrix_elements(
929            &bit_matrix,
930            mw,
931            symbology::is_two_width(format).then_some(ratio),
932        );
933
934        self.draw_elements(
935            x,
936            y,
937            orientation,
938            height,
939            module_width,
940            &elements,
941            reverse_print,
942            interpretation_line,
943            interpretation_line_above,
944            data,
945        )
946    }
947
948    /// Paints a 1-D barcode from pre-computed dot-width elements and draws its
949    /// interpretation line.
950    ///
951    /// Shared by the `rxing`-encoded symbologies and the natively-encoded ones,
952    /// so bar placement, rotation and interpretation-line layout stay identical.
953    #[allow(clippy::too_many_arguments)]
954    fn draw_elements(
955        &mut self,
956        x: u32,
957        y: u32,
958        orientation: char,
959        height: u32,
960        module_width: u32,
961        elements: &[symbology::Element],
962        reverse_print: bool,
963        interpretation_line: char,
964        interpretation_line_above: char,
965        data: &str,
966    ) -> ZplResult<()> {
967        let bh = height;
968        let bw: u32 = elements.iter().map(|e| e.width).sum();
969
970        let (full_w, full_h) = match orientation {
971            'N' | 'I' => (bw, bh),
972            'R' | 'B' => (bh, bw),
973            _ => (bw, bh),
974        };
975
976        let transform_rect = |lx: i32, ly: i32, w: u32, h: u32| -> Rect {
977            match orientation {
978                'N' => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
979                'R' => {
980                    let new_x = bh as i32 - (ly + h as i32);
981                    let new_y = lx;
982                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
983                }
984                'I' => {
985                    let new_x = bw as i32 - (lx + w as i32);
986                    let new_y = bh as i32 - (ly + h as i32);
987                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(w, h)
988                }
989                'B' => {
990                    let new_x = ly;
991                    let new_y = bw as i32 - (lx + w as i32);
992                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
993                }
994                _ => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
995            }
996        };
997
998        let mut cursor = 0u32;
999        for element in elements {
1000            if element.bar {
1001                let rect = transform_rect(cursor as i32, 0, element.width, bh);
1002                if reverse_print {
1003                    self.invert_rect(rect);
1004                } else {
1005                    draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
1006                }
1007            }
1008            cursor += element.width;
1009        }
1010
1011        if interpretation_line == 'Y' {
1012            let (font_char, text_h, text_w, gap) =
1013                crate::engine::font::interpretation_metrics(module_width);
1014            let text_y = if interpretation_line_above == 'Y' {
1015                y.saturating_sub(text_h + gap)
1016            } else {
1017                y + full_h + gap
1018            };
1019
1020            let text_width = self.get_text_width(data, font_char, Some(text_h), Some(text_w));
1021            let text_x = if full_w > text_width {
1022                x + (full_w - text_width) / 2
1023            } else {
1024                x
1025            };
1026
1027            self.draw_text(
1028                text_x,
1029                text_y,
1030                font_char,
1031                Some(text_h),
1032                Some(text_w),
1033                'N',
1034                data,
1035                false,
1036                None,
1037            )?;
1038        }
1039
1040        Ok(())
1041    }
1042
1043    /// Paints a POSTNET symbol.
1044    ///
1045    /// POSTNET is the one supported symbology that encodes data in bar *height*
1046    /// rather than bar width, so it needs its own painter: all bars share the
1047    /// module width and sit on a fixed pitch, and a binary 0 is a bottom-aligned
1048    /// half-height bar.
1049    #[allow(clippy::too_many_arguments)]
1050    fn draw_postnet(
1051        &mut self,
1052        x: u32,
1053        y: u32,
1054        orientation: char,
1055        height: u32,
1056        module_width: u32,
1057        data: &str,
1058        reverse_print: bool,
1059        interpretation_line: char,
1060        interpretation_line_above: char,
1061    ) -> ZplResult<()> {
1062        let bars = symbology::postnet_bars(data);
1063        let (bar_w, gap) = symbology::postnet_pitch(module_width);
1064        let pitch = bar_w + gap;
1065        let short_h = ((height as f32) * symbology::POSTNET_SHORT_RATIO).round() as u32;
1066        let full_w = bars.len() as u32 * bar_w + bars.len().saturating_sub(1) as u32 * gap;
1067
1068        for (i, bar) in bars.iter().enumerate() {
1069            let h = match bar {
1070                symbology::BarHeight::Full => height,
1071                symbology::BarHeight::Half => short_h,
1072            };
1073            // Short bars are bottom-aligned with the full-height bars.
1074            let top = height.saturating_sub(h);
1075            let lx = i as u32 * pitch;
1076            let rect = match orientation {
1077                'R' => Rect::at(x as i32 + top as i32, y as i32 + lx as i32).of_size(h, bar_w),
1078                'I' => Rect::at(
1079                    x as i32 + (full_w - lx - bar_w) as i32,
1080                    y as i32 + (height - h - top) as i32,
1081                )
1082                .of_size(bar_w, h),
1083                'B' => Rect::at(
1084                    x as i32 + (height - h - top) as i32,
1085                    y as i32 + (full_w - lx - bar_w) as i32,
1086                )
1087                .of_size(h, bar_w),
1088                _ => Rect::at(x as i32 + lx as i32, y as i32 + top as i32).of_size(bar_w, h),
1089            };
1090            if reverse_print {
1091                self.invert_rect(rect);
1092            } else {
1093                draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
1094            }
1095        }
1096
1097        if interpretation_line == 'Y' {
1098            let (font_char, text_h, text_w, gap) =
1099                crate::engine::font::interpretation_metrics(module_width);
1100            let text_y = if interpretation_line_above == 'Y' {
1101                y.saturating_sub(text_h + gap)
1102            } else {
1103                y + height + gap
1104            };
1105            let digits: String = data.chars().filter(|c| c.is_ascii_digit()).collect();
1106            let text_width = self.get_text_width(&digits, font_char, Some(text_h), Some(text_w));
1107            let text_x = if full_w > text_width {
1108                x + (full_w - text_width) / 2
1109            } else {
1110                x
1111            };
1112            self.draw_text(
1113                text_x,
1114                text_y,
1115                font_char,
1116                Some(text_h),
1117                Some(text_w),
1118                'N',
1119                &digits,
1120                false,
1121                None,
1122            )?;
1123        }
1124
1125        Ok(())
1126    }
1127}