Skip to main content

busybar_render/
lib.rs

1use std::fmt;
2use std::io::Cursor;
3use std::path::Path;
4
5use base64::Engine as _;
6use base64::engine::general_purpose::STANDARD;
7use image::{DynamicImage, GrayImage, RgbImage};
8
9#[derive(Debug, thiserror::Error)]
10pub enum RenderError {
11    #[error("{width}x{height} in {layout} needs {expected} bytes, but {actual} were given")]
12    UnexpectedSize {
13        width: u32,
14        height: u32,
15        layout: PixelLayout,
16        expected: usize,
17        actual: usize,
18    },
19
20    #[error("a {width}x{height} image does not fit a raster of {pixel} with a {gap} gap")]
21    RasterTooLarge {
22        width: u32,
23        height: u32,
24        pixel: u32,
25        gap: u32,
26    },
27
28    #[error("could not encode a {width}x{height} image as {format}")]
29    Encode {
30        width: u32,
31        height: u32,
32        format: ImageFormat,
33        #[source]
34        source: image::ImageError,
35    },
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ImageFormat {
40    Bmp,
41    Jpeg,
42    Png,
43}
44
45impl ImageFormat {
46    pub fn from_path(path: &Path) -> Option<Self> {
47        let extension = path.extension()?.to_str()?.to_ascii_lowercase();
48
49        match extension.as_str() {
50            "bmp" => Some(Self::Bmp),
51            "jpg" | "jpeg" => Some(Self::Jpeg),
52            "png" => Some(Self::Png),
53            _ => None,
54        }
55    }
56
57    fn encoding(self) -> image::ImageFormat {
58        match self {
59            Self::Bmp => image::ImageFormat::Bmp,
60            Self::Jpeg => image::ImageFormat::Jpeg,
61            Self::Png => image::ImageFormat::Png,
62        }
63    }
64}
65
66impl fmt::Display for ImageFormat {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Bmp => f.write_str("bmp"),
70            Self::Jpeg => f.write_str("jpeg"),
71            Self::Png => f.write_str("png"),
72        }
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Raster {
78    pixel: u32,
79    gap: u32,
80}
81
82impl Raster {
83    pub fn new(pixel: u32, gap: u32) -> Option<Self> {
84        (pixel > 0).then_some(Self { pixel, gap })
85    }
86
87    pub fn pixel(self) -> u32 {
88        self.pixel
89    }
90
91    pub fn gap(self) -> u32 {
92        self.gap
93    }
94
95    fn extent(self, count: u32) -> Option<u32> {
96        let cells = count.checked_mul(self.pixel)?;
97        let gaps = count.saturating_sub(1).checked_mul(self.gap)?;
98
99        cells.checked_add(gaps)
100    }
101}
102
103impl Default for Raster {
104    fn default() -> Self {
105        Self { pixel: 3, gap: 1 }
106    }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum PixelLayout {
111    Rgb888,
112    Bgr888,
113    Gray8,
114    Gray4,
115}
116
117impl PixelLayout {
118    fn byte_len(self, width: u32, height: u32) -> usize {
119        let pixels = (width as usize) * (height as usize);
120
121        match self {
122            Self::Rgb888 | Self::Bgr888 => pixels * 3,
123            Self::Gray8 => pixels,
124            Self::Gray4 => pixels.div_ceil(2),
125        }
126    }
127}
128
129impl fmt::Display for PixelLayout {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Self::Rgb888 => f.write_str("rgb888"),
133            Self::Bgr888 => f.write_str("bgr888"),
134            Self::Gray8 => f.write_str("l8"),
135            Self::Gray4 => f.write_str("l4"),
136        }
137    }
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub struct RawImage {
142    width: u32,
143    height: u32,
144    image: DynamicImage,
145}
146
147impl RawImage {
148    pub fn new(
149        width: u32,
150        height: u32,
151        layout: PixelLayout,
152        pixels: &[u8],
153    ) -> Result<Self, RenderError> {
154        let expected = layout.byte_len(width, height);
155
156        if pixels.len() != expected {
157            return Err(RenderError::UnexpectedSize {
158                width,
159                height,
160                layout,
161                expected,
162                actual: pixels.len(),
163            });
164        }
165
166        let image = match layout {
167            PixelLayout::Rgb888 => DynamicImage::ImageRgb8(
168                RgbImage::from_raw(width, height, pixels.to_vec()).expect("checked above"),
169            ),
170            PixelLayout::Bgr888 => {
171                let mut pixels = pixels.to_vec();
172
173                // The device says it outputs RGB but it really doesn't, so we swap
174                for pixel in pixels.chunks_exact_mut(3) {
175                    pixel.swap(0, 2);
176                }
177
178                DynamicImage::ImageRgb8(
179                    RgbImage::from_raw(width, height, pixels).expect("checked above"),
180                )
181            }
182            PixelLayout::Gray8 => DynamicImage::ImageLuma8(
183                GrayImage::from_raw(width, height, pixels.to_vec()).expect("checked above"),
184            ),
185            PixelLayout::Gray4 => {
186                let mut expanded = Vec::with_capacity(pixels.len() * 2);
187
188                for byte in pixels {
189                    expanded.push((byte >> 4) * 17);
190                    expanded.push((byte & 0x0F) * 17);
191                }
192
193                expanded.truncate((width as usize) * (height as usize));
194
195                DynamicImage::ImageLuma8(
196                    GrayImage::from_raw(width, height, expanded).expect("checked above"),
197                )
198            }
199        };
200
201        Ok(Self {
202            width,
203            height,
204            image,
205        })
206    }
207
208    pub fn width(&self) -> u32 {
209        self.width
210    }
211
212    pub fn height(&self) -> u32 {
213        self.height
214    }
215
216    pub fn with_raster(&self, raster: Raster) -> Result<Self, RenderError> {
217        let too_large = || RenderError::RasterTooLarge {
218            width: self.width,
219            height: self.height,
220            pixel: raster.pixel,
221            gap: raster.gap,
222        };
223
224        let width = raster.extent(self.width).ok_or_else(too_large)?;
225        let height = raster.extent(self.height).ok_or_else(too_large)?;
226
227        let step = raster.pixel + raster.gap;
228
229        let image = match &self.image {
230            DynamicImage::ImageLuma8(source) => {
231                let mut target = GrayImage::new(width, height);
232                self::paint(source, &mut target, step, raster.pixel);
233                DynamicImage::ImageLuma8(target)
234            }
235            source => {
236                let source = source.to_rgb8();
237                let mut target = RgbImage::new(width, height);
238                self::paint(&source, &mut target, step, raster.pixel);
239                DynamicImage::ImageRgb8(target)
240            }
241        };
242
243        Ok(Self {
244            width,
245            height,
246            image,
247        })
248    }
249
250    pub fn encode(&self, format: ImageFormat) -> Result<Vec<u8>, RenderError> {
251        let mut buffer = Cursor::new(Vec::new());
252
253        self.image
254            .write_to(&mut buffer, format.encoding())
255            .map_err(|source| RenderError::Encode {
256                width: self.width,
257                height: self.height,
258                format,
259                source,
260            })?;
261
262        Ok(buffer.into_inner())
263    }
264
265    pub fn encode_base64(&self, format: ImageFormat) -> Result<String, RenderError> {
266        self.encode(format).map(|bytes| STANDARD.encode(bytes))
267    }
268}
269
270fn paint<P: image::Pixel<Subpixel=u8>>(
271    source: &image::ImageBuffer<P, Vec<u8>>,
272    target: &mut image::ImageBuffer<P, Vec<u8>>,
273    step: u32,
274    size: u32,
275) {
276    for (x, y, pixel) in source.enumerate_pixels() {
277        for offset_y in 0..size {
278            for offset_x in 0..size {
279                target.put_pixel(x * step + offset_x, y * step + offset_y, *pixel);
280            }
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn picks_a_format_from_the_file_extension() {
291        assert_eq!(
292            ImageFormat::from_path(Path::new("frame.bmp")),
293            Some(ImageFormat::Bmp)
294        );
295        assert_eq!(
296            ImageFormat::from_path(Path::new("frame.JPEG")),
297            Some(ImageFormat::Jpeg)
298        );
299        assert_eq!(
300            ImageFormat::from_path(Path::new("./out/frame.png")),
301            Some(ImageFormat::Png)
302        );
303        assert_eq!(ImageFormat::from_path(Path::new("frame.raw")), None);
304        assert_eq!(ImageFormat::from_path(Path::new("frame")), None);
305    }
306
307    #[test]
308    fn reads_rgb_in_the_order_it_is_given() {
309        let image = RawImage::new(1, 1, PixelLayout::Rgb888, &[0x11, 0x22, 0x33]).unwrap();
310
311        assert_eq!(
312            image.image.as_rgb8().unwrap().get_pixel(0, 0).0,
313            [0x11, 0x22, 0x33]
314        );
315    }
316
317    #[test]
318    fn swaps_the_channels_of_a_bgr_buffer() {
319        let image = RawImage::new(1, 1, PixelLayout::Bgr888, &[0x11, 0x22, 0x33]).unwrap();
320
321        assert_eq!(
322            image.image.as_rgb8().unwrap().get_pixel(0, 0).0,
323            [0x33, 0x22, 0x11]
324        );
325    }
326
327    #[test]
328    fn expands_four_bit_grayscale_to_eight() {
329        let image = RawImage::new(2, 1, PixelLayout::Gray4, &[0xf0]).unwrap();
330        let luma = image.image.as_luma8().unwrap();
331
332        assert_eq!(luma.get_pixel(0, 0).0, [0xff]);
333        assert_eq!(luma.get_pixel(1, 0).0, [0x00]);
334    }
335
336    #[test]
337    fn rejects_a_buffer_which_does_not_match_the_geometry() {
338        let error = RawImage::new(72, 16, PixelLayout::Rgb888, &[0; 16]).unwrap_err();
339
340        assert_eq!(
341            error.to_string(),
342            "72x16 in rgb888 needs 3456 bytes, but 16 were given"
343        );
344    }
345
346    #[test]
347    fn encodes_every_format_it_supports() {
348        let image = RawImage::new(2, 2, PixelLayout::Gray8, &[0, 64, 128, 255]).unwrap();
349
350        assert!(image.encode(ImageFormat::Bmp).unwrap().starts_with(b"BM"));
351        assert!(
352            image
353                .encode(ImageFormat::Jpeg)
354                .unwrap()
355                .starts_with(&[0xff, 0xd8])
356        );
357        assert!(
358            image
359                .encode(ImageFormat::Png)
360                .unwrap()
361                .starts_with(b"\x89PNG")
362        );
363    }
364
365    #[test]
366    fn a_raster_spaces_the_pixels_out_and_leaves_black_between_them() {
367        let image = RawImage::new(
368            2,
369            2,
370            PixelLayout::Rgb888,
371            &[
372                0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
373            ],
374        )
375            .unwrap();
376
377        let raster = Raster::new(3, 1).unwrap();
378        let rastered = image.with_raster(raster).unwrap();
379
380        assert_eq!((rastered.width(), rastered.height()), (7, 7));
381
382        let pixels = rastered.image.to_rgb8();
383
384        assert_eq!(pixels.get_pixel(0, 0).0, [0xff, 0x00, 0x00]);
385        assert_eq!(pixels.get_pixel(2, 2).0, [0xff, 0x00, 0x00]);
386        assert_eq!(pixels.get_pixel(3, 0).0, [0x00, 0x00, 0x00]);
387        assert_eq!(pixels.get_pixel(0, 3).0, [0x00, 0x00, 0x00]);
388        assert_eq!(pixels.get_pixel(4, 0).0, [0x00, 0xff, 0x00]);
389        assert_eq!(pixels.get_pixel(4, 4).0, [0xff, 0xff, 0xff]);
390    }
391
392    #[test]
393    fn a_raster_keeps_a_grayscale_image_grayscale() {
394        let image = RawImage::new(2, 1, PixelLayout::Gray8, &[0xff, 0x40]).unwrap();
395        let rastered = image.with_raster(Raster::default()).unwrap();
396
397        assert_eq!((rastered.width(), rastered.height()), (7, 3));
398
399        let pixels = rastered.image.as_luma8().expect("stays grayscale");
400
401        assert_eq!(pixels.get_pixel(0, 0).0, [0xff]);
402        assert_eq!(pixels.get_pixel(3, 0).0, [0x00]);
403        assert_eq!(pixels.get_pixel(4, 0).0, [0x40]);
404    }
405
406    #[test]
407    fn a_raster_needs_a_pixel_size() {
408        assert!(Raster::new(0, 1).is_none());
409        assert!(Raster::new(1, 0).is_some());
410    }
411
412    #[test]
413    fn encodes_to_base64() {
414        let image = RawImage::new(1, 1, PixelLayout::Gray8, &[0x7f]).unwrap();
415        let encoded = image.encode_base64(ImageFormat::Png).unwrap();
416
417        assert_eq!(
418            STANDARD.decode(&encoded).unwrap(),
419            image.encode(ImageFormat::Png).unwrap()
420        );
421    }
422}