Skip to main content

zpl_forge/forge/
pdf_native.rs

1//! Native vector PDF rendering backend for ZPL label output.
2//!
3//! This backend renders text, shapes, barcodes and images as native PDF
4//! vector operations for maximum quality and minimal file size.
5
6use std::cmp::max;
7use std::collections::{HashMap, HashSet};
8use std::io::Write;
9use std::sync::Arc;
10
11use ab_glyph::{Font, FontArc};
12use base64::{Engine as _, engine::general_purpose};
13use flate2::Compression;
14use flate2::write::ZlibEncoder;
15use lopdf::{Document, FontData, Object, Stream, dictionary};
16use rxing::common::BitMatrix;
17use rxing::datamatrix::encoder::SymbolShapeHint;
18use rxing::{BarcodeFormat, EncodeHintType, EncodeHintValue, EncodeHints};
19
20use super::{barcode_1d_format, barcode_cache, symbology};
21use crate::engine::{Barcode1DKind, FontManager, ZplForgeBackend};
22use crate::{ZplError, ZplResult};
23
24/// Bézier control-point factor for approximating a quarter-circle arc.
25const KAPPA: f64 = 0.5522847498;
26
27// ─── WinAnsi (CP1252) encoding ──────────────────────────────────────────────
28//
29// Embedded fonts are declared with /Encoding WinAnsiEncoding, so text shown
30// with `Tj` must be CP1252 bytes — not UTF-8. This is what makes accented
31// characters (ñ, á, é...) render and copy correctly.
32
33/// Unicode characters for CP1252 codes 0x80..=0x9F (`\u{0}` = undefined).
34const CP1252_80_9F: [char; 32] = [
35    '\u{20AC}', '\u{0}', '\u{201A}', '\u{0192}', '\u{201E}', '\u{2026}', '\u{2020}', '\u{2021}',
36    '\u{02C6}', '\u{2030}', '\u{0160}', '\u{2039}', '\u{0152}', '\u{0}', '\u{017D}', '\u{0}',
37    '\u{0}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', '\u{2022}', '\u{2013}', '\u{2014}',
38    '\u{02DC}', '\u{2122}', '\u{0161}', '\u{203A}', '\u{0153}', '\u{0}', '\u{017E}', '\u{0178}',
39];
40
41/// Encodes a Unicode char to its CP1252 byte, when representable.
42fn char_to_winansi(c: char) -> Option<u8> {
43    let cp = c as u32;
44    match cp {
45        0x20..=0x7E => Some(cp as u8),
46        // CP1252 0xA0..=0xFF is identical to Latin-1.
47        0xA0..=0xFF => Some(cp as u8),
48        _ => CP1252_80_9F
49            .iter()
50            .position(|&m| m == c && m != '\u{0}')
51            .map(|i| 0x80 + i as u8),
52    }
53}
54
55/// Decodes a CP1252 byte back to its Unicode char, when defined.
56fn winansi_to_char(code: u8) -> Option<char> {
57    match code {
58        0x20..=0x7E => Some(code as char),
59        0xA0..=0xFF => Some(code as char),
60        0x80..=0x9F => {
61            let c = CP1252_80_9F[(code - 0x80) as usize];
62            (c != '\u{0}').then_some(c)
63        }
64        _ => None,
65    }
66}
67
68/// Builds a ToUnicode CMap stream body for the WinAnsi code range.
69fn build_tounicode_cmap() -> Vec<u8> {
70    let mut s = String::with_capacity(4096);
71    s.push_str(
72        "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
73         /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
74         /CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n\
75         1 begincodespacerange\n<20> <FF>\nendcodespacerange\n",
76    );
77    let entries: Vec<(u8, char)> = (0x20..=0xFFu32)
78        .filter_map(|c| winansi_to_char(c as u8).map(|ch| (c as u8, ch)))
79        .collect();
80    for chunk in entries.chunks(100) {
81        s.push_str(&format!("{} beginbfchar\n", chunk.len()));
82        for (code, ch) in chunk {
83            s.push_str(&format!("<{:02X}> <{:04X}>\n", code, *ch as u32));
84        }
85        s.push_str("endbfchar\n");
86    }
87    s.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
88    s.into_bytes()
89}
90
91// ─── Internal types ─────────────────────────────────────────────────────────
92
93/// Collected image data to be embedded as a PDF XObject during [`PdfNativeBackend::finalize`].
94struct ImageXObject {
95    name: String,
96    data: Vec<u8>,
97    width: u32,
98    height: u32,
99    /// `true` for 1-bit stencil masks (`^GF` bitmaps), `false` for 8-bit RGB.
100    is_mask: bool,
101}
102
103// ─── Public struct ──────────────────────────────────────────────────────────
104
105/// A rendering backend that produces PDF documents with native vector operations.
106///
107/// Text is rendered using an embedded TrueType font, shapes are drawn as PDF
108/// paths with Bézier curves, and barcodes are composed of filled rectangles.
109/// Bitmap data (graphic fields, custom images) is embedded as compressed
110/// XObject image streams.
111pub struct PdfNativeBackend {
112    width_dots: f64,
113    height_dots: f64,
114    width_pt: f64,
115    height_pt: f64,
116    resolution: f32,
117    /// `72.0 / dpi` – multiplier that converts dots to PDF points.
118    scale: f64,
119    /// Raw PDF content-stream bytes for the page currently being drawn.
120    content: Vec<u8>,
121    /// Content streams of pages already finished via [`ZplForgeBackend::new_page`].
122    finished_pages: Vec<Vec<u8>>,
123    font_manager: Option<Arc<FontManager>>,
124    images: Vec<ImageXObject>,
125    image_counter: usize,
126    /// Tracks which font identifiers (e.g. 'A', 'B', '0') have been used during rendering.
127    used_fonts: HashSet<char>,
128    compression: Compression,
129    /// Optional document title for the PDF Info dictionary.
130    title: Option<String>,
131    /// Solid rectangles painted on the current page, in dots, with their
132    /// fill colour. Used to compute `^FR` (reverse print) geometrically —
133    /// blend modes are unreliable across viewers and print RIPs.
134    #[allow(clippy::type_complexity)]
135    backdrop_rects: Vec<(f64, f64, f64, f64, (f64, f64, f64))>,
136}
137
138impl Default for PdfNativeBackend {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144// ─── Construction ───────────────────────────────────────────────────────────
145
146impl PdfNativeBackend {
147    /// Creates a new `PdfNativeBackend` with default settings.
148    pub fn new() -> Self {
149        Self {
150            width_dots: 0.0,
151            height_dots: 0.0,
152            width_pt: 0.0,
153            height_pt: 0.0,
154            resolution: 0.0,
155            scale: 0.0,
156            content: Vec::with_capacity(4096),
157            finished_pages: Vec::new(),
158            font_manager: None,
159            images: Vec::new(),
160            image_counter: 0,
161            used_fonts: HashSet::new(),
162            compression: Compression::default(),
163            title: None,
164            backdrop_rects: Vec::new(),
165        }
166    }
167
168    /// Sets the zlib compression level for the PDF output (builder pattern).
169    pub fn with_compression(mut self, compression: Compression) -> Self {
170        self.compression = compression;
171        self
172    }
173
174    /// Sets the document title written to the PDF Info dictionary (builder pattern).
175    pub fn with_title(mut self, title: impl Into<String>) -> Self {
176        self.title = Some(title.into());
177        self
178    }
179}
180
181// ─── Private helpers ────────────────────────────────────────────────────────
182
183impl PdfNativeBackend {
184    // ── coordinate helpers ──────────────────────────────────────────
185
186    /// Convert a measurement in dots to PDF points.
187    #[inline]
188    fn d2pt(&self, dots: f64) -> f64 {
189        dots * self.scale
190    }
191
192    /// ZPL x-dot → PDF x-point (origin stays at the left).
193    #[inline]
194    fn x_pt(&self, x: f64) -> f64 {
195        x * self.scale
196    }
197
198    /// PDF y for the **bottom** edge of an object whose top-left is at ZPL row
199    /// `y` with height `h` (both in dots).
200    #[inline]
201    fn y_pt_bottom(&self, y: f64, h: f64) -> f64 {
202        self.height_pt - (y + h) * self.scale
203    }
204
205    // ── colour helpers ─────────────────────────────────────────────
206
207    /// Parse `#RRGGBB` / `#RGB` into `(r, g, b)` in 0.0 – 1.0.  Defaults to
208    /// black when the string is absent or malformed.
209    fn parse_hex_color_f64(color: &Option<String>) -> (f64, f64, f64) {
210        if let Some(hex) = color {
211            let hex = hex.trim_start_matches('#');
212            if hex.len() == 6 {
213                if let (Ok(r), Ok(g), Ok(b)) = (
214                    u8::from_str_radix(&hex[0..2], 16),
215                    u8::from_str_radix(&hex[2..4], 16),
216                    u8::from_str_radix(&hex[4..6], 16),
217                ) {
218                    return (r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
219                }
220            } else if hex.len() == 3
221                && let (Ok(r), Ok(g), Ok(b)) = (
222                    u8::from_str_radix(&hex[0..1], 16),
223                    u8::from_str_radix(&hex[1..2], 16),
224                    u8::from_str_radix(&hex[2..3], 16),
225                )
226            {
227                return (
228                    r as f64 * 17.0 / 255.0,
229                    g as f64 * 17.0 / 255.0,
230                    b as f64 * 17.0 / 255.0,
231                );
232            }
233        }
234        (0.0, 0.0, 0.0)
235    }
236
237    /// Resolve the *draw* and *clear* colours for a graphic element.
238    ///
239    /// Follows the same logic as `PngBackend`:
240    /// - custom hex colour → (custom, white)
241    /// - `'B'` → (black, white)
242    /// - `'W'` → (white, black)
243    fn resolve_colors(
244        color: char,
245        custom_color: &Option<String>,
246    ) -> ((f64, f64, f64), (f64, f64, f64)) {
247        if custom_color.is_some() {
248            (Self::parse_hex_color_f64(custom_color), (1.0, 1.0, 1.0))
249        } else if color == 'B' {
250            ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0))
251        } else {
252            ((1.0, 1.0, 1.0), (0.0, 0.0, 0.0))
253        }
254    }
255
256    // ── low-level PDF operation emitters ────────────────────────────
257    //
258    // Operators are written directly as content-stream bytes instead of
259    // accumulating `lopdf::content::Operation` values: barcodes and QR codes
260    // emit thousands of `re` rectangles, and the per-operation allocations
261    // dominated render time.
262
263    /// Write a number with up to 3 decimals, trimming trailing zeros.
264    fn put_num(buf: &mut Vec<u8>, v: f64) {
265        if v == v.trunc() && v.abs() < 1e12 {
266            let mut itoa = [0u8; 20];
267            let mut n = v as i64;
268            if n < 0 {
269                buf.push(b'-');
270                n = -n;
271            }
272            let mut i = itoa.len();
273            loop {
274                i -= 1;
275                itoa[i] = b'0' + (n % 10) as u8;
276                n /= 10;
277                if n == 0 {
278                    break;
279                }
280            }
281            buf.extend_from_slice(&itoa[i..]);
282        } else {
283            let mut s = format!("{:.3}", v);
284            while s.ends_with('0') {
285                s.pop();
286            }
287            if s.ends_with('.') {
288                s.pop();
289            }
290            buf.extend_from_slice(s.as_bytes());
291        }
292    }
293
294    /// Emit `n1 n2 ... op\n`.
295    fn emit_nums(&mut self, nums: &[f64], op: &str) {
296        for n in nums {
297            Self::put_num(&mut self.content, *n);
298            self.content.push(b' ');
299        }
300        self.content.extend_from_slice(op.as_bytes());
301        self.content.push(b'\n');
302    }
303
304    /// Emit a bare operator: `op\n`.
305    fn emit_op(&mut self, op: &str) {
306        self.content.extend_from_slice(op.as_bytes());
307        self.content.push(b'\n');
308    }
309
310    /// Emit `/Name op\n`.
311    fn emit_name_op(&mut self, name: &str, op: &str) {
312        self.content.push(b'/');
313        self.content.extend_from_slice(name.as_bytes());
314        self.content.push(b' ');
315        self.content.extend_from_slice(op.as_bytes());
316        self.content.push(b'\n');
317    }
318
319    /// Emit `(escaped) Tj\n`, encoding the text as WinAnsi (CP1252) to match
320    /// the embedded fonts' /Encoding. Unmappable characters become '?'.
321    fn emit_tj(&mut self, text: &str) {
322        self.content.push(b'(');
323        for c in text.chars() {
324            let b = char_to_winansi(c).unwrap_or(b'?');
325            match b {
326                b'(' | b')' | b'\\' => {
327                    self.content.push(b'\\');
328                    self.content.push(b);
329                }
330                _ => self.content.push(b),
331            }
332        }
333        self.content.extend_from_slice(b") Tj\n");
334    }
335
336    fn set_fill_color(&mut self, r: f64, g: f64, b: f64) {
337        self.emit_nums(&[r, g, b], "rg");
338    }
339
340    fn save_state(&mut self) {
341        self.emit_op("q");
342    }
343
344    fn restore_state(&mut self) {
345        self.emit_op("Q");
346    }
347
348    // ── reverse-print (geometric) ──────────────────────────────────
349    //
350    // ZPL `^FR` inverts the element against whatever lies beneath it. Instead
351    // of relying on the `Difference` blend mode (poorly supported by Quartz/
352    // Preview and ignored by many print RIPs), the backend tracks the solid
353    // rectangles already painted and repaints their inverse inside a clip
354    // shaped like the reversed element.
355
356    /// Records a solid filled rectangle (in dots) as part of the backdrop.
357    fn track_backdrop_rect(&mut self, x: f64, y: f64, w: f64, h: f64, color: (f64, f64, f64)) {
358        if w > 0.0 && h > 0.0 {
359            self.backdrop_rects.push((x, y, w, h, color));
360        }
361    }
362
363    /// Topmost backdrop colour at a point (in dots); white when unpainted.
364    fn backdrop_color_at(&self, px: f64, py: f64) -> (f64, f64, f64) {
365        let mut color = (1.0, 1.0, 1.0);
366        for (rx, ry, rw, rh, c) in &self.backdrop_rects {
367            if px >= *rx && px < rx + rw && py >= *ry && py < ry + rh {
368                color = *c;
369            }
370        }
371        color
372    }
373
374    /// Paints the inverse of the backdrop across the element bounding box
375    /// `(ex, ey, ew, eh)` in dots. The caller must have already established a
376    /// clipping path shaped like the reversed element.
377    fn fill_inverse_backdrop(&mut self, ex: f64, ey: f64, ew: f64, eh: f64) {
378        // Unpainted page is white → its inverse is black.
379        self.set_fill_color(0.0, 0.0, 0.0);
380        let px = self.x_pt(ex);
381        let py = self.y_pt_bottom(ey, eh);
382        let (pw, ph) = (self.d2pt(ew), self.d2pt(eh));
383        self.emit_nums(&[px, py, pw, ph], "re");
384        self.emit_op("f");
385
386        // Repaint intersections with tracked rects using their inverse, in
387        // z-order so later fills win exactly like the original painting did.
388        let rects = self.backdrop_rects.clone();
389        for (rx, ry, rw, rh, (cr, cg, cb)) in rects {
390            let ix0 = rx.max(ex);
391            let iy0 = ry.max(ey);
392            let ix1 = (rx + rw).min(ex + ew);
393            let iy1 = (ry + rh).min(ey + eh);
394            if ix1 > ix0 && iy1 > iy0 {
395                self.set_fill_color(1.0 - cr, 1.0 - cg, 1.0 - cb);
396                let px = self.x_pt(ix0);
397                let py = self.y_pt_bottom(iy0, iy1 - iy0);
398                self.emit_nums(&[px, py, self.d2pt(ix1 - ix0), self.d2pt(iy1 - iy0)], "re");
399                self.emit_op("f");
400            }
401        }
402    }
403
404    // ── path construction ──────────────────────────────────────────
405
406    /// Append path operators for a rounded rectangle.
407    ///
408    /// `(x, y)` is the **bottom-left** corner in PDF coordinates; `w` and `h`
409    /// extend to the right and upward.
410    fn push_rounded_rect_path(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
411        let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
412        if r < 0.001 {
413            self.emit_nums(&[x, y, w, h], "re");
414            return;
415        }
416        let kr = KAPPA * r;
417        // bottom-left → right along bottom edge
418        self.emit_nums(&[x + r, y], "m");
419        self.emit_nums(&[x + w - r, y], "l");
420        // bottom-right corner
421        self.emit_nums(&[x + w - r + kr, y, x + w, y + r - kr, x + w, y + r], "c");
422        // right edge upward
423        self.emit_nums(&[x + w, y + h - r], "l");
424        // top-right corner
425        self.emit_nums(
426            &[
427                x + w,
428                y + h - r + kr,
429                x + w - r + kr,
430                y + h,
431                x + w - r,
432                y + h,
433            ],
434            "c",
435        );
436        // top edge leftward
437        self.emit_nums(&[x + r, y + h], "l");
438        // top-left corner
439        self.emit_nums(&[x + r - kr, y + h, x, y + h - r + kr, x, y + h - r], "c");
440        // left edge downward
441        self.emit_nums(&[x, y + r], "l");
442        // bottom-left corner
443        self.emit_nums(&[x, y + r - kr, x + r - kr, y, x + r, y], "c");
444        self.emit_op("h");
445    }
446
447    /// Append path operators for an ellipse centred at `(cx, cy)` with radii
448    /// `(rx, ry)`, approximated by four cubic Bézier curves.
449    fn push_ellipse_path(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) {
450        let kx = KAPPA * rx;
451        let ky = KAPPA * ry;
452        // start at 3-o'clock
453        self.emit_nums(&[cx + rx, cy], "m");
454        // → 12-o'clock
455        self.emit_nums(&[cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry], "c");
456        // → 9-o'clock
457        self.emit_nums(&[cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy], "c");
458        // → 6-o'clock
459        self.emit_nums(&[cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry], "c");
460        // → back to 3-o'clock
461        self.emit_nums(&[cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy], "c");
462        self.emit_op("h");
463    }
464
465    // ── font / text helpers ────────────────────────────────
466
467    fn get_text_width(
468        &self,
469        text: &str,
470        font_char: char,
471        height: Option<u32>,
472        width: Option<u32>,
473    ) -> u32 {
474        match self.font_manager.as_ref() {
475            Some(fm) => fm.measure_text(font_char, height, width, text),
476            None => 0,
477        }
478    }
479
480    // ── image embedding ────────────────────────────────────────────
481
482    /// Store raw RGB image data as a future XObject and emit the `cm` + `Do`
483    /// operators that place it on the page.
484    fn embed_rgb_image(
485        &mut self,
486        x_dots: f64,
487        y_dots: f64,
488        img_w: u32,
489        img_h: u32,
490        rgb_data: Vec<u8>,
491    ) {
492        let name = format!("Im{}", self.image_counter);
493        self.image_counter += 1;
494
495        let px = self.x_pt(x_dots);
496        let py = self.y_pt_bottom(y_dots, img_h as f64);
497        let pw = self.d2pt(img_w as f64);
498        let ph = self.d2pt(img_h as f64);
499
500        self.save_state();
501        self.emit_nums(&[pw, 0.0, 0.0, ph, px, py], "cm");
502        self.emit_name_op(&name, "Do");
503        self.restore_state();
504
505        self.images.push(ImageXObject {
506            name,
507            data: rgb_data,
508            width: img_w,
509            height: img_h,
510            is_mask: false,
511        });
512    }
513
514    /// Store 1-bit bitmap data as a future stencil-mask XObject and emit the
515    /// operators that paint it on the page. Set bits (ZPL black) are painted
516    /// with the current fill colour; clear bits are transparent.
517    fn embed_mask_image(
518        &mut self,
519        x_dots: f64,
520        y_dots: f64,
521        img_w: u32,
522        img_h: u32,
523        bits: Vec<u8>,
524        reverse_print: bool,
525    ) {
526        let name = format!("Im{}", self.image_counter);
527        self.image_counter += 1;
528
529        let px = self.x_pt(x_dots);
530        let py = self.y_pt_bottom(y_dots, img_h as f64);
531        let pw = self.d2pt(img_w as f64);
532        let ph = self.d2pt(img_h as f64);
533
534        self.save_state();
535        if reverse_print {
536            // Stencil masks can't be clipped per-pixel without SMasks, so
537            // approximate: paint with the inverse of the backdrop colour at
538            // the bitmap centre.
539            let (br, bg, bb) =
540                self.backdrop_color_at(x_dots + img_w as f64 / 2.0, y_dots + img_h as f64 / 2.0);
541            self.set_fill_color(1.0 - br, 1.0 - bg, 1.0 - bb);
542        } else {
543            self.set_fill_color(0.0, 0.0, 0.0);
544        }
545        self.emit_nums(&[pw, 0.0, 0.0, ph, px, py], "cm");
546        self.emit_name_op(&name, "Do");
547        self.restore_state();
548
549        self.images.push(ImageXObject {
550            name,
551            data: bits,
552            width: img_w,
553            height: img_h,
554            is_mask: true,
555        });
556    }
557
558    // ── barcode orientation transforms ─────────────────────────────
559
560    /// Map a local rectangle inside a 1-D barcode to absolute dot coordinates
561    /// according to the requested orientation.
562    ///
563    /// Returns `(abs_x, abs_y, width, height)` – all in dots.
564    #[allow(clippy::too_many_arguments)]
565    fn transform_1d_bar(
566        orientation: char,
567        base_x: u32,
568        base_y: u32,
569        lx: i32,
570        ly: i32,
571        w: u32,
572        h: u32,
573        bw: u32,
574        bh: u32,
575    ) -> (i32, i32, u32, u32) {
576        match orientation {
577            'R' => {
578                let nx = bh as i32 - (ly + h as i32);
579                let ny = lx;
580                (base_x as i32 + nx, base_y as i32 + ny, h, w)
581            }
582            'I' => {
583                let nx = bw as i32 - (lx + w as i32);
584                let ny = bh as i32 - (ly + h as i32);
585                (base_x as i32 + nx, base_y as i32 + ny, w, h)
586            }
587            'B' => {
588                let nx = ly;
589                let ny = bw as i32 - (lx + w as i32);
590                (base_x as i32 + nx, base_y as i32 + ny, h, w)
591            }
592            _ => (base_x as i32 + lx, base_y as i32 + ly, w, h),
593        }
594    }
595
596    /// Same as [`Self::transform_1d_bar`] but for 2-D codes (QR).
597    #[allow(clippy::too_many_arguments)]
598    fn transform_2d_cell(
599        orientation: char,
600        base_x: u32,
601        base_y: u32,
602        lx: i32,
603        ly: i32,
604        w: u32,
605        h: u32,
606        full_w: u32,
607        full_h: u32,
608    ) -> (i32, i32, u32, u32) {
609        match orientation {
610            'R' => {
611                let nx = full_h as i32 - (ly + h as i32);
612                let ny = lx;
613                (base_x as i32 + nx, base_y as i32 + ny, h, w)
614            }
615            'I' => {
616                let nx = full_w as i32 - (lx + w as i32);
617                let ny = full_h as i32 - (ly + h as i32);
618                (base_x as i32 + nx, base_y as i32 + ny, w, h)
619            }
620            'B' => {
621                let nx = ly;
622                let ny = full_w as i32 - (lx + w as i32);
623                (base_x as i32 + nx, base_y as i32 + ny, h, w)
624            }
625            _ => (base_x as i32 + lx, base_y as i32 + ly, w, h),
626        }
627    }
628
629    // ── 1-D barcode rendering (shared by Code 128 / Code 39) ──────
630
631    #[allow(clippy::too_many_arguments)]
632    fn draw_1d_barcode(
633        &mut self,
634        x: u32,
635        y: u32,
636        orientation: char,
637        height: u32,
638        module_width: u32,
639        ratio: f32,
640        data: &str,
641        format: BarcodeFormat,
642        reverse_print: bool,
643        interpretation_line: char,
644        interpretation_line_above: char,
645        hints: Option<EncodeHints>,
646        hints_key: &str,
647    ) -> ZplResult<()> {
648        let numeric_data;
649        let data = match format {
650            BarcodeFormat::EAN_13
651            | BarcodeFormat::EAN_8
652            | BarcodeFormat::UPC_A
653            | BarcodeFormat::UPC_E
654            | BarcodeFormat::ITF => {
655                numeric_data = data
656                    .chars()
657                    .filter(|c| c.is_ascii_digit())
658                    .collect::<String>();
659                &numeric_data
660            }
661            _ => data,
662        };
663
664        let padded_data;
665        let data = if format == BarcodeFormat::ITF && data.len() % 2 != 0 {
666            padded_data = format!("0{}", data);
667            &padded_data
668        } else {
669            data
670        };
671
672        let bit_matrix = barcode_cache::encode_cached(format, data, hints_key, hints.as_ref())?;
673
674        let mw = max(module_width, 1);
675        // Two-width symbologies are re-laid-out at the ^BY ratio; every other
676        // symbology keeps the encoder's module widths.
677        let elements = symbology::matrix_elements(
678            &bit_matrix,
679            mw,
680            symbology::is_two_width(format).then_some(ratio),
681        );
682
683        self.draw_elements(
684            x,
685            y,
686            orientation,
687            height,
688            module_width,
689            &elements,
690            reverse_print,
691            interpretation_line,
692            interpretation_line_above,
693            data,
694        )
695    }
696
697    /// Emits a 1-D barcode from pre-computed dot-width elements plus its
698    /// interpretation line, shared by rxing-encoded and natively-encoded
699    /// symbologies.
700    #[allow(clippy::too_many_arguments)]
701    fn draw_elements(
702        &mut self,
703        x: u32,
704        y: u32,
705        orientation: char,
706        height: u32,
707        module_width: u32,
708        elements: &[symbology::Element],
709        reverse_print: bool,
710        interpretation_line: char,
711        interpretation_line_above: char,
712        data: &str,
713    ) -> ZplResult<()> {
714        let bh = height;
715        let bw: u32 = elements.iter().map(|e| e.width).sum();
716
717        let (full_w, full_h) = match orientation {
718            'R' | 'B' => (bh, bw),
719            _ => (bw, bh),
720        };
721
722        // ── emit bar rectangles ────────────────────────────────────
723        self.save_state();
724        if !reverse_print {
725            self.set_fill_color(0.0, 0.0, 0.0);
726        }
727
728        let mut cursor = 0u32;
729        for element in elements {
730            if element.bar {
731                let (rx, ry, rw, rh) = Self::transform_1d_bar(
732                    orientation,
733                    x,
734                    y,
735                    cursor as i32,
736                    0,
737                    element.width,
738                    bh,
739                    bw,
740                    bh,
741                );
742                let px = self.d2pt(rx as f64);
743                let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
744                let pw = self.d2pt(rw as f64);
745                let ph = self.d2pt(rh as f64);
746                self.emit_nums(&[px, py, pw, ph], "re");
747            }
748            cursor += element.width;
749        }
750        if reverse_print {
751            // Use the bars as a clip and invert the backdrop inside them.
752            self.emit_op("W");
753            self.emit_op("n");
754            self.fill_inverse_backdrop(x as f64, y as f64, full_w as f64, full_h as f64);
755        } else {
756            self.emit_op("f");
757        }
758        self.restore_state();
759
760        // ── interpretation line ────────────────────────────────────
761        if interpretation_line == 'Y' {
762            self.draw_interpretation_line(
763                x,
764                y,
765                full_w,
766                full_h,
767                module_width,
768                data,
769                interpretation_line_above,
770            )?;
771        }
772
773        Ok(())
774    }
775
776    /// Emits a POSTNET symbol.
777    ///
778    /// POSTNET encodes data in bar *height* rather than bar width, so it needs
779    /// its own painter: all bars share the module width on a fixed pitch, and a
780    /// binary 0 is a bottom-aligned half-height bar.
781    #[allow(clippy::too_many_arguments)]
782    fn draw_postnet(
783        &mut self,
784        x: u32,
785        y: u32,
786        orientation: char,
787        height: u32,
788        module_width: u32,
789        data: &str,
790        reverse_print: bool,
791        interpretation_line: char,
792        interpretation_line_above: char,
793    ) -> ZplResult<()> {
794        let bars = symbology::postnet_bars(data);
795        let (bar_w, gap) = symbology::postnet_pitch(module_width);
796        let pitch = bar_w + gap;
797        let short_h = ((height as f32) * symbology::POSTNET_SHORT_RATIO).round() as u32;
798        let full_w = bars.len() as u32 * bar_w + bars.len().saturating_sub(1) as u32 * gap;
799        let (span_w, span_h) = match orientation {
800            'R' | 'B' => (height, full_w),
801            _ => (full_w, height),
802        };
803
804        self.save_state();
805        if !reverse_print {
806            self.set_fill_color(0.0, 0.0, 0.0);
807        }
808        for (i, bar) in bars.iter().enumerate() {
809            let h = match bar {
810                symbology::BarHeight::Full => height,
811                symbology::BarHeight::Half => short_h,
812            };
813            // Short bars are bottom-aligned with the full-height bars.
814            let top = height.saturating_sub(h);
815            let (rx, ry, rw, rh) = Self::transform_1d_bar(
816                orientation,
817                x,
818                y,
819                (i as u32 * pitch) as i32,
820                top as i32,
821                bar_w,
822                h,
823                full_w,
824                height,
825            );
826            let px = self.d2pt(rx as f64);
827            let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
828            self.emit_nums(&[px, py, self.d2pt(rw as f64), self.d2pt(rh as f64)], "re");
829        }
830        if reverse_print {
831            self.emit_op("W");
832            self.emit_op("n");
833            self.fill_inverse_backdrop(x as f64, y as f64, span_w as f64, span_h as f64);
834        } else {
835            self.emit_op("f");
836        }
837        self.restore_state();
838
839        if interpretation_line == 'Y' {
840            let digits: String = data.chars().filter(|c| c.is_ascii_digit()).collect();
841            self.draw_interpretation_line(
842                x,
843                y,
844                span_w,
845                span_h,
846                module_width,
847                &digits,
848                interpretation_line_above,
849            )?;
850        }
851
852        Ok(())
853    }
854
855    #[allow(clippy::too_many_arguments)]
856    fn draw_interpretation_line(
857        &mut self,
858        x: u32,
859        y: u32,
860        full_w: u32,
861        full_h: u32,
862        module_width: u32,
863        data: &str,
864        interpretation_line_above: char,
865    ) -> ZplResult<()> {
866        {
867            let (font_char, text_h, text_w, gap) =
868                crate::engine::font::interpretation_metrics(module_width);
869            let text_y = if interpretation_line_above == 'Y' {
870                y.saturating_sub(text_h + gap)
871            } else {
872                y + full_h + gap
873            };
874
875            let text_width = self.get_text_width(data, font_char, Some(text_h), Some(text_w));
876            let text_x = if full_w > text_width {
877                x + (full_w - text_width) / 2
878            } else {
879                x
880            };
881
882            self.draw_text(
883                text_x,
884                text_y,
885                font_char,
886                Some(text_h),
887                Some(text_w),
888                'N',
889                data,
890                false,
891                None,
892            )?;
893        }
894
895        Ok(())
896    }
897
898    /// Paints every set cell of a 2-D bit matrix as a filled rectangle,
899    /// scaling each cell to `cell_w` × `cell_h` dots and applying the
900    /// requested orientation.
901    #[allow(clippy::too_many_arguments)]
902    fn fill_matrix_cells(
903        &mut self,
904        x: u32,
905        y: u32,
906        orientation: char,
907        cell_w: u32,
908        cell_h: u32,
909        bit_matrix: &BitMatrix,
910        reverse_print: bool,
911    ) {
912        let bw = bit_matrix.getWidth();
913        let bh = bit_matrix.getHeight();
914        let full_w = bw * cell_w;
915        let full_h = bh * cell_h;
916
917        self.save_state();
918        if !reverse_print {
919            self.set_fill_color(0.0, 0.0, 0.0);
920        }
921
922        for gy in 0..bh {
923            for gx in 0..bw {
924                if bit_matrix.get(gx, gy) {
925                    let (rx, ry, rw, rh) = Self::transform_2d_cell(
926                        orientation,
927                        x,
928                        y,
929                        (gx * cell_w) as i32,
930                        (gy * cell_h) as i32,
931                        cell_w,
932                        cell_h,
933                        full_w,
934                        full_h,
935                    );
936                    let px = self.d2pt(rx as f64);
937                    let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
938                    let pw = self.d2pt(rw as f64);
939                    let ph = self.d2pt(rh as f64);
940                    self.emit_nums(&[px, py, pw, ph], "re");
941                }
942            }
943        }
944        if reverse_print {
945            self.emit_op("W");
946            self.emit_op("n");
947            let (fw, fh) = match orientation {
948                'R' | 'B' => (full_h, full_w),
949                _ => (full_w, full_h),
950            };
951            self.fill_inverse_backdrop(x as f64, y as f64, fw as f64, fh as f64);
952        } else {
953            self.emit_op("f");
954        }
955        self.restore_state();
956    }
957}
958
959// ─── ZplForgeBackend ────────────────────────────────────────────────────────
960
961impl ZplForgeBackend for PdfNativeBackend {
962    fn setup_page(&mut self, width: f64, height: f64, resolution: f32) {
963        let dpi = if resolution == 0.0 { 203.2 } else { resolution };
964        self.width_dots = width;
965        self.height_dots = height;
966        self.resolution = dpi;
967        self.scale = 72.0 / dpi as f64;
968        self.width_pt = width * self.scale;
969        self.height_pt = height * self.scale;
970    }
971
972    fn setup_font_manager(&mut self, font_manager: &FontManager) {
973        self.font_manager = Some(Arc::new(font_manager.clone()));
974    }
975
976    fn new_page(&mut self) -> ZplResult<()> {
977        self.finished_pages.push(std::mem::take(&mut self.content));
978        self.backdrop_rects.clear();
979        Ok(())
980    }
981
982    // ── text ───────────────────────────────────────────────────────
983
984    fn draw_text(
985        &mut self,
986        x: u32,
987        y: u32,
988        font: char,
989        height: Option<u32>,
990        width: Option<u32>,
991        orientation: char,
992        text: &str,
993        reverse_print: bool,
994        color: Option<String>,
995    ) -> ZplResult<()> {
996        if text.is_empty() {
997            return Ok(());
998        }
999
1000        let layout = {
1001            let fm = self
1002                .font_manager
1003                .as_ref()
1004                .ok_or_else(|| ZplError::FontError("Font manager not initialized".into()))?;
1005            fm.text_layout(font, height, width)
1006                .ok_or_else(|| ZplError::FontError(format!("Font not found: {}", font)))?
1007                .1
1008        };
1009
1010        self.used_fonts.insert(font);
1011
1012        // PDF text space: `Tf 1` + a Tm scale of `s` renders a glyph em of
1013        // `s` points, so the matrix carries the em sizes (not the ^A values).
1014        let em_x_pt = self.d2pt(layout.em_x as f64);
1015        let em_y_pt = self.d2pt(layout.em_y as f64);
1016        // Distance from the character-cell top to the baseline, in dots.
1017        let baseline_dots = layout.baseline as f64;
1018        let h_dots = layout.cell_h as f64;
1019        let x = x as f64;
1020        let y = y as f64;
1021
1022        // Text width anchors 'I'/'B' rotations and sizes the reverse bbox.
1023        let tw_dots = if reverse_print || orientation == 'I' || orientation == 'B' {
1024            self.get_text_width(text, font, height, width) as f64
1025        } else {
1026            0.0
1027        };
1028
1029        // Text matrix [a b c d tx ty]: scale plus the ^A rotation, with
1030        // (x, y) anchoring the top-left corner of the rotated cell.
1031        let tm = match orientation {
1032            'R' => [
1033                0.0,
1034                -em_x_pt,
1035                em_y_pt,
1036                0.0,
1037                self.x_pt(x + h_dots - baseline_dots),
1038                self.height_pt - y * self.scale,
1039            ],
1040            'I' => [
1041                -em_x_pt,
1042                0.0,
1043                0.0,
1044                -em_y_pt,
1045                self.x_pt(x + tw_dots),
1046                self.height_pt - (y + h_dots - baseline_dots) * self.scale,
1047            ],
1048            'B' => [
1049                0.0,
1050                em_x_pt,
1051                -em_y_pt,
1052                0.0,
1053                self.x_pt(x + baseline_dots),
1054                self.height_pt - (y + tw_dots) * self.scale,
1055            ],
1056            _ => [
1057                em_x_pt,
1058                0.0,
1059                0.0,
1060                em_y_pt,
1061                self.x_pt(x),
1062                self.height_pt - (y + baseline_dots) * self.scale,
1063            ],
1064        };
1065
1066        self.save_state();
1067        if !reverse_print {
1068            let (r, g, b) = Self::parse_hex_color_f64(&color);
1069            self.set_fill_color(r, g, b);
1070        }
1071
1072        self.emit_op("BT");
1073        if reverse_print {
1074            // Text rendering mode 7: glyph outlines become the clipping path.
1075            self.emit_nums(&[7.0], "Tr");
1076        }
1077        self.emit_nums(&tm, "Tm");
1078        let font_resource_name = format!("F_{}", font);
1079        self.emit_name_op(&format!("{} 1", font_resource_name), "Tf");
1080        self.emit_tj(text);
1081        self.emit_op("ET");
1082
1083        if reverse_print {
1084            let (bw_dots, bh_dots) = match orientation {
1085                'R' | 'B' => (h_dots, tw_dots),
1086                _ => (tw_dots, h_dots),
1087            };
1088            self.fill_inverse_backdrop(x, y, bw_dots, bh_dots);
1089        }
1090        self.restore_state();
1091
1092        Ok(())
1093    }
1094
1095    // ── graphic box (rounded rectangle) ────────────────────────────
1096
1097    fn draw_graphic_box(
1098        &mut self,
1099        x: u32,
1100        y: u32,
1101        width: u32,
1102        height: u32,
1103        thickness: u32,
1104        color: char,
1105        custom_color: Option<String>,
1106        rounding: u32,
1107        reverse_print: bool,
1108    ) -> ZplResult<()> {
1109        let w = max(width, 1) as f64;
1110        let h = max(height, 1) as f64;
1111        let t = thickness as f64;
1112        let r_dots = rounding as f64 * 8.0;
1113
1114        let (draw_color, clear_color) = Self::resolve_colors(color, &custom_color);
1115
1116        let bx = self.x_pt(x as f64);
1117        let by = self.y_pt_bottom(y as f64, h);
1118        let bw = self.d2pt(w);
1119        let bh = self.d2pt(h);
1120        let br = self.d2pt(r_dots);
1121
1122        let has_inner = t * 2.0 < w && t * 2.0 < h;
1123
1124        if reverse_print {
1125            // Clip to the box (solid) or its border ring (even-odd) and
1126            // repaint the inverse of the backdrop inside it.
1127            self.save_state();
1128            self.push_rounded_rect_path(bx, by, bw, bh, br);
1129            if has_inner {
1130                let tp = self.d2pt(t);
1131                let inner_r = self.d2pt((r_dots - t).max(0.0));
1132                self.push_rounded_rect_path(
1133                    bx + tp,
1134                    by + tp,
1135                    bw - tp * 2.0,
1136                    bh - tp * 2.0,
1137                    inner_r,
1138                );
1139                self.emit_op("W*");
1140            } else {
1141                self.emit_op("W");
1142            }
1143            self.emit_op("n");
1144            self.fill_inverse_backdrop(x as f64, y as f64, w, h);
1145            self.restore_state();
1146        } else {
1147            self.save_state();
1148            let (r, g, b) = draw_color;
1149            self.set_fill_color(r, g, b);
1150            self.push_rounded_rect_path(bx, by, bw, bh, br);
1151            self.emit_op("f");
1152            self.track_backdrop_rect(x as f64, y as f64, w, h, draw_color);
1153
1154            if has_inner {
1155                let (cr, cg, cb) = clear_color;
1156                self.set_fill_color(cr, cg, cb);
1157                let tp = self.d2pt(t);
1158                let inner_r = self.d2pt((r_dots - t).max(0.0));
1159                self.push_rounded_rect_path(
1160                    bx + tp,
1161                    by + tp,
1162                    bw - tp * 2.0,
1163                    bh - tp * 2.0,
1164                    inner_r,
1165                );
1166                self.emit_op("f");
1167                self.track_backdrop_rect(
1168                    x as f64 + t,
1169                    y as f64 + t,
1170                    w - t * 2.0,
1171                    h - t * 2.0,
1172                    clear_color,
1173                );
1174            }
1175            self.restore_state();
1176        }
1177
1178        Ok(())
1179    }
1180
1181    // ── graphic circle ─────────────────────────────────────────────
1182
1183    fn draw_graphic_circle(
1184        &mut self,
1185        x: u32,
1186        y: u32,
1187        radius: u32,
1188        thickness: u32,
1189        _color: char,
1190        custom_color: Option<String>,
1191        reverse_print: bool,
1192    ) -> ZplResult<()> {
1193        let (draw_color, _) = Self::resolve_colors('B', &custom_color);
1194
1195        let r_pt = self.d2pt(radius as f64);
1196        // ZPL (x,y) = top-left of bounding box → centre
1197        let cx_pt = self.x_pt(x as f64) + r_pt;
1198        let cy_pt = self.height_pt - (y as f64 + radius as f64) * self.scale;
1199
1200        if reverse_print {
1201            self.save_state();
1202            self.push_ellipse_path(cx_pt, cy_pt, r_pt, r_pt);
1203            if radius > thickness {
1204                let inner_r = self.d2pt((radius - thickness) as f64);
1205                self.push_ellipse_path(cx_pt, cy_pt, inner_r, inner_r);
1206                self.emit_op("W*");
1207            } else {
1208                self.emit_op("W");
1209            }
1210            self.emit_op("n");
1211            self.fill_inverse_backdrop(
1212                x as f64,
1213                y as f64,
1214                radius as f64 * 2.0,
1215                radius as f64 * 2.0,
1216            );
1217            self.restore_state();
1218        } else {
1219            self.save_state();
1220            let (r, g, b) = draw_color;
1221            self.set_fill_color(r, g, b);
1222            self.push_ellipse_path(cx_pt, cy_pt, r_pt, r_pt);
1223            self.emit_op("f");
1224
1225            if radius > thickness {
1226                self.set_fill_color(1.0, 1.0, 1.0);
1227                let inner_r = self.d2pt((radius - thickness) as f64);
1228                self.push_ellipse_path(cx_pt, cy_pt, inner_r, inner_r);
1229                self.emit_op("f");
1230            }
1231            self.restore_state();
1232        }
1233
1234        Ok(())
1235    }
1236
1237    // ── graphic ellipse ────────────────────────────────────────────
1238
1239    fn draw_graphic_ellipse(
1240        &mut self,
1241        x: u32,
1242        y: u32,
1243        width: u32,
1244        height: u32,
1245        thickness: u32,
1246        _color: char,
1247        custom_color: Option<String>,
1248        reverse_print: bool,
1249    ) -> ZplResult<()> {
1250        let (draw_color, _) = Self::resolve_colors('B', &custom_color);
1251
1252        let rx_pt = self.d2pt(width as f64 / 2.0);
1253        let ry_pt = self.d2pt(height as f64 / 2.0);
1254        let cx_pt = self.x_pt(x as f64) + rx_pt;
1255        let cy_pt = self.height_pt - (y as f64 + height as f64 / 2.0) * self.scale;
1256
1257        let t = thickness as f64;
1258
1259        if reverse_print {
1260            self.save_state();
1261            self.push_ellipse_path(cx_pt, cy_pt, rx_pt, ry_pt);
1262            if (width as f64 / 2.0) > t && (height as f64 / 2.0) > t {
1263                let irx = self.d2pt(width as f64 / 2.0 - t);
1264                let iry = self.d2pt(height as f64 / 2.0 - t);
1265                self.push_ellipse_path(cx_pt, cy_pt, irx, iry);
1266                self.emit_op("W*");
1267            } else {
1268                self.emit_op("W");
1269            }
1270            self.emit_op("n");
1271            self.fill_inverse_backdrop(x as f64, y as f64, width as f64, height as f64);
1272            self.restore_state();
1273        } else {
1274            self.save_state();
1275            let (r, g, b) = draw_color;
1276            self.set_fill_color(r, g, b);
1277            self.push_ellipse_path(cx_pt, cy_pt, rx_pt, ry_pt);
1278            self.emit_op("f");
1279
1280            if (width as f64 / 2.0) > t && (height as f64 / 2.0) > t {
1281                self.set_fill_color(1.0, 1.0, 1.0);
1282                let irx = self.d2pt(width as f64 / 2.0 - t);
1283                let iry = self.d2pt(height as f64 / 2.0 - t);
1284                self.push_ellipse_path(cx_pt, cy_pt, irx, iry);
1285                self.emit_op("f");
1286            }
1287            self.restore_state();
1288        }
1289
1290        Ok(())
1291    }
1292
1293    // ── graphic field (1-bit bitmap) ───────────────────────────────
1294
1295    fn draw_graphic_field(
1296        &mut self,
1297        x: u32,
1298        y: u32,
1299        width: u32,
1300        height: u32,
1301        data: &[u8],
1302        reverse_print: bool,
1303    ) -> ZplResult<()> {
1304        if width == 0 || height == 0 {
1305            return Ok(());
1306        }
1307
1308        // ZPL ^GF rows are already byte-padded (ceil(width/8) bytes per row),
1309        // exactly the layout a 1-bit PDF image expects. Pad or truncate to the
1310        // full bitmap size; padding bytes are 0 (unpainted with Decode [1 0]).
1311        let row_bytes = width.div_ceil(8) as usize;
1312        let total_bytes = row_bytes * height as usize;
1313        let mut bits = data.to_vec();
1314        bits.resize(total_bytes, 0x00);
1315
1316        self.embed_mask_image(x as f64, y as f64, width, height, bits, reverse_print);
1317        Ok(())
1318    }
1319
1320    // ── custom colour image (base64) ───────────────────────────────
1321
1322    fn draw_graphic_image_custom(
1323        &mut self,
1324        x: u32,
1325        y: u32,
1326        width: u32,
1327        height: u32,
1328        data: &str,
1329    ) -> ZplResult<()> {
1330        let image_data = general_purpose::STANDARD
1331            .decode(data.trim())
1332            .map_err(|e| ZplError::ImageError(format!("Failed to decode base64: {}", e)))?;
1333
1334        let img = image::load_from_memory(&image_data)
1335            .map_err(|e| ZplError::ImageError(format!("Failed to load image: {}", e)))?
1336            .to_rgb8();
1337
1338        let (orig_w, orig_h) = img.dimensions();
1339        let (target_w, target_h) = match (width, height) {
1340            (0, 0) => (orig_w, orig_h),
1341            (w, 0) => {
1342                let h = (orig_h as f32 * (w as f32 / orig_w as f32)).round() as u32;
1343                (w, h)
1344            }
1345            (0, h) => {
1346                let w = (orig_w as f32 * (h as f32 / orig_h as f32)).round() as u32;
1347                (w, h)
1348            }
1349            (w, h) => (w, h),
1350        };
1351
1352        let final_img = if target_w != orig_w || target_h != orig_h {
1353            image::imageops::resize(
1354                &img,
1355                target_w,
1356                target_h,
1357                image::imageops::FilterType::Lanczos3,
1358            )
1359        } else {
1360            img
1361        };
1362
1363        let rgb_data = final_img.into_raw();
1364        self.embed_rgb_image(x as f64, y as f64, target_w, target_h, rgb_data);
1365        Ok(())
1366    }
1367
1368    // ── Code 128 barcode ───────────────────────────────────────────
1369
1370    fn draw_code128(
1371        &mut self,
1372        x: u32,
1373        y: u32,
1374        orientation: char,
1375        height: u32,
1376        module_width: u32,
1377        interpretation_line: char,
1378        interpretation_line_above: char,
1379        _check_digit: char,
1380        _mode: char,
1381        data: &str,
1382        reverse_print: bool,
1383    ) -> ZplResult<()> {
1384        let (clean_data, code_set) = super::code128_code_set(data);
1385
1386        let mut h = HashMap::new();
1387        h.insert(
1388            EncodeHintType::FORCE_CODE_SET,
1389            EncodeHintValue::ForceCodeSet(code_set.to_string()),
1390        );
1391        let hints = Some(EncodeHints::from(h));
1392
1393        self.draw_1d_barcode(
1394            x,
1395            y,
1396            orientation,
1397            height,
1398            module_width,
1399            // Code 128 is a four-width symbology; ^BY's ratio does not apply.
1400            0.0,
1401            clean_data,
1402            BarcodeFormat::CODE_128,
1403            reverse_print,
1404            interpretation_line,
1405            interpretation_line_above,
1406            hints,
1407            code_set,
1408        )
1409    }
1410
1411    // ── QR code ────────────────────────────────────────────────────
1412
1413    fn draw_qr_code(
1414        &mut self,
1415        x: u32,
1416        y: u32,
1417        orientation: char,
1418        _model: u32,
1419        magnification: u32,
1420        error_correction: char,
1421        _mask: u32,
1422        data: &str,
1423        reverse_print: bool,
1424    ) -> ZplResult<()> {
1425        let (ec, payload) = super::qr_field_data(data, error_correction);
1426        let level = match ec {
1427            'L' => "L",
1428            'Q' => "Q",
1429            'H' => "H",
1430            _ => "M",
1431        };
1432
1433        let mut hints = HashMap::new();
1434        hints.insert(
1435            EncodeHintType::ERROR_CORRECTION,
1436            EncodeHintValue::ErrorCorrection(level.to_string()),
1437        );
1438        let hints: EncodeHints = hints.into();
1439
1440        let bit_matrix = barcode_cache::encode_cached(
1441            BarcodeFormat::QR_CODE,
1442            payload,
1443            &format!("ec:{}", level),
1444            Some(&hints),
1445        )?;
1446
1447        let mag = max(magnification, 1);
1448        self.fill_matrix_cells(x, y, orientation, mag, mag, &bit_matrix, reverse_print);
1449        Ok(())
1450    }
1451
1452    // ── Data Matrix barcode ────────────────────────────────────────
1453
1454    fn draw_datamatrix(
1455        &mut self,
1456        x: u32,
1457        y: u32,
1458        orientation: char,
1459        module_size: u32,
1460        data: &str,
1461        reverse_print: bool,
1462    ) -> ZplResult<()> {
1463        // Match Zebra's square default rather than rxing's smallest-fit
1464        // rectangle. See the PNG backend for the measured module counts.
1465        let hints = EncodeHints {
1466            DataMatrixShape: Some(SymbolShapeHint::FORCE_SQUARE),
1467            ..Default::default()
1468        };
1469
1470        let bit_matrix =
1471            barcode_cache::encode_cached(BarcodeFormat::DATA_MATRIX, data, "sq", Some(&hints))?;
1472
1473        let m = max(module_size, 1);
1474        self.fill_matrix_cells(x, y, orientation, m, m, &bit_matrix, reverse_print);
1475        Ok(())
1476    }
1477
1478    // ── PDF417 barcode ─────────────────────────────────────────────
1479
1480    fn draw_pdf417(
1481        &mut self,
1482        x: u32,
1483        y: u32,
1484        orientation: char,
1485        row_height: u32,
1486        module_width: u32,
1487        security_level: u32,
1488        data: &str,
1489        reverse_print: bool,
1490    ) -> ZplResult<()> {
1491        let mut hints = HashMap::new();
1492        hints.insert(
1493            EncodeHintType::ERROR_CORRECTION,
1494            EncodeHintValue::ErrorCorrection(security_level.min(8).to_string()),
1495        );
1496        hints.insert(
1497            EncodeHintType::MARGIN,
1498            EncodeHintValue::Margin("0".to_owned()),
1499        );
1500        let hints: EncodeHints = hints.into();
1501
1502        let bit_matrix = barcode_cache::encode_cached(
1503            BarcodeFormat::PDF_417,
1504            data,
1505            &format!("ec:{}", security_level.min(8)),
1506            Some(&hints),
1507        )?;
1508
1509        let cw = max(module_width, 1);
1510        let ch = max(row_height, 1);
1511        self.fill_matrix_cells(x, y, orientation, cw, ch, &bit_matrix, reverse_print);
1512        Ok(())
1513    }
1514
1515    fn draw_micropdf417(
1516        &mut self,
1517        x: u32,
1518        y: u32,
1519        orientation: char,
1520        height: u32,
1521        _mode: u32,
1522        data: &str,
1523        reverse_print: bool,
1524    ) -> ZplResult<()> {
1525        let bit_matrix = barcode_cache::encode_cached(BarcodeFormat::PDF_417, data, "", None)?;
1526        let ch = max(height, 1);
1527        self.fill_matrix_cells(x, y, orientation, 1, ch, &bit_matrix, reverse_print);
1528        Ok(())
1529    }
1530
1531    fn draw_aztec_code(
1532        &mut self,
1533        x: u32,
1534        y: u32,
1535        orientation: char,
1536        magnification: u32,
1537        data: &str,
1538        reverse_print: bool,
1539    ) -> ZplResult<()> {
1540        let bit_matrix = barcode_cache::encode_cached(BarcodeFormat::AZTEC, data, "", None)?;
1541        let m = max(magnification, 1);
1542        self.fill_matrix_cells(x, y, orientation, m, m, &bit_matrix, reverse_print);
1543        Ok(())
1544    }
1545
1546    // ── Code 39 barcode ────────────────────────────────────────────
1547
1548    fn draw_code39(
1549        &mut self,
1550        x: u32,
1551        y: u32,
1552        orientation: char,
1553        _check_digit: char,
1554        height: u32,
1555        module_width: u32,
1556        ratio: f32,
1557        interpretation_line: char,
1558        interpretation_line_above: char,
1559        data: &str,
1560        reverse_print: bool,
1561    ) -> ZplResult<()> {
1562        self.draw_1d_barcode(
1563            x,
1564            y,
1565            orientation,
1566            height,
1567            module_width,
1568            ratio,
1569            data,
1570            BarcodeFormat::CODE_39,
1571            reverse_print,
1572            interpretation_line,
1573            interpretation_line_above,
1574            None,
1575            "",
1576        )
1577    }
1578
1579    // ── generic 1-D barcodes (EAN-13, UPC-A, ITF, Code 93) ────────
1580
1581    fn draw_barcode_1d(
1582        &mut self,
1583        kind: Barcode1DKind,
1584        x: u32,
1585        y: u32,
1586        orientation: char,
1587        height: u32,
1588        module_width: u32,
1589        ratio: f32,
1590        check_digit: char,
1591        interpretation_line: char,
1592        interpretation_line_above: char,
1593        data: &str,
1594        reverse_print: bool,
1595    ) -> ZplResult<()> {
1596        // MSI and POSTNET have no rxing writer; both are encoded natively.
1597        match kind {
1598            Barcode1DKind::Msi => {
1599                let check = symbology::MsiCheck::from_zpl(check_digit);
1600                let elements = symbology::msi_elements(data, check, module_width, ratio);
1601                let text = symbology::msi_text(data, check);
1602                return self.draw_elements(
1603                    x,
1604                    y,
1605                    orientation,
1606                    height,
1607                    module_width,
1608                    &elements,
1609                    reverse_print,
1610                    interpretation_line,
1611                    interpretation_line_above,
1612                    &text,
1613                );
1614            }
1615            Barcode1DKind::Postnet => {
1616                return self.draw_postnet(
1617                    x,
1618                    y,
1619                    orientation,
1620                    height,
1621                    module_width,
1622                    data,
1623                    reverse_print,
1624                    interpretation_line,
1625                    interpretation_line_above,
1626                );
1627            }
1628            _ => {}
1629        }
1630
1631        self.draw_1d_barcode(
1632            x,
1633            y,
1634            orientation,
1635            height,
1636            module_width,
1637            ratio,
1638            data,
1639            barcode_1d_format(kind),
1640            reverse_print,
1641            interpretation_line,
1642            interpretation_line_above,
1643            None,
1644            "",
1645        )
1646    }
1647
1648    // ── diagonal line (^GD) ────────────────────────────────────────
1649
1650    fn draw_graphic_diagonal(
1651        &mut self,
1652        x: u32,
1653        y: u32,
1654        width: u32,
1655        height: u32,
1656        thickness: u32,
1657        color: char,
1658        custom_color: Option<String>,
1659        diagonal_orientation: char,
1660        reverse_print: bool,
1661    ) -> ZplResult<()> {
1662        let (draw_color, _) = Self::resolve_colors(color, &custom_color);
1663
1664        let w = max(width, 1) as f64;
1665        let h = max(height, 1) as f64;
1666        let t = (max(thickness, 1) as f64).min(w);
1667        let x = x as f64;
1668        let y = y as f64;
1669
1670        // Filled parallelogram with horizontal thickness `t`.
1671        let pts: [(f64, f64); 4] = if diagonal_orientation == 'L' {
1672            // '\' top-left → bottom-right
1673            [(x, y), (x + t, y), (x + w, y + h), (x + w - t, y + h)]
1674        } else {
1675            // '/' bottom-left → top-right
1676            [(x, y + h), (x + t, y + h), (x + w, y), (x + w - t, y)]
1677        };
1678
1679        self.save_state();
1680        if !reverse_print {
1681            let (r, g, b) = draw_color;
1682            self.set_fill_color(r, g, b);
1683        }
1684
1685        for (i, (dx, dy)) in pts.iter().enumerate() {
1686            let px = self.x_pt(*dx);
1687            let py = self.height_pt - dy * self.scale;
1688            self.emit_nums(&[px, py], if i == 0 { "m" } else { "l" });
1689        }
1690        self.emit_op("h");
1691        if reverse_print {
1692            self.emit_op("W");
1693            self.emit_op("n");
1694            self.fill_inverse_backdrop(x, y, w, h);
1695        } else {
1696            self.emit_op("f");
1697        }
1698        self.restore_state();
1699
1700        Ok(())
1701    }
1702
1703    // ── finalize ───────────────────────────────────────────────────
1704
1705    fn finalize(&mut self) -> ZplResult<Vec<u8>> {
1706        let mut doc = Document::with_version("1.5");
1707        let pages_id = doc.new_object_id();
1708
1709        // ── embed fonts ────────────────────────────────────────────
1710        //
1711        // Font objects are built manually instead of using `lopdf::Document::
1712        // add_font`, which omits /Widths and /ToUnicode and stores descriptor
1713        // metrics in raw font units. Here every metric is normalized to the
1714        // 1000/em glyph space and a ToUnicode CMap makes text extraction
1715        // (copy/paste, search) work for the full WinAnsi range.
1716        let default_font_bytes: &[u8] = include_bytes!("../assets/IosevkaTermSlab-Regular.ttf");
1717        let mut font_dict = lopdf::Dictionary::new();
1718        // Dedup: multiple ZPL identifiers often map to the same font.
1719        let mut embedded_fonts: HashMap<String, lopdf::ObjectId> = HashMap::new();
1720        let tounicode_id = doc.add_object(Stream::new(dictionary! {}, build_tounicode_cmap()));
1721
1722        for font_char in &self.used_fonts {
1723            let font_key = font_char.to_string();
1724            let resource_name = format!("F_{}", font_char);
1725
1726            let actual_name = self
1727                .font_manager
1728                .as_ref()
1729                .and_then(|fm| fm.get_font_name(&font_key).map(|s| s.to_string()))
1730                .unwrap_or_else(|| "Iosevka Term Slab".to_string());
1731
1732            if let Some(font_id) = embedded_fonts.get(&actual_name) {
1733                font_dict.set(resource_name.as_str(), *font_id);
1734                continue;
1735            }
1736
1737            let raw_bytes = self
1738                .font_manager
1739                .as_ref()
1740                .and_then(|fm| fm.get_font_bytes(&font_key))
1741                .unwrap_or(default_font_bytes);
1742
1743            let face = FontArc::try_from_vec(raw_bytes.to_vec())
1744                .map_err(|e| ZplError::FontError(format!("Invalid font data: {}", e)))?;
1745            let upem = face.units_per_em().unwrap_or(1000.0) as f64;
1746            let to_glyph_space = |v: f64| (v * 1000.0 / upem).round() as i64;
1747
1748            // /Widths for the WinAnsi code range 32..=255.
1749            let widths: Vec<Object> = (0x20..=0xFFu32)
1750                .map(|code| {
1751                    let w = winansi_to_char(code as u8)
1752                        .map(|ch| to_glyph_space(face.h_advance_unscaled(face.glyph_id(ch)) as f64))
1753                        .unwrap_or(0);
1754                    w.into()
1755                })
1756                .collect();
1757
1758            // Bounding box and style metrics via ttf-parser (lopdf::FontData).
1759            let fd = FontData::new(raw_bytes, actual_name.clone());
1760
1761            let font_stream = Stream::new(
1762                dictionary! { "Length1" => raw_bytes.len() as i64 },
1763                raw_bytes.to_vec(),
1764            );
1765            let font_file_id = doc.add_object(font_stream);
1766
1767            let descriptor_id = doc.add_object(dictionary! {
1768                "Type" => "FontDescriptor",
1769                "FontName" => Object::Name(actual_name.clone().into_bytes()),
1770                "Flags" => 32_i64,
1771                "FontBBox" => vec![
1772                    to_glyph_space(fd.font_bbox.0 as f64).into(),
1773                    to_glyph_space(fd.font_bbox.1 as f64).into(),
1774                    to_glyph_space(fd.font_bbox.2 as f64).into(),
1775                    to_glyph_space(fd.font_bbox.3 as f64).into(),
1776                ],
1777                "ItalicAngle" => fd.italic_angle,
1778                "Ascent" => to_glyph_space(fd.ascent as f64),
1779                "Descent" => to_glyph_space(fd.descent as f64),
1780                "CapHeight" => to_glyph_space(fd.cap_height as f64),
1781                "StemV" => 80_i64,
1782                "FontFile2" => font_file_id,
1783            });
1784
1785            let font_id = doc.add_object(dictionary! {
1786                "Type" => "Font",
1787                "Subtype" => "TrueType",
1788                "BaseFont" => Object::Name(actual_name.clone().into_bytes()),
1789                "FirstChar" => 32_i64,
1790                "LastChar" => 255_i64,
1791                "Widths" => widths,
1792                "FontDescriptor" => descriptor_id,
1793                "Encoding" => "WinAnsiEncoding",
1794                "ToUnicode" => tounicode_id,
1795            });
1796
1797            font_dict.set(resource_name.as_str(), font_id);
1798            embedded_fonts.insert(actual_name, font_id);
1799        }
1800
1801        // ── XObject images ─────────────────────────────────────────
1802        let mut xobject_dict = lopdf::Dictionary::new();
1803        for img in &self.images {
1804            let mut encoder = ZlibEncoder::new(Vec::new(), self.compression);
1805            encoder
1806                .write_all(&img.data)
1807                .map_err(|e| ZplError::BackendError(e.to_string()))?;
1808            let compressed = encoder
1809                .finish()
1810                .map_err(|e| ZplError::BackendError(e.to_string()))?;
1811
1812            let dict = if img.is_mask {
1813                // Stencil mask: sample 1 paints with the current fill colour
1814                // (Decode [1 0]), sample 0 leaves the page untouched.
1815                dictionary! {
1816                    "Type" => "XObject",
1817                    "Subtype" => "Image",
1818                    "Width" => img.width as i64,
1819                    "Height" => img.height as i64,
1820                    "ImageMask" => true,
1821                    "BitsPerComponent" => 1,
1822                    "Decode" => vec![0.into(), 1.into()],
1823                    "Filter" => "FlateDecode",
1824                }
1825            } else {
1826                dictionary! {
1827                    "Type" => "XObject",
1828                    "Subtype" => "Image",
1829                    "Width" => img.width as i64,
1830                    "Height" => img.height as i64,
1831                    "ColorSpace" => "DeviceRGB",
1832                    "BitsPerComponent" => 8,
1833                    "Filter" => "FlateDecode",
1834                }
1835            };
1836            let img_stream = Stream::new(dict, compressed);
1837            let img_id = doc.add_object(img_stream);
1838            xobject_dict.set(img.name.as_str(), img_id);
1839        }
1840
1841        // ── resources ──────────────────────────────────────────────
1842        let resources_id = doc.add_object(dictionary! {
1843            "Font" => lopdf::Object::Dictionary(font_dict),
1844            "XObject" => lopdf::Object::Dictionary(xobject_dict),
1845        });
1846
1847        // ── pages (one content stream each, shared resources) ──────
1848        let mut page_contents = std::mem::take(&mut self.finished_pages);
1849        page_contents.push(std::mem::take(&mut self.content));
1850
1851        let mut kids: Vec<Object> = Vec::with_capacity(page_contents.len());
1852        for content_bytes in page_contents {
1853            let content_id = doc.add_object(Stream::new(dictionary! {}, content_bytes));
1854            let page_id = doc.add_object(dictionary! {
1855                "Type" => "Page",
1856                "Parent" => pages_id,
1857                "MediaBox" => vec![
1858                    0.into(),
1859                    0.into(),
1860                    Object::Real(self.width_pt as f32),
1861                    Object::Real(self.height_pt as f32),
1862                ],
1863                "Contents" => content_id,
1864                "Resources" => resources_id,
1865            });
1866            kids.push(page_id.into());
1867        }
1868
1869        // ── pages tree ─────────────────────────────────────────────
1870        let pages_dict = dictionary! {
1871            "Type" => "Pages",
1872            "Count" => kids.len() as i64,
1873            "Kids" => kids,
1874        };
1875        doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
1876
1877        // ── catalogue ──────────────────────────────────────────────
1878        let catalog_id = doc.add_object(dictionary! {
1879            "Type" => "Catalog",
1880            "Pages" => pages_id,
1881        });
1882        doc.trailer.set("Root", catalog_id);
1883
1884        // ── document info ──────────────────────────────────────────
1885        let mut info = lopdf::Dictionary::new();
1886        info.set(
1887            "Producer",
1888            Object::string_literal(concat!("zpl-forge ", env!("CARGO_PKG_VERSION"))),
1889        );
1890        if let Some(title) = &self.title {
1891            info.set("Title", Object::string_literal(title.as_str()));
1892        }
1893        let info_id = doc.add_object(Object::Dictionary(info));
1894        doc.trailer.set("Info", info_id);
1895
1896        doc.compress();
1897
1898        // ── serialize ──────────────────────────────────────────────
1899        let mut buf = std::io::BufWriter::new(Vec::new());
1900        doc.save_to(&mut buf)
1901            .map_err(|e| ZplError::BackendError(format!("Failed to save PDF: {}", e)))?;
1902        buf.into_inner()
1903            .map_err(|e| ZplError::BackendError(format!("Failed to flush: {}", e)))
1904    }
1905}