1use std::cmp::max;
7use std::collections::{HashMap, HashSet};
8use std::io::Write;
9use std::sync::Arc;
10
11use ab_glyph::{Font, FontArc, PxScale, ScaleFont};
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
23const KAPPA: f64 = 0.5522847498;
25
26const 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
40fn char_to_winansi(c: char) -> Option<u8> {
42 let cp = c as u32;
43 match cp {
44 0x20..=0x7E => Some(cp as u8),
45 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
54fn 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
67fn 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
90struct ImageXObject {
94 name: String,
95 data: Vec<u8>,
96 width: u32,
97 height: u32,
98 is_mask: bool,
100}
101
102pub struct PdfNativeBackend {
111 width_dots: f64,
112 height_dots: f64,
113 width_pt: f64,
114 height_pt: f64,
115 resolution: f32,
116 scale: f64,
118 content: Vec<u8>,
120 finished_pages: Vec<Vec<u8>>,
122 font_manager: Option<Arc<FontManager>>,
123 images: Vec<ImageXObject>,
124 image_counter: usize,
125 used_fonts: HashSet<char>,
127 compression: Compression,
128 title: Option<String>,
130 #[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
143impl PdfNativeBackend {
146 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 pub fn with_compression(mut self, compression: Compression) -> Self {
169 self.compression = compression;
170 self
171 }
172
173 pub fn with_title(mut self, title: impl Into<String>) -> Self {
175 self.title = Some(title.into());
176 self
177 }
178}
179
180impl PdfNativeBackend {
183 #[inline]
187 fn d2pt(&self, dots: f64) -> f64 {
188 dots * self.scale
189 }
190
191 #[inline]
193 fn x_pt(&self, x: f64) -> f64 {
194 x * self.scale
195 }
196
197 #[inline]
200 fn y_pt_bottom(&self, y: f64, h: f64) -> f64 {
201 self.height_pt - (y + h) * self.scale
202 }
203
204 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 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 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 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 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 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 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 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 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 fn fill_inverse_backdrop(&mut self, ex: f64, ey: f64, ew: f64, eh: f64) {
377 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 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 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 self.emit_nums(&[x + r, y], "m");
418 self.emit_nums(&[x + w - r, y], "l");
419 self.emit_nums(&[x + w - r + kr, y, x + w, y + r - kr, x + w, y + r], "c");
421 self.emit_nums(&[x + w, y + h - r], "l");
423 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 self.emit_nums(&[x + r, y + h], "l");
437 self.emit_nums(&[x + r - kr, y + h, x, y + h - r + kr, x, y + h - r], "c");
439 self.emit_nums(&[x, y + r], "l");
441 self.emit_nums(&[x, y + r - kr, x + r - kr, y, x + r, y], "c");
443 self.emit_op("h");
444 }
445
446 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 self.emit_nums(&[cx + rx, cy], "m");
453 self.emit_nums(&[cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry], "c");
455 self.emit_nums(&[cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy], "c");
457 self.emit_nums(&[cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry], "c");
459 self.emit_nums(&[cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy], "c");
461 self.emit_op("h");
462 }
463
464 fn get_font_arc(&self, font_char: char) -> ZplResult<&ab_glyph::FontArc> {
467 let fm = self
468 .font_manager
469 .as_ref()
470 .ok_or_else(|| ZplError::FontError("Font manager not initialized".into()))?;
471 fm.get_font(&font_char.to_string())
472 .or_else(|| fm.get_font("0"))
473 .ok_or_else(|| ZplError::FontError(format!("Font not found: {}", font_char)))
474 }
475
476 fn get_text_width(
477 &self,
478 text: &str,
479 font_char: char,
480 height: Option<u32>,
481 width: Option<u32>,
482 ) -> u32 {
483 let font = match self.get_font_arc(font_char) {
484 Ok(f) => f,
485 Err(_) => return 0,
486 };
487 let scale_y = height.unwrap_or(9) as f32;
488 let scale_x = width.unwrap_or(scale_y as u32) as f32;
489 let scale = PxScale {
490 x: scale_x,
491 y: scale_y,
492 };
493 let scaled = font.as_scaled(scale);
494 let mut w = 0.0_f32;
495 let mut last_glyph = None;
496 for c in text.chars() {
497 let gid = font.glyph_id(c);
498 if let Some(prev) = last_glyph {
499 w += scaled.kern(prev, gid);
500 }
501 w += scaled.h_advance(gid);
502 last_glyph = Some(gid);
503 }
504 w.ceil() as u32
505 }
506
507 fn embed_rgb_image(
512 &mut self,
513 x_dots: f64,
514 y_dots: f64,
515 img_w: u32,
516 img_h: u32,
517 rgb_data: Vec<u8>,
518 ) {
519 let name = format!("Im{}", self.image_counter);
520 self.image_counter += 1;
521
522 let px = self.x_pt(x_dots);
523 let py = self.y_pt_bottom(y_dots, img_h as f64);
524 let pw = self.d2pt(img_w as f64);
525 let ph = self.d2pt(img_h as f64);
526
527 self.save_state();
528 self.emit_nums(&[pw, 0.0, 0.0, ph, px, py], "cm");
529 self.emit_name_op(&name, "Do");
530 self.restore_state();
531
532 self.images.push(ImageXObject {
533 name,
534 data: rgb_data,
535 width: img_w,
536 height: img_h,
537 is_mask: false,
538 });
539 }
540
541 fn embed_mask_image(
545 &mut self,
546 x_dots: f64,
547 y_dots: f64,
548 img_w: u32,
549 img_h: u32,
550 bits: Vec<u8>,
551 reverse_print: bool,
552 ) {
553 let name = format!("Im{}", self.image_counter);
554 self.image_counter += 1;
555
556 let px = self.x_pt(x_dots);
557 let py = self.y_pt_bottom(y_dots, img_h as f64);
558 let pw = self.d2pt(img_w as f64);
559 let ph = self.d2pt(img_h as f64);
560
561 self.save_state();
562 if reverse_print {
563 let (br, bg, bb) =
567 self.backdrop_color_at(x_dots + img_w as f64 / 2.0, y_dots + img_h as f64 / 2.0);
568 self.set_fill_color(1.0 - br, 1.0 - bg, 1.0 - bb);
569 } else {
570 self.set_fill_color(0.0, 0.0, 0.0);
571 }
572 self.emit_nums(&[pw, 0.0, 0.0, ph, px, py], "cm");
573 self.emit_name_op(&name, "Do");
574 self.restore_state();
575
576 self.images.push(ImageXObject {
577 name,
578 data: bits,
579 width: img_w,
580 height: img_h,
581 is_mask: true,
582 });
583 }
584
585 #[allow(clippy::too_many_arguments)]
592 fn transform_1d_bar(
593 orientation: char,
594 base_x: u32,
595 base_y: u32,
596 lx: i32,
597 ly: i32,
598 w: u32,
599 h: u32,
600 bw: u32,
601 bh: u32,
602 ) -> (i32, i32, u32, u32) {
603 match orientation {
604 'R' => {
605 let nx = bh as i32 - (ly + h as i32);
606 let ny = lx;
607 (base_x as i32 + nx, base_y as i32 + ny, h, w)
608 }
609 'I' => {
610 let nx = bw as i32 - (lx + w as i32);
611 let ny = bh as i32 - (ly + h as i32);
612 (base_x as i32 + nx, base_y as i32 + ny, w, h)
613 }
614 'B' => {
615 let nx = ly;
616 let ny = bw as i32 - (lx + w as i32);
617 (base_x as i32 + nx, base_y as i32 + ny, h, w)
618 }
619 _ => (base_x as i32 + lx, base_y as i32 + ly, w, h),
620 }
621 }
622
623 #[allow(clippy::too_many_arguments)]
625 fn transform_2d_cell(
626 orientation: char,
627 base_x: u32,
628 base_y: u32,
629 lx: i32,
630 ly: i32,
631 w: u32,
632 h: u32,
633 full_w: u32,
634 full_h: u32,
635 ) -> (i32, i32, u32, u32) {
636 match orientation {
637 'R' => {
638 let nx = full_h as i32 - (ly + h as i32);
639 let ny = lx;
640 (base_x as i32 + nx, base_y as i32 + ny, h, w)
641 }
642 'I' => {
643 let nx = full_w as i32 - (lx + w as i32);
644 let ny = full_h as i32 - (ly + h as i32);
645 (base_x as i32 + nx, base_y as i32 + ny, w, h)
646 }
647 'B' => {
648 let nx = ly;
649 let ny = full_w as i32 - (lx + w as i32);
650 (base_x as i32 + nx, base_y as i32 + ny, h, w)
651 }
652 _ => (base_x as i32 + lx, base_y as i32 + ly, w, h),
653 }
654 }
655
656 #[allow(clippy::too_many_arguments)]
659 fn draw_1d_barcode(
660 &mut self,
661 x: u32,
662 y: u32,
663 orientation: char,
664 height: u32,
665 module_width: u32,
666 data: &str,
667 format: BarcodeFormat,
668 reverse_print: bool,
669 interpretation_line: char,
670 interpretation_line_above: char,
671 hints: Option<EncodeHints>,
672 hints_key: &str,
673 ) -> ZplResult<()> {
674 let bit_matrix = barcode_cache::encode_cached(format, data, hints_key, hints.as_ref())?;
675
676 let mw = max(module_width, 1);
677 let bh = height;
678 let bw = bit_matrix.getWidth() * mw;
679
680 let (full_w, full_h) = match orientation {
681 'R' | 'B' => (bh, bw),
682 _ => (bw, bh),
683 };
684
685 self.save_state();
687 if !reverse_print {
688 self.set_fill_color(0.0, 0.0, 0.0);
689 }
690
691 for gx in 0..bit_matrix.getWidth() {
692 if bit_matrix.get(gx, 0) {
693 let (rx, ry, rw, rh) =
694 Self::transform_1d_bar(orientation, x, y, (gx * mw) as i32, 0, mw, bh, bw, bh);
695 let px = self.d2pt(rx as f64);
696 let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
697 let pw = self.d2pt(rw as f64);
698 let ph = self.d2pt(rh as f64);
699 self.emit_nums(&[px, py, pw, ph], "re");
700 }
701 }
702 if reverse_print {
703 self.emit_op("W");
705 self.emit_op("n");
706 self.fill_inverse_backdrop(x as f64, y as f64, full_w as f64, full_h as f64);
707 } else {
708 self.emit_op("f");
709 }
710 self.restore_state();
711
712 if interpretation_line == 'Y' {
714 self.draw_interpretation_line(x, y, full_w, full_h, data, interpretation_line_above)?;
715 }
716
717 Ok(())
718 }
719
720 #[allow(clippy::too_many_arguments)]
721 fn draw_interpretation_line(
722 &mut self,
723 x: u32,
724 y: u32,
725 full_w: u32,
726 full_h: u32,
727 data: &str,
728 interpretation_line_above: char,
729 ) -> ZplResult<()> {
730 {
731 let font_char = '0';
732 let text_h: u32 = 18;
733 let text_y = if interpretation_line_above == 'Y' {
734 y.saturating_sub(text_h)
735 } else {
736 y + full_h
737 } + 6;
738
739 let text_width = self.get_text_width(data, font_char, Some(text_h), None);
740 let text_x = if full_w > text_width {
741 x + (full_w - text_width) / 2
742 } else {
743 x
744 };
745
746 self.draw_text(
747 text_x,
748 text_y,
749 font_char,
750 Some(text_h),
751 None,
752 'N',
753 data,
754 false,
755 None,
756 )?;
757 }
758
759 Ok(())
760 }
761
762 #[allow(clippy::too_many_arguments)]
766 fn fill_matrix_cells(
767 &mut self,
768 x: u32,
769 y: u32,
770 orientation: char,
771 cell_w: u32,
772 cell_h: u32,
773 bit_matrix: &BitMatrix,
774 reverse_print: bool,
775 ) {
776 let bw = bit_matrix.getWidth();
777 let bh = bit_matrix.getHeight();
778 let full_w = bw * cell_w;
779 let full_h = bh * cell_h;
780
781 self.save_state();
782 if !reverse_print {
783 self.set_fill_color(0.0, 0.0, 0.0);
784 }
785
786 for gy in 0..bh {
787 for gx in 0..bw {
788 if bit_matrix.get(gx, gy) {
789 let (rx, ry, rw, rh) = Self::transform_2d_cell(
790 orientation,
791 x,
792 y,
793 (gx * cell_w) as i32,
794 (gy * cell_h) as i32,
795 cell_w,
796 cell_h,
797 full_w,
798 full_h,
799 );
800 let px = self.d2pt(rx as f64);
801 let py = self.height_pt - self.d2pt(ry as f64 + rh as f64);
802 let pw = self.d2pt(rw as f64);
803 let ph = self.d2pt(rh as f64);
804 self.emit_nums(&[px, py, pw, ph], "re");
805 }
806 }
807 }
808 if reverse_print {
809 self.emit_op("W");
810 self.emit_op("n");
811 let (fw, fh) = match orientation {
812 'R' | 'B' => (full_h, full_w),
813 _ => (full_w, full_h),
814 };
815 self.fill_inverse_backdrop(x as f64, y as f64, fw as f64, fh as f64);
816 } else {
817 self.emit_op("f");
818 }
819 self.restore_state();
820 }
821}
822
823impl ZplForgeBackend for PdfNativeBackend {
826 fn setup_page(&mut self, width: f64, height: f64, resolution: f32) {
827 let dpi = if resolution == 0.0 { 203.2 } else { resolution };
828 self.width_dots = width;
829 self.height_dots = height;
830 self.resolution = dpi;
831 self.scale = 72.0 / dpi as f64;
832 self.width_pt = width * self.scale;
833 self.height_pt = height * self.scale;
834 }
835
836 fn setup_font_manager(&mut self, font_manager: &FontManager) {
837 self.font_manager = Some(Arc::new(font_manager.clone()));
838 }
839
840 fn new_page(&mut self) -> ZplResult<()> {
841 self.finished_pages.push(std::mem::take(&mut self.content));
842 self.backdrop_rects.clear();
843 Ok(())
844 }
845
846 fn draw_text(
849 &mut self,
850 x: u32,
851 y: u32,
852 font: char,
853 height: Option<u32>,
854 width: Option<u32>,
855 orientation: char,
856 text: &str,
857 reverse_print: bool,
858 color: Option<String>,
859 ) -> ZplResult<()> {
860 if text.is_empty() {
861 return Ok(());
862 }
863
864 let scale_y_dots = height.unwrap_or(9) as f32;
865 let scale_x_dots = width.unwrap_or(scale_y_dots as u32) as f32;
866 let px_scale = PxScale {
867 x: scale_x_dots,
868 y: scale_y_dots,
869 };
870
871 let ascent_dots = {
873 let font_arc = self.get_font_arc(font)?;
874 font_arc.as_scaled(px_scale).ascent()
875 } as f64;
876
877 self.used_fonts.insert(font);
878
879 let scale_x_pt = self.d2pt(scale_x_dots as f64);
880 let scale_y_pt = self.d2pt(scale_y_dots as f64);
881 let h_dots = scale_y_dots as f64;
882 let x = x as f64;
883 let y = y as f64;
884
885 let tw_dots = if reverse_print || orientation == 'I' || orientation == 'B' {
887 self.get_text_width(text, font, height, width) as f64
888 } else {
889 0.0
890 };
891
892 let tm = match orientation {
895 'R' => [
896 0.0,
897 -scale_x_pt,
898 scale_y_pt,
899 0.0,
900 self.x_pt(x + h_dots - ascent_dots),
901 self.height_pt - y * self.scale,
902 ],
903 'I' => [
904 -scale_x_pt,
905 0.0,
906 0.0,
907 -scale_y_pt,
908 self.x_pt(x + tw_dots),
909 self.height_pt - (y + h_dots - ascent_dots) * self.scale,
910 ],
911 'B' => [
912 0.0,
913 scale_x_pt,
914 -scale_y_pt,
915 0.0,
916 self.x_pt(x + ascent_dots),
917 self.height_pt - (y + tw_dots) * self.scale,
918 ],
919 _ => [
920 scale_x_pt,
921 0.0,
922 0.0,
923 scale_y_pt,
924 self.x_pt(x),
925 self.height_pt - (y + ascent_dots) * self.scale,
926 ],
927 };
928
929 self.save_state();
930 if !reverse_print {
931 let (r, g, b) = Self::parse_hex_color_f64(&color);
932 self.set_fill_color(r, g, b);
933 }
934
935 self.emit_op("BT");
936 if reverse_print {
937 self.emit_nums(&[7.0], "Tr");
939 }
940 self.emit_nums(&tm, "Tm");
941 let font_resource_name = format!("F_{}", font);
942 self.emit_name_op(&format!("{} 1", font_resource_name), "Tf");
943 self.emit_tj(text);
944 self.emit_op("ET");
945
946 if reverse_print {
947 let (bw_dots, bh_dots) = match orientation {
948 'R' | 'B' => (h_dots, tw_dots),
949 _ => (tw_dots, h_dots),
950 };
951 self.fill_inverse_backdrop(x, y, bw_dots, bh_dots);
952 }
953 self.restore_state();
954
955 Ok(())
956 }
957
958 fn draw_graphic_box(
961 &mut self,
962 x: u32,
963 y: u32,
964 width: u32,
965 height: u32,
966 thickness: u32,
967 color: char,
968 custom_color: Option<String>,
969 rounding: u32,
970 reverse_print: bool,
971 ) -> ZplResult<()> {
972 let w = max(width, 1) as f64;
973 let h = max(height, 1) as f64;
974 let t = thickness as f64;
975 let r_dots = rounding as f64 * 8.0;
976
977 let (draw_color, clear_color) = Self::resolve_colors(color, &custom_color);
978
979 let bx = self.x_pt(x as f64);
980 let by = self.y_pt_bottom(y as f64, h);
981 let bw = self.d2pt(w);
982 let bh = self.d2pt(h);
983 let br = self.d2pt(r_dots);
984
985 let has_inner = t * 2.0 < w && t * 2.0 < h;
986
987 if reverse_print {
988 self.save_state();
991 self.push_rounded_rect_path(bx, by, bw, bh, br);
992 if has_inner {
993 let tp = self.d2pt(t);
994 let inner_r = self.d2pt((r_dots - t).max(0.0));
995 self.push_rounded_rect_path(
996 bx + tp,
997 by + tp,
998 bw - tp * 2.0,
999 bh - tp * 2.0,
1000 inner_r,
1001 );
1002 self.emit_op("W*");
1003 } else {
1004 self.emit_op("W");
1005 }
1006 self.emit_op("n");
1007 self.fill_inverse_backdrop(x as f64, y as f64, w, h);
1008 self.restore_state();
1009 } else {
1010 self.save_state();
1011 let (r, g, b) = draw_color;
1012 self.set_fill_color(r, g, b);
1013 self.push_rounded_rect_path(bx, by, bw, bh, br);
1014 self.emit_op("f");
1015 self.track_backdrop_rect(x as f64, y as f64, w, h, draw_color);
1016
1017 if has_inner {
1018 let (cr, cg, cb) = clear_color;
1019 self.set_fill_color(cr, cg, cb);
1020 let tp = self.d2pt(t);
1021 let inner_r = self.d2pt((r_dots - t).max(0.0));
1022 self.push_rounded_rect_path(
1023 bx + tp,
1024 by + tp,
1025 bw - tp * 2.0,
1026 bh - tp * 2.0,
1027 inner_r,
1028 );
1029 self.emit_op("f");
1030 self.track_backdrop_rect(
1031 x as f64 + t,
1032 y as f64 + t,
1033 w - t * 2.0,
1034 h - t * 2.0,
1035 clear_color,
1036 );
1037 }
1038 self.restore_state();
1039 }
1040
1041 Ok(())
1042 }
1043
1044 fn draw_graphic_circle(
1047 &mut self,
1048 x: u32,
1049 y: u32,
1050 radius: u32,
1051 thickness: u32,
1052 _color: char,
1053 custom_color: Option<String>,
1054 reverse_print: bool,
1055 ) -> ZplResult<()> {
1056 let (draw_color, _) = Self::resolve_colors('B', &custom_color);
1057
1058 let r_pt = self.d2pt(radius as f64);
1059 let cx_pt = self.x_pt(x as f64) + r_pt;
1061 let cy_pt = self.height_pt - (y as f64 + radius as f64) * self.scale;
1062
1063 if reverse_print {
1064 self.save_state();
1065 self.push_ellipse_path(cx_pt, cy_pt, r_pt, r_pt);
1066 if radius > thickness {
1067 let inner_r = self.d2pt((radius - thickness) as f64);
1068 self.push_ellipse_path(cx_pt, cy_pt, inner_r, inner_r);
1069 self.emit_op("W*");
1070 } else {
1071 self.emit_op("W");
1072 }
1073 self.emit_op("n");
1074 self.fill_inverse_backdrop(
1075 x as f64,
1076 y as f64,
1077 radius as f64 * 2.0,
1078 radius as f64 * 2.0,
1079 );
1080 self.restore_state();
1081 } else {
1082 self.save_state();
1083 let (r, g, b) = draw_color;
1084 self.set_fill_color(r, g, b);
1085 self.push_ellipse_path(cx_pt, cy_pt, r_pt, r_pt);
1086 self.emit_op("f");
1087
1088 if radius > thickness {
1089 self.set_fill_color(1.0, 1.0, 1.0);
1090 let inner_r = self.d2pt((radius - thickness) as f64);
1091 self.push_ellipse_path(cx_pt, cy_pt, inner_r, inner_r);
1092 self.emit_op("f");
1093 }
1094 self.restore_state();
1095 }
1096
1097 Ok(())
1098 }
1099
1100 fn draw_graphic_ellipse(
1103 &mut self,
1104 x: u32,
1105 y: u32,
1106 width: u32,
1107 height: u32,
1108 thickness: u32,
1109 _color: char,
1110 custom_color: Option<String>,
1111 reverse_print: bool,
1112 ) -> ZplResult<()> {
1113 let (draw_color, _) = Self::resolve_colors('B', &custom_color);
1114
1115 let rx_pt = self.d2pt(width as f64 / 2.0);
1116 let ry_pt = self.d2pt(height as f64 / 2.0);
1117 let cx_pt = self.x_pt(x as f64) + rx_pt;
1118 let cy_pt = self.height_pt - (y as f64 + height as f64 / 2.0) * self.scale;
1119
1120 let t = thickness as f64;
1121
1122 if reverse_print {
1123 self.save_state();
1124 self.push_ellipse_path(cx_pt, cy_pt, rx_pt, ry_pt);
1125 if (width as f64 / 2.0) > t && (height as f64 / 2.0) > t {
1126 let irx = self.d2pt(width as f64 / 2.0 - t);
1127 let iry = self.d2pt(height as f64 / 2.0 - t);
1128 self.push_ellipse_path(cx_pt, cy_pt, irx, iry);
1129 self.emit_op("W*");
1130 } else {
1131 self.emit_op("W");
1132 }
1133 self.emit_op("n");
1134 self.fill_inverse_backdrop(x as f64, y as f64, width as f64, height as f64);
1135 self.restore_state();
1136 } else {
1137 self.save_state();
1138 let (r, g, b) = draw_color;
1139 self.set_fill_color(r, g, b);
1140 self.push_ellipse_path(cx_pt, cy_pt, rx_pt, ry_pt);
1141 self.emit_op("f");
1142
1143 if (width as f64 / 2.0) > t && (height as f64 / 2.0) > t {
1144 self.set_fill_color(1.0, 1.0, 1.0);
1145 let irx = self.d2pt(width as f64 / 2.0 - t);
1146 let iry = self.d2pt(height as f64 / 2.0 - t);
1147 self.push_ellipse_path(cx_pt, cy_pt, irx, iry);
1148 self.emit_op("f");
1149 }
1150 self.restore_state();
1151 }
1152
1153 Ok(())
1154 }
1155
1156 fn draw_graphic_field(
1159 &mut self,
1160 x: u32,
1161 y: u32,
1162 width: u32,
1163 height: u32,
1164 data: &[u8],
1165 reverse_print: bool,
1166 ) -> ZplResult<()> {
1167 if width == 0 || height == 0 {
1168 return Ok(());
1169 }
1170
1171 let row_bytes = width.div_ceil(8) as usize;
1175 let total_bytes = row_bytes * height as usize;
1176 let mut bits = data.to_vec();
1177 bits.resize(total_bytes, 0x00);
1178
1179 self.embed_mask_image(x as f64, y as f64, width, height, bits, reverse_print);
1180 Ok(())
1181 }
1182
1183 fn draw_graphic_image_custom(
1186 &mut self,
1187 x: u32,
1188 y: u32,
1189 width: u32,
1190 height: u32,
1191 data: &str,
1192 ) -> ZplResult<()> {
1193 let image_data = general_purpose::STANDARD
1194 .decode(data.trim())
1195 .map_err(|e| ZplError::ImageError(format!("Failed to decode base64: {}", e)))?;
1196
1197 let img = image::load_from_memory(&image_data)
1198 .map_err(|e| ZplError::ImageError(format!("Failed to load image: {}", e)))?
1199 .to_rgb8();
1200
1201 let (orig_w, orig_h) = img.dimensions();
1202 let (target_w, target_h) = match (width, height) {
1203 (0, 0) => (orig_w, orig_h),
1204 (w, 0) => {
1205 let h = (orig_h as f32 * (w as f32 / orig_w as f32)).round() as u32;
1206 (w, h)
1207 }
1208 (0, h) => {
1209 let w = (orig_w as f32 * (h as f32 / orig_h as f32)).round() as u32;
1210 (w, h)
1211 }
1212 (w, h) => (w, h),
1213 };
1214
1215 let final_img = if target_w != orig_w || target_h != orig_h {
1216 image::imageops::resize(
1217 &img,
1218 target_w,
1219 target_h,
1220 image::imageops::FilterType::Lanczos3,
1221 )
1222 } else {
1223 img
1224 };
1225
1226 let rgb_data = final_img.into_raw();
1227 self.embed_rgb_image(x as f64, y as f64, target_w, target_h, rgb_data);
1228 Ok(())
1229 }
1230
1231 fn draw_code128(
1234 &mut self,
1235 x: u32,
1236 y: u32,
1237 orientation: char,
1238 height: u32,
1239 module_width: u32,
1240 interpretation_line: char,
1241 interpretation_line_above: char,
1242 _check_digit: char,
1243 _mode: char,
1244 data: &str,
1245 reverse_print: bool,
1246 ) -> ZplResult<()> {
1247 let (clean_data, hint_val) = if let Some(stripped) = data.strip_prefix(">:") {
1248 (stripped, Some("B"))
1249 } else if let Some(stripped) = data.strip_prefix(">;") {
1250 (stripped, Some("C"))
1251 } else if let Some(stripped) = data.strip_prefix(">9") {
1252 (stripped, Some("A"))
1253 } else {
1254 (data, Some("B")) };
1256
1257 let hints = hint_val.map(|v| {
1258 let mut h = HashMap::new();
1259 h.insert(
1260 EncodeHintType::FORCE_CODE_SET,
1261 EncodeHintValue::ForceCodeSet(v.to_string()),
1262 );
1263 EncodeHints::from(h)
1264 });
1265
1266 self.draw_1d_barcode(
1267 x,
1268 y,
1269 orientation,
1270 height,
1271 module_width,
1272 clean_data,
1273 BarcodeFormat::CODE_128,
1274 reverse_print,
1275 interpretation_line,
1276 interpretation_line_above,
1277 hints,
1278 hint_val.unwrap_or(""),
1279 )
1280 }
1281
1282 fn draw_qr_code(
1285 &mut self,
1286 x: u32,
1287 y: u32,
1288 orientation: char,
1289 _model: u32,
1290 magnification: u32,
1291 error_correction: char,
1292 _mask: u32,
1293 data: &str,
1294 reverse_print: bool,
1295 ) -> ZplResult<()> {
1296 let level = match error_correction {
1297 'L' => "L",
1298 'M' => "M",
1299 'Q' => "Q",
1300 'H' => "H",
1301 _ => "M",
1302 };
1303
1304 let mut hints = HashMap::new();
1305 hints.insert(
1306 EncodeHintType::ERROR_CORRECTION,
1307 EncodeHintValue::ErrorCorrection(level.to_string()),
1308 );
1309 hints.insert(
1310 EncodeHintType::MARGIN,
1311 EncodeHintValue::Margin("0".to_owned()),
1312 );
1313 let hints: EncodeHints = hints.into();
1314
1315 let bit_matrix = barcode_cache::encode_cached(
1316 BarcodeFormat::QR_CODE,
1317 data,
1318 &format!("ec:{}", level),
1319 Some(&hints),
1320 )?;
1321
1322 let mag = max(magnification, 1);
1323 self.fill_matrix_cells(x, y, orientation, mag, mag, &bit_matrix, reverse_print);
1324 Ok(())
1325 }
1326
1327 fn draw_datamatrix(
1330 &mut self,
1331 x: u32,
1332 y: u32,
1333 orientation: char,
1334 module_size: u32,
1335 data: &str,
1336 reverse_print: bool,
1337 ) -> ZplResult<()> {
1338 let bit_matrix = barcode_cache::encode_cached(BarcodeFormat::DATA_MATRIX, data, "", None)?;
1339
1340 let m = max(module_size, 1);
1341 self.fill_matrix_cells(x, y, orientation, m, m, &bit_matrix, reverse_print);
1342 Ok(())
1343 }
1344
1345 fn draw_pdf417(
1348 &mut self,
1349 x: u32,
1350 y: u32,
1351 orientation: char,
1352 row_height: u32,
1353 module_width: u32,
1354 security_level: u32,
1355 data: &str,
1356 reverse_print: bool,
1357 ) -> ZplResult<()> {
1358 let mut hints = HashMap::new();
1359 hints.insert(
1360 EncodeHintType::ERROR_CORRECTION,
1361 EncodeHintValue::ErrorCorrection(security_level.min(8).to_string()),
1362 );
1363 hints.insert(
1364 EncodeHintType::MARGIN,
1365 EncodeHintValue::Margin("0".to_owned()),
1366 );
1367 let hints: EncodeHints = hints.into();
1368
1369 let bit_matrix = barcode_cache::encode_cached(
1370 BarcodeFormat::PDF_417,
1371 data,
1372 &format!("ec:{}", security_level.min(8)),
1373 Some(&hints),
1374 )?;
1375
1376 let cw = max(module_width, 1);
1377 let ch = max(row_height, 1);
1378 self.fill_matrix_cells(x, y, orientation, cw, ch, &bit_matrix, reverse_print);
1379 Ok(())
1380 }
1381
1382 fn draw_code39(
1385 &mut self,
1386 x: u32,
1387 y: u32,
1388 orientation: char,
1389 _check_digit: char,
1390 height: u32,
1391 module_width: u32,
1392 interpretation_line: char,
1393 interpretation_line_above: char,
1394 data: &str,
1395 reverse_print: bool,
1396 ) -> ZplResult<()> {
1397 self.draw_1d_barcode(
1398 x,
1399 y,
1400 orientation,
1401 height,
1402 module_width,
1403 data,
1404 BarcodeFormat::CODE_39,
1405 reverse_print,
1406 interpretation_line,
1407 interpretation_line_above,
1408 None,
1409 "",
1410 )
1411 }
1412
1413 fn draw_barcode_1d(
1416 &mut self,
1417 kind: Barcode1DKind,
1418 x: u32,
1419 y: u32,
1420 orientation: char,
1421 height: u32,
1422 module_width: u32,
1423 interpretation_line: char,
1424 interpretation_line_above: char,
1425 data: &str,
1426 reverse_print: bool,
1427 ) -> ZplResult<()> {
1428 self.draw_1d_barcode(
1429 x,
1430 y,
1431 orientation,
1432 height,
1433 module_width,
1434 data,
1435 barcode_1d_format(kind),
1436 reverse_print,
1437 interpretation_line,
1438 interpretation_line_above,
1439 None,
1440 "",
1441 )
1442 }
1443
1444 fn draw_graphic_diagonal(
1447 &mut self,
1448 x: u32,
1449 y: u32,
1450 width: u32,
1451 height: u32,
1452 thickness: u32,
1453 color: char,
1454 custom_color: Option<String>,
1455 diagonal_orientation: char,
1456 reverse_print: bool,
1457 ) -> ZplResult<()> {
1458 let (draw_color, _) = Self::resolve_colors(color, &custom_color);
1459
1460 let w = max(width, 1) as f64;
1461 let h = max(height, 1) as f64;
1462 let t = (max(thickness, 1) as f64).min(w);
1463 let x = x as f64;
1464 let y = y as f64;
1465
1466 let pts: [(f64, f64); 4] = if diagonal_orientation == 'L' {
1468 [(x, y), (x + t, y), (x + w, y + h), (x + w - t, y + h)]
1470 } else {
1471 [(x, y + h), (x + t, y + h), (x + w, y), (x + w - t, y)]
1473 };
1474
1475 self.save_state();
1476 if !reverse_print {
1477 let (r, g, b) = draw_color;
1478 self.set_fill_color(r, g, b);
1479 }
1480
1481 for (i, (dx, dy)) in pts.iter().enumerate() {
1482 let px = self.x_pt(*dx);
1483 let py = self.height_pt - dy * self.scale;
1484 self.emit_nums(&[px, py], if i == 0 { "m" } else { "l" });
1485 }
1486 self.emit_op("h");
1487 if reverse_print {
1488 self.emit_op("W");
1489 self.emit_op("n");
1490 self.fill_inverse_backdrop(x, y, w, h);
1491 } else {
1492 self.emit_op("f");
1493 }
1494 self.restore_state();
1495
1496 Ok(())
1497 }
1498
1499 fn finalize(&mut self) -> ZplResult<Vec<u8>> {
1502 let mut doc = Document::with_version("1.5");
1503 let pages_id = doc.new_object_id();
1504
1505 let default_font_bytes: &[u8] = include_bytes!("../assets/IosevkaTermSlab-Regular.ttf");
1513 let mut font_dict = lopdf::Dictionary::new();
1514 let mut embedded_fonts: HashMap<String, lopdf::ObjectId> = HashMap::new();
1516 let tounicode_id = doc.add_object(Stream::new(dictionary! {}, build_tounicode_cmap()));
1517
1518 for font_char in &self.used_fonts {
1519 let font_key = font_char.to_string();
1520 let resource_name = format!("F_{}", font_char);
1521
1522 let actual_name = self
1523 .font_manager
1524 .as_ref()
1525 .and_then(|fm| fm.get_font_name(&font_key).map(|s| s.to_string()))
1526 .unwrap_or_else(|| "Iosevka Term Slab".to_string());
1527
1528 if let Some(font_id) = embedded_fonts.get(&actual_name) {
1529 font_dict.set(resource_name.as_str(), *font_id);
1530 continue;
1531 }
1532
1533 let raw_bytes = self
1534 .font_manager
1535 .as_ref()
1536 .and_then(|fm| fm.get_font_bytes(&font_key))
1537 .unwrap_or(default_font_bytes);
1538
1539 let face = FontArc::try_from_vec(raw_bytes.to_vec())
1540 .map_err(|e| ZplError::FontError(format!("Invalid font data: {}", e)))?;
1541 let upem = face.units_per_em().unwrap_or(1000.0) as f64;
1542 let to_glyph_space = |v: f64| (v * 1000.0 / upem).round() as i64;
1543
1544 let widths: Vec<Object> = (0x20..=0xFFu32)
1546 .map(|code| {
1547 let w = winansi_to_char(code as u8)
1548 .map(|ch| to_glyph_space(face.h_advance_unscaled(face.glyph_id(ch)) as f64))
1549 .unwrap_or(0);
1550 w.into()
1551 })
1552 .collect();
1553
1554 let fd = FontData::new(raw_bytes, actual_name.clone());
1556
1557 let font_stream = Stream::new(
1558 dictionary! { "Length1" => raw_bytes.len() as i64 },
1559 raw_bytes.to_vec(),
1560 );
1561 let font_file_id = doc.add_object(font_stream);
1562
1563 let descriptor_id = doc.add_object(dictionary! {
1564 "Type" => "FontDescriptor",
1565 "FontName" => Object::Name(actual_name.clone().into_bytes()),
1566 "Flags" => 32_i64,
1567 "FontBBox" => vec![
1568 to_glyph_space(fd.font_bbox.0 as f64).into(),
1569 to_glyph_space(fd.font_bbox.1 as f64).into(),
1570 to_glyph_space(fd.font_bbox.2 as f64).into(),
1571 to_glyph_space(fd.font_bbox.3 as f64).into(),
1572 ],
1573 "ItalicAngle" => fd.italic_angle,
1574 "Ascent" => to_glyph_space(fd.ascent as f64),
1575 "Descent" => to_glyph_space(fd.descent as f64),
1576 "CapHeight" => to_glyph_space(fd.cap_height as f64),
1577 "StemV" => 80_i64,
1578 "FontFile2" => font_file_id,
1579 });
1580
1581 let font_id = doc.add_object(dictionary! {
1582 "Type" => "Font",
1583 "Subtype" => "TrueType",
1584 "BaseFont" => Object::Name(actual_name.clone().into_bytes()),
1585 "FirstChar" => 32_i64,
1586 "LastChar" => 255_i64,
1587 "Widths" => widths,
1588 "FontDescriptor" => descriptor_id,
1589 "Encoding" => "WinAnsiEncoding",
1590 "ToUnicode" => tounicode_id,
1591 });
1592
1593 font_dict.set(resource_name.as_str(), font_id);
1594 embedded_fonts.insert(actual_name, font_id);
1595 }
1596
1597 let mut xobject_dict = lopdf::Dictionary::new();
1599 for img in &self.images {
1600 let mut encoder = ZlibEncoder::new(Vec::new(), self.compression);
1601 encoder
1602 .write_all(&img.data)
1603 .map_err(|e| ZplError::BackendError(e.to_string()))?;
1604 let compressed = encoder
1605 .finish()
1606 .map_err(|e| ZplError::BackendError(e.to_string()))?;
1607
1608 let dict = if img.is_mask {
1609 dictionary! {
1612 "Type" => "XObject",
1613 "Subtype" => "Image",
1614 "Width" => img.width as i64,
1615 "Height" => img.height as i64,
1616 "ImageMask" => true,
1617 "BitsPerComponent" => 1,
1618 "Decode" => vec![0.into(), 1.into()],
1619 "Filter" => "FlateDecode",
1620 }
1621 } else {
1622 dictionary! {
1623 "Type" => "XObject",
1624 "Subtype" => "Image",
1625 "Width" => img.width as i64,
1626 "Height" => img.height as i64,
1627 "ColorSpace" => "DeviceRGB",
1628 "BitsPerComponent" => 8,
1629 "Filter" => "FlateDecode",
1630 }
1631 };
1632 let img_stream = Stream::new(dict, compressed);
1633 let img_id = doc.add_object(img_stream);
1634 xobject_dict.set(img.name.as_str(), img_id);
1635 }
1636
1637 let resources_id = doc.add_object(dictionary! {
1639 "Font" => lopdf::Object::Dictionary(font_dict),
1640 "XObject" => lopdf::Object::Dictionary(xobject_dict),
1641 });
1642
1643 let mut page_contents = std::mem::take(&mut self.finished_pages);
1645 page_contents.push(std::mem::take(&mut self.content));
1646
1647 let mut kids: Vec<Object> = Vec::with_capacity(page_contents.len());
1648 for content_bytes in page_contents {
1649 let content_id = doc.add_object(Stream::new(dictionary! {}, content_bytes));
1650 let page_id = doc.add_object(dictionary! {
1651 "Type" => "Page",
1652 "Parent" => pages_id,
1653 "MediaBox" => vec![
1654 0.into(),
1655 0.into(),
1656 Object::Real(self.width_pt as f32),
1657 Object::Real(self.height_pt as f32),
1658 ],
1659 "Contents" => content_id,
1660 "Resources" => resources_id,
1661 });
1662 kids.push(page_id.into());
1663 }
1664
1665 let pages_dict = dictionary! {
1667 "Type" => "Pages",
1668 "Count" => kids.len() as i64,
1669 "Kids" => kids,
1670 };
1671 doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
1672
1673 let catalog_id = doc.add_object(dictionary! {
1675 "Type" => "Catalog",
1676 "Pages" => pages_id,
1677 });
1678 doc.trailer.set("Root", catalog_id);
1679
1680 let mut info = lopdf::Dictionary::new();
1682 info.set(
1683 "Producer",
1684 Object::string_literal(concat!("zpl-forge ", env!("CARGO_PKG_VERSION"))),
1685 );
1686 if let Some(title) = &self.title {
1687 info.set("Title", Object::string_literal(title.as_str()));
1688 }
1689 let info_id = doc.add_object(Object::Dictionary(info));
1690 doc.trailer.set("Info", info_id);
1691
1692 doc.compress();
1693
1694 let mut buf = std::io::BufWriter::new(Vec::new());
1696 doc.save_to(&mut buf)
1697 .map_err(|e| ZplError::BackendError(format!("Failed to save PDF: {}", e)))?;
1698 buf.into_inner()
1699 .map_err(|e| ZplError::BackendError(format!("Failed to flush: {}", e)))
1700 }
1701}