Skip to main content

zpl_forge/forge/
pdf_native.rs

1//! Native vector PDF rendering backend for ZPL label output.
2//!
3//! Unlike [`PdfBackend`](super::pdf::PdfBackend), which rasterizes labels to PNG
4//! first, this backend renders text, shapes, barcodes and images as native PDF
5//! vector operations for maximum quality and minimal file size.
6
7use std::cmp::max;
8use std::collections::{HashMap, HashSet};
9use std::io::Write;
10use std::sync::Arc;
11
12use ab_glyph::{Font, PxScale, ScaleFont};
13use base64::{Engine as _, engine::general_purpose};
14use flate2::Compression;
15use flate2::write::ZlibEncoder;
16use lopdf::content::{Content, Operation};
17use lopdf::{Document, FontData, Object, Stream, dictionary};
18use rxing::{
19    BarcodeFormat, EncodeHintType, EncodeHintValue, EncodeHints, MultiFormatWriter, Writer,
20};
21
22use crate::engine::{FontManager, ZplForgeBackend};
23use crate::{ZplError, ZplResult};
24
25/// Bézier control-point factor for approximating a quarter-circle arc.
26const KAPPA: f64 = 0.5522847498;
27
28// ─── Internal types ─────────────────────────────────────────────────────────
29
30/// Collected image data to be embedded as a PDF XObject during [`PdfNativeBackend::finalize`].
31struct ImageXObject {
32    name: String,
33    data: Vec<u8>,
34    width: u32,
35    height: u32,
36}
37
38// ─── Public struct ──────────────────────────────────────────────────────────
39
40/// A rendering backend that produces PDF documents with native vector operations.
41///
42/// Text is rendered using an embedded TrueType font, shapes are drawn as PDF
43/// paths with Bézier curves, and barcodes are composed of filled rectangles.
44/// Bitmap data (graphic fields, custom images) is embedded as compressed
45/// XObject image streams.
46pub struct PdfNativeBackend {
47    width_dots: f64,
48    height_dots: f64,
49    width_pt: f64,
50    height_pt: f64,
51    resolution: f32,
52    /// `72.0 / dpi` – multiplier that converts dots to PDF points.
53    scale: f64,
54    operations: Vec<Operation>,
55    font_manager: Option<Arc<FontManager>>,
56    images: Vec<ImageXObject>,
57    image_counter: usize,
58    /// Tracks which font identifiers (e.g. 'A', 'B', '0') have been used during rendering.
59    used_fonts: HashSet<char>,
60    compression: Compression,
61}
62
63impl Default for PdfNativeBackend {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69// ─── Construction ───────────────────────────────────────────────────────────
70
71impl PdfNativeBackend {
72    /// Creates a new `PdfNativeBackend` with default settings.
73    pub fn new() -> Self {
74        Self {
75            width_dots: 0.0,
76            height_dots: 0.0,
77            width_pt: 0.0,
78            height_pt: 0.0,
79            resolution: 0.0,
80            scale: 0.0,
81            operations: Vec::new(),
82            font_manager: None,
83            images: Vec::new(),
84            image_counter: 0,
85            used_fonts: HashSet::new(),
86            compression: Compression::default(),
87        }
88    }
89
90    /// Sets the zlib compression level for the PDF output (builder pattern).
91    pub fn with_compression(mut self, compression: Compression) -> Self {
92        self.compression = compression;
93        self
94    }
95}
96
97// ─── Private helpers ────────────────────────────────────────────────────────
98
99impl PdfNativeBackend {
100    // ── coordinate helpers ──────────────────────────────────────────
101
102    /// Convert a measurement in dots to PDF points.
103    #[inline]
104    fn d2pt(&self, dots: f64) -> f64 {
105        dots * self.scale
106    }
107
108    /// ZPL x-dot → PDF x-point (origin stays at the left).
109    #[inline]
110    fn x_pt(&self, x: f64) -> f64 {
111        x * self.scale
112    }
113
114    /// PDF y for the **bottom** edge of an object whose top-left is at ZPL row
115    /// `y` with height `h` (both in dots).
116    #[inline]
117    fn y_pt_bottom(&self, y: f64, h: f64) -> f64 {
118        self.height_pt - (y + h) * self.scale
119    }
120
121    // ── colour helpers ─────────────────────────────────────────────
122
123    /// Parse `#RRGGBB` / `#RGB` into `(r, g, b)` in 0.0 – 1.0.  Defaults to
124    /// black when the string is absent or malformed.
125    fn parse_hex_color_f64(color: &Option<String>) -> (f64, f64, f64) {
126        if let Some(hex) = color {
127            let hex = hex.trim_start_matches('#');
128            if hex.len() == 6 {
129                if let (Ok(r), Ok(g), Ok(b)) = (
130                    u8::from_str_radix(&hex[0..2], 16),
131                    u8::from_str_radix(&hex[2..4], 16),
132                    u8::from_str_radix(&hex[4..6], 16),
133                ) {
134                    return (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
135                }
136            } else if hex.len() == 3
137                && let (Ok(r), Ok(g), Ok(b)) = (
138                    u8::from_str_radix(&hex[0..1], 16),
139                    u8::from_str_radix(&hex[1..2], 16),
140                    u8::from_str_radix(&hex[2..3], 16),
141                )
142            {
143                return (
144                    r as f64 * 17.0 / 255.0,
145                    g as f64 * 17.0 / 255.0,
146                    b as f64 * 17.0 / 255.0,
147                );
148            }
149        }
150        (0.0, 0.0, 0.0)
151    }
152
153    /// Resolve the *draw* and *clear* colours for a graphic element.
154    ///
155    /// Follows the same logic as `PngBackend`:
156    /// - custom hex colour → (custom, white)
157    /// - `'B'` → (black, white)
158    /// - `'W'` → (white, black)
159    fn resolve_colors(
160        color: char,
161        custom_color: &Option<String>,
162    ) -> ((f64, f64, f64), (f64, f64, f64)) {
163        if custom_color.is_some() {
164            (Self::parse_hex_color_f64(custom_color), (1.0, 1.0, 1.0))
165        } else if color == 'B' {
166            ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0))
167        } else {
168            ((1.0, 1.0, 1.0), (0.0, 0.0, 0.0))
169        }
170    }
171
172    // ── low-level PDF operation emitters ────────────────────────────
173
174    fn op(&mut self, operator: &str, operands: Vec<Object>) {
175        self.operations.push(Operation::new(operator, operands));
176    }
177
178    fn set_fill_color(&mut self, r: f64, g: f64, b: f64) {
179        self.op("rg", vec![r.into(), g.into(), b.into()]);
180    }
181
182    fn save_state(&mut self) {
183        self.op("q", vec![]);
184    }
185
186    fn restore_state(&mut self) {
187        self.op("Q", vec![]);
188    }
189
190    /// Enter *reverse-print* mode: save state, activate the `Difference` blend
191    /// mode ExtGState, and set the fill colour to white so that drawing
192    /// effectively XOR-inverts the background.
193    fn begin_reverse(&mut self) {
194        self.save_state();
195        self.op("gs", vec!["GSDiff".into()]);
196        self.set_fill_color(1.0, 1.0, 1.0);
197    }
198
199    fn end_reverse(&mut self) {
200        self.restore_state();
201    }
202
203    // ── path construction ──────────────────────────────────────────
204
205    /// Append path operators for a rounded rectangle.
206    ///
207    /// `(x, y)` is the **bottom-left** corner in PDF coordinates; `w` and `h`
208    /// extend to the right and upward.
209    fn push_rounded_rect_path(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
210        let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
211        if r < 0.001 {
212            self.op("re", vec![x.into(), y.into(), w.into(), h.into()]);
213            return;
214        }
215        let kr = KAPPA * r;
216        // bottom-left → right along bottom edge
217        self.op("m", vec![(x + r).into(), y.into()]);
218        self.op("l", vec![(x + w - r).into(), y.into()]);
219        // bottom-right corner
220        self.op(
221            "c",
222            vec![
223                (x + w - r + kr).into(),
224                y.into(),
225                (x + w).into(),
226                (y + r - kr).into(),
227                (x + w).into(),
228                (y + r).into(),
229            ],
230        );
231        // right edge upward
232        self.op("l", vec![(x + w).into(), (y + h - r).into()]);
233        // top-right corner
234        self.op(
235            "c",
236            vec![
237                (x + w).into(),
238                (y + h - r + kr).into(),
239                (x + w - r + kr).into(),
240                (y + h).into(),
241                (x + w - r).into(),
242                (y + h).into(),
243            ],
244        );
245        // top edge leftward
246        self.op("l", vec![(x + r).into(), (y + h).into()]);
247        // top-left corner
248        self.op(
249            "c",
250            vec![
251                (x + r - kr).into(),
252                (y + h).into(),
253                x.into(),
254                (y + h - r + kr).into(),
255                x.into(),
256                (y + h - r).into(),
257            ],
258        );
259        // left edge downward
260        self.op("l", vec![x.into(), (y + r).into()]);
261        // bottom-left corner
262        self.op(
263            "c",
264            vec![
265                x.into(),
266                (y + r - kr).into(),
267                (x + r - kr).into(),
268                y.into(),
269                (x + r).into(),
270                y.into(),
271            ],
272        );
273        self.op("h", vec![]);
274    }
275
276    /// Append path operators for an ellipse centred at `(cx, cy)` with radii
277    /// `(rx, ry)`, approximated by four cubic Bézier curves.
278    fn push_ellipse_path(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) {
279        let kx = KAPPA * rx;
280        let ky = KAPPA * ry;
281        // start at 3-o'clock
282        self.op("m", vec![(cx + rx).into(), cy.into()]);
283        // → 12-o'clock
284        self.op(
285            "c",
286            vec![
287                (cx + rx).into(),
288                (cy + ky).into(),
289                (cx + kx).into(),
290                (cy + ry).into(),
291                cx.into(),
292                (cy + ry).into(),
293            ],
294        );
295        // → 9-o'clock
296        self.op(
297            "c",
298            vec![
299                (cx - kx).into(),
300                (cy + ry).into(),
301                (cx - rx).into(),
302                (cy + ky).into(),
303                (cx - rx).into(),
304                cy.into(),
305            ],
306        );
307        // → 6-o'clock
308        self.op(
309            "c",
310            vec![
311                (cx - rx).into(),
312                (cy - ky).into(),
313                (cx - kx).into(),
314                (cy - ry).into(),
315                cx.into(),
316                (cy - ry).into(),
317            ],
318        );
319        // → back to 3-o'clock
320        self.op(
321            "c",
322            vec![
323                (cx + kx).into(),
324                (cy - ry).into(),
325                (cx + rx).into(),
326                (cy - ky).into(),
327                (cx + rx).into(),
328                cy.into(),
329            ],
330        );
331        self.op("h", vec![]);
332    }
333
334    // ── font / text helpers ────────────────────────────────────────
335
336    fn get_font_arc(&self, font_char: char) -> ZplResult<&ab_glyph::FontArc> {
337        let fm = self
338            .font_manager
339            .as_ref()
340            .ok_or_else(|| ZplError::FontError("Font manager not initialized".into()))?;
341        fm.get_font(&font_char.to_string())
342            .or_else(|| fm.get_font("0"))
343            .ok_or_else(|| ZplError::FontError(format!("Font not found: {}", font_char)))
344    }
345
346    fn get_text_width(
347        &self,
348        text: &str,
349        font_char: char,
350        height: Option<u32>,
351        width: Option<u32>,
352    ) -> u32 {
353        let font = match self.get_font_arc(font_char) {
354            Ok(f) => f,
355            Err(_) => return 0,
356        };
357        let scale_y = height.unwrap_or(9) as f32;
358        let scale_x = width.unwrap_or(scale_y as u32) as f32;
359        let scale = PxScale {
360            x: scale_x,
361            y: scale_y,
362        };
363        let scaled = font.as_scaled(scale);
364        let mut w = 0.0_f32;
365        let mut last_glyph = None;
366        for c in text.chars() {
367            let gid = font.glyph_id(c);
368            if let Some(prev) = last_glyph {
369                w += scaled.kern(prev, gid);
370            }
371            w += scaled.h_advance(gid);
372            last_glyph = Some(gid);
373        }
374        w.ceil() as u32
375    }
376
377    // ── image embedding ────────────────────────────────────────────
378
379    /// Store raw RGB image data as a future XObject and emit the `cm` + `Do`
380    /// operators that place it on the page.
381    fn embed_rgb_image(
382        &mut self,
383        x_dots: f64,
384        y_dots: f64,
385        img_w: u32,
386        img_h: u32,
387        rgb_data: Vec<u8>,
388    ) {
389        let name = format!("Im{}", self.image_counter);
390        self.image_counter += 1;
391
392        let px = self.x_pt(x_dots);
393        let py = self.y_pt_bottom(y_dots, img_h as f64);
394        let pw = self.d2pt(img_w as f64);
395        let ph = self.d2pt(img_h as f64);
396
397        self.save_state();
398        self.op(
399            "cm",
400            vec![
401                pw.into(),
402                0.into(),
403                0.into(),
404                ph.into(),
405                px.into(),
406                py.into(),
407            ],
408        );
409        self.op("Do", vec![Object::Name(name.as_bytes().to_vec())]);
410        self.restore_state();
411
412        self.images.push(ImageXObject {
413            name,
414            data: rgb_data,
415            width: img_w,
416            height: img_h,
417        });
418    }
419
420    // ── barcode orientation transforms ─────────────────────────────
421
422    /// Map a local rectangle inside a 1-D barcode to absolute dot coordinates
423    /// according to the requested orientation.
424    ///
425    /// Returns `(abs_x, abs_y, width, height)` – all in dots.
426    #[allow(clippy::too_many_arguments)]
427    fn transform_1d_bar(
428        orientation: char,
429        base_x: u32,
430        base_y: u32,
431        lx: i32,
432        ly: i32,
433        w: u32,
434        h: u32,
435        bw: u32,
436        bh: u32,
437    ) -> (i32, i32, u32, u32) {
438        match orientation {
439            'R' => {
440                let nx = bh as i32 - (ly + h as i32);
441                let ny = lx;
442                (base_x as i32 + nx, base_y as i32 + ny, h, w)
443            }
444            'I' => {
445                let nx = bw as i32 - (lx + w as i32);
446                let ny = bh as i32 - (ly + h as i32);
447                (base_x as i32 + nx, base_y as i32 + ny, w, h)
448            }
449            'B' => {
450                let nx = ly;
451                let ny = bw as i32 - (lx + w as i32);
452                (base_x as i32 + nx, base_y as i32 + ny, h, w)
453            }
454            _ => (base_x as i32 + lx, base_y as i32 + ly, w, h),
455        }
456    }
457
458    /// Same as [`Self::transform_1d_bar`] but for 2-D codes (QR).
459    #[allow(clippy::too_many_arguments)]
460    fn transform_2d_cell(
461        orientation: char,
462        base_x: u32,
463        base_y: u32,
464        lx: i32,
465        ly: i32,
466        w: u32,
467        h: u32,
468        full_w: u32,
469        full_h: u32,
470    ) -> (i32, i32, u32, u32) {
471        match orientation {
472            'R' => {
473                let nx = full_h as i32 - (ly + h as i32);
474                let ny = lx;
475                (base_x as i32 + nx, base_y as i32 + ny, h, w)
476            }
477            'I' => {
478                let nx = full_w as i32 - (lx + w as i32);
479                let ny = full_h as i32 - (ly + h as i32);
480                (base_x as i32 + nx, base_y as i32 + ny, w, h)
481            }
482            'B' => {
483                let nx = ly;
484                let ny = full_w as i32 - (lx + w as i32);
485                (base_x as i32 + nx, base_y as i32 + ny, h, w)
486            }
487            _ => (base_x as i32 + lx, base_y as i32 + ly, w, h),
488        }
489    }
490
491    // ── 1-D barcode rendering (shared by Code 128 / Code 39) ──────
492
493    #[allow(clippy::too_many_arguments)]
494    fn draw_1d_barcode(
495        &mut self,
496        x: u32,
497        y: u32,
498        orientation: char,
499        height: u32,
500        module_width: u32,
501        data: &str,
502        format: BarcodeFormat,
503        reverse_print: bool,
504        interpretation_line: char,
505        interpretation_line_above: char,
506        hints: Option<EncodeHints>,
507    ) -> ZplResult<()> {
508        let writer = MultiFormatWriter;
509        let bit_matrix = if let Some(h) = hints {
510            writer.encode_with_hints(data, &format, 0, 0, &h)
511        } else {
512            writer.encode(data, &format, 0, 0)
513        }
514        .map_err(|e| ZplError::BackendError(format!("Barcode Generation Error: {}", e)))?;
515
516        let mw = max(module_width, 1);
517        let bh = height;
518        let bw = bit_matrix.getWidth() * mw;
519
520        let (full_w, full_h) = match orientation {
521            'R' | 'B' => (bh, bw),
522            _ => (bw, bh),
523        };
524
525        // ── emit bar rectangles ────────────────────────────────────
526        if reverse_print {
527            self.begin_reverse();
528        } else {
529            self.save_state();
530            self.set_fill_color(0.0, 0.0, 0.0);
531        }
532
533        for gx in 0..bit_matrix.getWidth() {
534            if bit_matrix.get(gx, 0) {
535                let (rx, ry, rw, rh) =
536                    Self::transform_1d_bar(orientation, x, y, (gx * mw) as i32, 0, mw, bh, bw, bh);
537                let px = self.d2pt(rx as f64);
538                let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
539                let pw = self.d2pt(rw as f64);
540                let ph = self.d2pt(rh as f64);
541                self.op("re", vec![px.into(), py.into(), pw.into(), ph.into()]);
542            }
543        }
544        self.op("f", vec![]);
545
546        if reverse_print {
547            self.end_reverse();
548        } else {
549            self.restore_state();
550        }
551
552        // ── interpretation line ────────────────────────────────────
553        if interpretation_line == 'Y' {
554            let font_char = '0';
555            let text_h: u32 = 18;
556            let text_y = if interpretation_line_above == 'Y' {
557                y.saturating_sub(text_h)
558            } else {
559                y + full_h
560            } + 6;
561
562            let text_width = self.get_text_width(data, font_char, Some(text_h), None);
563            let text_x = if full_w > text_width {
564                x + (full_w - text_width) / 2
565            } else {
566                x
567            };
568
569            self.draw_text(
570                text_x,
571                text_y,
572                font_char,
573                Some(text_h),
574                None,
575                data,
576                false,
577                None,
578            )?;
579        }
580
581        Ok(())
582    }
583}
584
585// ─── ZplForgeBackend ────────────────────────────────────────────────────────
586
587impl ZplForgeBackend for PdfNativeBackend {
588    fn setup_page(&mut self, width: f64, height: f64, resolution: f32) {
589        let dpi = if resolution == 0.0 { 203.2 } else { resolution };
590        self.width_dots = width;
591        self.height_dots = height;
592        self.resolution = dpi;
593        self.scale = 72.0 / dpi as f64;
594        self.width_pt = width * self.scale;
595        self.height_pt = height * self.scale;
596    }
597
598    fn setup_font_manager(&mut self, font_manager: &FontManager) {
599        self.font_manager = Some(Arc::new(font_manager.clone()));
600    }
601
602    // ── text ───────────────────────────────────────────────────────
603
604    fn draw_text(
605        &mut self,
606        x: u32,
607        y: u32,
608        font: char,
609        height: Option<u32>,
610        width: Option<u32>,
611        text: &str,
612        reverse_print: bool,
613        color: Option<String>,
614    ) -> ZplResult<()> {
615        if text.is_empty() {
616            return Ok(());
617        }
618
619        let scale_y_dots = height.unwrap_or(9) as f32;
620        let scale_x_dots = width.unwrap_or(scale_y_dots as u32) as f32;
621        let px_scale = PxScale {
622            x: scale_x_dots,
623            y: scale_y_dots,
624        };
625
626        // Compute ascent in a scoped borrow so `font_arc` is dropped before the mutable insert.
627        let ascent_dots = {
628            let font_arc = self.get_font_arc(font)?;
629            font_arc.as_scaled(px_scale).ascent()
630        };
631
632        self.used_fonts.insert(font);
633
634        let scale_x_pt = self.d2pt(scale_x_dots as f64);
635        let scale_y_pt = self.d2pt(scale_y_dots as f64);
636        let tx = self.x_pt(x as f64);
637        // Baseline position: page_height - (y_top + ascent) * scale
638        let ty = self.height_pt - (y as f64 + ascent_dots as f64) * self.scale;
639
640        if reverse_print {
641            self.begin_reverse();
642        } else {
643            let (r, g, b) = Self::parse_hex_color_f64(&color);
644            self.save_state();
645            self.set_fill_color(r, g, b);
646        }
647
648        self.op("BT", vec![]);
649        self.op(
650            "Tm",
651            vec![
652                scale_x_pt.into(),
653                0.into(),
654                0.into(),
655                scale_y_pt.into(),
656                tx.into(),
657                ty.into(),
658            ],
659        );
660        let font_resource_name = format!("F_{}", font);
661        self.op(
662            "Tf",
663            vec![
664                Object::Name(font_resource_name.into_bytes()),
665                Object::Real(1.0),
666            ],
667        );
668        self.op("Tj", vec![Object::string_literal(text.as_bytes().to_vec())]);
669        self.op("ET", vec![]);
670
671        if reverse_print {
672            self.end_reverse();
673        } else {
674            self.restore_state();
675        }
676
677        Ok(())
678    }
679
680    // ── graphic box (rounded rectangle) ────────────────────────────
681
682    fn draw_graphic_box(
683        &mut self,
684        x: u32,
685        y: u32,
686        width: u32,
687        height: u32,
688        thickness: u32,
689        color: char,
690        custom_color: Option<String>,
691        rounding: u32,
692        reverse_print: bool,
693    ) -> ZplResult<()> {
694        let w = max(width, 1) as f64;
695        let h = max(height, 1) as f64;
696        let t = thickness as f64;
697        let r_dots = rounding as f64 * 8.0;
698
699        let (draw_color, clear_color) = Self::resolve_colors(color, &custom_color);
700
701        let bx = self.x_pt(x as f64);
702        let by = self.y_pt_bottom(y as f64, h);
703        let bw = self.d2pt(w);
704        let bh = self.d2pt(h);
705        let br = self.d2pt(r_dots);
706
707        if reverse_print {
708            // With Difference blend-mode, drawing white inverts the area.
709            // Drawing the inner cutout a second time re-inverts it back to
710            // the original, leaving only the border ring inverted.
711            self.begin_reverse();
712            self.push_rounded_rect_path(bx, by, bw, bh, br);
713            self.op("f", vec![]);
714            if t * 2.0 < w && t * 2.0 < h {
715                let tp = self.d2pt(t);
716                let inner_r = self.d2pt((r_dots - t).max(0.0));
717                self.push_rounded_rect_path(
718                    bx + tp,
719                    by + tp,
720                    bw - tp * 2.0,
721                    bh - tp * 2.0,
722                    inner_r,
723                );
724                self.op("f", vec![]);
725            }
726            self.end_reverse();
727        } else {
728            self.save_state();
729            let (r, g, b) = draw_color;
730            self.set_fill_color(r, g, b);
731            self.push_rounded_rect_path(bx, by, bw, bh, br);
732            self.op("f", vec![]);
733
734            if t * 2.0 < w && t * 2.0 < h {
735                let (cr, cg, cb) = clear_color;
736                self.set_fill_color(cr, cg, cb);
737                let tp = self.d2pt(t);
738                let inner_r = self.d2pt((r_dots - t).max(0.0));
739                self.push_rounded_rect_path(
740                    bx + tp,
741                    by + tp,
742                    bw - tp * 2.0,
743                    bh - tp * 2.0,
744                    inner_r,
745                );
746                self.op("f", vec![]);
747            }
748            self.restore_state();
749        }
750
751        Ok(())
752    }
753
754    // ── graphic circle ─────────────────────────────────────────────
755
756    fn draw_graphic_circle(
757        &mut self,
758        x: u32,
759        y: u32,
760        radius: u32,
761        thickness: u32,
762        _color: char,
763        custom_color: Option<String>,
764        reverse_print: bool,
765    ) -> ZplResult<()> {
766        let (draw_color, _) = Self::resolve_colors('B', &custom_color);
767
768        let r_pt = self.d2pt(radius as f64);
769        // ZPL (x,y) = top-left of bounding box → centre
770        let cx_pt = self.x_pt(x as f64) + r_pt;
771        let cy_pt = self.height_pt - (y as f64 + radius as f64) * self.scale;
772
773        if reverse_print {
774            self.begin_reverse();
775            self.push_ellipse_path(cx_pt, cy_pt, r_pt, r_pt);
776            self.op("f", vec![]);
777            if radius > thickness {
778                let inner_r = self.d2pt((radius - thickness) as f64);
779                self.push_ellipse_path(cx_pt, cy_pt, inner_r, inner_r);
780                self.op("f", vec![]);
781            }
782            self.end_reverse();
783        } else {
784            self.save_state();
785            let (r, g, b) = draw_color;
786            self.set_fill_color(r, g, b);
787            self.push_ellipse_path(cx_pt, cy_pt, r_pt, r_pt);
788            self.op("f", vec![]);
789
790            if radius > thickness {
791                self.set_fill_color(1.0, 1.0, 1.0);
792                let inner_r = self.d2pt((radius - thickness) as f64);
793                self.push_ellipse_path(cx_pt, cy_pt, inner_r, inner_r);
794                self.op("f", vec![]);
795            }
796            self.restore_state();
797        }
798
799        Ok(())
800    }
801
802    // ── graphic ellipse ────────────────────────────────────────────
803
804    fn draw_graphic_ellipse(
805        &mut self,
806        x: u32,
807        y: u32,
808        width: u32,
809        height: u32,
810        thickness: u32,
811        _color: char,
812        custom_color: Option<String>,
813        reverse_print: bool,
814    ) -> ZplResult<()> {
815        let (draw_color, _) = Self::resolve_colors('B', &custom_color);
816
817        let rx_pt = self.d2pt(width as f64 / 2.0);
818        let ry_pt = self.d2pt(height as f64 / 2.0);
819        let cx_pt = self.x_pt(x as f64) + rx_pt;
820        let cy_pt = self.height_pt - (y as f64 + height as f64 / 2.0) * self.scale;
821
822        let t = thickness as f64;
823
824        if reverse_print {
825            self.begin_reverse();
826            self.push_ellipse_path(cx_pt, cy_pt, rx_pt, ry_pt);
827            self.op("f", vec![]);
828            if (width as f64 / 2.0) > t && (height as f64 / 2.0) > t {
829                let irx = self.d2pt(width as f64 / 2.0 - t);
830                let iry = self.d2pt(height as f64 / 2.0 - t);
831                self.push_ellipse_path(cx_pt, cy_pt, irx, iry);
832                self.op("f", vec![]);
833            }
834            self.end_reverse();
835        } else {
836            self.save_state();
837            let (r, g, b) = draw_color;
838            self.set_fill_color(r, g, b);
839            self.push_ellipse_path(cx_pt, cy_pt, rx_pt, ry_pt);
840            self.op("f", vec![]);
841
842            if (width as f64 / 2.0) > t && (height as f64 / 2.0) > t {
843                self.set_fill_color(1.0, 1.0, 1.0);
844                let irx = self.d2pt(width as f64 / 2.0 - t);
845                let iry = self.d2pt(height as f64 / 2.0 - t);
846                self.push_ellipse_path(cx_pt, cy_pt, irx, iry);
847                self.op("f", vec![]);
848            }
849            self.restore_state();
850        }
851
852        Ok(())
853    }
854
855    // ── graphic field (1-bit bitmap) ───────────────────────────────
856
857    fn draw_graphic_field(
858        &mut self,
859        x: u32,
860        y: u32,
861        width: u32,
862        height: u32,
863        data: &[u8],
864        _reverse_print: bool,
865    ) -> ZplResult<()> {
866        if width == 0 || height == 0 {
867            return Ok(());
868        }
869
870        let row_bytes = width.div_ceil(8) as usize;
871        let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
872
873        for row_idx in 0..height {
874            let row_start = row_idx as usize * row_bytes;
875            let row_end = (row_start + row_bytes).min(data.len());
876            let row_data = if row_start < data.len() {
877                &data[row_start..row_end]
878            } else {
879                &[]
880            };
881
882            for col in 0..width {
883                let byte_idx = (col / 8) as usize;
884                let bit_idx = 7 - (col % 8);
885                let is_set =
886                    byte_idx < row_data.len() && (row_data[byte_idx] & (1 << bit_idx)) != 0;
887                if is_set {
888                    rgb_data.extend_from_slice(&[0, 0, 0]);
889                } else {
890                    rgb_data.extend_from_slice(&[255, 255, 255]);
891                }
892            }
893        }
894
895        self.embed_rgb_image(x as f64, y as f64, width, height, rgb_data);
896        Ok(())
897    }
898
899    // ── custom colour image (base64) ───────────────────────────────
900
901    fn draw_graphic_image_custom(
902        &mut self,
903        x: u32,
904        y: u32,
905        width: u32,
906        height: u32,
907        data: &str,
908    ) -> ZplResult<()> {
909        let image_data = general_purpose::STANDARD
910            .decode(data.trim())
911            .map_err(|e| ZplError::ImageError(format!("Failed to decode base64: {}", e)))?;
912
913        let img = image::load_from_memory(&image_data)
914            .map_err(|e| ZplError::ImageError(format!("Failed to load image: {}", e)))?
915            .to_rgb8();
916
917        let (orig_w, orig_h) = img.dimensions();
918        let (target_w, target_h) = match (width, height) {
919            (0, 0) => (orig_w, orig_h),
920            (w, 0) => {
921                let h = (orig_h as f32 * (w as f32 / orig_w as f32)).round() as u32;
922                (w, h)
923            }
924            (0, h) => {
925                let w = (orig_w as f32 * (h as f32 / orig_h as f32)).round() as u32;
926                (w, h)
927            }
928            (w, h) => (w, h),
929        };
930
931        let final_img = if target_w != orig_w || target_h != orig_h {
932            image::imageops::resize(
933                &img,
934                target_w,
935                target_h,
936                image::imageops::FilterType::Lanczos3,
937            )
938        } else {
939            img
940        };
941
942        let rgb_data = final_img.into_raw();
943        self.embed_rgb_image(x as f64, y as f64, target_w, target_h, rgb_data);
944        Ok(())
945    }
946
947    // ── Code 128 barcode ───────────────────────────────────────────
948
949    fn draw_code128(
950        &mut self,
951        x: u32,
952        y: u32,
953        orientation: char,
954        height: u32,
955        module_width: u32,
956        interpretation_line: char,
957        interpretation_line_above: char,
958        _check_digit: char,
959        _mode: char,
960        data: &str,
961        reverse_print: bool,
962    ) -> ZplResult<()> {
963        let (clean_data, hint_val) = if let Some(stripped) = data.strip_prefix(">:") {
964            (stripped, Some("B"))
965        } else if let Some(stripped) = data.strip_prefix(">;") {
966            (stripped, Some("C"))
967        } else if let Some(stripped) = data.strip_prefix(">9") {
968            (stripped, Some("A"))
969        } else {
970            (data, None)
971        };
972
973        let hints = hint_val.map(|v| {
974            let mut h = HashMap::new();
975            h.insert(
976                EncodeHintType::FORCE_CODE_SET,
977                EncodeHintValue::ForceCodeSet(v.to_string()),
978            );
979            EncodeHints::from(h)
980        });
981
982        self.draw_1d_barcode(
983            x,
984            y,
985            orientation,
986            height,
987            module_width,
988            clean_data,
989            BarcodeFormat::CODE_128,
990            reverse_print,
991            interpretation_line,
992            interpretation_line_above,
993            hints,
994        )
995    }
996
997    // ── QR code ────────────────────────────────────────────────────
998
999    fn draw_qr_code(
1000        &mut self,
1001        x: u32,
1002        y: u32,
1003        orientation: char,
1004        _model: u32,
1005        magnification: u32,
1006        error_correction: char,
1007        _mask: u32,
1008        data: &str,
1009        reverse_print: bool,
1010    ) -> ZplResult<()> {
1011        let level = match error_correction {
1012            'L' => "L",
1013            'M' => "M",
1014            'Q' => "Q",
1015            'H' => "H",
1016            _ => "M",
1017        };
1018
1019        let mut hints = HashMap::new();
1020        hints.insert(
1021            EncodeHintType::ERROR_CORRECTION,
1022            EncodeHintValue::ErrorCorrection(level.to_string()),
1023        );
1024        hints.insert(
1025            EncodeHintType::MARGIN,
1026            EncodeHintValue::Margin("0".to_owned()),
1027        );
1028        let hints: EncodeHints = hints.into();
1029
1030        let writer = MultiFormatWriter;
1031        let bit_matrix = writer
1032            .encode_with_hints(data, &BarcodeFormat::QR_CODE, 0, 0, &hints)
1033            .map_err(|e| ZplError::BackendError(format!("QR Generation Error: {}", e)))?;
1034
1035        let mag = max(magnification, 1);
1036        let bw = bit_matrix.getWidth();
1037        let bh = bit_matrix.getHeight();
1038        let full_w = bw * mag;
1039        let full_h = bh * mag;
1040
1041        if reverse_print {
1042            self.begin_reverse();
1043        } else {
1044            self.save_state();
1045            self.set_fill_color(0.0, 0.0, 0.0);
1046        }
1047
1048        for gy in 0..bh {
1049            for gx in 0..bw {
1050                if bit_matrix.get(gx, gy) {
1051                    let (rx, ry, rw, rh) = Self::transform_2d_cell(
1052                        orientation,
1053                        x,
1054                        y,
1055                        (gx * mag) as i32,
1056                        (gy * mag) as i32,
1057                        mag,
1058                        mag,
1059                        full_w,
1060                        full_h,
1061                    );
1062                    let px = self.d2pt(rx as f64);
1063                    let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
1064                    let pw = self.d2pt(rw as f64);
1065                    let ph = self.d2pt(rh as f64);
1066                    self.op("re", vec![px.into(), py.into(), pw.into(), ph.into()]);
1067                }
1068            }
1069        }
1070        self.op("f", vec![]);
1071
1072        if reverse_print {
1073            self.end_reverse();
1074        } else {
1075            self.restore_state();
1076        }
1077
1078        Ok(())
1079    }
1080
1081    // ── Code 39 barcode ────────────────────────────────────────────
1082
1083    fn draw_code39(
1084        &mut self,
1085        x: u32,
1086        y: u32,
1087        orientation: char,
1088        _check_digit: char,
1089        height: u32,
1090        module_width: u32,
1091        interpretation_line: char,
1092        interpretation_line_above: char,
1093        data: &str,
1094        reverse_print: bool,
1095    ) -> ZplResult<()> {
1096        self.draw_1d_barcode(
1097            x,
1098            y,
1099            orientation,
1100            height,
1101            module_width,
1102            data,
1103            BarcodeFormat::CODE_39,
1104            reverse_print,
1105            interpretation_line,
1106            interpretation_line_above,
1107            None,
1108        )
1109    }
1110
1111    // ── finalize ───────────────────────────────────────────────────
1112
1113    fn finalize(&mut self) -> ZplResult<Vec<u8>> {
1114        let mut doc = Document::with_version("1.5");
1115        let pages_id = doc.new_object_id();
1116
1117        // ── embed fonts ────────────────────────────────────────────
1118        let default_font_bytes: &[u8] = include_bytes!("../assets/Oswald-Regular.ttf");
1119        let mut font_dict = lopdf::Dictionary::new();
1120        let mut embedded_fonts: HashSet<String> = HashSet::new();
1121
1122        for font_char in &self.used_fonts {
1123            let font_key = font_char.to_string();
1124            let resource_name = format!("F_{}", font_char);
1125
1126            // Get the font name to deduplicate (multiple chars may map to same font)
1127            let font_name = self
1128                .font_manager
1129                .as_ref()
1130                .and_then(|fm| fm.get_font_name(&font_key).map(|s| s.to_string()));
1131
1132            let actual_name = font_name.unwrap_or_else(|| "Oswald".to_string());
1133
1134            // Skip if we already embedded this font under a different char
1135            // but still add the resource alias
1136            if embedded_fonts.contains(&actual_name) {
1137                // Find the already-embedded font id by looking through font_dict
1138                // Simpler: just embed again (lopdf handles dedup at compression)
1139            }
1140
1141            let raw_bytes = self
1142                .font_manager
1143                .as_ref()
1144                .and_then(|fm| fm.get_font_bytes(&font_key))
1145                .unwrap_or(default_font_bytes);
1146
1147            let font_data = FontData::new(raw_bytes, actual_name.clone());
1148            let font_id = doc
1149                .add_font(font_data)
1150                .map_err(|e| ZplError::BackendError(format!("Failed to embed font: {}", e)))?;
1151
1152            font_dict.set(resource_name.as_str(), font_id);
1153            embedded_fonts.insert(actual_name);
1154        }
1155
1156        // ── XObject images ─────────────────────────────────────────
1157        let mut xobject_dict = lopdf::Dictionary::new();
1158        for img in &self.images {
1159            let mut encoder = ZlibEncoder::new(Vec::new(), self.compression);
1160            encoder
1161                .write_all(&img.data)
1162                .map_err(|e| ZplError::BackendError(e.to_string()))?;
1163            let compressed = encoder
1164                .finish()
1165                .map_err(|e| ZplError::BackendError(e.to_string()))?;
1166
1167            let img_stream = Stream::new(
1168                dictionary! {
1169                    "Type" => "XObject",
1170                    "Subtype" => "Image",
1171                    "Width" => img.width as i64,
1172                    "Height" => img.height as i64,
1173                    "ColorSpace" => "DeviceRGB",
1174                    "BitsPerComponent" => 8,
1175                    "Filter" => "FlateDecode",
1176                },
1177                compressed,
1178            );
1179            let img_id = doc.add_object(img_stream);
1180            xobject_dict.set(img.name.as_str(), img_id);
1181        }
1182
1183        // ── ExtGState for reverse-print blend modes ────────────────
1184        let mut gs_dict = lopdf::Dictionary::new();
1185        let gs_diff = doc.add_object(dictionary! {
1186            "Type" => "ExtGState",
1187            "BM" => "Difference",
1188        });
1189        gs_dict.set("GSDiff", gs_diff);
1190
1191        let gs_normal = doc.add_object(dictionary! {
1192            "Type" => "ExtGState",
1193            "BM" => "Normal",
1194        });
1195        gs_dict.set("GSNormal", gs_normal);
1196
1197        // ── resources ──────────────────────────────────────────────
1198        let resources_id = doc.add_object(dictionary! {
1199            "Font" => lopdf::Object::Dictionary(font_dict),
1200            "XObject" => lopdf::Object::Dictionary(xobject_dict),
1201            "ExtGState" => lopdf::Object::Dictionary(gs_dict),
1202        });
1203
1204        // ── content stream ─────────────────────────────────────────
1205        let content = Content {
1206            operations: std::mem::take(&mut self.operations),
1207        };
1208        let content_bytes = content
1209            .encode()
1210            .map_err(|e| ZplError::BackendError(format!("Failed to encode content: {}", e)))?;
1211        let content_id = doc.add_object(Stream::new(dictionary! {}, content_bytes));
1212
1213        // ── page ───────────────────────────────────────────────────
1214        let page_id = doc.add_object(dictionary! {
1215            "Type" => "Page",
1216            "Parent" => pages_id,
1217            "MediaBox" => vec![
1218                0.into(),
1219                0.into(),
1220                Object::Real(self.width_pt as f32),
1221                Object::Real(self.height_pt as f32),
1222            ],
1223            "Contents" => content_id,
1224            "Resources" => resources_id,
1225        });
1226
1227        // ── pages tree ─────────────────────────────────────────────
1228        let pages_dict = dictionary! {
1229            "Type" => "Pages",
1230            "Count" => 1_i64,
1231            "Kids" => vec![page_id.into()],
1232        };
1233        doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
1234
1235        // ── catalogue ──────────────────────────────────────────────
1236        let catalog_id = doc.add_object(dictionary! {
1237            "Type" => "Catalog",
1238            "Pages" => pages_id,
1239        });
1240        doc.trailer.set("Root", catalog_id);
1241        doc.compress();
1242
1243        // ── serialize ──────────────────────────────────────────────
1244        let mut buf = std::io::BufWriter::new(Vec::new());
1245        doc.save_to(&mut buf)
1246            .map_err(|e| ZplError::BackendError(format!("Failed to save PDF: {}", e)))?;
1247        buf.into_inner()
1248            .map_err(|e| ZplError::BackendError(format!("Failed to flush: {}", e)))
1249    }
1250}