Skip to main content

rich_embed/
pixels.rs

1//! Pixel frames drawn in cells: each cell is an upper half block (`▀`)
2//! whose foreground is the upper pixel and whose background the lower one,
3//! so a pane of `columns` x `rows` cells shows `columns` x `2 * rows`
4//! pixels, averaged down from the frame.
5
6use rich::{Color, Segment, Style};
7
8/// A pixel's red, green and blue.
9type Rgb = (u8, u8, u8);
10
11/// A frame of pixels: `width` x `height`, three bytes (red, green, blue)
12/// per pixel, rows top to bottom.
13#[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    /// A frame from its RGB bytes. `None` when there are not
22    /// `width * height * 3` of them.
23    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    /// The `width` x `height` pixels from (`x`, `y`), clipped to the
32    /// frame. `None` when nothing of it is in the frame.
33    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    /// The pixels as red, green, blue and alpha (opaque), for encoders that
49    /// take RGBA.
50    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    /// The average colour of the pixels in `x0..x1` x `y0..y1`.
60    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
78/// The part of `0..size` that output cell `i` of `count` covers: at least
79/// one pixel.
80fn 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
87/// `pixels` scaled to `columns` x `rows` cells of half blocks.
88pub 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        // 4 x 2: the left half red over blue, the right half white over black.
138        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        // Scaled up, neighbours of one colour merge into one run.
153        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        // 3 x 2, each pixel's red its index.
162        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}