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