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
42fn normalized_component(value: f64) -> f64 {
43    if value.is_nan() || value < 0.0 {
44        0.0
45    } else if value > 1.0 {
46        1.0
47    } else {
48        value
49    }
50}
51
52/// A PDF color (`[R, G, B]`).
53///
54/// Rendering clips components to `0.0..=1.0`, including infinities. NaN is
55/// rendered as `0.0`. Values already in range, including signed zero, are kept.
56#[derive(Copy, Clone, Default, PartialEq, PartialOrd)]
57pub struct Color(pub [f64; 3]);
58
59/// A PDF CMYK color (`[C, M, Y, K]`).
60///
61/// Rendering uses the same component normalization as [`Color`].
62#[derive(Copy, Clone, Default, PartialEq, PartialOrd)]
63pub struct CmykColor(pub [f64; 4]);
64
65impl Pixel for Color {
66    type Canvas = Canvas;
67    type Image = Vec<u8>;
68
69    fn default_color(color: ModuleColor) -> Self {
70        Self(color.select(Default::default(), [1.0; 3]))
71    }
72}
73
74impl StyledPixel for Color {
75    fn from_hex(hex: &str) -> Self {
76        let (r, g, b) = qrcode_render::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
77        Self([r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0])
78    }
79}
80
81impl From<RgbColor> for Color {
82    fn from(color: RgbColor) -> Self {
83        Self(color.to_array())
84    }
85}
86
87impl Pixel for CmykColor {
88    type Canvas = CmykCanvas;
89    type Image = Vec<u8>;
90
91    fn default_color(color: ModuleColor) -> Self {
92        Self(color.select([0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0]))
93    }
94}
95
96impl StyledPixel for CmykColor {
97    fn from_hex(hex: &str) -> Self {
98        let (r, g, b) = qrcode_render::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
99        SharedCmykColor::from_rgb(RgbColor::new(r, g, b)).into()
100    }
101}
102
103impl From<SharedCmykColor> for CmykColor {
104    fn from(color: SharedCmykColor) -> Self {
105        Self(color.to_array())
106    }
107}
108
109#[doc(hidden)]
110pub struct Canvas {
111    stream: String,
112    width: u32,
113    height: u32,
114    fg_r: f64,
115    fg_g: f64,
116    fg_b: f64,
117    foreground_prefix: Option<String>,
118    has_flushed: bool,
119    pending_left: u32,
120    pending_bottom: u32,
121    pending_width: u32,
122    pending_height: u32,
123    has_pending: bool,
124}
125
126#[doc(hidden)]
127pub struct CmykCanvas {
128    stream: String,
129    width: u32,
130    height: u32,
131    fg_c: f64,
132    fg_m: f64,
133    fg_y: f64,
134    fg_k: f64,
135    foreground_prefix: Option<String>,
136    has_flushed: bool,
137    pending_left: u32,
138    pending_bottom: u32,
139    pending_width: u32,
140    pending_height: u32,
141    has_pending: bool,
142}
143
144impl Canvas {
145    fn flush_pending(&mut self) {
146        if self.has_pending {
147            if self.has_flushed {
148                let prefix = self
149                    .foreground_prefix
150                    .get_or_insert_with(|| format!("{} {} {} rg ", self.fg_r, self.fg_g, self.fg_b));
151                self.stream.push_str(prefix);
152                writeln!(
153                    self.stream,
154                    "{} {} {} {} re f",
155                    self.pending_left, self.pending_bottom, self.pending_width, self.pending_height
156                )
157                .unwrap();
158            } else {
159                // Empty and single-rectangle canvases need no prefix allocation.
160                writeln!(
161                    self.stream,
162                    "{} {} {} rg {} {} {} {} re f",
163                    self.fg_r,
164                    self.fg_g,
165                    self.fg_b,
166                    self.pending_left,
167                    self.pending_bottom,
168                    self.pending_width,
169                    self.pending_height
170                )
171                .unwrap();
172                self.has_flushed = true;
173            }
174            self.has_pending = false;
175        }
176    }
177}
178
179impl RenderCanvas for Canvas {
180    type Pixel = Color;
181    type Image = Vec<u8>;
182
183    fn new(width: u32, height: u32, dark_pixel: Color, light_pixel: Color) -> Self {
184        let dark_pixel = Color(dark_pixel.0.map(normalized_component));
185        let light_pixel = Color(light_pixel.0.map(normalized_component));
186        let mut stream = String::with_capacity(stream_capacity(width, height, 48));
187        writeln!(stream, "{} {} {} rg 0 0 {width} {height} re f", light_pixel.0[0], light_pixel.0[1], light_pixel.0[2])
188            .unwrap();
189        Canvas {
190            stream,
191            width,
192            height,
193            fg_r: dark_pixel.0[0],
194            fg_g: dark_pixel.0[1],
195            fg_b: dark_pixel.0[2],
196            foreground_prefix: None,
197            has_flushed: false,
198            pending_left: 0,
199            pending_bottom: 0,
200            pending_width: 0,
201            pending_height: 0,
202            has_pending: false,
203        }
204    }
205
206    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
207        self.draw_dark_rect(x, y, 1, 1);
208    }
209
210    fn draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
211        let bottom = self.height - top - height;
212        if self.has_pending
213            && bottom == self.pending_bottom
214            && height == self.pending_height
215            && left == self.pending_left + self.pending_width
216        {
217            self.pending_width += width;
218        } else {
219            self.flush_pending();
220            self.pending_left = left;
221            self.pending_bottom = bottom;
222            self.pending_width = width;
223            self.pending_height = height;
224            self.has_pending = true;
225        }
226    }
227
228    fn into_image(mut self) -> Vec<u8> {
229        self.flush_pending();
230
231        let w = self.width;
232        let h = self.height;
233        let stream_content = &self.stream;
234        let stream_len = stream_content.len();
235
236        let mut obj_offsets = Vec::with_capacity(5);
237        let mut pos: usize = 0;
238
239        // Header
240        let header = "%PDF-1.4\n";
241        pos += header.len();
242
243        // Object 1: Catalog
244        obj_offsets.push(pos);
245        let obj1 = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
246        pos += obj1.len();
247
248        // Object 2: Pages
249        obj_offsets.push(pos);
250        let obj2 = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
251        pos += obj2.len();
252
253        // Object 3: Page
254        obj_offsets.push(pos);
255        let mut obj3 = String::new();
256        write!(
257            &mut obj3,
258            "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Contents 4 0 R /Resources << >> >>\nendobj\n",
259        )
260        .unwrap();
261        pos += obj3.len();
262
263        // Object 4: Content stream
264        obj_offsets.push(pos);
265        let mut obj4_head = String::new();
266        write!(&mut obj4_head, "4 0 obj\n<< /Length {stream_len} >>\nstream\n").unwrap();
267        pos += obj4_head.len();
268        pos += stream_len;
269        let obj4_tail = "\nendstream\nendobj\n";
270        pos += obj4_tail.len();
271
272        // Cross-reference table
273        let xref_pos = pos;
274        let mut xref = String::new();
275        xref.push_str("xref\n0 5\n");
276        xref.push_str("0000000000 65535 f \n");
277        for &off in &obj_offsets {
278            writeln!(&mut xref, "{off:010} 00000 n ").unwrap();
279        }
280
281        // Trailer
282        let mut trailer = String::new();
283        write!(&mut trailer, "trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",).unwrap();
284
285        // Assemble PDF
286        let total = pos + xref.len() + trailer.len();
287        let mut pdf = Vec::with_capacity(total);
288        pdf.extend_from_slice(header.as_bytes());
289        pdf.extend_from_slice(obj1.as_bytes());
290        pdf.extend_from_slice(obj2.as_bytes());
291        pdf.extend_from_slice(obj3.as_bytes());
292        pdf.extend_from_slice(obj4_head.as_bytes());
293        pdf.extend_from_slice(stream_content.as_bytes());
294        pdf.extend_from_slice(obj4_tail.as_bytes());
295        pdf.extend_from_slice(xref.as_bytes());
296        pdf.extend_from_slice(trailer.as_bytes());
297        pdf
298    }
299}
300
301impl CmykCanvas {
302    fn flush_pending(&mut self) {
303        if self.has_pending {
304            if self.has_flushed {
305                let prefix = self
306                    .foreground_prefix
307                    .get_or_insert_with(|| format!("{} {} {} {} k ", self.fg_c, self.fg_m, self.fg_y, self.fg_k));
308                self.stream.push_str(prefix);
309                writeln!(
310                    self.stream,
311                    "{} {} {} {} re f",
312                    self.pending_left, self.pending_bottom, self.pending_width, self.pending_height
313                )
314                .unwrap();
315            } else {
316                writeln!(
317                    self.stream,
318                    "{} {} {} {} k {} {} {} {} re f",
319                    self.fg_c,
320                    self.fg_m,
321                    self.fg_y,
322                    self.fg_k,
323                    self.pending_left,
324                    self.pending_bottom,
325                    self.pending_width,
326                    self.pending_height
327                )
328                .unwrap();
329                self.has_flushed = true;
330            }
331            self.has_pending = false;
332        }
333    }
334}
335
336impl RenderCanvas for CmykCanvas {
337    type Pixel = CmykColor;
338    type Image = Vec<u8>;
339
340    fn new(width: u32, height: u32, dark_pixel: CmykColor, light_pixel: CmykColor) -> Self {
341        let dark_pixel = CmykColor(dark_pixel.0.map(normalized_component));
342        let light_pixel = CmykColor(light_pixel.0.map(normalized_component));
343        let mut stream = String::with_capacity(stream_capacity(width, height, 56));
344        writeln!(
345            stream,
346            "{} {} {} {} k 0 0 {width} {height} re f",
347            light_pixel.0[0], light_pixel.0[1], light_pixel.0[2], light_pixel.0[3]
348        )
349        .unwrap();
350        CmykCanvas {
351            stream,
352            width,
353            height,
354            fg_c: dark_pixel.0[0],
355            fg_m: dark_pixel.0[1],
356            fg_y: dark_pixel.0[2],
357            fg_k: dark_pixel.0[3],
358            foreground_prefix: None,
359            has_flushed: false,
360            pending_left: 0,
361            pending_bottom: 0,
362            pending_width: 0,
363            pending_height: 0,
364            has_pending: false,
365        }
366    }
367
368    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
369        self.draw_dark_rect(x, y, 1, 1);
370    }
371
372    fn draw_dark_rect(&mut self, left: u32, top: u32, width: u32, height: u32) {
373        let bottom = self.height - top - height;
374        if self.has_pending
375            && bottom == self.pending_bottom
376            && height == self.pending_height
377            && left == self.pending_left + self.pending_width
378        {
379            self.pending_width += width;
380        } else {
381            self.flush_pending();
382            self.pending_left = left;
383            self.pending_bottom = bottom;
384            self.pending_width = width;
385            self.pending_height = height;
386            self.has_pending = true;
387        }
388    }
389
390    fn into_image(mut self) -> Vec<u8> {
391        self.flush_pending();
392
393        let w = self.width;
394        let h = self.height;
395        let stream_content = &self.stream;
396        let stream_len = stream_content.len();
397
398        let mut obj_offsets = Vec::with_capacity(5);
399        let mut pos: usize = 0;
400
401        let header = "%PDF-1.4\n";
402        pos += header.len();
403
404        obj_offsets.push(pos);
405        let obj1 = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
406        pos += obj1.len();
407
408        obj_offsets.push(pos);
409        let obj2 = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
410        pos += obj2.len();
411
412        obj_offsets.push(pos);
413        let mut obj3 = String::new();
414        write!(
415            &mut obj3,
416            "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Contents 4 0 R /Resources << >> >>\nendobj\n",
417        )
418        .unwrap();
419        pos += obj3.len();
420
421        obj_offsets.push(pos);
422        let mut obj4_head = String::new();
423        write!(&mut obj4_head, "4 0 obj\n<< /Length {stream_len} >>\nstream\n").unwrap();
424        pos += obj4_head.len();
425        pos += stream_len;
426        let obj4_tail = "\nendstream\nendobj\n";
427        pos += obj4_tail.len();
428
429        let xref_pos = pos;
430        let mut xref = String::new();
431        xref.push_str("xref\n0 5\n");
432        xref.push_str("0000000000 65535 f \n");
433        for &off in &obj_offsets {
434            writeln!(&mut xref, "{off:010} 00000 n ").unwrap();
435        }
436
437        let mut trailer = String::new();
438        write!(&mut trailer, "trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",).unwrap();
439
440        let total = pos + xref.len() + trailer.len();
441        let mut pdf = Vec::with_capacity(total);
442        pdf.extend_from_slice(header.as_bytes());
443        pdf.extend_from_slice(obj1.as_bytes());
444        pdf.extend_from_slice(obj2.as_bytes());
445        pdf.extend_from_slice(obj3.as_bytes());
446        pdf.extend_from_slice(obj4_head.as_bytes());
447        pdf.extend_from_slice(stream_content.as_bytes());
448        pdf.extend_from_slice(obj4_tail.as_bytes());
449        pdf.extend_from_slice(xref.as_bytes());
450        pdf.extend_from_slice(trailer.as_bytes());
451        pdf
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use qrcode_core::Color as ModuleColor;
459    use qrcode_render::{Renderer, StyledPixel};
460
461    fn content_stream(pdf: &[u8]) -> &str {
462        core::str::from_utf8(pdf).unwrap().split_once("stream\n").unwrap().1.split_once("\nendstream").unwrap().0
463    }
464
465    fn legacy_stream<const N: usize>(
466        width: u32,
467        height: u32,
468        dark: [f64; N],
469        light: [f64; N],
470        rects: &[(u32, u32, u32, u32)],
471    ) -> String {
472        fn emit<const N: usize>(out: &mut String, color: [f64; N], rect: (u32, u32, u32, u32)) {
473            let (left, bottom, width, height) = rect;
474            match N {
475                3 => writeln!(out, "{} {} {} rg {left} {bottom} {width} {height} re f", color[0], color[1], color[2]),
476                4 => writeln!(
477                    out,
478                    "{} {} {} {} k {left} {bottom} {width} {height} re f",
479                    color[0], color[1], color[2], color[3]
480                ),
481                _ => unreachable!(),
482            }
483            .unwrap();
484        }
485        let mut output = String::new();
486        emit(&mut output, light, (0, 0, width, height));
487        let mut pending: Option<(u32, u32, u32, u32)> = None;
488        for &(left, top, width, rect_height) in rects {
489            let bottom = height - top - rect_height;
490            if let Some((old_left, old_bottom, old_width, old_height)) = pending {
491                if old_bottom == bottom && old_height == rect_height && left == old_left + old_width {
492                    pending = Some((old_left, bottom, old_width + width, rect_height));
493                    continue;
494                }
495                emit(&mut output, dark, (old_left, old_bottom, old_width, old_height));
496            }
497            pending = Some((left, bottom, width, rect_height));
498        }
499        if let Some(rect) = pending {
500            emit(&mut output, dark, rect);
501        }
502        output
503    }
504
505    // Build the original four-object document in one buffer, recording actual
506    // offsets as objects are appended rather than sharing production assembly.
507    fn legacy_pdf(width: u32, height: u32, stream: &str) -> Vec<u8> {
508        let objects = [
509            "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".into(),
510            "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n".into(),
511            format!(
512                "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] /Contents 4 0 R /Resources << >> >>\nendobj\n"
513            ),
514            format!("4 0 obj\n<< /Length {} >>\nstream\n{stream}\nendstream\nendobj\n", stream.len()),
515        ];
516        let mut output = String::from("%PDF-1.4\n");
517        let mut offsets = [0; 4];
518        for (index, object) in objects.iter().enumerate() {
519            offsets[index] = output.len();
520            output.push_str(object);
521        }
522        let xref = output.len();
523        output.push_str("xref\n0 5\n0000000000 65535 f \n");
524        for offset in offsets {
525            writeln!(output, "{offset:010} 00000 n ").unwrap();
526        }
527        write!(output, "trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").unwrap();
528        output.into_bytes()
529    }
530
531    #[test]
532    fn lazy_rgb_and_cmyk_prefixes_keep_complete_legacy_pdf_bytes() {
533        let rects = [(1, 2, 3, 4), (4, 2, 2, 4), (10, 2, 2, 4), (10, 8, 2, 3), (0, 0, 1, 1), (31, 23, 1, 1)];
534        for count in 0..=rects.len() {
535            for (dark, light) in [([0.0; 3], [1.0; 3]), ([-0.0, 0.12345, 1.0], [0.25, 0.5, 0.75])] {
536                let mut canvas = Canvas::new(32, 24, Color(dark), Color(light));
537                for &(left, top, width, height) in &rects[..count] {
538                    canvas.draw_dark_rect(left, top, width, height);
539                }
540                let expected = legacy_pdf(32, 24, &legacy_stream(32, 24, dark, light, &rects[..count]));
541                assert_eq!(canvas.into_image(), expected, "RGB rects {count}");
542            }
543            for (dark, light) in
544                [([0.0, 0.0, 0.0, 1.0], [0.0; 4]), ([-0.0, 0.12345, 0.25, 0.5], [0.75, 0.5, 0.25, 0.0])]
545            {
546                let mut canvas = CmykCanvas::new(32, 24, CmykColor(dark), CmykColor(light));
547                for &(left, top, width, height) in &rects[..count] {
548                    canvas.draw_dark_rect(left, top, width, height);
549                }
550                let expected = legacy_pdf(32, 24, &legacy_stream(32, 24, dark, light, &rects[..count]));
551                assert_eq!(canvas.into_image(), expected, "CMYK rects {count}");
552            }
553        }
554    }
555
556    #[test]
557    fn lazy_prefixes_start_only_after_a_second_distinct_flush() {
558        let mut rgb = Canvas::new(8, 2, Color([0.2, 0.4, 0.6]), Color([1.0; 3]));
559        rgb.flush_pending();
560        assert!(rgb.foreground_prefix.is_none() && !rgb.has_flushed);
561        rgb.draw_dark_rect(0, 0, 1, 1);
562        rgb.draw_dark_rect(1, 0, 1, 1);
563        rgb.flush_pending();
564        assert!(rgb.foreground_prefix.is_none() && rgb.has_flushed);
565        rgb.draw_dark_rect(3, 0, 1, 1);
566        rgb.flush_pending();
567        assert_eq!(rgb.foreground_prefix.as_deref(), Some("0.2 0.4 0.6 rg "));
568
569        let mut cmyk = CmykCanvas::new(8, 2, CmykColor([0.1, 0.2, 0.3, 0.4]), CmykColor([0.0; 4]));
570        cmyk.flush_pending();
571        assert!(cmyk.foreground_prefix.is_none() && !cmyk.has_flushed);
572        cmyk.draw_dark_rect(0, 0, 1, 1);
573        cmyk.draw_dark_rect(1, 0, 1, 1);
574        cmyk.flush_pending();
575        assert!(cmyk.foreground_prefix.is_none() && cmyk.has_flushed);
576        cmyk.draw_dark_rect(3, 0, 1, 1);
577        cmyk.flush_pending();
578        assert_eq!(cmyk.foreground_prefix.as_deref(), Some("0.1 0.2 0.3 0.4 k "));
579    }
580
581    #[test]
582    fn test_pdf_header() {
583        let colors = vec![ModuleColor::Dark; 4];
584        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
585        assert!(pdf.starts_with(b"%PDF-1.4\n"));
586        assert!(pdf.ends_with(b"%%EOF\n"));
587    }
588
589    #[test]
590    fn test_pdf_contains_rect() {
591        let colors = vec![ModuleColor::Dark; 4];
592        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
593        let content = String::from_utf8_lossy(&pdf);
594        assert!(content.contains(" re f"));
595        assert!(content.contains(" rg"));
596    }
597
598    #[test]
599    fn test_pdf_xref() {
600        let colors = vec![ModuleColor::Light, ModuleColor::Dark, ModuleColor::Dark, ModuleColor::Light];
601        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 1).module_dimensions(1, 1).build();
602        let content = String::from_utf8_lossy(&pdf);
603        assert!(content.contains("xref"));
604        assert!(content.contains("trailer"));
605        assert!(content.contains("startxref"));
606    }
607
608    #[test]
609    fn test_pdf_empty_rects() {
610        let colors = vec![ModuleColor::Light; 4];
611        let pdf: Vec<u8> = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
612        // All-light produces no dark rects, but PDF is still valid.
613        assert!(pdf.starts_with(b"%PDF-1.4"));
614        assert!(pdf.ends_with(b"%%EOF\n"));
615        assert_eq!(content_stream(&pdf), "1 1 1 rg 0 0 2 2 re f\n");
616    }
617
618    #[test]
619    fn test_pdf_color_parses_hex_for_templates() {
620        let color = Color::from_hex("#336699");
621
622        assert!((color.0[0] - 0.2).abs() < f64::EPSILON);
623        assert!((color.0[1] - 0.4).abs() < f64::EPSILON);
624        assert!((color.0[2] - 0.6).abs() < f64::EPSILON);
625    }
626
627    #[test]
628    fn test_pdf_cmyk_renderer_outputs_native_cmyk_operator() {
629        let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
630        let pdf: Vec<u8> = Renderer::<CmykColor>::new(&colors, 2, 0)
631            .dark_color(CmykColor([1.0, 0.0, 0.0, 0.25]))
632            .module_dimensions(1, 1)
633            .build();
634        let content = String::from_utf8_lossy(&pdf);
635
636        assert!(content.contains("1 0 0 0.25 k"));
637        assert!(!content.contains(" rg"));
638    }
639
640    #[test]
641    fn pdf_rgb_background_covers_light_modules_and_quiet_zone_before_dark_rectangles() {
642        let colors = [ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
643        let pdf = Renderer::<Color>::new(&colors, 2, 1)
644            .dark_color(Color([1.0, 0.0, 0.0]))
645            .light_color(Color([0.25, 0.5, 0.75]))
646            .module_dimensions(2, 3)
647            .build();
648
649        assert_eq!(
650            content_stream(&pdf),
651            "0.25 0.5 0.75 rg 0 0 8 12 re f\n1 0 0 rg 2 6 2 3 re f\n1 0 0 rg 4 3 2 3 re f\n"
652        );
653    }
654
655    #[test]
656    fn pdf_cmyk_background_covers_light_modules_and_quiet_zone_before_dark_rectangles() {
657        let colors = [ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
658        let pdf = Renderer::<CmykColor>::new(&colors, 2, 1)
659            .dark_color(CmykColor([1.0, 0.0, 0.0, 0.25]))
660            .light_color(CmykColor([0.0, 0.25, 0.5, 0.0]))
661            .module_dimensions(2, 3)
662            .build();
663
664        assert_eq!(
665            content_stream(&pdf),
666            "0 0.25 0.5 0 k 0 0 8 12 re f\n1 0 0 0.25 k 2 6 2 3 re f\n1 0 0 0.25 k 4 3 2 3 re f\n"
667        );
668    }
669
670    #[test]
671    fn pdf_cmyk_all_light_canvas_is_filled_with_configured_background() {
672        let colors = [ModuleColor::Light; 4];
673        let pdf = Renderer::<CmykColor>::new(&colors, 2, 0)
674            .light_color(CmykColor([0.0, 0.25, 0.5, 0.0]))
675            .module_dimensions(2, 3)
676            .build();
677
678        assert_eq!(content_stream(&pdf), "0 0.25 0.5 0 k 0 0 4 6 re f\n");
679    }
680
681    #[test]
682    fn pdf_stream_preallocation_is_bounded() {
683        assert_eq!(stream_capacity(u32::MAX, u32::MAX, 64), MAX_STREAM_PREALLOC);
684        assert_eq!(stream_capacity(2, 3, 48), 288);
685    }
686
687    #[test]
688    fn pdf_normalization_keeps_valid_components_bit_for_bit() {
689        for value in [-0.0, 0.0, f64::from_bits(1), f64::MIN_POSITIVE, 0.2, 0.5, 1.0] {
690            assert_eq!(normalized_component(value).to_bits(), value.to_bits());
691        }
692    }
693
694    #[test]
695    fn pdf_rgb_nonfinite_and_out_of_range_components_become_valid_operands() {
696        let mut canvas =
697            Canvas::new(8, 12, Color([f64::NAN, f64::NEG_INFINITY, f64::INFINITY]), Color([1.5, -0.5, 0.25]));
698        canvas.draw_dark_rect(2, 3, 4, 5);
699        assert_eq!(content_stream(&canvas.into_image()), "1 0 0.25 rg 0 0 8 12 re f\n0 0 1 rg 2 4 4 5 re f\n");
700        let canvas = Canvas::new(1, 1, Color([0.0; 3]), Color([f64::NAN, f64::NEG_INFINITY, f64::INFINITY]));
701        assert_eq!(content_stream(&canvas.into_image()), "0 0 1 rg 0 0 1 1 re f\n");
702    }
703
704    #[test]
705    fn pdf_cmyk_nonfinite_and_out_of_range_components_become_valid_operands() {
706        let mut canvas = CmykCanvas::new(
707            8,
708            12,
709            CmykColor([f64::NAN, f64::NEG_INFINITY, f64::INFINITY, 2.0]),
710            CmykColor([f64::NAN, 1.5, -0.5, 0.5]),
711        );
712        canvas.draw_dark_rect(2, 3, 4, 5);
713        assert_eq!(content_stream(&canvas.into_image()), "0 1 0 0.5 k 0 0 8 12 re f\n0 0 1 1 k 2 4 4 5 re f\n");
714    }
715
716    #[test]
717    fn pdf_signed_zero_stays_in_background_and_foreground_operands() {
718        let mut canvas = Canvas::new(1, 1, Color([-0.0, 0.5, 1.0]), Color([1.0, -0.0, 0.5]));
719        canvas.draw_dark_pixel(0, 0);
720        assert_eq!(content_stream(&canvas.into_image()), "1 -0 0.5 rg 0 0 1 1 re f\n-0 0.5 1 rg 0 0 1 1 re f\n");
721    }
722}