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
42#[derive(Copy, Clone, Default, PartialEq, PartialOrd)]
46pub struct Color(pub [f64; 3]);
47
48#[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 let header = "%PDF-1.4\n";
205 pos += header.len();
206
207 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 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 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 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 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 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 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 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}