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