use crate::render::{Canvas as RenderCanvas, Pixel};
use crate::types::Color as ModuleColor;
#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Color {
r: u8,
g: u8,
b: u8,
}
impl Color {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
fn fg_ansi(self) -> String {
format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
}
fn bg_ansi(self) -> String {
format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
}
}
impl Pixel for Color {
type Image = String;
type Canvas = CanvasAnsi;
fn default_unit_size() -> (u32, u32) {
(1, 1)
}
fn default_color(color: ModuleColor) -> Self {
match color {
ModuleColor::Dark => Color::new(0, 0, 0),
ModuleColor::Light => Color::new(255, 255, 255),
}
}
}
pub struct CanvasAnsi {
canvas: Vec<u8>,
width: u32,
dark_pixel: u8,
dark_color: Color,
light_color: Color,
}
impl RenderCanvas for CanvasAnsi {
type Pixel = Color;
type Image = String;
fn new(width: u32, height: u32, dark_pixel: Color, light_pixel: Color) -> Self {
CanvasAnsi {
canvas: vec![0u8; (width * height) as usize],
width,
dark_pixel: 1,
dark_color: dark_pixel,
light_color: light_pixel,
}
}
fn draw_dark_pixel(&mut self, x: u32, y: u32) {
self.canvas[(x + y * self.width) as usize] = self.dark_pixel;
}
fn into_image(self) -> String {
let w = self.width as usize;
let dark = 1u8;
let reset = "\x1b[0m";
self.canvas
.chunks_exact(w)
.collect::<Vec<&[u8]>>()
.chunks(2)
.map(|rows| {
let top_row = rows[0];
let bot_row = rows.get(1).map_or(&[][..], |r| *r);
let mut line = String::with_capacity(w * 40);
let mut last_fg = None;
let mut last_bg = None;
for col in 0..w {
let top = top_row.get(col).copied().unwrap_or(0);
let bot = bot_row.get(col).copied().unwrap_or(0);
let (fg, bg) = if top == dark && bot == dark {
(self.dark_color, self.dark_color)
} else if top == dark && bot != dark {
(self.dark_color, self.light_color)
} else if top != dark && bot == dark {
(self.light_color, self.dark_color)
} else {
(self.light_color, self.light_color)
};
if last_bg != Some(bg) {
line.push_str(&bg.bg_ansi());
last_bg = Some(bg);
}
if last_fg != Some(fg) {
line.push_str(&fg.fg_ansi());
last_fg = Some(fg);
}
if top == dark && bot == dark {
line.push('█');
} else if top == dark {
line.push('▀');
} else if bot == dark {
line.push('▄');
} else {
line.push(' ');
}
}
line.push_str(reset);
line
})
.collect::<Vec<String>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::Renderer;
#[test]
fn test_ansi_all_dark() {
let colors = vec![ModuleColor::Dark; 4];
let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
assert!(image.contains('█'));
assert!(image.contains("\x1b["));
assert!(image.contains("\x1b[0m"));
}
#[test]
fn test_ansi_all_light() {
let colors = vec![ModuleColor::Light; 4];
let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
assert!(image.contains(' '));
assert!(image.contains("\x1b[0m"));
}
#[test]
fn test_ansi_mixed() {
let colors = vec![ModuleColor::Dark, ModuleColor::Light, ModuleColor::Light, ModuleColor::Dark];
let image: String = Renderer::<Color>::new(&colors, 2, 0).module_dimensions(1, 1).build();
assert!(image.contains('▀'));
}
#[test]
fn test_ansi_custom_colors() {
let code = crate::QrCode::new(b"Hi").unwrap();
let image = code
.render::<Color>()
.dark_color(Color::new(0, 51, 102))
.light_color(Color::new(224, 224, 224))
.module_dimensions(1, 1)
.build();
assert!(image.contains("0;51;102"));
assert!(image.contains("224;224;224"));
}
#[test]
fn test_ansi_color_optimization() {
let colors = vec![ModuleColor::Dark; 16]; let image: String = Renderer::<Color>::new(&colors, 4, 0).module_dimensions(1, 1).build();
let lines: Vec<&str> = image.split('\n').collect();
assert_eq!(lines.len(), 2);
for line in &lines {
let esc_count = line.matches("\x1b[").count();
assert_eq!(esc_count, 3);
}
}
}