1#![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#[derive(Copy, Clone, Default, PartialEq, PartialOrd)]
57pub struct Color(pub [f64; 3]);
58
59#[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 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 let header = "%PDF-1.4\n";
241 pos += header.len();
242
243 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 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 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 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 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 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 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 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 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}