zpl_forge/forge/
png.rs

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