image_gameboy/
lib.rs

1use image::DynamicImage;
2
3pub trait GameBoy2bpp {
4    fn into_gb2bpp(self) -> Vec<u8>;
5}
6
7impl GameBoy2bpp for DynamicImage {
8    /// Converts the image into the Game Boy 2bpp (2 bits per pixel) format.
9    ///
10    /// This format is used for Game Boy graphics, where each pixel is represented
11    /// by 2 bits, allowing for 4 shades of gray. The image is divided into 8x8 tiles,
12    /// and each tile is encoded row by row. Each row consists of two bytes:
13    /// - The first byte contains the least significant bits of the 8 pixels in the row.
14    /// - The second byte contains the most significant bits of the 8 pixels in the row.
15    ///
16    /// The input image must have dimensions that are multiples of 8, as the Game Boy
17    /// graphics system operates on 8x8 tiles.
18    ///
19    /// # Panics
20    ///
21    /// This function will panic if the input image dimensions are not multiples of 8.
22    ///
23    /// # Returns
24    ///
25    /// A `Vec<u8>` containing the image data in Game Boy 2bpp format.
26    fn into_gb2bpp(self) -> Vec<u8> {
27        let img = self.into_luma8();
28        let (width, height) = img.dimensions();
29
30        assert!(
31            width % 8 == 0 && height % 8 == 0,
32            "Image dimensions must be a multiple of 8"
33        );
34
35        let mut result = Vec::new();
36
37        for tile_y in 0..(height / 8) {
38            for tile_x in 0..(width / 8) {
39                for row in 0..8 {
40                    let mut byte1 = 0u8;
41                    let mut byte2 = 0u8;
42
43                    for col in 0..8 {
44                        let pixel = img.get_pixel(tile_x * 8 + col, tile_y * 8 + row)[0];
45                        let value = match pixel {
46                            0..=63 => 3,
47                            64..=127 => 2,
48                            128..=191 => 1,
49                            _ => 0,
50                        };
51
52                        byte1 |= ((value & 1) as u8) << (7 - col);
53                        byte2 |= ((value >> 1) as u8) << (7 - col);
54                    }
55
56                    result.push(byte1);
57                    result.push(byte2);
58                }
59            }
60        }
61
62        result
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::GameBoy2bpp;
69
70    macro_rules! test_case {
71        ($name:ident) => {
72            #[test]
73            fn $name() {
74                let png_data = include_bytes!(concat!("../gfx/", stringify!($name), ".png"));
75                let expected_bpp = include_bytes!(concat!("../gfx/", stringify!($name), ".2bpp"));
76
77                let image = image::load_from_memory(png_data).unwrap();
78
79                let actual = image.into_gb2bpp();
80                assert_eq!(
81                    actual,
82                    expected_bpp,
83                    "Converted data does not match for `{}`",
84                    stringify!($name)
85                );
86            }
87        };
88    }
89
90    test_case!(bird);
91    test_case!(boulder);
92    test_case!(cavern);
93    test_case!(font_battle_extra);
94    test_case!(font_extra);
95    test_case!(red);
96}