1use std::fmt;
23
24use rich::measure::Measurement;
25use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
26
27use crate::chart::{Charset, has_colour, user_style};
28
29const SPEC_QUIET_ZONE: usize = 4;
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum ErrorCorrection {
36 Low,
38 #[default]
40 Medium,
41 Quartile,
43 High,
45}
46
47impl ErrorCorrection {
48 fn ecc(self) -> qrcodegen::QrCodeEcc {
49 match self {
50 ErrorCorrection::Low => qrcodegen::QrCodeEcc::Low,
51 ErrorCorrection::Medium => qrcodegen::QrCodeEcc::Medium,
52 ErrorCorrection::Quartile => qrcodegen::QrCodeEcc::Quartile,
53 ErrorCorrection::High => qrcodegen::QrCodeEcc::High,
54 }
55 }
56
57 fn from_ecc(ecc: qrcodegen::QrCodeEcc) -> Self {
58 match ecc {
59 qrcodegen::QrCodeEcc::Low => ErrorCorrection::Low,
60 qrcodegen::QrCodeEcc::Medium => ErrorCorrection::Medium,
61 qrcodegen::QrCodeEcc::Quartile => ErrorCorrection::Quartile,
62 qrcodegen::QrCodeEcc::High => ErrorCorrection::High,
63 }
64 }
65
66 pub fn word(self) -> &'static str {
68 match self {
69 ErrorCorrection::Low => "low",
70 ErrorCorrection::Medium => "medium",
71 ErrorCorrection::Quartile => "quartile",
72 ErrorCorrection::High => "high",
73 }
74 }
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum QrError {
81 TooLong {
84 bytes: usize,
86 },
87}
88
89impl fmt::Display for QrError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 QrError::TooLong { bytes } => write!(
93 f,
94 "{bytes} bytes do not fit a QR code at this error correction"
95 ),
96 }
97 }
98}
99
100impl std::error::Error for QrError {}
101
102#[derive(Clone, Debug, PartialEq)]
143#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
144pub struct QrCode {
145 modules: Vec<bool>,
146 size: usize,
147 version: u8,
148 ecc: ErrorCorrection,
149 text: Option<String>,
150 quiet: usize,
151 charset: Charset,
152 invert: bool,
153 dark: String,
154 light: String,
155}
156
157impl QrCode {
158 pub fn new(text: &str) -> Result<Self, QrError> {
160 Self::with_error_correction(text, ErrorCorrection::Medium)
161 }
162
163 pub fn with_error_correction(text: &str, ecc: ErrorCorrection) -> Result<Self, QrError> {
167 let code = qrcodegen::QrCode::encode_text(text, ecc.ecc())
168 .map_err(|_| QrError::TooLong { bytes: text.len() })?;
169 Ok(Self::from_code(&code, Some(text.to_string())))
170 }
171
172 pub fn from_bytes(data: &[u8], ecc: ErrorCorrection) -> Result<Self, QrError> {
174 let code = qrcodegen::QrCode::encode_binary(data, ecc.ecc())
175 .map_err(|_| QrError::TooLong { bytes: data.len() })?;
176 let text = std::str::from_utf8(data).ok().map(str::to_string);
177 Ok(Self::from_code(&code, text))
178 }
179
180 fn from_code(code: &qrcodegen::QrCode, text: Option<String>) -> Self {
181 let size = usize::try_from(code.size()).unwrap_or(0);
182 let mut modules = Vec::with_capacity(size * size);
183 for y in 0..code.size() {
184 for x in 0..code.size() {
185 modules.push(code.get_module(x, y));
186 }
187 }
188 QrCode {
189 modules,
190 size,
191 version: code.version().value(),
192 ecc: ErrorCorrection::from_ecc(code.error_correction_level()),
193 text,
194 quiet: SPEC_QUIET_ZONE,
195 charset: Charset::Auto,
196 invert: false,
197 dark: "#000000".to_string(),
198 light: "#ffffff".to_string(),
199 }
200 }
201
202 pub fn size(&self) -> usize {
204 self.size
205 }
206
207 pub fn version(&self) -> u8 {
209 self.version
210 }
211
212 pub fn error_correction(&self) -> ErrorCorrection {
215 self.ecc
216 }
217
218 pub fn module(&self, x: usize, y: usize) -> bool {
221 x < self.size && y < self.size && self.modules[y * self.size + x]
222 }
223
224 pub fn quiet_zone(mut self, modules: usize) -> Self {
227 self.quiet = modules.min(SPEC_QUIET_ZONE * 4);
228 self
229 }
230
231 pub fn charset(mut self, charset: Charset) -> Self {
233 self.charset = charset;
234 self
235 }
236
237 pub fn invert(mut self, invert: bool) -> Self {
240 self.invert = invert;
241 self
242 }
243
244 pub fn colours(mut self, dark: impl Into<String>, light: impl Into<String>) -> Self {
248 self.dark = dark.into();
249 self.light = light.into();
250 self
251 }
252
253 fn dark_at(&self, quiet: usize, x: usize, y: usize) -> bool {
256 x >= quiet && y >= quiet && self.module(x - quiet, y - quiet)
257 }
258
259 fn width_for(&self, quiet: usize, ascii: bool) -> usize {
261 (self.size + 2 * quiet) * if ascii { 2 } else { 1 }
262 }
263
264 fn fit(&self, width: usize, ascii: bool) -> Option<usize> {
267 (self.quiet.min(1)..=self.quiet)
268 .rev()
269 .find(|&q| self.width_for(q, ascii) <= width)
270 }
271
272 fn note(&self, ascii: bool) -> String {
274 format!(
275 "QR code needs {} columns",
276 self.width_for(self.quiet.min(1), ascii)
277 )
278 }
279
280 fn colour_styles(&self, console: &Console) -> (Style, Style, Style) {
283 let colour_of = |spec: &str| user_style(console, spec).color().cloned();
284 let (dark, light) = (colour_of(&self.dark), colour_of(&self.light));
285 (
286 Style::from_color(dark.clone(), light.clone()),
287 Style::from_color(None, dark),
288 Style::from_color(None, light),
289 )
290 }
291
292 fn lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
293 let charset = self.charset.resolve(console, options, Charset::Blocks);
294 let ascii = charset == Charset::Ascii;
295 let Some(quiet) = self.fit(options.max_width, ascii) else {
296 let note = crate::chart::truncate(&self.note(ascii), options.max_width, ascii);
297 return vec![Segment::new(note, None)];
298 };
299 let colour = has_colour(console);
300 let (ink, dark_bg, light_bg) = self.colour_styles(console);
301 let side = self.size + 2 * quiet;
302 let dark = |x: usize, y: usize| self.dark_at(quiet, x, y);
303 let inked = |x: usize, y: usize| {
307 y < side
308 && if colour || self.invert {
309 dark(x, y)
310 } else {
311 !dark(x, y)
312 }
313 };
314 let mut out = Vec::new();
315 for y in 0..side {
316 if ascii {
317 if y > 0 {
318 out.push(Segment::line());
319 }
320 let mut x = 0;
321 while x < side {
322 let here = if colour { dark(x, y) } else { inked(x, y) };
323 let run = (x..side)
324 .take_while(|&i| (if colour { dark(i, y) } else { inked(i, y) }) == here)
325 .count();
326 let segment = if colour {
327 let bg = if here { &dark_bg } else { &light_bg };
328 Segment::new(" ".repeat(run * 2), Some(bg.clone()))
329 } else {
330 Segment::new(if here { "##" } else { " " }.repeat(run), None)
331 };
332 out.push(segment);
333 x += run;
334 }
335 } else if y % 2 == 0 {
336 if y > 0 {
337 out.push(Segment::line());
338 }
339 let row: String = (0..side)
340 .map(|x| match (inked(x, y), inked(x, y + 1)) {
341 (true, true) => '█',
342 (true, false) => '▀',
343 (false, true) => '▄',
344 (false, false) => ' ',
345 })
346 .collect();
347 out.push(Segment::new(row, colour.then(|| ink.clone())));
348 }
349 }
350 out
351 }
352}
353
354impl Renderable for QrCode {
355 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
356 self.lines(console, options)
357 }
358
359 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
360 let ascii = self.charset.resolve(console, options, Charset::Blocks) == Charset::Ascii;
361 let natural = self.width_for(self.quiet, ascii);
362 let least = self.width_for(self.quiet.min(1), ascii);
363 Measurement::new(least, natural)
364 .with_maximum(options.max_width)
365 .normalize()
366 }
367}
368
369impl crate::a11y::AccessibleText for QrCode {
372 fn accessible_text(&self, _width: usize) -> String {
373 let mut out = format!(
374 "QR code, version {}, {} by {} modules, {} error correction",
375 self.version,
376 self.size,
377 self.size,
378 self.ecc.word()
379 );
380 match &self.text {
381 Some(text) => out.push_str(&format!("\nEncodes: {text}")),
382 None => out.push_str("\nEncodes binary data"),
383 }
384 out
385 }
386}