Skip to main content

qrcode_render/
unicode.rs

1//! UTF-8 rendering, with various pixel densities.
2
3#[cfg(not(feature = "std"))]
4#[allow(unused_imports)]
5use alloc::{
6    borrow::ToOwned,
7    format,
8    string::{String, ToString},
9    vec,
10    vec::Vec,
11};
12
13use crate::{Canvas as RenderCanvas, Color, Pixel};
14
15//{{{ Shared macro for bit-packed canvas
16
17/// Generates a `Canvas` implementation for Unicode renderers that pack multiple
18/// vertical pixels into a single `u8` cell. The `into_image` method processes
19/// `ROW_GROUP` rows at a time with zero intermediate allocations.
20macro_rules! impl_bit_canvas {
21    ($canvas:ident, $pixel:ident, $row_group:expr, $col_step:expr, $encode:expr) => {
22        #[doc(hidden)]
23        pub struct $canvas {
24            canvas: Vec<u8>,
25            width: u32,
26            dark_pixel: u8,
27        }
28
29        impl RenderCanvas for $canvas {
30            type Pixel = $pixel;
31            type Image = String;
32
33            fn new(width: u32, height: u32, dark_pixel: $pixel, light_pixel: $pixel) -> Self {
34                let a = vec![light_pixel.value(); (width * height) as usize];
35                $canvas { width, canvas: a, dark_pixel: dark_pixel.value() }
36            }
37
38            fn draw_dark_pixel(&mut self, x: u32, y: u32) {
39                self.canvas[(x + y * self.width) as usize] = self.dark_pixel;
40            }
41
42            fn into_image(self) -> String {
43                let w = self.width as usize;
44                let data = &self.canvas;
45                let row_group = $row_group;
46                let empty: &[u8] = &[];
47                let col_step: usize = $col_step;
48                let row_count = data.len() / w;
49                let output_rows = row_count.div_ceil(row_group);
50                let output_cols = w.div_ceil(col_step);
51                let mut out = String::with_capacity(output_rows * (output_cols + 1));
52
53                for group_start in (0..row_count).step_by(row_group) {
54                    let actual = row_group.min(row_count - group_start);
55                    let mut group: [&[u8]; $row_group] = [empty; $row_group];
56                    for i in 0..actual {
57                        let row_start = (group_start + i) * w;
58                        group[i] = &data[row_start..row_start + w];
59                    }
60                    for col in (0..w).step_by(col_step) {
61                        out.push_str($encode(&group, col));
62                    }
63                    out.push('\n');
64                }
65                if out.ends_with('\n') {
66                    out.pop();
67                }
68                out
69            }
70        }
71    };
72}
73
74//}}}
75//{{{ Dense1x2 — half-block, 2 rows per character
76
77const CODEPAGE: [&str; 4] = [" ", "\u{2584}", "\u{2580}", "\u{2588}"];
78
79/// Unicode renderer packing 2 vertical pixels per character using half-block
80/// elements (U+2580–U+2588). Use with `QrCode::render::<Dense1x2>()`.
81#[derive(Copy, Clone, PartialEq, Eq)]
82pub enum Dense1x2 {
83    /// A dark module.
84    Dark,
85    /// A light module.
86    Light,
87}
88
89impl Pixel for Dense1x2 {
90    type Image = String;
91    type Canvas = Canvas1x2;
92    fn default_unit_size() -> (u32, u32) {
93        (1, 1)
94    }
95    fn default_color(color: Color) -> Dense1x2 {
96        color.select(Dense1x2::Dark, Dense1x2::Light)
97    }
98}
99
100impl Dense1x2 {
101    const fn value(self) -> u8 {
102        match self {
103            Dense1x2::Dark => 1,
104            Dense1x2::Light => 0,
105        }
106    }
107}
108
109fn encode_1x2(rows: &[&[u8]], col: usize) -> &'static str {
110    let top = rows[0].get(col).copied().unwrap_or(0);
111    let bot = rows[1].get(col).copied().unwrap_or(0);
112    CODEPAGE[usize::from(top * 2 + bot)]
113}
114
115impl_bit_canvas!(Canvas1x2, Dense1x2, 2, 1, encode_1x2 as fn(&[&[u8]], usize) -> &'static str);
116
117//}}}
118//{{{ Dense2x2 — quadrant blocks (U+2596–U+259F), 2×2 per character
119
120/// The 16 quadrant characters.
121/// Bit layout: bit 0 = top-left, bit 1 = top-right, bit 2 = bottom-left, bit 3 = bottom-right.
122const QUADRANT: [&str; 16] = [
123    " ",        // 0b0000
124    "\u{2598}", // 0b0001 top-left
125    "\u{259D}", // 0b0010 top-right
126    "\u{2580}", // 0b0011 top
127    "\u{2596}", // 0b0100 bottom-left
128    "\u{258C}", // 0b0101 left
129    "\u{259E}", // 0b0110 anti-diagonal
130    "\u{259B}", // 0b0111 all except bottom-right
131    "\u{2597}", // 0b1000 bottom-right
132    "\u{259A}", // 0b1001 diagonal
133    "\u{2590}", // 0b1010 right
134    "\u{259C}", // 0b1011 all except bottom-left
135    "\u{2584}", // 0b1100 bottom
136    "\u{2599}", // 0b1101 all except top-right
137    "\u{259F}", // 0b1110 all except top-left
138    "\u{2588}", // 0b1111 full
139];
140
141/// Unicode renderer packing a 2×2 block of pixels per character using
142/// quadrant elements (U+2596–U+259F).
143#[derive(Copy, Clone, PartialEq, Eq)]
144pub enum Dense2x2 {
145    /// A dark module.
146    Dark,
147    /// A light module.
148    Light,
149}
150
151impl Pixel for Dense2x2 {
152    type Image = String;
153    type Canvas = Canvas2x2;
154    fn default_unit_size() -> (u32, u32) {
155        (1, 1)
156    }
157    fn default_color(color: Color) -> Dense2x2 {
158        color.select(Dense2x2::Dark, Dense2x2::Light)
159    }
160}
161
162impl Dense2x2 {
163    const fn value(self) -> u8 {
164        match self {
165            Dense2x2::Dark => 1,
166            Dense2x2::Light => 0,
167        }
168    }
169}
170
171fn encode_2x2(rows: &[&[u8]], col: usize) -> &'static str {
172    let tl = rows[0][col] & 1;
173    let tr = rows[0].get(col + 1).copied().unwrap_or(0) & 1;
174    let bl = rows[1].get(col).copied().unwrap_or(0) & 1;
175    let br = rows[1].get(col + 1).copied().unwrap_or(0) & 1;
176    QUADRANT[(tl | (tr << 1) | (bl << 2) | (br << 3)) as usize]
177}
178
179impl_bit_canvas!(Canvas2x2, Dense2x2, 2, 2, encode_2x2 as fn(&[&[u8]], usize) -> &'static str);
180
181//}}}
182//{{{ Braille (U+2800–U+28FF), 2×4 dots per character
183
184/// UTF-8 rendering using Braille characters (U+2800–U+28FF).
185///
186/// Each character encodes a 2×4 grid of 8 dots, yielding 8 pixels per
187/// character — the highest density among Unicode renderers.
188///
189/// # Example
190///
191/// ```
192/// use qrcode_core::Color as ModuleColor;
193/// use qrcode_render::{Renderer, unicode::Braille};
194///
195/// let modules = [ModuleColor::Dark; 16];
196/// let text = Renderer::<Braille>::new(&modules, 4, 0).module_dimensions(1, 1).build();
197/// println!("{}", text);
198/// ```
199/// Unicode renderer packing a 2×4 block of pixels per character using Braille
200/// patterns (U+2800–U+28FF) — the densest text output.
201#[derive(Copy, Clone, PartialEq, Eq)]
202pub enum Braille {
203    /// A dark module.
204    Dark,
205    /// A light module.
206    Light,
207}
208
209impl Pixel for Braille {
210    type Image = String;
211    type Canvas = CanvasBraille;
212    fn default_unit_size() -> (u32, u32) {
213        (1, 1)
214    }
215    fn default_color(color: Color) -> Braille {
216        color.select(Braille::Dark, Braille::Light)
217    }
218}
219
220impl Braille {
221    const fn value(self) -> u8 {
222        match self {
223            Braille::Dark => 1,
224            Braille::Light => 0,
225        }
226    }
227}
228
229/// Precomputed UTF-8 encodings for all 256 Braille code points (U+2800–U+28FF).
230const BRAILLE_UTF8: [[u8; 3]; 256] = {
231    let mut table = [[0u8; 3]; 256];
232    let mut i = 0usize;
233    while i < 256 {
234        let cp = 0x2800u32 + i as u32;
235        table[i][0] = ((cp >> 12) & 0x0F) as u8 | 0xE0;
236        table[i][1] = ((cp >> 6) & 0x3F) as u8 | 0x80;
237        table[i][2] = (cp & 0x3F) as u8 | 0x80;
238        i += 1;
239    }
240    table
241};
242
243fn encode_braille(rows: &[&[u8]], col: usize) -> &'static str {
244    let d1 = rows[0].get(col).copied().unwrap_or(0) & 1;
245    let d2 = rows[1].get(col).copied().unwrap_or(0) & 1;
246    let d3 = rows[2].get(col).copied().unwrap_or(0) & 1;
247    let d4 = rows[0].get(col + 1).copied().unwrap_or(0) & 1;
248    let d5 = rows[1].get(col + 1).copied().unwrap_or(0) & 1;
249    let d6 = rows[2].get(col + 1).copied().unwrap_or(0) & 1;
250    let d7 = rows[3].get(col).copied().unwrap_or(0) & 1;
251    let d8 = rows[3].get(col + 1).copied().unwrap_or(0) & 1;
252
253    let bits = d1 | (d2 << 1) | (d3 << 2) | (d4 << 3) | (d5 << 4) | (d6 << 5) | (d7 << 6) | (d8 << 7);
254    // SAFETY: BRAILLE_UTF8[bits] is valid UTF-8 for U+2800+bits.
255    unsafe { core::str::from_utf8_unchecked(&BRAILLE_UTF8[bits as usize]) }
256}
257
258impl_bit_canvas!(CanvasBraille, Braille, 4, 2, encode_braille as fn(&[&[u8]], usize) -> &'static str);
259
260//}}}
261//{{{ Dense3x2 — sextant characters (U+1FB00–U+1FB3F), 3×2 per character
262
263/// UTF-8 rendering using Unicode sextant characters (U+1FB00–U+1FB3F).
264///
265/// Each character encodes a 3×2 grid of 6 cells, yielding 6 pixels per
266/// character — between Dense2x2 (4 px) and Braille (8 px) in density.
267///
268/// Bit layout (per Unicode sextant specification):
269///
270/// ```text
271/// bit0 bit3     row0[col] row0[col+1]
272/// bit1 bit4  =  row1[col] row1[col+1]
273/// bit2 bit5     row2[col] row2[col+1]
274/// ```
275///
276/// # Example
277///
278/// ```
279/// use qrcode_core::Color as ModuleColor;
280/// use qrcode_render::{Renderer, unicode::Dense3x2};
281///
282/// let modules = [ModuleColor::Dark; 36];
283/// let text = Renderer::<Dense3x2>::new(&modules, 6, 0).module_dimensions(1, 1).build();
284/// println!("{}", text);
285/// ```
286/// Unicode renderer packing a 3×2 block of pixels per character using
287/// sextant elements (U+1FB00–U+1FB3F).
288#[derive(Copy, Clone, PartialEq, Eq)]
289pub enum Dense3x2 {
290    /// A dark module.
291    Dark,
292    /// A light module.
293    Light,
294}
295
296impl Pixel for Dense3x2 {
297    type Image = String;
298    type Canvas = Canvas3x2;
299    fn default_unit_size() -> (u32, u32) {
300        (1, 1)
301    }
302    fn default_color(color: Color) -> Dense3x2 {
303        color.select(Dense3x2::Dark, Dense3x2::Light)
304    }
305}
306
307impl Dense3x2 {
308    const fn value(self) -> u8 {
309        match self {
310            Dense3x2::Dark => 1,
311            Dense3x2::Light => 0,
312        }
313    }
314}
315
316/// Precomputed UTF-8 encodings for all 64 sextant code points (U+1FB00–U+1FB3F).
317/// Each entry is 4 bytes (these are supplementary plane characters).
318const SEXTANT_UTF8: [[u8; 4]; 64] = {
319    let mut table = [[0u8; 4]; 64];
320    let mut i = 0usize;
321    while i < 64 {
322        let cp = 0x1FB00u32 + i as u32;
323        table[i][0] = 0xF0u8 | ((cp >> 18) & 0x07) as u8;
324        table[i][1] = 0x80u8 | ((cp >> 12) & 0x3F) as u8;
325        table[i][2] = 0x80u8 | ((cp >> 6) & 0x3F) as u8;
326        table[i][3] = 0x80u8 | (cp & 0x3F) as u8;
327        i += 1;
328    }
329    table
330};
331
332/// Encodes a 3×2 block of pixels into a sextant character.
333/// Pattern 0 (all light) maps to ASCII space for visual consistency.
334fn encode_3x2(rows: &[&[u8]], col: usize) -> &'static str {
335    let d0 = rows[0].get(col).copied().unwrap_or(0) & 1;
336    let d1 = rows[1].get(col).copied().unwrap_or(0) & 1;
337    let d2 = rows[2].get(col).copied().unwrap_or(0) & 1;
338    let d3 = rows[0].get(col + 1).copied().unwrap_or(0) & 1;
339    let d4 = rows[1].get(col + 1).copied().unwrap_or(0) & 1;
340    let d5 = rows[2].get(col + 1).copied().unwrap_or(0) & 1;
341
342    let bits = d0 | (d1 << 1) | (d2 << 2) | (d3 << 3) | (d4 << 4) | (d5 << 5);
343    if bits == 0 {
344        " "
345    } else {
346        // SAFETY: SEXTANT_UTF8[bits] is valid UTF-8 for U+1FB00+bits (bits > 0).
347        unsafe { core::str::from_utf8_unchecked(&SEXTANT_UTF8[bits as usize]) }
348    }
349}
350
351impl_bit_canvas!(Canvas3x2, Dense3x2, 3, 2, encode_3x2 as fn(&[&[u8]], usize) -> &'static str);
352
353//}}}
354
355#[test]
356fn test_render_to_utf8_string() {
357    use crate::Renderer;
358    let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
359    let image: String = Renderer::<Dense1x2>::new(colors, 2, 1).build();
360
361    assert_eq!(&image, " ▄  \n  ▀ ");
362
363    let image2 = Renderer::<Dense1x2>::new(colors, 2, 1).module_dimensions(2, 2).build();
364
365    assert_eq!(&image2, "        \n  ██    \n    ██  \n        ");
366}
367
368#[test]
369fn integration_render_utf8_1x2() {
370    use crate::Renderer;
371    use crate::unicode::Dense1x2;
372
373    let colors = [Color::Dark, Color::Light, Color::Light, Color::Dark];
374    let image = Renderer::<Dense1x2>::new(&colors, 2, 0).module_dimensions(1, 1).build();
375    assert_eq!(image, "▀▄");
376}
377
378#[test]
379fn integration_render_utf8_1x2_inverted() {
380    use crate::Renderer;
381    use crate::unicode::Dense1x2;
382
383    let colors = [Color::Dark, Color::Light, Color::Light, Color::Dark];
384    let image = Renderer::<Dense1x2>::new(&colors, 2, 0)
385        .dark_color(Dense1x2::Light)
386        .light_color(Dense1x2::Dark)
387        .module_dimensions(1, 1)
388        .build();
389    assert_eq!(image, "▄▀");
390}
391
392#[test]
393fn test_dense2x2_basic() {
394    use crate::Renderer;
395    let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
396    let image: String = Renderer::<Dense2x2>::new(colors, 2, 0).module_dimensions(1, 1).build();
397    assert_eq!(&image, "\u{259A}");
398}
399
400#[test]
401fn test_dense2x2_with_quiet_zone() {
402    use crate::Renderer;
403    let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark];
404    let image: String = Renderer::<Dense2x2>::new(colors, 2, 1).build();
405    assert!(image.chars().count() >= 1);
406}
407
408#[test]
409fn test_dense2x2_all_dark() {
410    use crate::Renderer;
411    let colors = vec![Color::Dark; 4];
412    let image: String = Renderer::<Dense2x2>::new(&colors, 2, 0).module_dimensions(1, 1).build();
413    assert_eq!(&image, "\u{2588}");
414}
415
416#[test]
417fn test_dense2x2_all_light() {
418    use crate::Renderer;
419    let colors = vec![Color::Light; 4];
420    let image: String = Renderer::<Dense2x2>::new(&colors, 2, 0).module_dimensions(1, 1).build();
421    assert_eq!(&image, " ");
422}
423
424#[test]
425fn integration_render_utf8_2x2() {
426    use crate::Renderer;
427    use crate::unicode::Dense2x2;
428
429    let colors = vec![
430        Color::Dark,
431        Color::Light,
432        Color::Light,
433        Color::Dark,
434        Color::Light,
435        Color::Dark,
436        Color::Dark,
437        Color::Light,
438        Color::Dark,
439        Color::Dark,
440        Color::Light,
441        Color::Light,
442        Color::Light,
443        Color::Light,
444        Color::Dark,
445        Color::Dark,
446    ];
447    let image = Renderer::<Dense2x2>::new(&colors, 4, 0).module_dimensions(1, 1).build();
448    assert!(!image.is_empty());
449    let dense1x2 = Renderer::<Dense1x2>::new(&colors, 4, 0).module_dimensions(1, 1).build();
450    assert!(image.len() < dense1x2.len());
451}
452
453#[test]
454fn test_braille_all_dark() {
455    use crate::Renderer;
456    let colors = vec![Color::Dark; 16];
457    let image: String = Renderer::<Braille>::new(&colors, 4, 0).module_dimensions(1, 1).build();
458    assert_eq!(&image, "\u{28FF}\u{28FF}");
459}
460
461#[test]
462fn test_braille_all_light() {
463    use crate::Renderer;
464    let colors = vec![Color::Light; 16];
465    let image: String = Renderer::<Braille>::new(&colors, 4, 0).module_dimensions(1, 1).build();
466    assert_eq!(&image, "\u{2800}\u{2800}");
467}
468
469#[test]
470fn test_braille_top_left_dot() {
471    use crate::Renderer;
472    let mut colors = vec![Color::Light; 16];
473    colors[0] = Color::Dark;
474    let image: String = Renderer::<Braille>::new(&colors, 4, 0).module_dimensions(1, 1).build();
475    assert_eq!(&image, "\u{2801}\u{2800}");
476}
477
478#[test]
479fn test_braille_density() {
480    use crate::Renderer;
481    use crate::unicode::{Braille, Dense1x2};
482
483    let colors = (0..64).map(|i| if i % 3 == 0 { Color::Dark } else { Color::Light }).collect::<Vec<_>>();
484    let braille = Renderer::<Braille>::new(&colors, 8, 0).module_dimensions(1, 1).build();
485    let dense1x2 = Renderer::<Dense1x2>::new(&colors, 8, 0).module_dimensions(1, 1).build();
486    assert!(braille.len() < dense1x2.len());
487}
488
489#[test]
490fn test_dense3x2_all_dark() {
491    use crate::Renderer;
492    // 6×6 all dark = 2 row groups × 3 cols = 6 full sextant chars (pattern 63 = U+1FB3F)
493    let colors = vec![Color::Dark; 36];
494    let image: String = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
495    assert_eq!(&image, "\u{1FB3F}\u{1FB3F}\u{1FB3F}\n\u{1FB3F}\u{1FB3F}\u{1FB3F}");
496}
497
498#[test]
499fn test_dense3x2_all_light() {
500    use crate::Renderer;
501    let colors = vec![Color::Light; 36];
502    let image: String = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
503    assert_eq!(&image, "   \n   ");
504}
505
506#[test]
507fn test_dense3x2_top_left_cell() {
508    use crate::Renderer;
509    // 6×6 grid with only (0,0) dark → bit0 set → pattern 1 = U+1FB01
510    let mut colors = vec![Color::Light; 36];
511    colors[0] = Color::Dark;
512    let image: String = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
513    // First char: U+1FB01, rest are spaces
514    assert!(image.starts_with('\u{1FB01}'));
515}
516
517#[test]
518fn test_dense3x2_density() {
519    use crate::Renderer;
520    use crate::unicode::{Dense1x2, Dense3x2};
521
522    let colors = (0..36).map(|i| if i % 2 == 0 { Color::Dark } else { Color::Light }).collect::<Vec<_>>();
523    let sextant = Renderer::<Dense3x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
524    let dense1x2 = Renderer::<Dense1x2>::new(&colors, 6, 0).module_dimensions(1, 1).build();
525    // Sextant should be smaller due to higher density (3 rows per char vs 2).
526    assert!(sextant.len() < dense1x2.len());
527}