1#[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#[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 pub const fn new(r: u8, g: u8, b: u8) -> Self {
53 Self { r, g, b }
54 }
55
56 fn push_fg_ansi(self, out: &mut String) {
57 use core::fmt::Write as _;
58
59 write!(out, "\x1b[38;2;{};{};{}m", self.r, self.g, self.b).expect("writing to String cannot fail");
60 }
61
62 fn push_bg_ansi(self, out: &mut String) {
63 use core::fmt::Write as _;
64
65 write!(out, "\x1b[48;2;{};{};{}m", self.r, self.g, self.b).expect("writing to String cannot fail");
66 }
67}
68
69impl Pixel for Color {
70 type Image = String;
71 type Canvas = CanvasAnsi;
72
73 fn default_unit_size() -> (u32, u32) {
74 (1, 1)
75 }
76
77 fn default_color(color: ModuleColor) -> Self {
78 match color {
79 ModuleColor::Dark => Color::new(0, 0, 0),
80 ModuleColor::Light => Color::new(255, 255, 255),
81 }
82 }
83}
84
85impl StyledPixel for Color {
86 fn from_hex(hex: &str) -> Self {
87 let (r, g, b) = crate::colors::hex_to_rgb(hex).unwrap_or((0, 0, 0));
88 Color::new(r, g, b)
89 }
90}
91
92pub struct CanvasAnsi {
98 canvas: Vec<u8>,
99 width: u32,
100 dark_pixel: u8,
101 dark_color: Color,
102 light_color: Color,
103}
104
105impl RenderCanvas for CanvasAnsi {
106 type Pixel = Color;
107 type Image = String;
108
109 fn new(width: u32, height: u32, dark_pixel: Color, light_pixel: Color) -> Self {
110 CanvasAnsi {
111 canvas: vec![0u8; (width * height) as usize],
112 width,
113 dark_pixel: 1,
114 dark_color: dark_pixel,
115 light_color: light_pixel,
116 }
117 }
118
119 fn draw_dark_pixel(&mut self, x: u32, y: u32) {
120 self.canvas[(x + y * self.width) as usize] = self.dark_pixel;
121 }
122
123 fn into_image(self) -> String {
124 let w = self.width as usize;
125 let dark = 1u8;
126 let reset = "\x1b[0m";
127 let row_count = self.canvas.len() / w;
128 let output_rows = row_count.div_ceil(2);
129 let mut out = String::with_capacity(output_rows * (w * 40 + reset.len() + 1));
130
131 for group_start in (0..row_count).step_by(2) {
132 if group_start > 0 {
133 out.push('\n');
134 }
135
136 let top_start = group_start * w;
137 let top_row = &self.canvas[top_start..top_start + w];
138 let bot_row = if group_start + 1 < row_count {
139 let bot_start = (group_start + 1) * w;
140 &self.canvas[bot_start..bot_start + w]
141 } else {
142 &[][..]
143 };
144
145 let mut last_fg = None;
146 let mut last_bg = None;
147
148 for col in 0..w {
149 let top = top_row.get(col).copied().unwrap_or(0);
150 let bot = bot_row.get(col).copied().unwrap_or(0);
151
152 let (fg, bg) = if top == dark && bot == dark {
153 (self.dark_color, self.dark_color)
154 } else if top == dark && bot != dark {
155 (self.dark_color, self.light_color)
156 } else if top != dark && bot == dark {
157 (self.light_color, self.dark_color)
158 } else {
159 (self.light_color, self.light_color)
160 };
161
162 if last_bg != Some(bg) {
163 bg.push_bg_ansi(&mut out);
164 last_bg = Some(bg);
165 }
166 if last_fg != Some(fg) {
167 fg.push_fg_ansi(&mut out);
168 last_fg = Some(fg);
169 }
170
171 if top == dark && bot == dark {
172 out.push('█');
173 } else if top == dark {
174 out.push('▀');
175 } else if bot == dark {
176 out.push('▄');
177 } else {
178 out.push(' ');
179 }
180 }
181
182 out.push_str(reset);
183 }
184 out
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::Renderer;
192
193 #[test]
194 fn test_ansi_all_dark() {
195 let colors = vec![ModuleColor::Dark; 4];
196 let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
197 assert!(image.contains('█'));
199 assert!(image.contains("\x1b["));
200 assert!(image.contains("\x1b[0m"));
201 }
202
203 #[test]
204 fn test_ansi_all_light() {
205 let colors = vec![ModuleColor::Light; 4];
206 let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
207 assert!(image.contains(' '));
208 assert!(image.contains("\x1b[0m"));
209 }
210
211 #[test]
212 fn test_ansi_mixed() {
213 let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
214 let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
215 assert!(image.contains('▀'));
217 }
218
219 #[test]
220 fn test_ansi_custom_colors() {
221 let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
222 let image = Renderer::<Color>::new(&colors, 2, 0)
223 .dark_color(Color::new(0, 51, 102))
224 .light_color(Color::new(224, 224, 224))
225 .module_dimensions(1, 1)
226 .build();
227 assert!(image.contains("0;51;102"));
229 assert!(image.contains("224;224;224"));
230 }
231
232 #[test]
233 fn test_ansi_color_optimization() {
234 let colors = vec![ModuleColor::Dark; 16]; let image: String = Renderer::<Color>::new(&colors, 4, 0).module_dimensions(1, 1).build();
237 let lines: Vec<&str> = image.split('\n').collect();
238 assert_eq!(lines.len(), 2);
239 for line in &lines {
241 let esc_count = line.matches("\x1b[").count();
242 assert_eq!(esc_count, 3);
243 }
244 }
245}