1use crate::gpui_compat::PixelsExt;
2use gpui::{
3 AnyElement, App, Hsla, IntoElement, Pixels, RenderImage, RenderOnce, Rgba, SharedString,
4 Window, div, img, prelude::*, px,
5};
6use image::{DynamicImage, ImageBuffer, Rgba as ImageRgba, RgbaImage};
7use liora_core::Config;
8use qrcode::{EcLevel, QrCode as QrEncoder, types::Color as QrModuleColor};
9use std::path::Path;
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum QrEcLevel {
14 Low,
15 Medium,
16 Quartile,
17 High,
18}
19
20impl QrEcLevel {
21 fn into_qrcode(self) -> EcLevel {
22 match self {
23 QrEcLevel::Low => EcLevel::L,
24 QrEcLevel::Medium => EcLevel::M,
25 QrEcLevel::Quartile => EcLevel::Q,
26 QrEcLevel::High => EcLevel::H,
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct QrDecoded {
33 pub content: SharedString,
34 pub ecc_level: u8,
35 pub version: i32,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum QrCodeError {
40 Encode(String),
41 Image(String),
42 NotFound,
43 Decode(String),
44}
45
46pub type QrCodeResult<T> = Result<T, QrCodeError>;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum QrModuleStyle {
50 #[default]
51 Square,
52 Rounded,
53 Dots,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum QrFinderStyle {
58 #[default]
59 Square,
60 Rounded,
61 Circle,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum QrGradientDirection {
66 ToTop,
67 ToTopRight,
68 ToRight,
69 ToBottomRight,
70 #[default]
71 ToBottom,
72 ToBottomLeft,
73 ToLeft,
74 ToTopLeft,
75}
76
77pub struct QrCode {
78 value: SharedString,
79 size: Pixels,
80 quiet_zone: u32,
81 module_radius: Pixels,
82 foreground: Option<Hsla>,
83 gradient_colors: Option<Vec<Hsla>>,
84 gradient_direction: QrGradientDirection,
85 background: Option<Hsla>,
86 ec_level: QrEcLevel,
87 show_text: bool,
88 module_style: QrModuleStyle,
89 finder_style: QrFinderStyle,
90 logo: Option<AnyElement>,
91 logo_text: Option<SharedString>,
92 logo_size_ratio: f32,
93 logo_background: Option<Hsla>,
94 logo_color: Option<Hsla>,
95 corner_logo: Option<AnyElement>,
96 corner_logo_text: Option<SharedString>,
97}
98
99impl QrCode {
100 pub fn new(value: impl Into<SharedString>) -> Self {
101 Self {
102 value: value.into(),
103 size: px(180.0),
104 quiet_zone: 4,
105 module_radius: px(0.0),
106 foreground: None,
107 gradient_colors: None,
108 gradient_direction: QrGradientDirection::ToBottom,
109 background: None,
110 ec_level: QrEcLevel::Medium,
111 show_text: false,
112 module_style: QrModuleStyle::Square,
113 finder_style: QrFinderStyle::Square,
114 logo: None,
115 logo_text: None,
116 logo_size_ratio: 0.24,
117 logo_background: None,
118 logo_color: None,
119 corner_logo: None,
120 corner_logo_text: None,
121 }
122 }
123
124 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
125 self.size = size.into();
126 self
127 }
128
129 pub fn quiet_zone(mut self, modules: u32) -> Self {
130 self.quiet_zone = modules;
131 self
132 }
133
134 pub fn module_radius(mut self, radius: impl Into<Pixels>) -> Self {
135 self.module_radius = radius.into();
136 self
137 }
138
139 pub fn foreground(mut self, color: Hsla) -> Self {
140 self.foreground = Some(color);
141 self.gradient_colors = None;
142 self
143 }
144
145 pub fn gradient(
146 mut self,
147 colors: impl IntoIterator<Item = Hsla>,
148 direction: QrGradientDirection,
149 ) -> Self {
150 let colors = colors.into_iter().collect::<Vec<_>>();
151 self.gradient_colors = (colors.len() >= 2).then_some(colors);
152 self.gradient_direction = direction;
153 self
154 }
155
156 pub fn foreground_gradient(
157 self,
158 colors: impl IntoIterator<Item = Hsla>,
159 direction: QrGradientDirection,
160 ) -> Self {
161 self.gradient(colors, direction)
162 }
163
164 pub fn gradient_colors(mut self, colors: impl IntoIterator<Item = Hsla>) -> Self {
165 let colors = colors.into_iter().collect::<Vec<_>>();
166 self.gradient_colors = (colors.len() >= 2).then_some(colors);
167 self
168 }
169
170 pub fn gradient_direction(mut self, direction: QrGradientDirection) -> Self {
171 self.gradient_direction = direction;
172 self
173 }
174
175 pub fn background(mut self, color: Hsla) -> Self {
176 self.background = Some(color);
177 self
178 }
179
180 pub fn colors(self, foreground: Hsla, background: Hsla) -> Self {
181 self.foreground(foreground).background(background)
182 }
183
184 pub fn ec_level(mut self, level: QrEcLevel) -> Self {
185 self.ec_level = level;
186 self
187 }
188
189 pub fn high_recovery(self) -> Self {
190 self.ec_level(QrEcLevel::High)
191 }
192
193 pub fn show_text(mut self, show: bool) -> Self {
194 self.show_text = show;
195 self
196 }
197
198 pub fn module_style(mut self, style: QrModuleStyle) -> Self {
199 self.module_style = style;
200 self
201 }
202
203 pub fn square_modules(self) -> Self {
204 self.module_style(QrModuleStyle::Square)
205 }
206
207 pub fn rounded_modules(self) -> Self {
208 self.module_style(QrModuleStyle::Rounded)
209 }
210
211 pub fn dot_modules(self) -> Self {
212 self.module_style(QrModuleStyle::Dots)
213 }
214
215 pub fn finder_style(mut self, style: QrFinderStyle) -> Self {
216 self.finder_style = style;
217 self
218 }
219
220 pub fn rounded_finders(self) -> Self {
221 self.finder_style(QrFinderStyle::Rounded)
222 }
223
224 pub fn circle_finders(self) -> Self {
225 self.finder_style(QrFinderStyle::Circle)
226 }
227
228 pub fn logo(mut self, logo: impl IntoElement) -> Self {
229 self.logo = Some(logo.into_any_element());
230 self.ec_level = QrEcLevel::High;
231 self
232 }
233
234 pub fn logo_text(mut self, text: impl Into<SharedString>) -> Self {
235 self.logo_text = Some(text.into());
236 self.ec_level = QrEcLevel::High;
237 self
238 }
239
240 pub fn logo_size_ratio(mut self, ratio: f32) -> Self {
241 self.logo_size_ratio = ratio.clamp(0.12, 0.36);
242 self
243 }
244
245 pub fn logo_background(mut self, color: Hsla) -> Self {
246 self.logo_background = Some(color);
247 self
248 }
249
250 pub fn logo_color(mut self, color: Hsla) -> Self {
251 self.logo_color = Some(color);
252 self
253 }
254
255 pub fn corner_logo(mut self, logo: impl IntoElement) -> Self {
256 self.corner_logo = Some(logo.into_any_element());
257 self.ec_level = QrEcLevel::High;
258 self
259 }
260
261 pub fn corner_logo_text(mut self, text: impl Into<SharedString>) -> Self {
262 self.corner_logo_text = Some(text.into());
263 self
264 }
265
266 pub fn encode_matrix(value: &str, ec_level: QrEcLevel) -> QrCodeResult<QrMatrix> {
267 let code = QrEncoder::with_error_correction_level(value.as_bytes(), ec_level.into_qrcode())
268 .map_err(|err| QrCodeError::Encode(err.to_string()))?;
269 let width = code.width();
270 let modules = code
271 .to_colors()
272 .into_iter()
273 .map(|color| color == QrModuleColor::Dark)
274 .collect();
275
276 Ok(QrMatrix { width, modules })
277 }
278
279 pub fn render_image(
280 value: &str,
281 size_px: u32,
282 quiet_zone: u32,
283 foreground: [u8; 4],
284 background: [u8; 4],
285 ec_level: QrEcLevel,
286 ) -> QrCodeResult<RgbaImage> {
287 let matrix = Self::encode_matrix(value, ec_level)?;
288 Ok(matrix.render_image(size_px, quiet_zone, foreground, background))
289 }
290
291 pub fn decode_image(image: DynamicImage) -> QrCodeResult<Vec<QrDecoded>> {
292 let luma = image.to_luma8();
293 let mut prepared = rqrr::PreparedImage::prepare(luma);
294 let grids = prepared.detect_grids();
295 if grids.is_empty() {
296 return Err(QrCodeError::NotFound);
297 }
298
299 let mut decoded = Vec::new();
300 for grid in grids {
301 let (meta, content) = grid
302 .decode()
303 .map_err(|err| QrCodeError::Decode(err.to_string()))?;
304 decoded.push(QrDecoded {
305 content: content.into(),
306 ecc_level: meta.ecc_level as u8,
307 version: meta.version.0 as i32,
308 });
309 }
310 Ok(decoded)
311 }
312
313 pub fn decode_bytes(bytes: &[u8]) -> QrCodeResult<Vec<QrDecoded>> {
314 let image =
315 image::load_from_memory(bytes).map_err(|err| QrCodeError::Image(err.to_string()))?;
316 Self::decode_image(image)
317 }
318
319 pub fn decode_file(path: impl AsRef<Path>) -> QrCodeResult<Vec<QrDecoded>> {
320 let image = image::open(path).map_err(|err| QrCodeError::Image(err.to_string()))?;
321 Self::decode_image(image)
322 }
323}
324
325pub struct QrMatrix {
326 pub width: usize,
327 pub modules: Vec<bool>,
328}
329
330impl QrMatrix {
331 pub fn is_dark(&self, x: usize, y: usize) -> bool {
332 self.modules[y * self.width + x]
333 }
334
335 pub fn render_image(
336 &self,
337 size_px: u32,
338 quiet_zone: u32,
339 foreground: [u8; 4],
340 background: [u8; 4],
341 ) -> RgbaImage {
342 self.render_styled_image(
343 size_px,
344 quiet_zone,
345 foreground,
346 background,
347 None,
348 QrModuleStyle::Square,
349 QrFinderStyle::Square,
350 None,
351 )
352 }
353
354 pub fn render_styled_image(
355 &self,
356 size_px: u32,
357 quiet_zone: u32,
358 foreground: [u8; 4],
359 background: [u8; 4],
360 gradient: Option<&QrGradientBytes>,
361 module_style: QrModuleStyle,
362 finder_style: QrFinderStyle,
363 logo_size_ratio: Option<f32>,
364 ) -> RgbaImage {
365 let total_modules = self.width as u32 + quiet_zone.saturating_mul(2);
366 let scale = (size_px / total_modules).max(1);
367 let actual = total_modules * scale;
368 let mut image = ImageBuffer::from_pixel(actual, actual, ImageRgba(background));
369
370 let logo_clear = logo_size_ratio.map(|ratio| {
371 let clear_modules = ((self.width as f32) * ratio.clamp(0.12, 0.36)).ceil() as usize;
372 let clear_modules = clear_modules.max(5);
373 let start = self.width.saturating_sub(clear_modules) / 2;
374 let end = (start + clear_modules).min(self.width);
375 (start, end)
376 });
377
378 for y in 0..self.width {
379 for x in 0..self.width {
380 if !self.is_dark(x, y) {
381 continue;
382 }
383 if let Some((start, end)) = logo_clear {
384 if x >= start && x < end && y >= start && y < end {
385 continue;
386 }
387 }
388 let start_x = (x as u32 + quiet_zone) * scale;
389 let start_y = (y as u32 + quiet_zone) * scale;
390 let is_finder = self.is_finder_module(x, y);
391 let style = if is_finder {
392 match finder_style {
393 QrFinderStyle::Square => QrModuleStyle::Square,
394 QrFinderStyle::Rounded => QrModuleStyle::Rounded,
395 QrFinderStyle::Circle => QrModuleStyle::Dots,
396 }
397 } else {
398 module_style
399 };
400 let module_color = gradient
401 .map(|gradient| {
402 gradient.color_at(start_x + scale / 2, start_y + scale / 2, actual)
403 })
404 .unwrap_or(foreground);
405 draw_module(&mut image, start_x, start_y, scale, module_color, style);
406 }
407 }
408
409 image
410 }
411
412 fn is_finder_module(&self, x: usize, y: usize) -> bool {
413 let w = self.width;
414 let in_top = y < 7;
415 let in_left = x < 7;
416 let in_right = x + 7 >= w;
417 let in_bottom = y + 7 >= w;
418 (in_top && (in_left || in_right)) || (in_bottom && in_left)
419 }
420}
421
422fn draw_module(
423 image: &mut RgbaImage,
424 start_x: u32,
425 start_y: u32,
426 scale: u32,
427 color: [u8; 4],
428 style: QrModuleStyle,
429) {
430 match style {
431 QrModuleStyle::Square => fill_rect(image, start_x, start_y, scale, color),
432 QrModuleStyle::Rounded => fill_rounded_rect(image, start_x, start_y, scale, color),
433 QrModuleStyle::Dots => fill_circle(image, start_x, start_y, scale, color),
434 }
435}
436
437fn fill_rect(image: &mut RgbaImage, start_x: u32, start_y: u32, scale: u32, color: [u8; 4]) {
438 for py in start_y..start_y + scale {
439 for px in start_x..start_x + scale {
440 image.put_pixel(px, py, ImageRgba(color));
441 }
442 }
443}
444
445fn fill_rounded_rect(
446 image: &mut RgbaImage,
447 start_x: u32,
448 start_y: u32,
449 scale: u32,
450 color: [u8; 4],
451) {
452 if scale <= 2 {
453 fill_rect(image, start_x, start_y, scale, color);
454 return;
455 }
456 let radius = scale as f32 * 0.32;
457 let max = scale as f32 - 1.0;
458 for y in 0..scale {
459 for x in 0..scale {
460 let xf = x as f32;
461 let yf = y as f32;
462 let cx = if xf < radius {
463 radius
464 } else if xf > max - radius {
465 max - radius
466 } else {
467 xf
468 };
469 let cy = if yf < radius {
470 radius
471 } else if yf > max - radius {
472 max - radius
473 } else {
474 yf
475 };
476 let dx = xf - cx;
477 let dy = yf - cy;
478 if dx * dx + dy * dy <= radius * radius + 0.75 {
479 image.put_pixel(start_x + x, start_y + y, ImageRgba(color));
480 }
481 }
482 }
483}
484
485fn fill_circle(image: &mut RgbaImage, start_x: u32, start_y: u32, scale: u32, color: [u8; 4]) {
486 if scale <= 2 {
487 fill_rect(image, start_x, start_y, scale, color);
488 return;
489 }
490 let center = (scale as f32 - 1.0) / 2.0;
491 let radius = scale as f32 * 0.43;
492 let radius_sq = radius * radius;
493 for y in 0..scale {
494 for x in 0..scale {
495 let dx = x as f32 - center;
496 let dy = y as f32 - center;
497 if dx * dx + dy * dy <= radius_sq {
498 image.put_pixel(start_x + x, start_y + y, ImageRgba(color));
499 }
500 }
501 }
502}
503
504fn hsla_to_rgba_bytes(color: Hsla) -> [u8; 4] {
505 let rgba = Rgba::from(color);
506 [
507 (rgba.r.clamp(0.0, 1.0) * 255.0).round() as u8,
508 (rgba.g.clamp(0.0, 1.0) * 255.0).round() as u8,
509 (rgba.b.clamp(0.0, 1.0) * 255.0).round() as u8,
510 (rgba.a.clamp(0.0, 1.0) * 255.0).round() as u8,
511 ]
512}
513
514#[derive(Clone)]
515pub struct QrGradientBytes {
516 colors: Vec<[u8; 4]>,
517 direction: QrGradientDirection,
518}
519
520impl QrGradientBytes {
521 fn new(colors: &[Hsla], direction: QrGradientDirection) -> Self {
522 Self {
523 colors: colors.iter().copied().map(hsla_to_rgba_bytes).collect(),
524 direction,
525 }
526 }
527
528 fn color_at(&self, x: u32, y: u32, size: u32) -> [u8; 4] {
529 if self.colors.is_empty() {
530 return [0, 0, 0, 255];
531 }
532 if self.colors.len() == 1 {
533 return self.colors[0];
534 }
535
536 let max = size.saturating_sub(1).max(1) as f32;
537 let nx = x as f32 / max;
538 let ny = y as f32 / max;
539 let t = match self.direction {
540 QrGradientDirection::ToTop => 1.0 - ny,
541 QrGradientDirection::ToTopRight => (nx + (1.0 - ny)) / 2.0,
542 QrGradientDirection::ToRight => nx,
543 QrGradientDirection::ToBottomRight => (nx + ny) / 2.0,
544 QrGradientDirection::ToBottom => ny,
545 QrGradientDirection::ToBottomLeft => ((1.0 - nx) + ny) / 2.0,
546 QrGradientDirection::ToLeft => 1.0 - nx,
547 QrGradientDirection::ToTopLeft => ((1.0 - nx) + (1.0 - ny)) / 2.0,
548 }
549 .clamp(0.0, 1.0);
550
551 let scaled = t * (self.colors.len() - 1) as f32;
552 let index = scaled.floor() as usize;
553 let next = (index + 1).min(self.colors.len() - 1);
554 let local_t = scaled - index as f32;
555 lerp_rgba(self.colors[index], self.colors[next], local_t)
556 }
557}
558
559fn lerp_rgba(from: [u8; 4], to: [u8; 4], t: f32) -> [u8; 4] {
560 [
561 lerp_u8(from[0], to[0], t),
562 lerp_u8(from[1], to[1], t),
563 lerp_u8(from[2], to[2], t),
564 lerp_u8(from[3], to[3], t),
565 ]
566}
567
568fn lerp_u8(from: u8, to: u8, t: f32) -> u8 {
569 (from as f32 + (to as f32 - from as f32) * t)
570 .round()
571 .clamp(0.0, 255.0) as u8
572}
573
574fn render_image_from_matrix(
575 matrix: &QrMatrix,
576 size_px: u32,
577 quiet_zone: u32,
578 foreground: Hsla,
579 background: Hsla,
580 gradient: Option<QrGradientBytes>,
581 module_style: QrModuleStyle,
582 finder_style: QrFinderStyle,
583 logo_size_ratio: Option<f32>,
584) -> Arc<RenderImage> {
585 let image = matrix.render_styled_image(
586 size_px,
587 quiet_zone,
588 hsla_to_rgba_bytes(foreground),
589 hsla_to_rgba_bytes(background),
590 gradient.as_ref(),
591 module_style,
592 finder_style,
593 logo_size_ratio,
594 );
595 Arc::new(RenderImage::new([image::Frame::new(image)]))
596}
597
598impl RenderOnce for QrCode {
599 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
600 let theme = cx.global::<Config>().theme.clone();
601 let foreground = self.foreground.unwrap_or(theme.neutral.text_1);
602 let gradient = self
603 .gradient_colors
604 .as_ref()
605 .map(|colors| QrGradientBytes::new(colors, self.gradient_direction));
606 let background = self.background.unwrap_or(theme.neutral.card);
607 let logo_bg = self.logo_background.unwrap_or(theme.neutral.card);
608 let logo_color = self.logo_color.unwrap_or(foreground);
609 let size_px = self.size.as_f32().max(24.0).round() as u32;
610 let logo_size = self.size * self.logo_size_ratio;
611 let corner_logo_size = self.size * 0.18;
612 let has_logo = self.logo.is_some() || self.logo_text.is_some();
613
614 let content = match Self::encode_matrix(self.value.as_ref(), self.ec_level) {
615 Ok(matrix) => {
616 let image = render_image_from_matrix(
617 &matrix,
618 size_px,
619 self.quiet_zone,
620 foreground,
621 background,
622 gradient,
623 self.module_style,
624 self.finder_style,
625 has_logo.then_some(self.logo_size_ratio),
626 );
627 let mut qr = div()
628 .relative()
629 .size(self.size)
630 .child(img(image).size(self.size));
631 if let Some(logo) = self.logo {
632 qr = qr.child(
633 div()
634 .absolute()
635 .top((self.size - logo_size) / 2.0)
636 .left((self.size - logo_size) / 2.0)
637 .size(logo_size)
638 .rounded_full()
639 .bg(logo_bg)
640 .border_1()
641 .border_color(background)
642 .flex()
643 .items_center()
644 .justify_center()
645 .child(logo),
646 );
647 } else if let Some(logo_text) = self.logo_text.clone() {
648 qr = qr.child(
649 div()
650 .absolute()
651 .top((self.size - logo_size) / 2.0)
652 .left((self.size - logo_size) / 2.0)
653 .size(logo_size)
654 .rounded_full()
655 .bg(logo_bg)
656 .border_1()
657 .border_color(background)
658 .flex()
659 .items_center()
660 .justify_center()
661 .text_color(logo_color)
662 .text_size(logo_size * 0.38)
663 .font_weight(gpui::FontWeight::BOLD)
664 .child(logo_text),
665 );
666 }
667 if let Some(corner_logo) = self.corner_logo {
668 qr = qr.child(
669 div()
670 .absolute()
671 .right(px(8.0))
672 .bottom(px(8.0))
673 .size(corner_logo_size)
674 .rounded_full()
675 .bg(logo_color)
676 .border_1()
677 .border_color(background)
678 .flex()
679 .items_center()
680 .justify_center()
681 .child(corner_logo),
682 );
683 } else if let Some(corner_logo_text) = self.corner_logo_text.clone() {
684 qr = qr.child(
685 div()
686 .absolute()
687 .right(px(8.0))
688 .bottom(px(8.0))
689 .size(corner_logo_size)
690 .rounded_full()
691 .bg(logo_color)
692 .border_1()
693 .border_color(background)
694 .flex()
695 .items_center()
696 .justify_center()
697 .text_color(background)
698 .text_size(corner_logo_size * 0.42)
699 .font_weight(gpui::FontWeight::BOLD)
700 .child(corner_logo_text),
701 );
702 }
703 qr.into_any_element()
704 }
705 Err(err) => div()
706 .flex()
707 .items_center()
708 .justify_center()
709 .size(self.size)
710 .rounded(px(theme.radius.md))
711 .border_1()
712 .border_color(theme.danger.base)
713 .text_color(theme.danger.base)
714 .text_size(px(theme.font_size.sm))
715 .child(format!("QR error: {err:?}"))
716 .into_any_element(),
717 };
718
719 div()
720 .flex()
721 .flex_col()
722 .items_center()
723 .gap_2()
724 .child(
725 div()
726 .flex()
727 .items_center()
728 .justify_center()
729 .p(px(6.0))
730 .rounded(px(theme.radius.lg))
731 .bg(background)
732 .child(content),
733 )
734 .when(self.show_text, |s| {
735 s.child(
736 div()
737 .max_w(self.size)
738 .text_xs()
739 .text_color(theme.neutral.text_3)
740 .child(self.value),
741 )
742 })
743 }
744}
745
746impl IntoElement for QrCode {
747 type Element = gpui::Component<Self>;
748
749 fn into_element(self) -> Self::Element {
750 gpui::Component::new(self)
751 }
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn qr_matrix_encodes_content() {
760 let matrix = QrCode::encode_matrix("https://liora-ui.dev", QrEcLevel::Medium).unwrap();
761 assert!(matrix.width >= 21);
762 assert_eq!(matrix.modules.len(), matrix.width * matrix.width);
763 assert!(matrix.modules.iter().any(|dark| *dark));
764 }
765
766 #[test]
767 fn qr_matrix_renders_styled_image() {
768 let matrix = QrCode::encode_matrix("styled", QrEcLevel::High).unwrap();
769 let image = matrix.render_styled_image(
770 240,
771 4,
772 [20, 20, 20, 255],
773 [255, 255, 255, 255],
774 None,
775 QrModuleStyle::Dots,
776 QrFinderStyle::Circle,
777 Some(0.24),
778 );
779 assert!(image.width() >= 200);
780 assert_eq!(image.width(), image.height());
781 }
782
783 #[test]
784 fn qr_gradient_interpolates_in_all_directions() {
785 let gradient = QrGradientBytes {
786 colors: vec![[0, 0, 0, 255], [255, 255, 255, 255]],
787 direction: QrGradientDirection::ToRight,
788 };
789 assert_eq!(gradient.color_at(0, 5, 10)[0], 0);
790 assert_eq!(gradient.color_at(9, 5, 10)[0], 255);
791
792 for direction in [
793 QrGradientDirection::ToTop,
794 QrGradientDirection::ToTopRight,
795 QrGradientDirection::ToRight,
796 QrGradientDirection::ToBottomRight,
797 QrGradientDirection::ToBottom,
798 QrGradientDirection::ToBottomLeft,
799 QrGradientDirection::ToLeft,
800 QrGradientDirection::ToTopLeft,
801 ] {
802 let gradient = QrGradientBytes {
803 colors: vec![[0, 0, 0, 255], [255, 255, 255, 255]],
804 direction,
805 };
806 let color = gradient.color_at(4, 4, 10);
807 assert_eq!(color[3], 255);
808 }
809 }
810
811 #[test]
812 fn qr_decode_round_trips_generated_image() {
813 let content = "liora://component/qr-code";
814 let image = QrCode::render_image(
815 content,
816 256,
817 4,
818 [0, 0, 0, 255],
819 [255, 255, 255, 255],
820 QrEcLevel::High,
821 )
822 .unwrap();
823 let decoded = QrCode::decode_image(DynamicImage::ImageRgba8(image)).unwrap();
824 assert_eq!(decoded[0].content.as_ref(), content);
825 }
826}