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}