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, PxScale, ScaleFont};
11use base64::{Engine as _, engine::general_purpose};
12use image::{ImageBuffer, Rgb, RgbImage, imageops::overlay};
13use imageproc::drawing::{
14    draw_filled_circle_mut, draw_filled_ellipse_mut, draw_filled_rect_mut, draw_text_mut,
15};
16use imageproc::rect::Rect;
17use rxing::{
18    BarcodeFormat, EncodeHintType, EncodeHintValue, EncodeHints, MultiFormatWriter, Writer,
19};
20
21use crate::engine::{FontManager, ZplForgeBackend};
22use crate::{ZplError, ZplResult};
23
24/// A rendering backend that produces PNG images.
25///
26/// This backend uses the `image` and `imageproc` crates to draw ZPL instructions
27/// onto an RGB canvas.
28pub struct PngBackend {
29    canvas: RgbImage,
30    font_manager: Option<Arc<FontManager>>,
31}
32
33impl Default for PngBackend {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl PngBackend {
40    /// Creates a new `PngBackend` instance with an empty canvas.
41    pub fn new() -> Self {
42        Self {
43            canvas: ImageBuffer::new(0, 0),
44            font_manager: None,
45        }
46    }
47
48    /// Performs an XOR overlay of a source image onto the canvas at (x, y).
49    fn xor_overlay(&mut self, src: &RgbImage, x: i64, y: i64) {
50        let (sw, sh) = src.dimensions();
51        let (cw, ch) = self.canvas.dimensions();
52
53        for sy in 0..sh {
54            let dy = y + sy as i64;
55            if dy < 0 || dy >= ch as i64 {
56                continue;
57            }
58
59            for sx in 0..sw {
60                let dx = x + sx as i64;
61                if dx < 0 || dx >= cw as i64 {
62                    continue;
63                }
64
65                let src_pixel = src[(sx, sy)];
66                if src_pixel.0 != [255, 255, 255] {
67                    let dest_pixel = &mut self.canvas[(dx as u32, dy as u32)];
68                    dest_pixel.0[0] ^= 255;
69                    dest_pixel.0[1] ^= 255;
70                    dest_pixel.0[2] ^= 255;
71                }
72            }
73        }
74    }
75
76    /// Inverts the colors within a specified rectangular area.
77    fn invert_rect(&mut self, rect: Rect) {
78        let (cw, ch) = self.canvas.dimensions();
79        let x_start = rect.left().max(0) as u32;
80        let y_start = rect.top().max(0) as u32;
81        let x_end = (rect.right() as u32).min(cw);
82        let y_end = (rect.bottom() as u32).min(ch);
83
84        for py in y_start..y_end {
85            for px in x_start..x_end {
86                let pixel = &mut self.canvas[(px, py)];
87                pixel.0[0] ^= 255;
88                pixel.0[1] ^= 255;
89                pixel.0[2] ^= 255;
90            }
91        }
92    }
93
94    /// Helper to execute a drawing operation.
95    fn draw_wrapper<F>(
96        &mut self,
97        x: u32,
98        y: u32,
99        width: u32,
100        height: u32,
101        reverse_print: bool,
102        draw_op: F,
103    ) -> ZplResult<()>
104    where
105        F: FnOnce(&mut RgbImage, i32, i32),
106    {
107        if reverse_print {
108            let mut temp_buf = ImageBuffer::from_pixel(width, height, Rgb([255, 255, 255]));
109            draw_op(&mut temp_buf, 0, 0);
110            self.xor_overlay(&temp_buf, x as i64, y as i64);
111        } else {
112            draw_op(&mut self.canvas, x as i32, y as i32);
113        }
114        Ok(())
115    }
116
117    fn parse_hex_color(&self, color: &Option<String>) -> Rgb<u8> {
118        if let Some(hex) = color {
119            let hex = hex.trim_start_matches('#');
120            if hex.len() == 6 {
121                if let (Ok(r), Ok(g), Ok(b)) = (
122                    u8::from_str_radix(&hex[0..2], 16),
123                    u8::from_str_radix(&hex[2..4], 16),
124                    u8::from_str_radix(&hex[4..6], 16),
125                ) {
126                    return Rgb([r, g, b]);
127                }
128            } else if hex.len() == 3
129                && let (Ok(r), Ok(g), Ok(b)) = (
130                    u8::from_str_radix(&hex[0..1], 16),
131                    u8::from_str_radix(&hex[1..2], 16),
132                    u8::from_str_radix(&hex[2..3], 16),
133                )
134            {
135                return Rgb([r * 17, g * 17, b * 17]);
136            }
137        }
138        Rgb([0, 0, 0])
139    }
140
141    fn get_text_width(
142        &self,
143        text: &str,
144        font_char: char,
145        height: Option<u32>,
146        width: Option<u32>,
147    ) -> u32 {
148        let font = match self.font_manager.as_ref() {
149            Some(fm) => match fm.get_font(&font_char.to_string()) {
150                Some(f) => f,
151                None => match fm.get_font("0") {
152                    Some(f) => f,
153                    None => return 0,
154                },
155            },
156            None => return 0,
157        };
158
159        let scale_y = height.unwrap_or(9) as f32;
160        let scale_x = width.unwrap_or(scale_y as u32) as f32;
161        let scale = PxScale {
162            x: scale_x,
163            y: scale_y,
164        };
165
166        let scaled_font = font.as_scaled(scale);
167        let mut width = 0.0;
168        let mut last_glyph_id = None;
169
170        for c in text.chars() {
171            let glyph_id = font.glyph_id(c);
172            if let Some(last) = last_glyph_id {
173                width += scaled_font.kern(last, glyph_id);
174            }
175            width += scaled_font.h_advance(glyph_id);
176            last_glyph_id = Some(glyph_id);
177        }
178
179        width.ceil() as u32
180    }
181}
182
183impl ZplForgeBackend for PngBackend {
184    fn setup_page(&mut self, width: f64, height: f64, _resolution: f32) {
185        // Safety limit to avoid OOM: 8192x8192 is enough for most labels
186        const MAX_DIM: u32 = 8192;
187        let w = (width as u32).min(MAX_DIM);
188        let h = (height as u32).min(MAX_DIM);
189        self.canvas = ImageBuffer::from_pixel(w, h, Rgb([255, 255, 255]));
190    }
191
192    fn setup_font_manager(&mut self, font_manager: &FontManager) {
193        self.font_manager = Some(Arc::new(font_manager.clone()));
194    }
195
196    fn draw_text(
197        &mut self,
198        x: u32,
199        y: u32,
200        font: char,
201        height: Option<u32>,
202        width: Option<u32>,
203        text: &str,
204        _reverse_print: bool,
205        color: Option<String>,
206    ) -> ZplResult<()> {
207        if text.is_empty() {
208            return Ok(());
209        }
210
211        let font_data = match self.font_manager.as_ref() {
212            Some(fm) => match fm.get_font(&font.to_string()) {
213                Some(f) => f,
214                None => match fm.get_font("0") {
215                    Some(f) => f,
216                    None => return Err(ZplError::FontError(format!("Font not found: {}", font))),
217                },
218            },
219            None => return Err(ZplError::FontError("Font manager not initialized".into())),
220        };
221
222        let scale_y = height.unwrap_or(9) as f32;
223        let scale_x = width.unwrap_or(scale_y as u32) as f32;
224        let scale = PxScale {
225            x: scale_x,
226            y: scale_y,
227        };
228
229        let text_color = self.parse_hex_color(&color);
230
231        draw_text_mut(
232            &mut self.canvas,
233            text_color,
234            x as i32,
235            y as i32,
236            scale,
237            font_data,
238            text,
239        );
240        Ok(())
241    }
242
243    fn draw_graphic_box(
244        &mut self,
245        x: u32,
246        y: u32,
247        width: u32,
248        height: u32,
249        thickness: u32,
250        color: char,
251        custom_color: Option<String>,
252        rounding: u32,
253        reverse_print: bool,
254    ) -> ZplResult<()> {
255        let w = max(width, 1);
256        let h = max(height, 1);
257        let t = thickness;
258        let r = (rounding as f64 * 8.0) as i32;
259
260        let (draw_color, clear_color) = if let Some(custom) = custom_color {
261            (self.parse_hex_color(&Some(custom)), Rgb([255, 255, 255]))
262        } else if color == 'B' {
263            (Rgb([0, 0, 0]), Rgb([255, 255, 255]))
264        } else {
265            (Rgb([255, 255, 255]), Rgb([0, 0, 0]))
266        };
267
268        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
269            let draw_rounded_fill =
270                |img: &mut RgbImage, px: i32, py: i32, pw: u32, ph: u32, pr: i32, pc: Rgb<u8>| {
271                    if pw == 0 || ph == 0 {
272                        return;
273                    }
274                    if pr <= 0 {
275                        draw_filled_rect_mut(img, Rect::at(px, py).of_size(pw, ph), pc);
276                    } else {
277                        let pr = pr.max(0).min((pw / 2) as i32).min((ph / 2) as i32);
278                        let inner_w = pw.saturating_sub(2 * pr as u32).max(1);
279                        let inner_h = ph.saturating_sub(2 * pr as u32).max(1);
280                        draw_filled_rect_mut(img, Rect::at(px + pr, py).of_size(inner_w, ph), pc);
281                        draw_filled_rect_mut(img, Rect::at(px, py + pr).of_size(pw, inner_h), pc);
282                        draw_filled_circle_mut(img, (px + pr, py + pr), pr, pc);
283                        draw_filled_circle_mut(img, (px + pw as i32 - pr - 1, py + pr), pr, pc);
284                        draw_filled_circle_mut(img, (px + pr, py + ph as i32 - pr - 1), pr, pc);
285                        draw_filled_circle_mut(
286                            img,
287                            (px + pw as i32 - pr - 1, py + ph as i32 - pr - 1),
288                            pr,
289                            pc,
290                        );
291                    }
292                };
293
294            draw_rounded_fill(img, px, py, w, h, r, draw_color);
295            if t * 2 < w && t * 2 < h {
296                draw_rounded_fill(
297                    img,
298                    px + t as i32,
299                    py + t as i32,
300                    w - t * 2,
301                    h - t * 2,
302                    (r - t as i32).max(0),
303                    clear_color,
304                );
305            }
306        };
307
308        self.draw_wrapper(x, y, w, h, reverse_print, draw_op)
309    }
310
311    fn draw_graphic_circle(
312        &mut self,
313        x: u32,
314        y: u32,
315        radius: u32,
316        thickness: u32,
317        _color: char,
318        custom_color: Option<String>,
319        reverse_print: bool,
320    ) -> ZplResult<()> {
321        let color = self.parse_hex_color(&custom_color);
322        let clear_color = Rgb([255, 255, 255]);
323
324        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
325            let center_x = px + radius as i32;
326            let center_y = py + radius as i32;
327            draw_filled_circle_mut(img, (center_x, center_y), radius as i32, color);
328
329            if radius > thickness {
330                draw_filled_circle_mut(
331                    img,
332                    (center_x, center_y),
333                    (radius - thickness) as i32,
334                    clear_color,
335                );
336            }
337        };
338
339        self.draw_wrapper(x, y, radius * 2, radius * 2, reverse_print, draw_op)
340    }
341
342    fn draw_graphic_ellipse(
343        &mut self,
344        x: u32,
345        y: u32,
346        width: u32,
347        height: u32,
348        thickness: u32,
349        _color: char,
350        custom_color: Option<String>,
351        reverse_print: bool,
352    ) -> ZplResult<()> {
353        let color = self.parse_hex_color(&custom_color);
354        let clear_color = Rgb([255, 255, 255]);
355
356        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
357            let rx = (width / 2) as i32;
358            let ry = (height / 2) as i32;
359            let center_x = px + rx;
360            let center_y = py + ry;
361            draw_filled_ellipse_mut(img, (center_x, center_y), rx, ry, color);
362
363            let t = thickness as i32;
364            if rx > t && ry > t {
365                draw_filled_ellipse_mut(img, (center_x, center_y), rx - t, ry - t, clear_color);
366            }
367        };
368
369        self.draw_wrapper(x, y, width, height, reverse_print, draw_op)
370    }
371
372    fn draw_graphic_field(
373        &mut self,
374        x: u32,
375        y: u32,
376        width: u32,
377        height: u32,
378        data: &[u8],
379        reverse_print: bool,
380    ) -> ZplResult<()> {
381        let draw_op = |img: &mut RgbImage, px: i32, py: i32| {
382            let row_bytes = width.div_ceil(8);
383            let (img_w, img_h) = (img.width() as i32, img.height() as i32);
384
385            for (row_idx, row_data) in data.chunks(row_bytes as usize).enumerate() {
386                let dy = py + row_idx as i32;
387                if dy < 0 || dy >= img_h || row_idx as u32 >= height {
388                    continue;
389                }
390
391                for (byte_idx, &byte) in row_data.iter().enumerate() {
392                    if byte == 0 {
393                        continue;
394                    }
395                    let base_x = px + (byte_idx as i32 * 8);
396                    for bit_idx in 0..8 {
397                        let col_idx = byte_idx as u32 * 8 + bit_idx;
398                        if col_idx >= width {
399                            break;
400                        }
401
402                        if (byte & (0x80 >> bit_idx)) != 0 {
403                            let dx = base_x + bit_idx as i32;
404                            if dx >= 0 && dx < img_w {
405                                img[(dx as u32, dy as u32)] = Rgb([0, 0, 0]);
406                            }
407                        }
408                    }
409                }
410            }
411        };
412
413        self.draw_wrapper(x, y, width, height, reverse_print, draw_op)
414    }
415
416    fn draw_graphic_image_custom(
417        &mut self,
418        x: u32,
419        y: u32,
420        width: u32,
421        height: u32,
422        data: &str,
423    ) -> ZplResult<()> {
424        let image_data = general_purpose::STANDARD
425            .decode(data.trim())
426            .map_err(|e| ZplError::ImageError(format!("Failed to decode base64: {}", e)))?;
427
428        let img = image::load_from_memory(&image_data)
429            .map_err(|e| ZplError::ImageError(format!("Failed to load image: {}", e)))?
430            .to_rgb8();
431
432        let (orig_w, orig_h) = img.dimensions();
433        let (target_w, target_h) = match (width, height) {
434            (0, 0) => (orig_w, orig_h),
435            (w, 0) => {
436                let h = (orig_h as f32 * (w as f32 / orig_w as f32)).round() as u32;
437                (w, h)
438            }
439            (0, h) => {
440                let w = (orig_w as f32 * (h as f32 / orig_h as f32)).round() as u32;
441                (w, h)
442            }
443            (w, h) => (w, h),
444        };
445
446        let resized_img = if target_w != orig_w || target_h != orig_h {
447            image::imageops::resize(
448                &img,
449                target_w,
450                target_h,
451                image::imageops::FilterType::Lanczos3,
452            )
453        } else {
454            img
455        };
456
457        overlay(&mut self.canvas, &resized_img, x as i64, y as i64);
458        Ok(())
459    }
460
461    fn draw_code128(
462        &mut self,
463        x: u32,
464        y: u32,
465        orientation: char,
466        height: u32,
467        module_width: u32,
468        interpretation_line: char,
469        interpretation_line_above: char,
470        _check_digit: char,
471        _mode: char,
472        data: &str,
473        reverse_print: bool,
474    ) -> ZplResult<()> {
475        let (clean_data, hint_val) = if let Some(stripped) = data.strip_prefix(">:") {
476            (stripped, Some("B"))
477        } else if let Some(stripped) = data.strip_prefix(">;") {
478            (stripped, Some("C"))
479        } else if let Some(stripped) = data.strip_prefix(">9") {
480            (stripped, Some("A"))
481        } else {
482            (data, None)
483        };
484
485        let hints = hint_val.map(|v| {
486            let mut h = HashMap::new();
487            h.insert(
488                EncodeHintType::FORCE_CODE_SET,
489                EncodeHintValue::ForceCodeSet(v.to_string()),
490            );
491            EncodeHints::from(h)
492        });
493
494        self.draw_1d_barcode(
495            x,
496            y,
497            orientation,
498            height,
499            module_width,
500            clean_data,
501            BarcodeFormat::CODE_128,
502            reverse_print,
503            interpretation_line,
504            interpretation_line_above,
505            hints,
506        )
507    }
508
509    fn draw_qr_code(
510        &mut self,
511        x: u32,
512        y: u32,
513        orientation: char,
514        _model: u32,
515        magnification: u32,
516        error_correction: char,
517        _mask: u32,
518        data: &str,
519        reverse_print: bool,
520    ) -> ZplResult<()> {
521        let level = match error_correction {
522            'L' => "L",
523            'M' => "M",
524            'Q' => "Q",
525            'H' => "H",
526            _ => "M",
527        };
528
529        let mut hints = HashMap::new();
530        hints.insert(
531            EncodeHintType::ERROR_CORRECTION,
532            EncodeHintValue::ErrorCorrection(level.to_string()),
533        );
534        hints.insert(
535            EncodeHintType::MARGIN,
536            EncodeHintValue::Margin("0".to_owned()),
537        );
538        let hints: EncodeHints = hints.into();
539
540        let writer = MultiFormatWriter;
541        let bit_matrix = writer
542            .encode_with_hints(data, &BarcodeFormat::QR_CODE, 0, 0, &hints)
543            .map_err(|e| ZplError::BackendError(format!("QR Generation Error: {}", e)))?;
544
545        let mag = max(magnification, 1);
546        let bw = bit_matrix.getWidth();
547        let bh = bit_matrix.getHeight();
548        let full_width = bw * mag;
549        let full_height = bh * mag;
550
551        let transform_rect = |lx: i32, ly: i32, w: u32, h: u32| -> Rect {
552            match orientation {
553                'N' => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
554                'R' => {
555                    let new_x = full_height as i32 - (ly + h as i32);
556                    let new_y = lx;
557                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
558                }
559                'I' => {
560                    let new_x = full_width as i32 - (lx + w as i32);
561                    let new_y = full_height as i32 - (ly + h as i32);
562                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(w, h)
563                }
564                'B' => {
565                    let new_x = ly;
566                    let new_y = full_width as i32 - (lx + w as i32);
567                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
568                }
569                _ => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
570            }
571        };
572
573        for gy in 0..bh {
574            for gx in 0..bw {
575                if bit_matrix.get(gx, gy) {
576                    let rect = transform_rect((gx * mag) as i32, (gy * mag) as i32, mag, mag);
577                    if reverse_print {
578                        self.invert_rect(rect);
579                    } else {
580                        draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
581                    }
582                }
583            }
584        }
585
586        Ok(())
587    }
588
589    fn draw_code39(
590        &mut self,
591        x: u32,
592        y: u32,
593        orientation: char,
594        _check_digit: char,
595        height: u32,
596        module_width: u32,
597        interpretation_line: char,
598        interpretation_line_above: char,
599        data: &str,
600        reverse_print: bool,
601    ) -> ZplResult<()> {
602        self.draw_1d_barcode(
603            x,
604            y,
605            orientation,
606            height,
607            module_width,
608            data,
609            BarcodeFormat::CODE_39,
610            reverse_print,
611            interpretation_line,
612            interpretation_line_above,
613            None,
614        )
615    }
616
617    fn finalize(&mut self) -> ZplResult<Vec<u8>> {
618        let mut bytes = Vec::new();
619        let mut cursor = std::io::Cursor::new(&mut bytes);
620        self.canvas
621            .write_to(&mut cursor, image::ImageFormat::Png)
622            .map_err(|e| ZplError::BackendError(format!("Failed to write PNG: {}", e)))?;
623        Ok(bytes)
624    }
625}
626
627impl PngBackend {
628    #[allow(clippy::too_many_arguments)]
629    fn draw_1d_barcode(
630        &mut self,
631        x: u32,
632        y: u32,
633        orientation: char,
634        height: u32,
635        module_width: u32,
636        data: &str,
637        format: BarcodeFormat,
638        reverse_print: bool,
639        interpretation_line: char,
640        interpretation_line_above: char,
641        hints: Option<EncodeHints>,
642    ) -> ZplResult<()> {
643        let writer = MultiFormatWriter;
644        let bit_matrix = if let Some(h) = hints {
645            writer.encode_with_hints(data, &format, 0, 0, &h)
646        } else {
647            writer.encode(data, &format, 0, 0)
648        }
649        .map_err(|e| ZplError::BackendError(format!("Barcode Generation Error: {}", e)))?;
650
651        let mw = max(module_width, 1);
652        let bh = height;
653        let bw = bit_matrix.getWidth() * mw;
654
655        let (full_w, full_h) = match orientation {
656            'N' | 'I' => (bw, bh),
657            'R' | 'B' => (bh, bw),
658            _ => (bw, bh),
659        };
660
661        let transform_rect = |lx: i32, ly: i32, w: u32, h: u32| -> Rect {
662            match orientation {
663                'N' => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
664                'R' => {
665                    let new_x = bh as i32 - (ly + h as i32);
666                    let new_y = lx;
667                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
668                }
669                'I' => {
670                    let new_x = bw as i32 - (lx + w as i32);
671                    let new_y = bh as i32 - (ly + h as i32);
672                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(w, h)
673                }
674                'B' => {
675                    let new_x = ly;
676                    let new_y = bw as i32 - (lx + w as i32);
677                    Rect::at(x as i32 + new_x, y as i32 + new_y).of_size(h, w)
678                }
679                _ => Rect::at(x as i32 + lx, y as i32 + ly).of_size(w, h),
680            }
681        };
682
683        for gx in 0..bit_matrix.getWidth() {
684            if bit_matrix.get(gx, 0) {
685                let rect = transform_rect((gx * mw) as i32, 0, mw, bh);
686                if reverse_print {
687                    self.invert_rect(rect);
688                } else {
689                    draw_filled_rect_mut(&mut self.canvas, rect, Rgb([0, 0, 0]));
690                }
691            }
692        }
693
694        if interpretation_line == 'Y' {
695            let font_char = '0';
696            let text_h = 18;
697            let text_y = if interpretation_line_above == 'Y' {
698                y.saturating_sub(text_h)
699            } else {
700                y + full_h
701            } + 6;
702
703            let text_width = self.get_text_width(data, font_char, Some(text_h), None);
704            let text_x = if full_w > text_width {
705                x + (full_w - text_width) / 2
706            } else {
707                x
708            };
709
710            self.draw_text(
711                text_x,
712                text_y,
713                font_char,
714                Some(text_h),
715                None,
716                data,
717                false,
718                None,
719            )?;
720        }
721
722        Ok(())
723    }
724}