1use std::f64::consts::PI;
4
5use crate::config::{Color, Gradient, QRCodeStylingOptions};
6use crate::core::QRMatrix;
7use crate::error::Result;
8use crate::figures::{QRCornerDot, QRCornerSquare, QRDot};
9use crate::types::{CornerSquareType, GradientType, ShapeType};
10
11pub struct SvgRenderer {
13 options: QRCodeStylingOptions,
14 instance_id: u64,
15}
16
17const SQUARE_MASK: [[u8; 7]; 7] = [
19 [1, 1, 1, 1, 1, 1, 1],
20 [1, 0, 0, 0, 0, 0, 1],
21 [1, 0, 0, 0, 0, 0, 1],
22 [1, 0, 0, 0, 0, 0, 1],
23 [1, 0, 0, 0, 0, 0, 1],
24 [1, 0, 0, 0, 0, 0, 1],
25 [1, 1, 1, 1, 1, 1, 1],
26];
27
28const DOT_MASK: [[u8; 7]; 7] = [
30 [0, 0, 0, 0, 0, 0, 0],
31 [0, 0, 0, 0, 0, 0, 0],
32 [0, 0, 1, 1, 1, 0, 0],
33 [0, 0, 1, 1, 1, 0, 0],
34 [0, 0, 1, 1, 1, 0, 0],
35 [0, 0, 0, 0, 0, 0, 0],
36 [0, 0, 0, 0, 0, 0, 0],
37];
38
39static INSTANCE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
40
41impl SvgRenderer {
42 pub fn new(options: QRCodeStylingOptions) -> Self {
44 let instance_id = INSTANCE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
45 Self {
46 options,
47 instance_id,
48 }
49 }
50
51 pub fn render(&self, matrix: &QRMatrix) -> Result<String> {
53 let count = matrix.module_count();
54 let min_size = self.options.width.min(self.options.height) - self.options.margin * 2;
55 let real_qr_size = if self.options.shape == ShapeType::Circle {
56 min_size as f64 / 2.0_f64.sqrt()
57 } else {
58 min_size as f64
59 };
60 let dot_size = self.round_size(real_qr_size / count as f64);
61
62 let (hide_x_dots, hide_y_dots) = if self.options.image.is_some() {
64 self.calculate_image_hide_area(count, dot_size)
65 } else {
66 (0, 0)
67 };
68
69 let mut svg_content = String::with_capacity(10000);
70 let mut defs_content = String::new();
71 let mut elements_content = String::new();
72
73 let (bg_defs, bg_elements) = self.render_background();
75 defs_content.push_str(&bg_defs);
76 elements_content.push_str(&bg_elements);
77
78 let (dots_defs, dots_elements) = self.render_dots(
80 matrix,
81 count,
82 dot_size,
83 hide_x_dots,
84 hide_y_dots,
85 );
86 defs_content.push_str(&dots_defs);
87 elements_content.push_str(&dots_elements);
88
89 let (corners_defs, corners_elements) = self.render_corners(count, dot_size);
91 defs_content.push_str(&corners_defs);
92 elements_content.push_str(&corners_elements);
93
94 if let Some(ref image_data) = self.options.image {
96 let image_svg = self.render_image(count, dot_size, hide_x_dots, hide_y_dots, image_data);
97 elements_content.push_str(&image_svg);
98 }
99
100 let shape_rendering = if self.options.dots_options.round_size {
102 ""
103 } else {
104 r#" shape-rendering="crispEdges""#
105 };
106
107 svg_content.push_str(&format!(
108 r#"<?xml version="1.0" encoding="UTF-8"?>
109<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{}" height="{}" viewBox="0 0 {} {}"{}>
110<defs>
111{}
112</defs>
113{}
114</svg>"#,
115 self.options.width,
116 self.options.height,
117 self.options.width,
118 self.options.height,
119 shape_rendering,
120 defs_content,
121 elements_content
122 ));
123
124 Ok(svg_content)
125 }
126
127 fn render_background(&self) -> (String, String) {
128 let mut defs = String::new();
129 let mut elements = String::new();
130
131 let bg = &self.options.background_options;
132 let name = format!("background-color-{}", self.instance_id);
133
134 let (width, height) = if bg.round > 0.0 {
135 let size = self.options.width.min(self.options.height);
136 (size, size)
137 } else {
138 (self.options.width, self.options.height)
139 };
140
141 let x = self.round_size((self.options.width - width) as f64 / 2.0);
142 let y = self.round_size((self.options.height - height) as f64 / 2.0);
143
144 let rx = if bg.round > 0.0 {
146 (height as f64 / 2.0) * bg.round
147 } else {
148 0.0
149 };
150
151 defs.push_str(&format!(
152 r#"<clipPath id="clip-path-{}"><rect x="{}" y="{}" width="{}" height="{}"{}/></clipPath>
153"#,
154 name,
155 x,
156 y,
157 width,
158 height,
159 if rx > 0.0 {
160 format!(r#" rx="{}""#, rx)
161 } else {
162 String::new()
163 }
164 ));
165
166 let (grad_defs, fill) = self.create_color(
168 bg.gradient.as_ref(),
169 &bg.color,
170 0.0,
171 0.0,
172 0.0,
173 self.options.height as f64,
174 self.options.width as f64,
175 &name,
176 );
177 defs.push_str(&grad_defs);
178
179 elements.push_str(&format!(
180 r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" clip-path="url(#clip-path-{})"/>
181"#,
182 0, 0, self.options.width, self.options.height, fill, name
183 ));
184
185 (defs, elements)
186 }
187
188 fn render_dots(
189 &self,
190 matrix: &QRMatrix,
191 count: usize,
192 dot_size: f64,
193 hide_x_dots: usize,
194 hide_y_dots: usize,
195 ) -> (String, String) {
196 let mut defs = String::new();
197 let mut clip_path_elements = String::new();
198
199 let x_beginning = self.round_size((self.options.width as f64 - count as f64 * dot_size) / 2.0);
200 let y_beginning = self.round_size((self.options.height as f64 - count as f64 * dot_size) / 2.0);
201
202 let dot_drawer = QRDot::new(self.options.dots_options.dot_type);
203 let name = format!("dot-color-{}", self.instance_id);
204
205 for row in 0..count {
207 for col in 0..count {
208 if !self.should_draw_dot(row, col, count, hide_x_dots, hide_y_dots) {
210 continue;
211 }
212
213 if !matrix.is_dark(row, col) {
214 continue;
215 }
216
217 let x = x_beginning + col as f64 * dot_size;
218 let y = y_beginning + row as f64 * dot_size;
219
220 let neighbor_fn = |x_offset: i32, y_offset: i32| -> bool {
221 let new_col = col as i32 + x_offset;
222 let new_row = row as i32 + y_offset;
223 if new_col < 0 || new_row < 0 || new_col >= count as i32 || new_row >= count as i32
224 {
225 return false;
226 }
227 if !self.should_draw_dot(
228 new_row as usize,
229 new_col as usize,
230 count,
231 hide_x_dots,
232 hide_y_dots,
233 ) {
234 return false;
235 }
236 matrix.is_dark(new_row as usize, new_col as usize)
237 };
238
239 let svg = dot_drawer.draw(x, y, dot_size, Some(&neighbor_fn));
240 clip_path_elements.push_str(&svg);
241 clip_path_elements.push('\n');
242 }
243 }
244
245 if self.options.shape == ShapeType::Circle {
247 let circle_dots = self.render_circle_edge_dots(matrix, count, dot_size, x_beginning, y_beginning, &dot_drawer);
248 clip_path_elements.push_str(&circle_dots);
249 }
250
251 defs.push_str(&format!(
252 r#"<clipPath id="clip-path-{}">
253{}
254</clipPath>
255"#,
256 name, clip_path_elements
257 ));
258
259 let (grad_defs, fill) = self.create_color(
261 self.options.dots_options.gradient.as_ref(),
262 &self.options.dots_options.color,
263 0.0,
264 0.0,
265 0.0,
266 self.options.height as f64,
267 self.options.width as f64,
268 &name,
269 );
270 defs.push_str(&grad_defs);
271
272 let elements = format!(
273 r#"<rect x="0" y="0" width="{}" height="{}" fill="{}" clip-path="url(#clip-path-{})"/>
274"#,
275 self.options.width, self.options.height, fill, name
276 );
277
278 (defs, elements)
279 }
280
281 fn render_circle_edge_dots(
282 &self,
283 matrix: &QRMatrix,
284 count: usize,
285 dot_size: f64,
286 x_beginning: f64,
287 y_beginning: f64,
288 dot_drawer: &QRDot,
289 ) -> String {
290 let mut result = String::new();
291 let min_size = (self.options.width.min(self.options.height) - self.options.margin * 2) as f64;
292 let additional_dots = self.round_size((min_size / dot_size - count as f64) / 2.0) as usize;
293 let fake_count = count + additional_dots * 2;
294 let x_fake_beginning = x_beginning - additional_dots as f64 * dot_size;
295 let y_fake_beginning = y_beginning - additional_dots as f64 * dot_size;
296 let center = fake_count as f64 / 2.0;
297
298 let mut fake_matrix = vec![vec![0u8; fake_count]; fake_count];
299
300 for row in 0..fake_count {
301 for col in 0..fake_count {
302 if row >= additional_dots.saturating_sub(1)
304 && row <= fake_count - additional_dots
305 && col >= additional_dots.saturating_sub(1)
306 && col <= fake_count - additional_dots
307 {
308 continue;
309 }
310
311 let dist = ((row as f64 - center).powi(2) + (col as f64 - center).powi(2)).sqrt();
313 if dist > center {
314 continue;
315 }
316
317 let source_col = if col < 2 * additional_dots {
319 col
320 } else if col >= count {
321 col.wrapping_sub(2 * additional_dots)
322 } else {
323 col.wrapping_sub(additional_dots)
324 };
325 let source_row = if row < 2 * additional_dots {
326 row
327 } else if row >= count {
328 row.wrapping_sub(2 * additional_dots)
329 } else {
330 row.wrapping_sub(additional_dots)
331 };
332
333 if source_row < count && source_col < count && matrix.is_dark(source_row, source_col) {
334 fake_matrix[row][col] = 1;
335 }
336 }
337 }
338
339 for row in 0..fake_count {
340 for col in 0..fake_count {
341 if fake_matrix[row][col] == 0 {
342 continue;
343 }
344
345 let x = x_fake_beginning + col as f64 * dot_size;
346 let y = y_fake_beginning + row as f64 * dot_size;
347
348 let neighbor_fn = |x_offset: i32, y_offset: i32| -> bool {
349 let new_col = col as i32 + x_offset;
350 let new_row = row as i32 + y_offset;
351 if new_col < 0 || new_row < 0 || new_col >= fake_count as i32 || new_row >= fake_count as i32 {
352 return false;
353 }
354 fake_matrix[new_row as usize][new_col as usize] == 1
355 };
356
357 let svg = dot_drawer.draw(x, y, dot_size, Some(&neighbor_fn));
358 result.push_str(&svg);
359 result.push('\n');
360 }
361 }
362
363 result
364 }
365
366 fn render_corners(&self, count: usize, dot_size: f64) -> (String, String) {
367 let mut defs = String::new();
368 let mut elements = String::new();
369
370 let x_beginning = self.round_size((self.options.width as f64 - count as f64 * dot_size) / 2.0);
371 let y_beginning = self.round_size((self.options.height as f64 - count as f64 * dot_size) / 2.0);
372
373 let corners_square_size = dot_size * 7.0;
374 let corners_dot_size = dot_size * 3.0;
375
376 let corner_positions = [
378 (0, 0, 0.0),
379 (1, 0, PI / 2.0),
380 (0, 1, -PI / 2.0),
381 ];
382
383 for (column, row, rotation) in corner_positions {
384 let x = x_beginning + column as f64 * dot_size * (count - 7) as f64;
385 let y = y_beginning + row as f64 * dot_size * (count - 7) as f64;
386
387 let (sq_defs, sq_elements) = self.render_corner_square(
389 x, y, corners_square_size, dot_size, rotation, column, row,
390 );
391 defs.push_str(&sq_defs);
392 elements.push_str(&sq_elements);
393
394 let (dot_defs, dot_elements) = self.render_corner_dot(
396 x + dot_size * 2.0,
397 y + dot_size * 2.0,
398 corners_dot_size,
399 dot_size,
400 rotation,
401 column,
402 row,
403 );
404 defs.push_str(&dot_defs);
405 elements.push_str(&dot_elements);
406 }
407
408 (defs, elements)
409 }
410
411 fn render_corner_square(
412 &self,
413 x: f64,
414 y: f64,
415 size: f64,
416 _dot_size: f64,
417 rotation: f64,
418 column: usize,
419 row: usize,
420 ) -> (String, String) {
421 let mut defs = String::new();
422 let mut clip_path_content = String::new();
423
424 let name = format!("corners-square-color-{}-{}-{}", column, row, self.instance_id);
425
426 let sq_options = &self.options.corners_square_options;
427
428 match sq_options.square_type {
430 CornerSquareType::Square | CornerSquareType::Dot | CornerSquareType::ExtraRounded => {
431 let drawer = QRCornerSquare::new(sq_options.square_type);
432 let svg = drawer.draw(x, y, size, rotation);
433 clip_path_content.push_str(&svg);
434 }
435 }
436
437 defs.push_str(&format!(
438 r#"<clipPath id="clip-path-{}">
439{}
440</clipPath>
441"#,
442 name, clip_path_content
443 ));
444
445 let (grad_defs, fill) = self.create_color(
447 sq_options.gradient.as_ref(),
448 &sq_options.color,
449 rotation,
450 x,
451 y,
452 size,
453 size,
454 &name,
455 );
456 defs.push_str(&grad_defs);
457
458 let elements = format!(
459 r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" clip-path="url(#clip-path-{})"/>
460"#,
461 x, y, size, size, fill, name
462 );
463
464 (defs, elements)
465 }
466
467 fn render_corner_dot(
468 &self,
469 x: f64,
470 y: f64,
471 size: f64,
472 _dot_size: f64,
473 rotation: f64,
474 column: usize,
475 row: usize,
476 ) -> (String, String) {
477 let mut defs = String::new();
478 let mut clip_path_content = String::new();
479
480 let name = format!("corners-dot-color-{}-{}-{}", column, row, self.instance_id);
481
482 let dot_options = &self.options.corners_dot_options;
483
484 let drawer = QRCornerDot::new(dot_options.dot_type);
486 let svg = drawer.draw(x, y, size, rotation);
487 clip_path_content.push_str(&svg);
488
489 defs.push_str(&format!(
490 r#"<clipPath id="clip-path-{}">
491{}
492</clipPath>
493"#,
494 name, clip_path_content
495 ));
496
497 let (grad_defs, fill) = self.create_color(
499 dot_options.gradient.as_ref(),
500 &dot_options.color,
501 rotation,
502 x,
503 y,
504 size,
505 size,
506 &name,
507 );
508 defs.push_str(&grad_defs);
509
510 let elements = format!(
511 r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}" clip-path="url(#clip-path-{})"/>
512"#,
513 x, y, size, size, fill, name
514 );
515
516 (defs, elements)
517 }
518
519 fn render_image(
520 &self,
521 count: usize,
522 dot_size: f64,
523 hide_x_dots: usize,
524 hide_y_dots: usize,
525 image_data: &[u8],
526 ) -> String {
527 let x_beginning = self.round_size((self.options.width as f64 - count as f64 * dot_size) / 2.0);
528 let y_beginning = self.round_size((self.options.height as f64 - count as f64 * dot_size) / 2.0);
529
530 let width = hide_x_dots as f64 * dot_size;
531 let height = hide_y_dots as f64 * dot_size;
532
533 let margin = self.options.image_options.margin as f64;
534 let dx = x_beginning + self.round_size(margin + (count as f64 * dot_size - width) / 2.0);
535 let dy = y_beginning + self.round_size(margin + (count as f64 * dot_size - height) / 2.0);
536 let dw = width - margin * 2.0;
537 let dh = height - margin * 2.0;
538
539 let base64_data = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, image_data);
541
542 let mime_type = if image_data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
544 "image/png"
545 } else if image_data.starts_with(&[0xFF, 0xD8]) {
546 "image/jpeg"
547 } else if image_data.starts_with(b"RIFF") && image_data.len() > 12 && &image_data[8..12] == b"WEBP" {
548 "image/webp"
549 } else {
550 "image/png" };
552
553 let data_url = format!("data:{};base64,{}", mime_type, base64_data);
554
555 format!(
556 r#"<image href="{}" xlink:href="{}" x="{}" y="{}" width="{}px" height="{}px"/>
557"#,
558 data_url, data_url, dx, dy, dw, dh
559 )
560 }
561
562 fn create_color(
563 &self,
564 gradient: Option<&Gradient>,
565 color: &Color,
566 additional_rotation: f64,
567 x: f64,
568 y: f64,
569 height: f64,
570 width: f64,
571 name: &str,
572 ) -> (String, String) {
573 let mut defs = String::new();
574
575 if let Some(grad) = gradient {
576 let size = width.max(height);
577
578 match grad.gradient_type {
579 GradientType::Radial => {
580 let cx = x + width / 2.0;
581 let cy = y + height / 2.0;
582 let r = size / 2.0;
583
584 defs.push_str(&format!(
585 r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" fx="{}" fy="{}" cx="{}" cy="{}" r="{}">
586"#,
587 name, cx, cy, cx, cy, r
588 ));
589
590 for stop in &grad.color_stops {
591 defs.push_str(&format!(
592 r#"<stop offset="{}%" stop-color="{}"/>
593"#,
594 stop.offset * 100.0,
595 stop.color.to_hex()
596 ));
597 }
598
599 defs.push_str("</radialGradient>\n");
600 }
601 GradientType::Linear => {
602 let rotation = (grad.rotation + additional_rotation) % (2.0 * PI);
603 let positive_rotation = (rotation + 2.0 * PI) % (2.0 * PI);
604
605 let (mut x0, mut y0, mut x1, mut y1) = (
606 x + width / 2.0,
607 y + height / 2.0,
608 x + width / 2.0,
609 y + height / 2.0,
610 );
611
612 if (positive_rotation >= 0.0 && positive_rotation <= 0.25 * PI)
613 || (positive_rotation > 1.75 * PI && positive_rotation <= 2.0 * PI)
614 {
615 x0 -= width / 2.0;
616 y0 -= (height / 2.0) * rotation.tan();
617 x1 += width / 2.0;
618 y1 += (height / 2.0) * rotation.tan();
619 } else if positive_rotation > 0.25 * PI && positive_rotation <= 0.75 * PI {
620 y0 -= height / 2.0;
621 x0 -= (width / 2.0) / rotation.tan();
622 y1 += height / 2.0;
623 x1 += (width / 2.0) / rotation.tan();
624 } else if positive_rotation > 0.75 * PI && positive_rotation <= 1.25 * PI {
625 x0 += width / 2.0;
626 y0 += (height / 2.0) * rotation.tan();
627 x1 -= width / 2.0;
628 y1 -= (height / 2.0) * rotation.tan();
629 } else if positive_rotation > 1.25 * PI && positive_rotation <= 1.75 * PI {
630 y0 += height / 2.0;
631 x0 += (width / 2.0) / rotation.tan();
632 y1 -= height / 2.0;
633 x1 -= (width / 2.0) / rotation.tan();
634 }
635
636 defs.push_str(&format!(
637 r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="{}" y1="{}" x2="{}" y2="{}">
638"#,
639 name,
640 x0.round(),
641 y0.round(),
642 x1.round(),
643 y1.round()
644 ));
645
646 for stop in &grad.color_stops {
647 defs.push_str(&format!(
648 r#"<stop offset="{}%" stop-color="{}"/>
649"#,
650 stop.offset * 100.0,
651 stop.color.to_hex()
652 ));
653 }
654
655 defs.push_str("</linearGradient>\n");
656 }
657 }
658
659 (defs, format!("url(#{})", name))
660 } else {
661 (String::new(), color.to_hex())
662 }
663 }
664
665 fn should_draw_dot(
666 &self,
667 row: usize,
668 col: usize,
669 count: usize,
670 hide_x_dots: usize,
671 hide_y_dots: usize,
672 ) -> bool {
673 if self.options.image_options.hide_background_dots && self.options.image.is_some() {
675 let x_start = (count - hide_x_dots) / 2;
676 let x_end = (count + hide_x_dots) / 2;
677 let y_start = (count - hide_y_dots) / 2;
678 let y_end = (count + hide_y_dots) / 2;
679
680 if row >= y_start && row < y_end && col >= x_start && col < x_end {
681 return false;
682 }
683 }
684
685 if row < 7 && col < 7 {
688 if SQUARE_MASK[row][col] == 1 || DOT_MASK[row][col] == 1 {
689 return false;
690 }
691 }
692
693 if row < 7 && col >= count - 7 {
695 let local_col = col - (count - 7);
696 if SQUARE_MASK[row][local_col] == 1 || DOT_MASK[row][local_col] == 1 {
697 return false;
698 }
699 }
700
701 if row >= count - 7 && col < 7 {
703 let local_row = row - (count - 7);
704 if SQUARE_MASK[local_row][col] == 1 || DOT_MASK[local_row][col] == 1 {
705 return false;
706 }
707 }
708
709 true
710 }
711
712 fn calculate_image_hide_area(&self, count: usize, _dot_size: f64) -> (usize, usize) {
713 let error_correction_percent = self.options.qr_options.error_correction_level.percentage();
715 let cover_level = self.options.image_options.image_size * error_correction_percent;
716 let max_hidden_dots = (cover_level * (count * count) as f64).floor() as usize;
717 let max_hidden_axis_dots = count.saturating_sub(14);
718
719 let mut hide_dots = (max_hidden_dots as f64).sqrt().floor() as usize;
722
723 if hide_dots % 2 == 0 {
725 hide_dots = hide_dots.saturating_sub(1);
726 }
727
728 hide_dots = hide_dots.min(max_hidden_axis_dots);
730
731 (hide_dots, hide_dots)
732 }
733
734 fn round_size(&self, value: f64) -> f64 {
735 if self.options.dots_options.round_size {
736 value.floor()
737 } else {
738 value
739 }
740 }
741}