Skip to main content

busybar_render/
raw.rs

1use std::fmt;
2
3use image::{DynamicImage, GrayImage, RgbImage};
4
5use crate::error::RenderError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct Raster {
9    pixel: u32,
10    gap: u32,
11}
12
13impl Raster {
14    pub const fn new(pixel: u32, gap: u32) -> Option<Self> {
15        if pixel == 0 {
16            return None;
17        }
18
19        Some(Self { pixel, gap })
20    }
21
22    pub fn pixel(self) -> u32 {
23        self.pixel
24    }
25
26    pub fn gap(self) -> u32 {
27        self.gap
28    }
29
30    fn extent(self, count: u32) -> Option<u32> {
31        let cells = count.checked_mul(self.pixel)?;
32        let gaps = count.saturating_sub(1).checked_mul(self.gap)?;
33
34        cells.checked_add(gaps)
35    }
36}
37
38impl Default for Raster {
39    fn default() -> Self {
40        Self { pixel: 3, gap: 1 }
41    }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum PixelLayout {
46    Rgb888,
47    Bgr888,
48    Gray8,
49    Gray4,
50}
51
52impl PixelLayout {
53    fn byte_len(self, width: u32, height: u32) -> usize {
54        let pixels = (width as usize) * (height as usize);
55
56        match self {
57            Self::Rgb888 | Self::Bgr888 => pixels * 3,
58            Self::Gray8 => pixels,
59            Self::Gray4 => pixels.div_ceil(2),
60        }
61    }
62}
63
64impl fmt::Display for PixelLayout {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::Rgb888 => f.write_str("rgb888"),
68            Self::Bgr888 => f.write_str("bgr888"),
69            Self::Gray8 => f.write_str("l8"),
70            Self::Gray4 => f.write_str("l4"),
71        }
72    }
73}
74
75#[derive(Debug, Clone, PartialEq)]
76pub struct RawImage {
77    width: u32,
78    height: u32,
79    image: DynamicImage,
80}
81
82impl RawImage {
83    pub fn new(
84        width: u32,
85        height: u32,
86        layout: PixelLayout,
87        pixels: &[u8],
88    ) -> Result<Self, RenderError> {
89        let expected = layout.byte_len(width, height);
90
91        if pixels.len() != expected {
92            return Err(RenderError::UnexpectedSize {
93                width,
94                height,
95                layout,
96                expected,
97                actual: pixels.len(),
98            });
99        }
100
101        let image = match layout {
102            PixelLayout::Rgb888 => DynamicImage::ImageRgb8(
103                RgbImage::from_raw(width, height, pixels.to_vec()).expect("checked above"),
104            ),
105            PixelLayout::Bgr888 => {
106                let mut pixels = pixels.to_vec();
107
108                // The device says it outputs RGB but it really doesn't, so we swap
109                for pixel in pixels.chunks_exact_mut(3) {
110                    pixel.swap(0, 2);
111                }
112
113                DynamicImage::ImageRgb8(
114                    RgbImage::from_raw(width, height, pixels).expect("checked above"),
115                )
116            }
117            PixelLayout::Gray8 => DynamicImage::ImageLuma8(
118                GrayImage::from_raw(width, height, pixels.to_vec()).expect("checked above"),
119            ),
120            PixelLayout::Gray4 => {
121                let mut expanded = Vec::with_capacity(pixels.len() * 2);
122
123                for byte in pixels {
124                    expanded.push((byte >> 4) * 17);
125                    expanded.push((byte & 0x0F) * 17);
126                }
127
128                expanded.truncate((width as usize) * (height as usize));
129
130                DynamicImage::ImageLuma8(
131                    GrayImage::from_raw(width, height, expanded).expect("checked above"),
132                )
133            }
134        };
135
136        Ok(Self {
137            width,
138            height,
139            image,
140        })
141    }
142
143    pub fn width(&self) -> u32 {
144        self.width
145    }
146
147    pub fn height(&self) -> u32 {
148        self.height
149    }
150
151    pub(crate) fn buffer(&self) -> &DynamicImage {
152        &self.image
153    }
154
155    pub fn with_raster(&self, raster: Raster) -> Result<Self, RenderError> {
156        let too_large = || RenderError::RasterTooLarge {
157            width: self.width,
158            height: self.height,
159            pixel: raster.pixel,
160            gap: raster.gap,
161        };
162
163        let width = raster.extent(self.width).ok_or_else(too_large)?;
164        let height = raster.extent(self.height).ok_or_else(too_large)?;
165
166        let step = raster.pixel + raster.gap;
167
168        let image = match &self.image {
169            DynamicImage::ImageLuma8(source) => {
170                let mut target = GrayImage::new(width, height);
171                paint(source, &mut target, step, raster.pixel);
172                DynamicImage::ImageLuma8(target)
173            }
174            source => {
175                let source = source.to_rgb8();
176                let mut target = RgbImage::new(width, height);
177                paint(&source, &mut target, step, raster.pixel);
178                DynamicImage::ImageRgb8(target)
179            }
180        };
181
182        Ok(Self {
183            width,
184            height,
185            image,
186        })
187    }
188}
189
190fn paint<P: image::Pixel<Subpixel = u8>>(
191    source: &image::ImageBuffer<P, Vec<u8>>,
192    target: &mut image::ImageBuffer<P, Vec<u8>>,
193    step: u32,
194    size: u32,
195) {
196    for (x, y, pixel) in source.enumerate_pixels() {
197        for offset_y in 0..size {
198            for offset_x in 0..size {
199                target.put_pixel(x * step + offset_x, y * step + offset_y, *pixel);
200            }
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn reads_rgb_in_the_order_it_is_given() {
211        let image = RawImage::new(1, 1, PixelLayout::Rgb888, &[0x11, 0x22, 0x33]).unwrap();
212
213        assert_eq!(
214            image.buffer().as_rgb8().unwrap().get_pixel(0, 0).0,
215            [0x11, 0x22, 0x33]
216        );
217    }
218
219    #[test]
220    fn swaps_the_channels_of_a_bgr_buffer() {
221        let image = RawImage::new(1, 1, PixelLayout::Bgr888, &[0x11, 0x22, 0x33]).unwrap();
222
223        assert_eq!(
224            image.buffer().as_rgb8().unwrap().get_pixel(0, 0).0,
225            [0x33, 0x22, 0x11]
226        );
227    }
228
229    #[test]
230    fn expands_four_bit_grayscale_to_eight() {
231        let image = RawImage::new(2, 1, PixelLayout::Gray4, &[0xf0]).unwrap();
232        let luma = image.buffer().as_luma8().unwrap();
233
234        assert_eq!(luma.get_pixel(0, 0).0, [0xff]);
235        assert_eq!(luma.get_pixel(1, 0).0, [0x00]);
236    }
237
238    #[test]
239    fn rejects_a_buffer_which_does_not_match_the_geometry() {
240        let error = RawImage::new(72, 16, PixelLayout::Rgb888, &[0; 16]).unwrap_err();
241
242        assert_eq!(
243            error.to_string(),
244            "72x16 in rgb888 needs 3456 bytes, but 16 were given"
245        );
246    }
247
248    #[test]
249    fn a_raster_spaces_the_pixels_out_and_leaves_black_between_them() {
250        let image = RawImage::new(
251            2,
252            2,
253            PixelLayout::Rgb888,
254            &[
255                0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
256            ],
257        )
258        .unwrap();
259
260        let raster = Raster::new(3, 1).unwrap();
261        let rastered = image.with_raster(raster).unwrap();
262
263        assert_eq!((rastered.width(), rastered.height()), (7, 7));
264
265        let pixels = rastered.buffer().to_rgb8();
266
267        assert_eq!(pixels.get_pixel(0, 0).0, [0xff, 0x00, 0x00]);
268        assert_eq!(pixels.get_pixel(2, 2).0, [0xff, 0x00, 0x00]);
269        assert_eq!(pixels.get_pixel(3, 0).0, [0x00, 0x00, 0x00]);
270        assert_eq!(pixels.get_pixel(0, 3).0, [0x00, 0x00, 0x00]);
271        assert_eq!(pixels.get_pixel(4, 0).0, [0x00, 0xff, 0x00]);
272        assert_eq!(pixels.get_pixel(4, 4).0, [0xff, 0xff, 0xff]);
273    }
274
275    #[test]
276    fn a_raster_keeps_a_grayscale_image_grayscale() {
277        let image = RawImage::new(2, 1, PixelLayout::Gray8, &[0xff, 0x40]).unwrap();
278        let rastered = image.with_raster(Raster::default()).unwrap();
279
280        assert_eq!((rastered.width(), rastered.height()), (7, 3));
281
282        let pixels = rastered.buffer().as_luma8().expect("stays grayscale");
283
284        assert_eq!(pixels.get_pixel(0, 0).0, [0xff]);
285        assert_eq!(pixels.get_pixel(3, 0).0, [0x00]);
286        assert_eq!(pixels.get_pixel(4, 0).0, [0x40]);
287    }
288
289    #[test]
290    fn a_raster_needs_a_pixel_size() {
291        assert!(Raster::new(0, 1).is_none());
292        assert!(Raster::new(1, 0).is_some());
293    }
294}