1use rich::{Color, Segment, Style};
7
8type Rgb = (u8, u8, u8);
10
11#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct Pixels {
15 pub width: u32,
16 pub height: u32,
17 pub rgb: Vec<u8>,
18}
19
20impl Pixels {
21 pub fn new(width: u32, height: u32, rgb: Vec<u8>) -> Option<Pixels> {
24 (rgb.len() as u64 == width as u64 * height as u64 * 3).then_some(Pixels {
25 width,
26 height,
27 rgb,
28 })
29 }
30
31 pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> Option<Pixels> {
34 let x1 = x.saturating_add(width).min(self.width);
35 let y1 = y.saturating_add(height).min(self.height);
36 if x >= x1 || y >= y1 {
37 return None;
38 }
39 let mut rgb = Vec::with_capacity(((x1 - x) * (y1 - y) * 3) as usize);
40 for row in y..y1 {
41 let start = ((row * self.width + x) * 3) as usize;
42 let end = ((row * self.width + x1) * 3) as usize;
43 rgb.extend_from_slice(&self.rgb[start..end]);
44 }
45 Pixels::new(x1 - x, y1 - y, rgb)
46 }
47
48 pub fn to_rgba(&self) -> Vec<u8> {
51 let mut out = Vec::with_capacity(self.rgb.len() / 3 * 4);
52 for pixel in self.rgb.as_chunks::<3>().0 {
53 out.extend_from_slice(pixel);
54 out.push(255);
55 }
56 out
57 }
58
59 fn average(&self, x0: u32, x1: u32, y0: u32, y1: u32) -> (u8, u8, u8) {
61 let mut sum = [0u64; 3];
62 let mut n = 0u64;
63 for y in y0..y1 {
64 let row = (y * self.width) as usize * 3;
65 for x in x0..x1 {
66 let at = row + x as usize * 3;
67 sum[0] += self.rgb[at] as u64;
68 sum[1] += self.rgb[at + 1] as u64;
69 sum[2] += self.rgb[at + 2] as u64;
70 n += 1;
71 }
72 }
73 let n = n.max(1);
74 ((sum[0] / n) as u8, (sum[1] / n) as u8, (sum[2] / n) as u8)
75 }
76}
77
78fn span(i: u32, count: u32, size: u32) -> (u32, u32) {
81 let start = (i as u64 * size as u64 / count as u64) as u32;
82 let end = ((i as u64 + 1) * size as u64 / count as u64) as u32;
83 let start = start.min(size.saturating_sub(1));
84 (start, end.max(start + 1).min(size))
85}
86
87pub fn half_blocks(pixels: &Pixels, columns: u16, rows: u16) -> Vec<Vec<Segment>> {
89 if pixels.width == 0 || pixels.height == 0 || columns == 0 || rows == 0 {
90 return Vec::new();
91 }
92 let (columns, rows) = (columns as u32, rows as u32);
93 let xs: Vec<(u32, u32)> = (0..columns)
94 .map(|x| span(x, columns, pixels.width))
95 .collect();
96 let mut lines = Vec::with_capacity(rows as usize);
97 for row in 0..rows {
98 let (top0, top1) = span(row * 2, rows * 2, pixels.height);
99 let (bottom0, bottom1) = span(row * 2 + 1, rows * 2, pixels.height);
100 let mut line: Vec<Segment> = Vec::new();
101 let mut run = 0usize;
102 let mut current: Option<(Rgb, Rgb)> = None;
103 for &(x0, x1) in &xs {
104 let colours = (
105 pixels.average(x0, x1, top0, top1),
106 pixels.average(x0, x1, bottom0, bottom1),
107 );
108 if current.is_some_and(|c| c != colours) {
109 line.push(block(run, current.expect("colours")));
110 run = 0;
111 }
112 current = Some(colours);
113 run += 1;
114 }
115 if let Some(colours) = current {
116 line.push(block(run, colours));
117 }
118 lines.push(line);
119 }
120 lines
121}
122
123fn block(run: usize, (top, bottom): (Rgb, Rgb)) -> Segment {
124 let style = Style::from_color(
125 Some(Color::from_rgb(top.0, top.1, top.2)),
126 Some(Color::from_rgb(bottom.0, bottom.1, bottom.2)),
127 );
128 Segment::new("▀".repeat(run), Some(style))
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn each_cell_is_two_pixels_averaged_down() {
137 let mut rgb = Vec::new();
139 for row in [[255, 0, 0, 255, 255, 255], [0, 0, 255, 0, 0, 0]] {
140 for half in [&row[..3], &row[..3], &row[3..], &row[3..]] {
141 rgb.extend_from_slice(half);
142 }
143 }
144 let pixels = Pixels::new(4, 2, rgb).unwrap();
145 let lines = half_blocks(&pixels, 2, 1);
146 assert_eq!(lines.len(), 1);
147 assert_eq!(lines[0].len(), 2);
148 assert_eq!(lines[0][0].text, "▀");
149 let style = lines[0][0].style.as_ref().unwrap();
150 assert_eq!(style.color(), Some(&Color::from_rgb(255, 0, 0)));
151 assert_eq!(style.bgcolor(), Some(&Color::from_rgb(0, 0, 255)));
152 let wide = half_blocks(&pixels, 8, 3);
154 assert_eq!(wide.len(), 3);
155 assert_eq!(wide[0][0].text, "▀▀▀▀");
156 assert!(Pixels::new(2, 2, vec![0; 3]).is_none());
157 }
158
159 #[test]
160 fn crops_clip_to_the_frame() {
161 let rgb = (0..6u8).flat_map(|i| [i, 0, 0]).collect();
163 let pixels = Pixels::new(3, 2, rgb).unwrap();
164 let part = pixels.crop(1, 0, 5, 1).unwrap();
165 assert_eq!((part.width, part.height), (2, 1));
166 assert_eq!(part.rgb, vec![1, 0, 0, 2, 0, 0]);
167 let lower = pixels.crop(0, 1, 1, 9).unwrap();
168 assert_eq!(lower.rgb, vec![3, 0, 0]);
169 assert!(pixels.crop(3, 0, 1, 1).is_none());
170 assert_eq!(part.to_rgba(), vec![1, 0, 0, 255, 2, 0, 0, 255]);
171 }
172}