Skip to main content

zpl_forge/forge/
pdf.rs

1use crate::engine::{FontManager, ZplForgeBackend};
2use crate::forge::png::PngBackend;
3use crate::{ZplError, ZplResult};
4
5use flate2::Compression;
6use flate2::write::ZlibEncoder;
7use image::ImageDecoder;
8use image::codecs::png::PngDecoder;
9use lopdf::content::{Content, Operation};
10use lopdf::{Document, Object, Stream, dictionary};
11use rayon::prelude::*;
12use std::io::{BufWriter, Write};
13
14/// A rendering backend that produces PDF documents.
15///
16/// This backend acts as a wrapper around [`PngBackend`]. It renders the ZPL
17/// commands into a high-resolution PNG image first, then embeds that image
18/// into a PDF document of the corresponding physical size.
19pub struct PdfBackend {
20    png_backend: PngBackend,
21    width_dots: f64,
22    height_dots: f64,
23    resolution: f32,
24    compression: Compression,
25}
26
27impl Default for PdfBackend {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl PdfBackend {
34    /// Sets the zlib compression level for the PDF output.
35    ///
36    /// This is a builder method. The default level is [`Compression::default()`].
37    pub fn with_compression(mut self, compression: Compression) -> Self {
38        self.compression = compression;
39        self
40    }
41
42    /// Creates a new `PdfBackend` instance with default settings.
43    ///
44    /// The label is first rendered as a PNG via [`PngBackend`], then embedded
45    /// into a single-page PDF. Zlib compression defaults to [`Compression::default()`].
46    pub fn new() -> Self {
47        Self {
48            png_backend: PngBackend::new(),
49            width_dots: 0.0,
50            height_dots: 0.0,
51            resolution: 0.0,
52            compression: Compression::default(),
53        }
54    }
55}
56
57/// Decodes a PNG buffer into zlib-compressed RGB pixels, returning (compressed_bytes, width, height).
58type PreparedPage = (Vec<u8>, u32, u32);
59
60fn decode_and_compress_png(
61    png_data: &[u8],
62    compression: Compression,
63) -> Result<PreparedPage, String> {
64    let decoder = PngDecoder::new(std::io::Cursor::new(png_data))
65        .map_err(|e| format!("Failed to create PNG decoder: {}", e))?;
66    let (w, h) = decoder.dimensions();
67    let channels = decoder.color_type().channel_count() as usize;
68    let mut raw_buf = vec![0u8; decoder.total_bytes() as usize];
69    decoder
70        .read_image(&mut raw_buf)
71        .map_err(|e| format!("Failed to decode PNG: {}", e))?;
72
73    // PngBackend generates RGB (3 channels). If for any reason it's RGBA (4 channels),
74    // composite against a white background to produce clean RGB.
75    let rgb_buf = if channels == 4 {
76        let mut rgb = Vec::with_capacity((w * h * 3) as usize);
77        for pixel in raw_buf.chunks_exact(4) {
78            let a = pixel[3] as u16;
79            let inv_a = 255 - a;
80            rgb.push(((pixel[0] as u16 * a + 255 * inv_a) / 255) as u8);
81            rgb.push(((pixel[1] as u16 * a + 255 * inv_a) / 255) as u8);
82            rgb.push(((pixel[2] as u16 * a + 255 * inv_a) / 255) as u8);
83        }
84        rgb
85    } else {
86        // Already RGB — use as-is
87        raw_buf
88    };
89
90    let mut encoder = ZlibEncoder::new(Vec::new(), compression);
91    encoder
92        .write_all(&rgb_buf)
93        .map_err(|e| format!("Failed to compress: {}", e))?;
94    let compressed = encoder
95        .finish()
96        .map_err(|e| format!("Failed to finish compression: {}", e))?;
97
98    Ok((compressed, w, h))
99}
100
101/// Builds a complete PDF document from pre-processed page data.
102fn build_pdf(
103    prepared_pages: &[(Vec<u8>, u32, u32)],
104    page_w_pt: f64,
105    page_h_pt: f64,
106) -> Result<Vec<u8>, String> {
107    let mut doc = Document::with_version("1.5");
108    let pages_id = doc.new_object_id();
109    let mut page_ids: Vec<Object> = Vec::with_capacity(prepared_pages.len());
110
111    for (compressed_pixels, img_w, img_h) in prepared_pages {
112        let img_stream = Stream::new(
113            dictionary! {
114                "Type" => "XObject",
115                "Subtype" => "Image",
116                "Width" => *img_w as i64,
117                "Height" => *img_h as i64,
118                "ColorSpace" => "DeviceRGB",
119                "BitsPerComponent" => 8,
120                "Filter" => "FlateDecode",
121                "Length" => compressed_pixels.len() as i64,
122            },
123            compressed_pixels.clone(),
124        );
125        let img_id = doc.add_object(img_stream);
126
127        let content = Content {
128            operations: vec![
129                Operation::new("q", vec![]),
130                Operation::new(
131                    "cm",
132                    vec![
133                        page_w_pt.into(),
134                        0.into(),
135                        0.into(),
136                        page_h_pt.into(),
137                        0.into(),
138                        0.into(),
139                    ],
140                ),
141                Operation::new("Do", vec!["Im0".into()]),
142                Operation::new("Q", vec![]),
143            ],
144        };
145        let content_bytes = content
146            .encode()
147            .map_err(|e| format!("Failed to encode content: {}", e))?;
148        let content_id = doc.add_object(Stream::new(dictionary! {}, content_bytes));
149
150        let resources = dictionary! {
151            "XObject" => dictionary! {
152                "Im0" => img_id,
153            },
154        };
155
156        let page_obj = dictionary! {
157            "Type" => "Page",
158            "Parent" => pages_id,
159            "MediaBox" => vec![
160                0.into(),
161                0.into(),
162                Object::Real(page_w_pt as f32),
163                Object::Real(page_h_pt as f32),
164            ],
165            "Contents" => content_id,
166            "Resources" => resources,
167        };
168        let page_id = doc.add_object(page_obj);
169        page_ids.push(page_id.into());
170    }
171
172    let pages_dict = dictionary! {
173        "Type" => "Pages",
174        "Count" => page_ids.len() as i64,
175        "Kids" => page_ids,
176    };
177    doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
178
179    let catalog = dictionary! {
180        "Type" => "Catalog",
181        "Pages" => pages_id,
182    };
183    let catalog_id = doc.add_object(catalog);
184    doc.trailer.set("Root", catalog_id);
185    doc.compress();
186
187    let mut buf = BufWriter::new(Vec::new());
188    doc.save_to(&mut buf)
189        .map_err(|e| format!("Failed to save PDF: {}", e))?;
190
191    buf.into_inner()
192        .map_err(|e| format!("Failed to flush PDF buffer: {}", e))
193}
194
195/// Merges multiple PNG images into a single multi-page PDF document.
196///
197/// Each PNG in `pages` becomes one page in the resulting PDF.
198/// All pages share the same dimensions and resolution.
199/// PNG decoding and zlib compression are parallelized across all available CPU cores.
200///
201/// # Arguments
202/// * `pages` - A slice of PNG byte buffers (each element is a complete PNG image).
203/// * `width_dots` - The width of each page in dots.
204/// * `height_dots` - The height of each page in dots.
205/// * `dpi` - The resolution in dots per inch.
206/// * `compression` - The zlib compression level applied to the raw pixel data before embedding.
207///
208/// # Example
209/// ```rust,no_run
210/// use zpl_forge::forge::pdf::png_merge_pages_to_pdf;
211/// let png1_bytes: Vec<u8> = vec![]; // PNG bytes from PngBackend
212/// let png2_bytes: Vec<u8> = vec![]; // PNG bytes from PngBackend
213/// let pdf_bytes = png_merge_pages_to_pdf(&[png1_bytes, png2_bytes], 812.0, 406.0, 203.2, flate2::Compression::default()).unwrap();
214/// ```
215pub fn png_merge_pages_to_pdf(
216    pages: &[Vec<u8>],
217    width_dots: f64,
218    height_dots: f64,
219    dpi: f32,
220    compression: Compression,
221) -> ZplResult<Vec<u8>> {
222    if pages.is_empty() {
223        return Err(ZplError::BackendError("No pages to merge".to_string()));
224    }
225
226    let dpi_f64 = if dpi == 0.0 { 203.2 } else { dpi as f64 };
227    let page_w_pt = (width_dots / dpi_f64) * 72.0;
228    let page_h_pt = (height_dots / dpi_f64) * 72.0;
229
230    // Parallel: decode PNGs and compress to zlib (one thread per CPU core)
231    let prepared: Vec<Result<PreparedPage, String>> = pages
232        .par_iter()
233        .map(|png_data| decode_and_compress_png(png_data, compression))
234        .collect();
235
236    // Check for errors
237    let prepared: Vec<(Vec<u8>, u32, u32)> = prepared
238        .into_iter()
239        .collect::<Result<Vec<_>, _>>()
240        .map_err(ZplError::BackendError)?;
241
242    // Sequential: assemble the PDF (preserving page order)
243    let pdf_bytes = build_pdf(&prepared, page_w_pt, page_h_pt).map_err(ZplError::BackendError)?;
244
245    Ok(pdf_bytes)
246}
247
248impl ZplForgeBackend for PdfBackend {
249    fn setup_page(&mut self, width: f64, height: f64, resolution: f32) {
250        self.width_dots = width;
251        self.height_dots = height;
252        self.resolution = resolution;
253        self.png_backend.setup_page(width, height, resolution);
254    }
255
256    fn setup_font_manager(&mut self, font_manager: &FontManager) {
257        self.png_backend.setup_font_manager(font_manager);
258    }
259
260    fn draw_text(
261        &mut self,
262        x: u32,
263        y: u32,
264        font: char,
265        height: Option<u32>,
266        width: Option<u32>,
267        text: &str,
268        reverse_print: bool,
269        color: Option<String>,
270    ) -> ZplResult<()> {
271        self.png_backend
272            .draw_text(x, y, font, height, width, text, reverse_print, color)
273    }
274
275    fn draw_graphic_box(
276        &mut self,
277        x: u32,
278        y: u32,
279        width: u32,
280        height: u32,
281        thickness: u32,
282        color: char,
283        custom_color: Option<String>,
284        rounding: u32,
285        reverse_print: bool,
286    ) -> ZplResult<()> {
287        self.png_backend.draw_graphic_box(
288            x,
289            y,
290            width,
291            height,
292            thickness,
293            color,
294            custom_color,
295            rounding,
296            reverse_print,
297        )
298    }
299
300    fn draw_graphic_circle(
301        &mut self,
302        x: u32,
303        y: u32,
304        radius: u32,
305        thickness: u32,
306        color: char,
307        custom_color: Option<String>,
308        reverse_print: bool,
309    ) -> ZplResult<()> {
310        self.png_backend.draw_graphic_circle(
311            x,
312            y,
313            radius,
314            thickness,
315            color,
316            custom_color,
317            reverse_print,
318        )
319    }
320
321    fn draw_graphic_ellipse(
322        &mut self,
323        x: u32,
324        y: u32,
325        width: u32,
326        height: u32,
327        thickness: u32,
328        color: char,
329        custom_color: Option<String>,
330        reverse_print: bool,
331    ) -> ZplResult<()> {
332        self.png_backend.draw_graphic_ellipse(
333            x,
334            y,
335            width,
336            height,
337            thickness,
338            color,
339            custom_color,
340            reverse_print,
341        )
342    }
343
344    fn draw_graphic_field(
345        &mut self,
346        x: u32,
347        y: u32,
348        width: u32,
349        height: u32,
350        data: &[u8],
351        reverse_print: bool,
352    ) -> ZplResult<()> {
353        self.png_backend
354            .draw_graphic_field(x, y, width, height, data, reverse_print)
355    }
356
357    fn draw_graphic_image_custom(
358        &mut self,
359        x: u32,
360        y: u32,
361        width: u32,
362        height: u32,
363        data: &str,
364    ) -> ZplResult<()> {
365        self.png_backend
366            .draw_graphic_image_custom(x, y, width, height, data)
367    }
368
369    fn draw_code128(
370        &mut self,
371        x: u32,
372        y: u32,
373        orientation: char,
374        height: u32,
375        module_width: u32,
376        interpretation_line: char,
377        interpretation_line_above: char,
378        check_digit: char,
379        mode: char,
380        data: &str,
381        reverse_print: bool,
382    ) -> ZplResult<()> {
383        self.png_backend.draw_code128(
384            x,
385            y,
386            orientation,
387            height,
388            module_width,
389            interpretation_line,
390            interpretation_line_above,
391            check_digit,
392            mode,
393            data,
394            reverse_print,
395        )
396    }
397
398    fn draw_qr_code(
399        &mut self,
400        x: u32,
401        y: u32,
402        orientation: char,
403        model: u32,
404        magnification: u32,
405        error_correction: char,
406        mask: u32,
407        data: &str,
408        reverse_print: bool,
409    ) -> ZplResult<()> {
410        self.png_backend.draw_qr_code(
411            x,
412            y,
413            orientation,
414            model,
415            magnification,
416            error_correction,
417            mask,
418            data,
419            reverse_print,
420        )
421    }
422
423    fn draw_code39(
424        &mut self,
425        x: u32,
426        y: u32,
427        orientation: char,
428        check_digit: char,
429        height: u32,
430        module_width: u32,
431        interpretation_line: char,
432        interpretation_line_above: char,
433        data: &str,
434        reverse_print: bool,
435    ) -> ZplResult<()> {
436        self.png_backend.draw_code39(
437            x,
438            y,
439            orientation,
440            check_digit,
441            height,
442            module_width,
443            interpretation_line,
444            interpretation_line_above,
445            data,
446            reverse_print,
447        )
448    }
449
450    fn finalize(&mut self) -> ZplResult<Vec<u8>> {
451        let png_data = self.png_backend.finalize()?;
452        png_merge_pages_to_pdf(
453            &[png_data],
454            self.width_dots,
455            self.height_dots,
456            self.resolution,
457            self.compression,
458        )
459    }
460}