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