pak 0.7.4

An easy-to-use data pak format for games.
Documentation
use serde::{Deserialize, Deserializer, Serialize, de::Error};

/// Holds a `Bitmap` in a `.pak` file. For data transport only.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct Bitmap {
    color: BitmapColor,
    fmt: BitmapFormat,
    mip_levels: u32,

    #[serde(with = "serde_bytes")]
    pixels: Vec<u8>,

    width: u32,
}

impl<'de> Deserialize<'de> for Bitmap {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct BitmapData {
            color: BitmapColor,
            fmt: BitmapFormat,
            mip_levels: u32,

            #[serde(with = "serde_bytes")]
            pixels: Vec<u8>,

            width: u32,
        }

        let data = BitmapData::deserialize(deserializer)?;
        if data.width == 0 {
            return Err(D::Error::custom("bitmap width must be greater than zero"));
        }

        let row_len = data.width as usize * data.fmt.byte_len();
        if !data.pixels.len().is_multiple_of(row_len) {
            return Err(D::Error::custom(
                "bitmap pixel byte length is not a whole number of rows",
            ));
        }

        Ok(Self {
            color: data.color,
            fmt: data.fmt,
            mip_levels: data.mip_levels,
            pixels: data.pixels,
            width: data.width,
        })
    }
}

impl Bitmap {
    /// Pixel data must be tightly packed (no additional stride)
    pub fn new(
        color: BitmapColor,
        fmt: BitmapFormat,
        width: u32,
        mip_levels: u32,
        pixels: impl Into<Vec<u8>>,
    ) -> Self {
        let pixels = pixels.into();
        assert!(width > 0);
        assert_eq!(pixels.len() % (width as usize * fmt.byte_len()), 0);

        Self {
            color,
            fmt,
            mip_levels,
            pixels,
            width,
        }
    }

    pub fn color(&self) -> BitmapColor {
        self.color
    }

    /// Gets the dimensions, in pixels, of this `Bitmap`.
    pub fn extent(&self) -> (u32, u32) {
        (self.width(), self.height())
    }

    // TODO: Maybe better naming.. Channels?
    /// Gets a description of the number of channels contained in this `Bitmap`.
    pub fn format(&self) -> BitmapFormat {
        self.fmt
    }

    pub fn height(&self) -> u32 {
        let len = self.pixels.len() as u32;
        let byte_height = len / self.width;

        match self.fmt {
            BitmapFormat::R => byte_height,
            BitmapFormat::Rg => byte_height / 2,
            BitmapFormat::Rgb => byte_height / 3,
            BitmapFormat::Rgba => byte_height >> 2,
        }
    }

    pub fn mip_levels(&self) -> u32 {
        self.mip_levels
    }

    pub fn pixel(&self, x: u32, y: u32) -> &[u8] {
        assert!(x < self.width);
        assert!(y < self.height());

        let offset = y as usize * self.stride() + x as usize * self.fmt.byte_len();
        &self.pixels[offset..offset + self.fmt.byte_len()]
    }

    pub fn pixels(&self) -> &[u8] {
        &self.pixels
    }

    pub fn pixels_as_format(&self, dst_fmt: BitmapFormat) -> impl Iterator<Item = u8> + '_ {
        let stride = self.fmt.byte_len().min(dst_fmt.byte_len());
        self.pixels
            .chunks(self.fmt.byte_len())
            .flat_map(move |src| {
                let mut dst = [0; 4];
                dst[0..stride].copy_from_slice(&src[0..stride]);
                dst.into_iter().take(dst_fmt.byte_len())
            })
    }

    /// Bytes per row of pixels (there is no padding)
    pub fn stride(&self) -> usize {
        self.width as usize * self.fmt.byte_len()
    }

    pub fn width(&self) -> u32 {
        self.width
    }
}

#[cfg(test)]
mod tests {
    use crate::bitmap::{Bitmap, BitmapColor, BitmapFormat};

    #[test]
    fn pixels_as_format_drops_extra_channels() {
        let bitmap = Bitmap::new(
            BitmapColor::Srgb,
            BitmapFormat::Rgba,
            2,
            1,
            [1, 2, 3, 4, 5, 6, 7, 8],
        );

        let pixels = bitmap
            .pixels_as_format(BitmapFormat::Rgb)
            .collect::<Vec<_>>();

        assert_eq!(pixels, [1, 2, 3, 5, 6, 7]);
    }

    #[test]
    fn pixels_as_format_pads_missing_channels() {
        let bitmap = Bitmap::new(BitmapColor::Srgb, BitmapFormat::R, 2, 1, [1, 2]);

        let pixels = bitmap
            .pixels_as_format(BitmapFormat::Rgb)
            .collect::<Vec<_>>();

        assert_eq!(pixels, [1, 0, 0, 2, 0, 0]);
    }

    #[test]
    #[should_panic]
    fn pixel_rejects_x_past_width() {
        let bitmap = Bitmap::new(BitmapColor::Srgb, BitmapFormat::R, 2, 1, [1, 2, 3, 4]);

        let _ = bitmap.pixel(2, 0);
    }

    #[test]
    #[should_panic]
    fn new_rejects_zero_width() {
        let _ = Bitmap::new(BitmapColor::Srgb, BitmapFormat::R, 0, 1, [1, 2]);
    }

    #[test]
    #[should_panic]
    fn new_rejects_partial_rows() {
        let _ = Bitmap::new(BitmapColor::Srgb, BitmapFormat::Rgb, 1, 1, [1, 2, 3, 4]);
    }

    #[test]
    fn deserialize_rejects_zero_width() {
        let invalid = Bitmap {
            color: BitmapColor::Srgb,
            fmt: BitmapFormat::R,
            mip_levels: 1,
            pixels: vec![1],
            width: 0,
        };
        let mut encoded = Vec::new();
        bincode::serde::encode_into_std_write(invalid, &mut encoded, bincode::config::legacy())
            .unwrap();

        let result =
            bincode::serde::decode_from_slice::<Bitmap, _>(&encoded, bincode::config::legacy());

        assert!(result.is_err());
    }
}

/// Describes the channels of a `Bitmap`.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum BitmapFormat {
    /// Red channel only.
    #[serde(rename = "r")]
    R,

    /// Red and green channels.
    #[serde(rename = "rg")]
    Rg,

    /// Red, green and blue channels.
    #[serde(rename = "rgb")]
    Rgb,

    /// Red, green, blue and alpha channels.
    #[serde(rename = "rgba")]
    Rgba,
}

impl BitmapFormat {
    /// Returns the number of bytes each pixel advances the bitmap stream.
    #[inline]
    pub const fn byte_len(self) -> usize {
        match self {
            Self::R => 1,
            Self::Rg => 2,
            Self::Rgb => 3,
            Self::Rgba => 4,
        }
    }

    /// Returns `true` if this format includes an alpha channel.
    pub const fn has_alpha(self) -> bool {
        matches!(self, Self::Rgba)
    }
}

/// Describes the color space of a `Bitmap`.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum BitmapColor {
    #[serde(rename = "linear")]
    Linear,

    #[serde(rename = "srgb")]
    Srgb,
}