1use super::Widget;
9use alloc::{string::String, vec, vec::Vec};
10use core::marker::PhantomData;
11use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
12use qrcodegen_no_heap::{DataTooLong, QrCode, QrCodeEcc, Version};
13use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
14use zest_theme::Theme;
15
16const DEFAULT_QUIET_ZONE: u32 = 4;
18
19#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
21pub enum EccLevel {
22 Low,
24 #[default]
26 Medium,
27 Quartile,
29 High,
31}
32
33impl EccLevel {
34 const fn into_qr(self) -> QrCodeEcc {
35 match self {
36 Self::Low => QrCodeEcc::Low,
37 Self::Medium => QrCodeEcc::Medium,
38 Self::Quartile => QrCodeEcc::Quartile,
39 Self::High => QrCodeEcc::High,
40 }
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum QrError {
47 SegmentTooLong,
49 DataOverCapacity {
51 used_bits: usize,
53 capacity_bits: usize,
55 },
56}
57
58impl From<DataTooLong> for QrError {
59 fn from(err: DataTooLong) -> Self {
60 match err {
61 DataTooLong::SegmentTooLong => Self::SegmentTooLong,
62 DataTooLong::DataOverCapacity(used_bits, capacity_bits) => Self::DataOverCapacity {
63 used_bits,
64 capacity_bits,
65 },
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71struct EncodedQr {
72 size: u32,
73 modules: Vec<u8>,
74}
75
76impl EncodedQr {
77 fn encode(data: &[u8], ecc: EccLevel) -> Result<Self, QrError> {
78 let mut temp = vec![0u8; Version::MAX.buffer_len()];
79 let mut out = vec![0u8; Version::MAX.buffer_len()];
80
81 if data.len() > temp.len() {
82 return Err(QrError::SegmentTooLong);
83 }
84 temp[..data.len()].copy_from_slice(data);
85
86 let qr = QrCode::encode_binary(
87 &mut temp,
88 data.len(),
89 &mut out,
90 ecc.into_qr(),
91 Version::MIN,
92 Version::MAX,
93 None,
94 true,
95 )
96 .map_err(QrError::from)?;
97
98 let size = qr.size() as u32;
99 let mut modules = vec![0u8; ((size * size) as usize).div_ceil(8)];
100 for y in 0..size {
101 for x in 0..size {
102 if qr.get_module(x as i32, y as i32) {
103 let index = (y * size + x) as usize;
104 modules[index >> 3] |= 1u8 << (index & 7);
105 }
106 }
107 }
108
109 Ok(Self { size, modules })
110 }
111
112 fn side_modules(&self, quiet_zone: u32) -> u32 {
113 self.size.saturating_add(quiet_zone.saturating_mul(2))
114 }
115
116 fn is_dark(&self, x: u32, y: u32) -> bool {
117 let index = (y * self.size + x) as usize;
118 (self.modules[index >> 3] >> (index & 7)) & 1 != 0
119 }
120}
121
122pub struct Qr<C: PixelColor, M: Clone> {
124 rect: Rectangle,
125 data: Vec<u8>,
126 encoded: Result<EncodedQr, QrError>,
127 ecc: EccLevel,
128 quiet_zone: u32,
129 dark: Option<C>,
130 light: Option<C>,
131 width: Length,
132 height: Length,
133 _phantom: PhantomData<M>,
134}
135
136impl<C: PixelColor, M: Clone> Qr<C, M> {
137 pub fn new(data: impl Into<String>) -> Self {
139 Self::from_bytes(data.into().into_bytes())
140 }
141
142 pub fn from_bytes(data: impl Into<Vec<u8>>) -> Self {
144 let data = data.into();
145 let ecc = EccLevel::default();
146 let encoded = EncodedQr::encode(&data, ecc);
147 Self {
148 rect: Rectangle::zero(),
149 data,
150 encoded,
151 ecc,
152 quiet_zone: DEFAULT_QUIET_ZONE,
153 dark: None,
154 light: None,
155 width: Length::Shrink,
156 height: Length::Shrink,
157 _phantom: PhantomData,
158 }
159 }
160
161 #[must_use]
163 pub fn width(mut self, width: impl Into<Length>) -> Self {
164 self.width = width.into();
165 self
166 }
167
168 #[must_use]
170 pub fn height(mut self, height: impl Into<Length>) -> Self {
171 self.height = height.into();
172 self
173 }
174
175 #[must_use]
177 pub fn ecc(mut self, ecc: EccLevel) -> Self {
178 self.ecc = ecc;
179 self.reencode();
180 self
181 }
182
183 #[must_use]
185 pub fn quiet_zone(mut self, quiet_zone: u32) -> Self {
186 self.quiet_zone = quiet_zone;
187 self
188 }
189
190 #[must_use]
192 pub fn dark(mut self, color: C) -> Self {
193 self.dark = Some(color);
194 self
195 }
196
197 #[must_use]
199 pub fn light(mut self, color: C) -> Self {
200 self.light = Some(color);
201 self
202 }
203
204 pub fn error(&self) -> Option<&QrError> {
206 self.encoded.as_ref().err()
207 }
208
209 fn reencode(&mut self) {
210 self.encoded = EncodedQr::encode(&self.data, self.ecc);
211 }
212
213 fn intrinsic_side(&self) -> u32 {
214 match self.encoded.as_ref() {
215 Ok(encoded) => encoded.side_modules(self.quiet_zone),
216 Err(_) => 0,
217 }
218 }
219}
220
221impl<C: PixelColor, M: Clone> Widget<C, M> for Qr<C, M> {
222 fn measure(&mut self, constraints: Constraints) -> Size {
223 let intrinsic = self.intrinsic_side();
224 let width = self.width.resolve(intrinsic, constraints.max.width);
225 let height = self.height.resolve(intrinsic, constraints.max.height);
226 constraints.clamp(Size::new(width, height))
227 }
228
229 fn preferred_size(&self) -> (Length, Length) {
230 (self.width, self.height)
231 }
232
233 fn arrange(&mut self, rect: Rectangle) {
234 self.rect = rect;
235 }
236
237 fn rect(&self) -> Rectangle {
238 self.rect
239 }
240
241 fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
242 None
243 }
244
245 fn draw<'t>(
246 &self,
247 renderer: &mut dyn Renderer<C>,
248 theme: &Theme<'t, C>,
249 ) -> Result<(), RenderError> {
250 let Ok(encoded) = self.encoded.as_ref() else {
251 return Ok(());
252 };
253
254 let side_modules = encoded.side_modules(self.quiet_zone);
255 if side_modules == 0 {
256 return Ok(());
257 }
258
259 let module_px =
260 (self.rect.size.width / side_modules).min(self.rect.size.height / side_modules);
261 if module_px == 0 {
262 return Ok(());
263 }
264
265 let qr_px = side_modules * module_px;
266 let dx = ((self.rect.size.width - qr_px) / 2) as i32;
267 let dy = ((self.rect.size.height - qr_px) / 2) as i32;
268 let origin = self.rect.top_left + Point::new(dx, dy);
269 let light = self.light.unwrap_or(theme.background.base);
270 let dark = self.dark.unwrap_or(theme.background.on_base);
271
272 renderer.fill_rect(Rectangle::new(origin, Size::new(qr_px, qr_px)), light)?;
273
274 for y in 0..encoded.size {
275 let y_px = origin.y + ((y + self.quiet_zone) * module_px) as i32;
276 let mut x = 0;
277 while x < encoded.size {
278 if !encoded.is_dark(x, y) {
279 x += 1;
280 continue;
281 }
282
283 let run_start = x;
284 x += 1;
285 while x < encoded.size && encoded.is_dark(x, y) {
286 x += 1;
287 }
288
289 let x_px = origin.x + ((run_start + self.quiet_zone) * module_px) as i32;
290 let run_w = (x - run_start) * module_px;
291 renderer.fill_rect(
292 Rectangle::new(Point::new(x_px, y_px), Size::new(run_w, module_px)),
293 dark,
294 )?;
295 }
296 }
297
298 Ok(())
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use alloc::vec::Vec;
306 use embedded_graphics::{mono_font::MonoFont, pixelcolor::BinaryColor, text::Alignment};
307 use zest_core::Renderer;
308 use zest_theme::{Theme, convert_theme, theme::dark};
309
310 struct RecordingRenderer {
311 fills: Vec<Rectangle>,
312 }
313
314 impl RecordingRenderer {
315 fn new() -> Self {
316 Self { fills: Vec::new() }
317 }
318 }
319
320 impl Renderer<BinaryColor> for RecordingRenderer {
321 fn fill_rect(&mut self, rect: Rectangle, _color: BinaryColor) -> Result<(), RenderError> {
322 self.fills.push(rect);
323 Ok(())
324 }
325
326 fn stroke_rect(
327 &mut self,
328 _rect: Rectangle,
329 _color: BinaryColor,
330 ) -> Result<(), RenderError> {
331 Ok(())
332 }
333
334 fn fill_circle(
335 &mut self,
336 _center: Point,
337 _radius: u32,
338 _color: BinaryColor,
339 ) -> Result<(), RenderError> {
340 Ok(())
341 }
342
343 fn stroke_line(
344 &mut self,
345 _start: Point,
346 _end: Point,
347 _color: BinaryColor,
348 _width: u32,
349 ) -> Result<(), RenderError> {
350 Ok(())
351 }
352
353 fn draw_text(
354 &mut self,
355 _text: &str,
356 _position: Point,
357 _font: &MonoFont<'_>,
358 _color: BinaryColor,
359 _alignment: Alignment,
360 ) -> Result<(), RenderError> {
361 Ok(())
362 }
363 }
364
365 fn theme() -> Theme<'static, BinaryColor> {
366 convert_theme(&dark::THEME)
367 }
368
369 #[test]
370 fn oversize_payload_surfaces_error() {
371 let qr = Qr::<BinaryColor, ()>::from_bytes(vec![0x41; 4096]);
372 assert!(matches!(
373 qr.error(),
374 Some(QrError::SegmentTooLong | QrError::DataOverCapacity { .. })
375 ));
376 }
377
378 #[test]
379 fn draw_centers_a_square_integer_scaled_symbol() {
380 let mut qr = Qr::<BinaryColor, ()>::new("https://bhh32.com")
381 .quiet_zone(4)
382 .width(Length::Fixed(120))
383 .height(Length::Fixed(100));
384 qr.arrange(Rectangle::new(Point::new(10, 20), Size::new(120, 100)));
385
386 let mut renderer = RecordingRenderer::new();
387 qr.draw(&mut renderer, &theme()).unwrap();
388
389 let bg = renderer.fills.first().copied().unwrap();
390 assert_eq!(bg.size.width, bg.size.height);
391 assert_eq!(bg.top_left.y, 20);
392 assert!(bg.top_left.x > 10);
393 assert!(renderer.fills.len() > 1);
394 }
395}