Skip to main content

qrcode_render/
ansi.rs

1//! ANSI terminal color rendering.
2//!
3//! Renders QR codes using 24-bit TrueColor ANSI escape codes with half-block
4//! characters. Each character represents 2 vertical pixels with independent
5//! foreground and background colors.
6//!
7//! # Example
8//!
9//! ```
10//! use qrcode_core::Color as ModuleColor;
11//! use qrcode_render::{Renderer, ansi::Color};
12//!
13//! let modules = [ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
14//! // Dark modules in black, light modules in white.
15//! let text = Renderer::<Color>::new(&modules, 2, 0).build();
16//! println!("{}", text);
17//!
18//! // Custom colors: dark blue on light gray.
19//! let text = Renderer::<Color>::new(&modules, 2, 0)
20//!     .dark_color(Color::new(0, 51, 102))
21//!     .light_color(Color::new(224, 224, 224))
22//!     .build();
23//! println!("{}", text);
24//! ```
25
26#[cfg(not(feature = "std"))]
27#[allow(unused_imports)]
28use alloc::{
29    borrow::ToOwned,
30    format,
31    string::{String, ToString},
32    vec,
33    vec::Vec,
34};
35
36use crate::{Canvas as RenderCanvas, Pixel, StyledPixel};
37use qrcode_core::Color as ModuleColor;
38
39/// An ANSI TrueColor (24-bit) pixel.
40///
41/// Each `Color` stores an RGB value that will be rendered using ANSI escape
42/// codes in the terminal.
43#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct Color {
45    r: u8,
46    g: u8,
47    b: u8,
48}
49
50impl Color {
51    /// Creates a new ANSI color from RGB components.
52    pub const fn new(r: u8, g: u8, b: u8) -> Self {
53        Self { r, g, b }
54    }
55
56    /// ANSI escape sequence for this color as a foreground color.
57    fn fg_ansi(self) -> String {
58        format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
59    }
60
61    /// ANSI escape sequence for this color as a background color.
62    fn bg_ansi(self) -> String {
63        format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
64    }
65}
66
67impl Pixel for Color {
68    type Image = String;
69    type Canvas = CanvasAnsi;
70
71    fn default_unit_size() -> (u32, u32) {
72        (1, 1)
73    }
74
75    fn default_color(color: ModuleColor) -> Self {
76        match color {
77            ModuleColor::Dark => Color::new(0, 0, 0),
78            ModuleColor::Light => Color::new(255, 255, 255),
79        }
80    }
81}
82
83impl StyledPixel for Color {
84    fn from_hex(hex: &str) -> Self {
85        let (r, g, b) = crate::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
86        Color::new(r, g, b)
87    }
88}
89
90/// Canvas for ANSI terminal rendering.
91///
92/// Uses Unicode half-block characters (▀ U+2580) where the foreground color
93/// paints the top half and the background color paints the bottom half.
94/// This yields 2 vertical pixels per character.
95pub struct CanvasAnsi {
96    canvas: Vec<u8>,
97    width: u32,
98    dark_pixel: u8,
99    dark_color: Color,
100    light_color: Color,
101}
102
103impl RenderCanvas for CanvasAnsi {
104    type Pixel = Color;
105    type Image = String;
106
107    fn new(width: u32, height: u32, dark_pixel: Color, light_pixel: Color) -> Self {
108        CanvasAnsi {
109            canvas: vec![0u8; (width * height) as usize],
110            width,
111            dark_pixel: 1,
112            dark_color: dark_pixel,
113            light_color: light_pixel,
114        }
115    }
116
117    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
118        self.canvas[(x + y * self.width) as usize] = self.dark_pixel;
119    }
120
121    fn into_image(self) -> String {
122        let w = self.width as usize;
123        let dark = 1u8;
124        let reset = "\x1b[0m";
125
126        self.canvas
127            .chunks_exact(w)
128            .collect::<Vec<&[u8]>>()
129            .chunks(2)
130            .map(|rows| {
131                let top_row = rows[0];
132                let bot_row = rows.get(1).map_or(&[][..], |r| *r);
133
134                let mut line = String::with_capacity(w * 40);
135                let mut last_fg = None;
136                let mut last_bg = None;
137
138                for col in 0..w {
139                    let top = top_row.get(col).copied().unwrap_or(0);
140                    let bot = bot_row.get(col).copied().unwrap_or(0);
141
142                    let (fg, bg) = if top == dark && bot == dark {
143                        (self.dark_color, self.dark_color)
144                    } else if top == dark && bot != dark {
145                        (self.dark_color, self.light_color)
146                    } else if top != dark && bot == dark {
147                        (self.light_color, self.dark_color)
148                    } else {
149                        (self.light_color, self.light_color)
150                    };
151
152                    // Only emit escape codes when colors change.
153                    if last_bg != Some(bg) {
154                        line.push_str(&bg.bg_ansi());
155                        last_bg = Some(bg);
156                    }
157                    if last_fg != Some(fg) {
158                        line.push_str(&fg.fg_ansi());
159                        last_fg = Some(fg);
160                    }
161
162                    if top == dark && bot == dark {
163                        line.push('█');
164                    } else if top == dark {
165                        line.push('▀');
166                    } else if bot == dark {
167                        line.push('▄');
168                    } else {
169                        line.push(' ');
170                    }
171                }
172
173                line.push_str(reset);
174                line
175            })
176            .collect::<Vec<String>>()
177            .join("\n")
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::Renderer;
185
186    #[test]
187    fn test_ansi_all_dark() {
188        let colors = vec![ModuleColor::Dark; 4];
189        let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
190        // Should contain the full-block character and ANSI codes.
191        assert!(image.contains('█'));
192        assert!(image.contains("\x1b["));
193        assert!(image.contains("\x1b[0m"));
194    }
195
196    #[test]
197    fn test_ansi_all_light() {
198        let colors = vec![ModuleColor::Light; 4];
199        let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
200        assert!(image.contains(' '));
201        assert!(image.contains("\x1b[0m"));
202    }
203
204    #[test]
205    fn test_ansi_mixed() {
206        let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
207        let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
208        // Dark on top, light on bottom → '▀' with dark fg, light bg.
209        assert!(image.contains('▀'));
210    }
211
212    #[test]
213    fn test_ansi_custom_colors() {
214        let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
215        let image = Renderer::<Color>::new(&colors, 2, 0)
216            .dark_color(Color::new(0, 51, 102))
217            .light_color(Color::new(224, 224, 224))
218            .module_dimensions(1, 1)
219            .build();
220        // Should contain the custom RGB values.
221        assert!(image.contains("0;51;102"));
222        assert!(image.contains("224;224;224"));
223    }
224
225    #[test]
226    fn test_ansi_color_optimization() {
227        // Consecutive same-colored pixels should not emit redundant escape codes.
228        let colors = vec![ModuleColor::Dark; 16]; // 4x4 all dark
229        let image: String = Renderer::<Color>::new(&colors, 4, 0).module_dimensions(1, 1).build();
230        let lines: Vec<&str> = image.split('\n').collect();
231        assert_eq!(lines.len(), 2);
232        // All '█' chars, same fg/bg — only 3 escape sequences per line (fg + bg + reset).
233        for line in &lines {
234            let esc_count = line.matches("\x1b[").count();
235            assert_eq!(esc_count, 3);
236        }
237    }
238}