Skip to main content

rich_ext/
qr.rs

1//! QR codes drawn with text: [`QrCode`] encodes text or bytes and draws
2//! the modules two to a cell with half blocks, inside a quiet zone, so a
3//! phone can read it off the terminal.
4//!
5//! Needs the `qr` feature. The encoder is Project Nayuki's
6//! [`qrcodegen`](https://crates.io/crates/qrcodegen) (MIT, no
7//! dependencies).
8//!
9//! ```
10//! use rich::Console;
11//! use rich_ext::qr::{ErrorCorrection, QrCode};
12//!
13//! let code = QrCode::with_error_correction("hello", ErrorCorrection::Low).unwrap();
14//! assert_eq!(code.size(), 21);
15//! let console = Console::builder().width(80).color_system(None).build();
16//! let out = console.render_export(&code.quiet_zone(1));
17//! // 21 modules and a module of quiet zone each side, two rows a line.
18//! assert_eq!(out.lines().count(), 12);
19//! assert!(out.lines().all(|line| line.chars().count() == 23));
20//! ```
21
22use std::fmt;
23
24use rich::measure::Measurement;
25use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
26
27use crate::chart::{Charset, has_colour, user_style};
28
29/// The quiet zone the QR specification asks for, in modules.
30const SPEC_QUIET_ZONE: usize = 4;
31
32/// How much of a [`QrCode`] can be damaged and still read: more
33/// correction makes a bigger code for the same data.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum ErrorCorrection {
36    /// About 7% of the code.
37    Low,
38    /// About 15% (the default).
39    #[default]
40    Medium,
41    /// About 25%.
42    Quartile,
43    /// About 30%.
44    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    /// The level as a word: `low`, `medium`, `quartile`, `high`.
67    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/// Why a [`QrCode`] could not be made.
78#[derive(Clone, Debug, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum QrError {
81    /// The data does not fit the largest code (version 40) at the error
82    /// correction asked for.
83    TooLong {
84        /// The data's length in bytes.
85        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/// A QR code as a renderable.
103///
104/// - [`Charset::Blocks`] (the default; [`Charset::Braille`] draws the same,
105///   as Braille dots are too sparse to scan) draws two modules a cell with
106///   half blocks (`▀▄█`), so the code is square and `size + 2 × quiet
107///   zone` cells wide.
108/// - With colour, every cell is painted: the dark modules in `#000000` and
109///   the light ones in `#ffffff`, whatever the terminal's theme makes of its
110///   named colours ([`colours`](Self::colours) changes
111///   them), so the code reads on a dark or a light terminal.
112/// - Without colour, the glyphs draw the light modules in the terminal's
113///   text colour, so on a dark background (the common case) the code reads
114///   dark on light. [`invert`](Self::invert) draws the dark modules
115///   instead, for a terminal with a light background.
116/// - [`Charset::Ascii`] (and an ASCII-only console) draws each module as
117///   two cells, `##` or spaces, or as painted spaces with colour, so the
118///   code is twice as tall.
119/// - The quiet zone is 4 modules, as the specification asks
120///   ([`quiet_zone`](Self::quiet_zone)). Given less width, the quiet zone
121///   narrows to a module; a code that still does not fit is replaced by a
122///   note saying how many columns it needs, as a cut code cannot be read.
123///
124/// ```
125/// use rich::Console;
126/// use rich_ext::qr::{ErrorCorrection, QrCode};
127///
128/// let code = QrCode::new("https://example.com").unwrap();
129/// assert_eq!((code.size(), code.version()), (25, 2));
130/// // Medium was asked for; quartile fits the same size, so it is used.
131/// assert_eq!(code.error_correction(), ErrorCorrection::Quartile);
132/// // 25 modules and 4 of quiet zone each side.
133/// let wide = Console::builder().width(40).color_system(None).build();
134/// assert!(wide.render_export(&code).lines().all(|l| l.chars().count() == 33));
135/// // The quiet zone narrows to fit, down to a module.
136/// let narrow = Console::builder().width(28).color_system(None).build();
137/// assert!(narrow.render_export(&code).lines().all(|l| l.chars().count() == 27));
138/// // Narrower still, a note.
139/// let tiny = Console::builder().width(26).color_system(None).build();
140/// assert_eq!(tiny.render_export(&code), "QR code needs 27 columns\n");
141/// ```
142#[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    /// `text` at [`ErrorCorrection::Medium`].
159    pub fn new(text: &str) -> Result<Self, QrError> {
160        Self::with_error_correction(text, ErrorCorrection::Medium)
161    }
162
163    /// `text` at `ecc`, or higher when the larger level fits the same
164    /// size of code. Digits and upper-case text are packed tighter than
165    /// other text.
166    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    /// Bytes at `ecc` (or higher, as [`with_error_correction`](Self::with_error_correction)).
173    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    /// Modules along a side, without the quiet zone (21 to 177).
203    pub fn size(&self) -> usize {
204        self.size
205    }
206
207    /// The QR version, 1 to 40: the size is `17 + 4 × version`.
208    pub fn version(&self) -> u8 {
209        self.version
210    }
211
212    /// The error correction the code was made with (it may be higher than
213    /// asked for).
214    pub fn error_correction(&self) -> ErrorCorrection {
215        self.ecc
216    }
217
218    /// Whether the module at column `x`, row `y` is dark; `false` outside
219    /// the code.
220    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    /// The light border around the code, in modules (default 4, as the
225    /// specification asks; scanners often manage with 2).
226    pub fn quiet_zone(mut self, modules: usize) -> Self {
227        self.quiet = modules.min(SPEC_QUIET_ZONE * 4);
228        self
229    }
230
231    /// Glyphs to draw with (default [`Charset::Auto`]: half blocks).
232    pub fn charset(mut self, charset: Charset) -> Self {
233        self.charset = charset;
234        self
235    }
236
237    /// Without colour, draw the dark modules rather than the light ones,
238    /// for a terminal with a light background (default off).
239    pub fn invert(mut self, invert: bool) -> Self {
240        self.invert = invert;
241        self
242    }
243
244    /// The dark and light modules' colours with colour on: theme keys or
245    /// colour definitions (default `#000000` and `#ffffff`, the
246    /// nearest a console has: black and bright white on 16 colours).
247    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    /// Whether the module at `(x, y)` counted from the quiet zone's corner
254    /// is dark (the quiet zone is light).
255    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    /// Cells across for `quiet` modules of quiet zone.
260    fn width_for(&self, quiet: usize, ascii: bool) -> usize {
261        (self.size + 2 * quiet) * if ascii { 2 } else { 1 }
262    }
263
264    /// The quiet zone that fits `width`: the one asked for, or narrower
265    /// down to a module.
266    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    /// What is written instead of a code too wide for the width given.
273    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    /// The styles with colour on: dark on light for half blocks, and the
281    /// two backgrounds for painted spaces.
282    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        // What the glyphs draw: the dark modules with colour (on a light
304        // background) or inverted; otherwise the light ones. Past the last
305        // row is nothing.
306        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
369/// `QR code, version 2, 25 by 25 modules, medium error correction`, then
370/// what it encodes, when that is text.
371impl 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}