Skip to main content

qrcode_pdf/
lib.rs

1//! PDF rendering support.
2//!
3//! # Example
4//!
5//! ```
6//! use qrcode_core::Color as ModuleColor;
7//! use qrcode_pdf::Color;
8//! use qrcode_render::Renderer;
9//!
10//! let modules = [ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
11//! let pdf_data = Renderer::<Color>::new(&modules, 2, 1).build();
12//! println!("PDF size: {} bytes", pdf_data.len());
13//! ```
14
15#![cfg_attr(not(feature = "std"), no_std)]
16
17#[cfg(not(feature = "std"))]
18extern crate alloc;
19
20#[cfg(not(feature = "std"))]
21#[allow(unused_imports)]
22use alloc::{
23    borrow::ToOwned,
24    format,
25    string::{String, ToString},
26    vec,
27    vec::Vec,
28};
29
30use core::fmt::Write;
31
32use qrcode_core::Color as ModuleColor;
33use qrcode_render::colors::{CmykColor as SharedCmykColor, ColorSpace, RgbColor};
34use qrcode_render::{Canvas as RenderCanvas, Pixel, StyledPixel};
35
36const MAX_STREAM_PREALLOC: usize = 8 * 1024 * 1024;
37
38fn stream_capacity(width: u32, height: u32, bytes_per_rect: usize) -> usize {
39    (width as usize).saturating_mul(height as usize).saturating_mul(bytes_per_rect).min(MAX_STREAM_PREALLOC)
40}
41
42/// A PDF color (`[R, G, B]`).
43///
44/// Each value must be in the range of 0.0 to 1.0.
45#[derive(Copy, Clone, Default, PartialEq, PartialOrd)]
46pub struct Color(pub [f64; 3]);
47
48/// A PDF CMYK color (`[C, M, Y, K]`).
49///
50/// Each value is expected to be in the range of 0.0 to 1.0. Values converted
51/// from the shared render color space are clamped there before rendering.
52#[derive(Copy, Clone, Default, PartialEq, PartialOrd)]
53pub struct CmykColor(pub [f64; 4]);
54
55impl Pixel for Color {
56    type Canvas = Canvas;
57    type Image = Vec<u8>;
58
59    fn default_color(color: ModuleColor) -> Self {
60        Self(color.select(Default::default(), [1.0; 3]))
61    }
62}
63
64impl StyledPixel for Color {
65    fn from_hex(hex: &str) -> Self {
66        let (r, g, b) = qrcode_render::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
67        Self([r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
68    }
69}
70
71impl From<RgbColor> for Color {
72    fn from(color: RgbColor) -> Self {
73        Self(color.to_array())
74    }
75}
76
77impl Pixel for CmykColor {
78    type Canvas = CmykCanvas;
79    type Image = Vec<u8>;
80
81    fn default_color(color: ModuleColor) -> Self {
82        Self(color.select([0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0]))
83    }
84}
85
86impl StyledPixel for CmykColor {
87    fn from_hex(hex: &str) -> Self {
88        let (r, g, b) = qrcode_render::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
89        SharedCmykColor::from_rgb(RgbColor::new(r, g, b)).into()
90    }
91}
92
93impl From<SharedCmykColor> for CmykColor {
94    fn from(color: SharedCmykColor) -> Self {
95        Self(color.to_array())
96    }
97}
98
99#[doc(hidden)]
100pub struct Canvas {
101    stream: String,
102    width: u32,
103    height: u32,
104    fg_r: f64,
105    fg_g: f64,
106    fg_b: f64,
107    pending_left: u32,
108    pending_bottom: u32,
109    pending_width: u32,
110    pending_height: u32,
111    has_pending: bool,
112}
113
114#[doc(hidden)]
115pub struct CmykCanvas {
116    stream: String,
117    width: u32,
118    height: u32,
119    fg_c: f64,
120    fg_m: f64,
121    fg_y: f64,
122    fg_k: f64,
123    pending_left: u32,
124    pending_bottom: u32,
125    pending_width: u32,
126    pending_height: u32,
127    has_pending: bool,
128}
129
130impl Canvas {
131    fn flush_pending(&mut self) {
132        if self.has_pending {
133            writeln!(
134                self.stream,
135                "{} {} {} rg {} {} {} {} re f",
136                self.fg_r,
137                self.fg_g,
138                self.fg_b,
139                self.pending_left,
140                self.pending_bottom,
141                self.pending_width,
142                self.pending_height
143            )
144            .unwrap();
145            self.has_pending = false;
146        }
147    }
148}
149
150impl RenderCanvas for Canvas {
151    type Pixel = Color;
152    type Image = Vec<u8>;
153
154    fn new(width: u32, height: u32, dark_pixel: Color, _light_pixel: Color) -> Self {
155        Canvas {
156            stream: String::with_capacity(stream_capacity(width, height, 48)),
157            width,
158            height,
159            fg_r: dark_pixel.0[0],
160            fg_g: dark_pixel.0[1],
161            fg_b: dark_pixel.0[2],
162            pending_left: 0,
163            pending_bottom: 0,
164            pending_width: 0,
165            pending_height: 0,
166            has_pending: false,
167        }
168    }
169
170    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
171        self.draw_dark_rect(x, y, 1, 1);
172    }
173
174    fn draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
175        let bottom = self.height - top - height;
176        if self.has_pending
177            && bottom == self.pending_bottom
178            && height == self.pending_height
179            && left == self.pending_left + self.pending_width
180        {
181            self.pending_width += width;
182        } else {
183            self.flush_pending();
184            self.pending_left = left;
185            self.pending_bottom = bottom;
186            self.pending_width = width;
187            self.pending_height = height;
188            self.has_pending = true;
189        }
190    }
191
192    fn into_image(mut self) -> Vec<u8> {
193        self.flush_pending();
194
195        let w = self.width;
196        let h = self.height;
197        let stream_content = &self.stream;
198        let stream_len = stream_content.len();
199
200        let mut obj_offsets = Vec::with_capacity(5);
201        let mut pos: usize = 0;
202
203        // Header
204        let header = "%PDF-1.4\n";
205        pos += header.len();
206
207        // Object 1: Catalog
208        obj_offsets.push(pos);
209        let obj1 = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
210        pos += obj1.len();
211
212        // Object 2: Pages
213        obj_offsets.push(pos);
214        let obj2 = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
215        pos += obj2.len();
216
217        // Object 3: Page
218        obj_offsets.push(pos);
219        let mut obj3 = String::new();
220        write!(
221            &mut obj3,
222            "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Contents 4 0 R /Resources << >> >>\nendobj\n",
223        )
224        .unwrap();
225        pos += obj3.len();
226
227        // Object 4: Content stream
228        obj_offsets.push(pos);
229        let mut obj4_head = String::new();
230        write!(&mut obj4_head, "4 0 obj\n<< /Length {stream_len} >>\nstream\n").unwrap();
231        pos += obj4_head.len();
232        pos += stream_len;
233        let obj4_tail = "\nendstream\nendobj\n";
234        pos += obj4_tail.len();
235
236        // Cross-reference table
237        let xref_pos = pos;
238        let mut xref = String::new();
239        xref.push_str("xref\n0 5\n");
240        xref.push_str("0000000000 65535 f \n");
241        for &off in &obj_offsets {
242            writeln!(&mut xref, "{off:010} 00000 n ").unwrap();
243        }
244
245        // Trailer
246        let mut trailer = String::new();
247        write!(&mut trailer, "trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",).unwrap();
248
249        // Assemble PDF
250        let total = pos + xref.len() + trailer.len();
251        let mut pdf = Vec::with_capacity(total);
252        pdf.extend_from_slice(header.as_bytes());
253        pdf.extend_from_slice(obj1.as_bytes());
254        pdf.extend_from_slice(obj2.as_bytes());
255        pdf.extend_from_slice(obj3.as_bytes());
256        pdf.extend_from_slice(obj4_head.as_bytes());
257        pdf.extend_from_slice(stream_content.as_bytes());
258        pdf.extend_from_slice(obj4_tail.as_bytes());
259        pdf.extend_from_slice(xref.as_bytes());
260        pdf.extend_from_slice(trailer.as_bytes());
261        pdf
262    }
263}
264
265impl CmykCanvas {
266    fn flush_pending(&mut self) {
267        if self.has_pending {
268            writeln!(
269                self.stream,
270                "{} {} {} {} k {} {} {} {} re f",
271                self.fg_c,
272                self.fg_m,
273                self.fg_y,
274                self.fg_k,
275                self.pending_left,
276                self.pending_bottom,
277                self.pending_width,
278                self.pending_height
279            )
280            .unwrap();
281            self.has_pending = false;
282        }
283    }
284}
285
286impl RenderCanvas for CmykCanvas {
287    type Pixel = CmykColor;
288    type Image = Vec<u8>;
289
290    fn new(width: u32, height: u32, dark_pixel: CmykColor, _light_pixel: CmykColor) -> Self {
291        CmykCanvas {
292            stream: String::with_capacity(stream_capacity(width, height, 56)),
293            width,
294            height,
295            fg_c: dark_pixel.0[0],
296            fg_m: dark_pixel.0[1],
297            fg_y: dark_pixel.0[2],
298            fg_k: dark_pixel.0[3],
299            pending_left: 0,
300            pending_bottom: 0,
301            pending_width: 0,
302            pending_height: 0,
303            has_pending: false,
304        }
305    }
306
307    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
308        self.draw_dark_rect(x, y, 1, 1);
309    }
310
311    fn draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
312        let bottom = self.height - top - height;
313        if self.has_pending
314            && bottom == self.pending_bottom
315            && height == self.pending_height
316            && left == self.pending_left + self.pending_width
317        {
318            self.pending_width += width;
319        } else {
320            self.flush_pending();
321            self.pending_left = left;
322            self.pending_bottom = bottom;
323            self.pending_width = width;
324            self.pending_height = height;
325            self.has_pending = true;
326        }
327    }
328
329    fn into_image(mut self) -> Vec<u8> {
330        self.flush_pending();
331
332        let w = self.width;
333        let h = self.height;
334        let stream_content = &self.stream;
335        let stream_len = stream_content.len();
336
337        let mut obj_offsets = Vec::with_capacity(5);
338        let mut pos: usize = 0;
339
340        let header = "%PDF-1.4\n";
341        pos += header.len();
342
343        obj_offsets.push(pos);
344        let obj1 = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
345        pos += obj1.len();
346
347        obj_offsets.push(pos);
348        let obj2 = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
349        pos += obj2.len();
350
351        obj_offsets.push(pos);
352        let mut obj3 = String::new();
353        write!(
354            &mut obj3,
355            "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Contents 4 0 R /Resources << >> >>\nendobj\n",
356        )
357        .unwrap();
358        pos += obj3.len();
359
360        obj_offsets.push(pos);
361        let mut obj4_head = String::new();
362        write!(&mut obj4_head, "4 0 obj\n<< /Length {stream_len} >>\nstream\n").unwrap();
363        pos += obj4_head.len();
364        pos += stream_len;
365        let obj4_tail = "\nendstream\nendobj\n";
366        pos += obj4_tail.len();
367
368        let xref_pos = pos;
369        let mut xref = String::new();
370        xref.push_str("xref\n0 5\n");
371        xref.push_str("0000000000 65535 f \n");
372        for &off in &obj_offsets {
373            writeln!(&mut xref, "{off:010} 00000 n ").unwrap();
374        }
375
376        let mut trailer = String::new();
377        write!(&mut trailer, "trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",).unwrap();
378
379        let total = pos + xref.len() + trailer.len();
380        let mut pdf = Vec::with_capacity(total);
381        pdf.extend_from_slice(header.as_bytes());
382        pdf.extend_from_slice(obj1.as_bytes());
383        pdf.extend_from_slice(obj2.as_bytes());
384        pdf.extend_from_slice(obj3.as_bytes());
385        pdf.extend_from_slice(obj4_head.as_bytes());
386        pdf.extend_from_slice(stream_content.as_bytes());
387        pdf.extend_from_slice(obj4_tail.as_bytes());
388        pdf.extend_from_slice(xref.as_bytes());
389        pdf.extend_from_slice(trailer.as_bytes());
390        pdf
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use qrcode_core::Color as ModuleColor;
398    use qrcode_render::{Renderer, StyledPixel};
399
400    #[test]
401    fn test_pdf_header() {
402        let colors = vec![ModuleColor::Dark; 4];
403        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
404        assert!(pdf.starts_with(b"%PDF-1.4\n"));
405        assert!(pdf.ends_with(b"%%EOF\n"));
406    }
407
408    #[test]
409    fn test_pdf_contains_rect() {
410        let colors = vec![ModuleColor::Dark; 4];
411        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
412        let content = String::from_utf8_lossy(&pdf);
413        assert!(content.contains(" re f"));
414        assert!(content.contains(" rg"));
415    }
416
417    #[test]
418    fn test_pdf_xref() {
419        let colors = vec![ModuleColor::Light, ModuleColor::Dark, ModuleColor::Dark, ModuleColor::Light];
420        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 1).module_dimensions(1, 1).build();
421        let content = String::from_utf8_lossy(&pdf);
422        assert!(content.contains("xref"));
423        assert!(content.contains("trailer"));
424        assert!(content.contains("startxref"));
425    }
426
427    #[test]
428    fn test_pdf_empty_rects() {
429        let colors = vec![ModuleColor::Light; 4];
430        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
431        // All-light produces no dark rects, but PDF is still valid.
432        assert!(pdf.starts_with(b"%PDF-1.4"));
433        assert!(pdf.ends_with(b"%%EOF\n"));
434    }
435
436    #[test]
437    fn test_pdf_color_parses_hex_for_templates() {
438        let color = Color::from_hex("#336699");
439
440        assert!((color.0[0] - 0.2).abs() < f64::EPSILON);
441        assert!((color.0[1] - 0.4).abs() < f64::EPSILON);
442        assert!((color.0[2] - 0.6).abs() < f64::EPSILON);
443    }
444
445    #[test]
446    fn test_pdf_cmyk_renderer_outputs_native_cmyk_operator() {
447        let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
448        let pdf: Vec<u8> = Renderer::<CmykColor>::new(&colors, 2, 0)
449            .dark_color(CmykColor([1.0, 0.0, 0.0, 0.25]))
450            .module_dimensions(1, 1)
451            .build();
452        let content = String::from_utf8_lossy(&pdf);
453
454        assert!(content.contains("1 0 0 0.25 k"));
455        assert!(!content.contains(" rg"));
456    }
457
458    #[test]
459    fn pdf_stream_preallocation_is_bounded() {
460        assert_eq!(stream_capacity(u32::MAX, u32::MAX, 64), MAX_STREAM_PREALLOC);
461        assert_eq!(stream_capacity(2, 3, 48), 288);
462    }
463}