busybar-render 0.0.11

Re-encodes BUSY Bar screen frames into more common image formats
Documentation
use std::fmt;
use std::io::Cursor;
use std::path::Path;

use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;

use crate::error::RenderError;
use crate::raw::RawImage;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFormat {
    Bmp,
    Jpeg,
    Png,
}

impl ImageFormat {
    pub fn from_path(path: &Path) -> Option<Self> {
        let extension = path.extension()?.to_str()?.to_ascii_lowercase();

        match extension.as_str() {
            "bmp" => Some(Self::Bmp),
            "jpg" | "jpeg" => Some(Self::Jpeg),
            "png" => Some(Self::Png),
            _ => None,
        }
    }

    fn encoding(self) -> image::ImageFormat {
        match self {
            Self::Bmp => image::ImageFormat::Bmp,
            Self::Jpeg => image::ImageFormat::Jpeg,
            Self::Png => image::ImageFormat::Png,
        }
    }
}

impl fmt::Display for ImageFormat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Bmp => f.write_str("bmp"),
            Self::Jpeg => f.write_str("jpeg"),
            Self::Png => f.write_str("png"),
        }
    }
}

impl RawImage {
    pub fn encode(&self, format: ImageFormat) -> Result<Vec<u8>, RenderError> {
        let mut buffer = Cursor::new(Vec::new());

        self.buffer()
            .write_to(&mut buffer, format.encoding())
            .map_err(|source| RenderError::Encode {
                width: self.width(),
                height: self.height(),
                format,
                source,
            })?;

        Ok(buffer.into_inner())
    }

    pub fn encode_base64(&self, format: ImageFormat) -> Result<String, RenderError> {
        self.encode(format).map(|bytes| STANDARD.encode(bytes))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::raw::PixelLayout;

    #[test]
    fn picks_a_format_from_the_file_extension() {
        assert_eq!(
            ImageFormat::from_path(Path::new("frame.bmp")),
            Some(ImageFormat::Bmp)
        );
        assert_eq!(
            ImageFormat::from_path(Path::new("frame.JPEG")),
            Some(ImageFormat::Jpeg)
        );
        assert_eq!(
            ImageFormat::from_path(Path::new("./out/frame.png")),
            Some(ImageFormat::Png)
        );
        assert_eq!(ImageFormat::from_path(Path::new("frame.raw")), None);
        assert_eq!(ImageFormat::from_path(Path::new("frame")), None);
    }

    #[test]
    fn encodes_every_format_it_supports() {
        let image = RawImage::new(2, 2, PixelLayout::Gray8, &[0, 64, 128, 255]).unwrap();

        assert!(image.encode(ImageFormat::Bmp).unwrap().starts_with(b"BM"));
        assert!(
            image
                .encode(ImageFormat::Jpeg)
                .unwrap()
                .starts_with(&[0xff, 0xd8])
        );
        assert!(
            image
                .encode(ImageFormat::Png)
                .unwrap()
                .starts_with(b"\x89PNG")
        );
    }

    #[test]
    fn encodes_to_base64() {
        let image = RawImage::new(1, 1, PixelLayout::Gray8, &[0x7f]).unwrap();
        let encoded = image.encode_base64(ImageFormat::Png).unwrap();

        assert_eq!(
            STANDARD.decode(&encoded).unwrap(),
            image.encode(ImageFormat::Png).unwrap()
        );
    }
}