pkmn_rom_extract/
graphics.rs

1//! Graphics types for GBA and DS.
2use alloc::vec;
3use alloc::vec::Vec;
4use core::fmt;
5use core::result;
6
7#[cfg(feature = "std")]
8#[allow(unused_imports)]
9use std::io::Write;
10
11#[cfg(not(feature = "std"))]
12#[allow(unused_imports)]
13use no_std_io2::io::Write;
14
15#[cfg(feature = "png")]
16use png::{BitDepth, ColorType, Encoder};
17
18const BYTES_PER_TILE: usize = 32;
19
20/// The number of colors in a single GBA/DS palette.
21pub const PALETTE_SIZE: usize = 16;
22
23/// The size of a GBA/DS palette in bytes.
24pub const PALETTE_SIZE_BYTES: usize = PALETTE_SIZE * 2;
25
26/// The size of uncompressed tile data for a 32x32 pixel sprite.
27pub const SPRITE_BYTES_32PX: usize = 4 * 4 * BYTES_PER_TILE;
28
29/// The size of uncompressed tile data for a 64x64 pixel sprite.
30pub const SPRITE_BYTES_64PX: usize = 8 * 8 * BYTES_PER_TILE;
31
32/// A palette for GBA or DS sprites.
33#[derive(Clone, Debug)]
34pub struct GbaPalette {
35    colors: [u16; PALETTE_SIZE],
36}
37
38/// A sprite with palette data for GBA/DS.
39#[derive(Clone, Debug)]
40pub struct GbaSprite {
41    width: u32, // number of 8px tiles
42    height: u32,
43    tiles: Vec<u8>,
44    palette: GbaPalette,
45}
46
47/// Background graphics data for GBA/DS.
48#[derive(Clone, Debug)]
49pub struct GbaBackground {
50    width: u32, // number of 8px tiles
51    height: u32,
52    pal_offset: u32,
53    tiles: Vec<u8>,
54    palettes: Vec<GbaPalette>,
55    tilemap: Vec<u16>,
56}
57
58/// Error type for graphics conversions.
59#[derive(Debug)]
60pub enum GraphicsError {
61    DimensionMismatch,
62    InvalidPalette,
63    #[cfg(feature = "png")]
64    PngError(png::EncodingError),
65}
66
67/// Result type for graphics conversions.
68pub type Result<T> = result::Result<T, GraphicsError>;
69
70impl fmt::Display for GraphicsError {
71    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72        match self {
73            GraphicsError::DimensionMismatch => write!(f, "Graphics data has invalid size."),
74            GraphicsError::InvalidPalette => write!(f, "Palette data is invalid."),
75            #[cfg(feature = "png")]
76            GraphicsError::PngError(e) => write!(f, "PNG Encoding Error: {e}."),
77        }
78    }
79}
80
81#[cfg(feature = "png")]
82impl From<png::EncodingError> for GraphicsError {
83    fn from(err: png::EncodingError) -> Self {
84        GraphicsError::PngError(err)
85    }
86}
87
88impl From<&[u8; 32]> for GbaPalette {
89    fn from(bytes: &[u8; PALETTE_SIZE_BYTES]) -> Self {
90        let mut out = GbaPalette {
91            colors: [0; PALETTE_SIZE],
92        };
93        for i in 0..PALETTE_SIZE {
94            out.colors[i] = u16::from_le_bytes(bytes[i * 2..i * 2 + 2].try_into().unwrap());
95        }
96        out
97    }
98}
99
100impl GbaPalette {
101    /// Construct a [`GbaPalette`] from a byte slice.
102    ///
103    /// The slice length must be equal to [`PALETTE_SIZE_BYTES`].
104    pub fn from_slice(slice: &[u8]) -> Result<Self> {
105        let bytes: &[u8; PALETTE_SIZE_BYTES] =
106            slice.try_into().or(Err(GraphicsError::InvalidPalette))?;
107        Ok(Self::from(bytes))
108    }
109}
110
111impl GbaSprite {
112    fn build(tile_width: u32, tiles: &[u8], palette: GbaPalette) -> Result<Self> {
113        let row_size = (tile_width as usize) * BYTES_PER_TILE;
114        if tiles.len() % row_size != 0 {
115            return Err(GraphicsError::DimensionMismatch);
116        }
117        Ok(GbaSprite {
118            width: tile_width,
119            height: (tiles.len() / row_size) as u32,
120            tiles: tiles.to_vec(),
121            palette: palette.clone(),
122        })
123    }
124
125    /// Construct a [`GbaSprite`] that is 24 pixels wide.
126    ///
127    /// The sprite's height is determined from the provided tile data and can be any multiple of
128    /// 8 pixels (whole tiles). If the slice length does not match any valid sprite size,
129    /// [`GraphicsError::DimensionMismatch`] is returned.
130    pub fn build_24px(tiles: &[u8], palette: GbaPalette) -> Result<Self> {
131        Self::build(3, tiles, palette)
132    }
133
134    /// Construct a [`GbaSprite`] that is 32 pixels wide.
135    ///
136    /// The sprite's height is determined from the provided tile data and can be any multiple of
137    /// 8 pixels (whole tiles). If the slice length does not match any valid sprite size,
138    /// [`GraphicsError::DimensionMismatch`] is returned.
139    pub fn build_32px(tiles: &[u8], palette: GbaPalette) -> Result<Self> {
140        Self::build(4, tiles, palette)
141    }
142
143    /// Construct a [`GbaSprite`] that is 64 pixels wide.
144    ///
145    /// The sprite's height is determined from the provided tile data and can be any multiple of
146    /// 8 pixels (whole tiles). If the slice length does not match any valid sprite size,
147    /// [`GraphicsError::DimensionMismatch`] is returned.
148    pub fn build_64px(tiles: &[u8], palette: GbaPalette) -> Result<Self> {
149        Self::build(8, tiles, palette)
150    }
151
152    /// Get the (width, height) dimensions of the sprite in pixels.
153    pub fn dimensions(&self) -> (u32, u32) {
154        (self.width * 8, self.height * 8)
155    }
156
157    /// Get a reference to the sprite's palette.
158    pub fn get_palette(&self) -> &GbaPalette {
159        &self.palette
160    }
161
162    /// Convert the palette's colors to 32-bit ARGB (`0xAARRGGBB`).
163    pub fn convert_palette(&self) -> [u32; PALETTE_SIZE] {
164        let mut out = [0; PALETTE_SIZE];
165        for (dest, c) in out.iter_mut().zip(self.palette.colors.iter()) {
166            *dest = rgb15_to_32(*c);
167        }
168        // Set opacity to 0 for the first color in the palette.
169        out[0] &= 0xFFFFFF;
170        out
171    }
172
173    /// Get pixel data for this sprite as 4 bits per pixel, left to right and top to bottom.
174    ///
175    /// Each byte has the least significant bits (mask `0x0F`) represent the left pixel and the
176    /// most significant bits (mask `0xF0`) represent the right pixel. The value of that nibble
177    /// corresponds to the palette color index.
178    /// Unlike the raw graphics data used by GBA and DS, pixel data is not grouped by tile; for
179    /// a 16x16 pixel sprite, for example, this function will return 4 bytes for the first row
180    /// of pixels in the first 8x8 tile followed by 4 bytes for the first row in the second tile,
181    /// 4 bytes for the second row of the first tile, and so on.
182    pub fn pixels_4bpp(&self) -> Vec<u8> {
183        let size_bytes = (self.width * self.height) as usize * BYTES_PER_TILE;
184        let mut out = Vec::with_capacity(size_bytes);
185        for tile_y in 0..self.height as usize {
186            let tile_row = tile_y * BYTES_PER_TILE * self.width as usize;
187            for pix_y in 0..8usize {
188                for tile_x in 0..self.width as usize {
189                    let off = tile_row + tile_x * BYTES_PER_TILE + pix_y * 4;
190                    out.extend_from_slice(&self.tiles[off..off + 4]);
191                }
192            }
193        }
194        out
195    }
196
197    /// Get pixel data for this sprite as 16 bits per pixel, left to right and top to bottom.
198    ///
199    /// Each value in the returned vector is a 15-bit color in the format used by GBA and DS, with
200    /// 5 bits per channel.
201    pub fn pixels_16bpp(&self) -> Vec<u16> {
202        let pal = &self.palette.colors;
203        self.pixels_4bpp()
204            .iter()
205            .flat_map(|b| {
206                let bo = *b as usize;
207                [pal[bo & 0xF], pal[bo >> 4]]
208            })
209            .collect()
210    }
211
212    /// Write a PNG file of this sprite.
213    ///
214    /// The resulting PNG file will be indexed with the palette preserved in the same order it
215    /// was originally in, simply converted from having 5 bits per color channel to 8. The alpha
216    /// channel will be fully transparent (0 opacity) for palette index 0 and fully opaque (255)
217    /// for all other colors.
218    #[cfg(feature = "png")]
219    pub fn write_png<T: Write>(&self, file: T) -> Result<()> {
220        let pal_bytes: Vec<u8> = self
221            .palette
222            .colors
223            .iter()
224            .flat_map(|c| rgb15_to_24(*c))
225            .collect();
226        let alpha = {
227            let mut alpha = [0xFFu8; 16];
228            alpha[0] = 0;
229            alpha
230        };
231        // PNG expects the leftmost pixel to be at the higher order nibble of the first byte.
232        // GBA/DS sprites give it the lower order nibble instead. ROR swaps these.
233        let data: Vec<u8> = self
234            .pixels_4bpp()
235            .iter()
236            .map(|b| b.rotate_right(4))
237            .collect();
238
239        let mut encoder = Encoder::new(file, self.width * 8, self.height * 8);
240        encoder.set_color(ColorType::Indexed);
241        encoder.set_depth(BitDepth::Four);
242        encoder.set_trns(&alpha);
243        encoder.set_palette(pal_bytes);
244
245        let mut writer = encoder.write_header()?;
246        Ok(writer.write_image_data(&data)?)
247    }
248}
249
250impl GbaBackground {
251    /// Construct a [`GbaBackground`] from tile, palette, and tilemap data.
252    ///
253    /// Tilemap data contains a palette index for each tile relative to the start of the system's
254    /// VRAM. The provided tilemap data is interpreted as if the given palettes are loaded into
255    /// VRAM at index `pal_offset`. Any tile that uses an unspecified palette is treated as fully
256    /// transparent.
257    pub fn build(
258        tile_width: u32,
259        pal_offset: u32,
260        tiles: &[u8],
261        palettes: &[GbaPalette],
262        tilemap: &[u16],
263    ) -> Result<Self> {
264        if tilemap.len() % tile_width as usize != 0 {
265            return Err(GraphicsError::DimensionMismatch);
266        }
267        Ok(Self {
268            width: tile_width,
269            height: tilemap.len() as u32 / tile_width,
270            pal_offset,
271            tiles: tiles.to_vec(),
272            palettes: palettes.to_vec(),
273            tilemap: tilemap.to_vec(),
274        })
275    }
276
277    /// Get the (width, height) dimensions of this background in pixels.
278    pub fn dimensions(&self) -> (u32, u32) {
279        (self.width * 8, self.height * 8)
280    }
281
282    /// Convert all palettes' colors to 32-bit ARGB (`0xAARRGGBB`).
283    pub fn convert_palette(&self) -> Vec<u32> {
284        let mut out = Vec::with_capacity(self.palettes.len() * 16);
285        for pal in &self.palettes {
286            for (idx, color) in pal.colors.iter().enumerate() {
287                if idx == 0 {
288                    // First color in every palette is transparent.
289                    out.push(rgb15_to_32(*color) & 0xFFFFFF);
290                } else {
291                    out.push(rgb15_to_32(*color));
292                }
293            }
294        }
295        out
296    }
297
298    /// Get pixel data for this background as 8 bits per pixel, left to right and top to bottom.
299    ///
300    /// The value of each byte represents the color index relative to the first palette that was
301    /// used when constructing this [`GbaBackground`].
302    pub fn pixels_8bpp(&self) -> Vec<u8> {
303        let size_bytes = (self.width * self.height) as usize * 8 * 8;
304        let mut out = vec![0; size_bytes];
305        let pal_limit = self.palettes.len() as u32 + self.pal_offset;
306        for ty in 0..self.height as usize {
307            for tx in 0..self.width as usize {
308                let tspec = self.tilemap[ty * self.width as usize + tx];
309                let pal = (tspec >> 12) as u8;
310                let tile_idx = (tspec & 0x3FF) as usize;
311                let xflip = (tspec & 0x400) != 0;
312                let yflip = (tspec & 0x800) != 0;
313                let dx = if xflip { -1 } else { 1 };
314                if (pal as u32) < self.pal_offset || (pal as u32) >= pal_limit {
315                    continue;
316                }
317                if tile_idx * BYTES_PER_TILE > self.tiles.len() {
318                    continue;
319                }
320                let pal = (pal - self.pal_offset as u8) * 16;
321                for py in 0..8 {
322                    let y = ty * 8 + if yflip { 7 - py } else { py };
323                    for px in 0..4 {
324                        let x = tx * 8 + if xflip { 7 - px * 2 } else { px * 2 };
325                        let dst = y * self.width as usize * 8 + x;
326                        let cur_byte = self.tiles[tile_idx * 32 + py * 4 + px];
327                        out[dst] = pal | cur_byte & 0xF;
328                        out[(dst as isize + dx) as usize] = pal | cur_byte >> 4;
329                    }
330                }
331            }
332        }
333        out
334    }
335
336    /// Write a PNG file of this background data.
337    ///
338    /// The resulting PNG file will be indexed with the palettes preserved in the same order they
339    /// were originally in, simply converted from having 5 bits per color channel to 8. The alpha
340    /// channel will be fully transparent (0 opacity) for color index 0 in all palettes and fully
341    /// opaque (255) for all other colors.
342    #[cfg(feature = "png")]
343    pub fn write_png<T: Write>(&self, file: T) -> Result<()> {
344        let pal_bytes: Vec<u8> = {
345            let mut bytes: Vec<u8> = Vec::with_capacity(self.palettes.len() * PALETTE_SIZE_BYTES);
346            for pal in &self.palettes {
347                bytes.extend(pal.colors.iter().flat_map(|c| rgb15_to_24(*c)));
348            }
349            bytes
350        };
351        let alpha = {
352            let mut alpha: Vec<u8> = vec![0xFFu8; 16 * self.palettes.len()];
353            // The first color of every palette is transparent
354            for idx in 0..self.palettes.len() {
355                alpha[idx * 16] = 0;
356            }
357            alpha
358        };
359
360        let data: Vec<u8> = self.pixels_8bpp();
361        let mut encoder = Encoder::new(file, self.width * 8, self.height * 8);
362        encoder.set_color(ColorType::Indexed);
363        encoder.set_depth(BitDepth::Eight);
364        encoder.set_trns(&alpha);
365        encoder.set_palette(pal_bytes);
366
367        let mut writer = encoder.write_header()?;
368        writer.write_image_data(&data)?;
369        Ok(())
370    }
371}
372
373/// Convert a 15-bit color to 32-bit ARGB.
374///
375/// The order of the returned channels is, from most to least significant: alpha, red, green, then
376/// blue. This is the same format you would get if you specify the color channels in hex with the
377/// format `0xAARRGGBB`.
378pub fn rgb15_to_32(color: u16) -> u32 {
379    let c32 = color as u32;
380    let r = (c32 & 31) * 255 / 31;
381    let g = ((c32 >> 5) & 31) * 255 / 31;
382    let b = ((c32 >> 10) & 31) * 255 / 31;
383
384    0xFF << 24 | r << 16 | g << 8 | b
385}
386
387/// Convert a 15-bit color to 24-bit RGB.
388///
389/// The order of the returned channels is red, green, then blue.
390pub fn rgb15_to_24(color: u16) -> [u8; 3] {
391    let c32 = color as u32;
392    let r = (c32 & 31) * 255 / 31;
393    let g = ((c32 >> 5) & 31) * 255 / 31;
394    let b = ((c32 >> 10) & 31) * 255 / 31;
395
396    [r as u8, g as u8, b as u8]
397}