Skip to main content

ds_rom/rom/
banner.rs

1use std::{
2    io,
3    path::{Path, PathBuf},
4};
5
6use image::{GenericImageView, ImageError, ImageReader, Rgba, RgbaImage};
7use serde::{Deserialize, Serialize};
8use snafu::{Backtrace, Snafu};
9
10use super::{
11    raw::{self, BannerBitmap, BannerPalette, BannerVersion, Language},
12    ImageSize,
13};
14use crate::{crc::CRC_16_MODBUS, str::Unicode16Array};
15
16/// ROM banner.
17#[derive(Serialize, Deserialize, Default)]
18pub struct Banner {
19    version: BannerVersion,
20    /// Game title in different languages.
21    pub title: BannerTitle,
22    /// Icon to show on the home screen.
23    pub images: BannerImages,
24    /// Keyframes for animated icons.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub keyframes: Option<Vec<BannerKeyframe>>,
27}
28
29/// Errors related to [`Banner`].
30#[derive(Debug, Snafu)]
31pub enum BannerError {
32    /// See [`BannerImageError`].
33    #[snafu(transparent)]
34    BannerFile {
35        /// Source error.
36        source: BannerImageError,
37    },
38    /// Occurs when trying to build a banner to place in the ROM, but there were too many keyframes.
39    #[snafu(display("maximum keyframe count is {max} but got {actual}:\n{backtrace}"))]
40    TooManyKeyframes {
41        /// Max allowed amount.
42        max: usize,
43        /// Actual amount.
44        actual: usize,
45        /// Backtrace to the source of the error.
46        backtrace: Backtrace,
47    },
48    /// Occurs when trying to build a banner to place in the ROM, but the version is not yet supported by this library.
49    #[snafu(display("maximum supported banner version is currently {max} but got {actual}:\n{backtrace}"))]
50    VersionNotSupported {
51        /// Max supported version.
52        max: BannerVersion,
53        /// Actual version.
54        actual: BannerVersion,
55        /// Backtrace to the source of the error.
56        backtrace: Backtrace,
57    },
58}
59
60impl Banner {
61    fn load_title(banner: &raw::Banner, version: BannerVersion, language: Language) -> Option<String> {
62        if version.supports_language(language) {
63            banner.title(language).map(|title| title.to_string())
64        } else {
65            None
66        }
67    }
68
69    /// Loads from a raw banner.
70    pub fn load_raw(banner: &raw::Banner) -> Self {
71        let version = banner.version();
72        Self {
73            version,
74            title: BannerTitle {
75                japanese: Self::load_title(banner, version, Language::Japanese).unwrap(),
76                english: Self::load_title(banner, version, Language::English).unwrap(),
77                french: Self::load_title(banner, version, Language::French).unwrap(),
78                german: Self::load_title(banner, version, Language::German).unwrap(),
79                italian: Self::load_title(banner, version, Language::Italian).unwrap(),
80                spanish: Self::load_title(banner, version, Language::Spanish).unwrap(),
81                chinese: Self::load_title(banner, version, Language::Chinese),
82                korean: Self::load_title(banner, version, Language::Korean),
83            },
84            images: BannerImages::from_bitmap(*banner.bitmap(), *banner.palette()),
85            keyframes: None,
86        }
87    }
88
89    fn crc(&self, banner: &mut raw::Banner, version: BannerVersion) {
90        if self.version >= version {
91            *banner.crc_mut(version.crc_index()) = CRC_16_MODBUS.checksum(&banner.full_data()[version.crc_range()]);
92        }
93    }
94
95    /// Builds a raw banner to place in a ROM.
96    ///
97    /// # Errors
98    ///
99    /// This function will return an error if the banner version is not yet supported by this library, or there are too many
100    /// keyframes.
101    pub fn build(&self) -> Result<raw::Banner<'_>, BannerError> {
102        // TODO: Increase max version to Animated
103        // The challenge is to convert the animated icon to indexed bitmaps. Each bitmap can use any of the 8 palettes at any
104        // given time according to the keyframes. This means that to convert the PNG animation frames to indexed bitmaps, we
105        // may need more than 8 PNG files if a palette is reused on multiple bitmaps. Then we have to deduplicate indexed
106        // bitmaps with precisely the same indexes. Not very efficient, but it may be our only option for modern image formats.
107        if self.version > BannerVersion::Korea {
108            return VersionNotSupportedSnafu { max: BannerVersion::Korea, actual: self.version }.fail();
109        }
110
111        let mut banner = raw::Banner::new(self.version);
112        self.title.copy_to_banner(&mut banner);
113
114        *banner.bitmap_mut() = self.images.bitmap;
115        *banner.palette_mut() = self.images.palette;
116
117        if let Some(keyframes) = &self.keyframes {
118            if keyframes.len() > 64 {
119                TooManyKeyframesSnafu { max: 64usize, actual: keyframes.len() }.fail()?;
120            }
121
122            let animation = banner.animation_mut().unwrap();
123            for i in 0..keyframes.len() {
124                animation.keyframes[i] = keyframes[i].build();
125            }
126            for i in keyframes.len()..64 {
127                animation.keyframes[i] = raw::BannerKeyframe::new();
128            }
129        }
130
131        self.crc(&mut banner, BannerVersion::Original);
132        self.crc(&mut banner, BannerVersion::China);
133        self.crc(&mut banner, BannerVersion::Korea);
134        self.crc(&mut banner, BannerVersion::Animated);
135
136        Ok(banner)
137    }
138}
139
140/// Icon for the [`Banner`].
141#[derive(Default, Serialize, Deserialize)]
142pub struct BannerImages {
143    /// Main bitmap.
144    #[serde(skip)]
145    pub bitmap: BannerBitmap,
146    /// Main palette.
147    #[serde(skip)]
148    pub palette: BannerPalette,
149    /// Bitmaps for animated icon.
150    #[serde(skip)]
151    pub animation_bitmaps: Option<Box<[BannerBitmap]>>,
152    /// Palettes for animated icon
153    #[serde(skip)]
154    pub animation_palettes: Option<Box<[BannerPalette]>>,
155
156    /// Path to bitmap PNG.
157    pub bitmap_path: PathBuf,
158    /// Path to palette PNG.
159    pub palette_path: PathBuf,
160}
161
162/// Errors related to [`BannerImages`].
163#[derive(Debug, Snafu)]
164pub enum BannerImageError {
165    /// See [`io::Error`].
166    #[snafu(transparent)]
167    Io {
168        /// Error source.
169        source: io::Error,
170    },
171    /// See [`ImageError`].
172    #[snafu(transparent)]
173    Image {
174        /// Source error.
175        source: ImageError,
176    },
177    /// Occurs when loading a banner image with the wrong size.
178    #[snafu(display("banner icon must be {expected} pixels but got {actual} pixels:\n{backtrace}"))]
179    WrongSize {
180        /// Expected size.
181        expected: ImageSize,
182        /// Actual input size.
183        actual: ImageSize,
184        /// Backtrace to the source of the error.
185        backtrace: Backtrace,
186    },
187    /// Occurs when the bitmap has a pixel not present in the palette.
188    #[snafu(display("banner icon {bitmap:?} contains a pixel at {x},{y} which is not present in the palette:\n{backtrace}"))]
189    InvalidPixel {
190        /// Path to the bitmap.
191        bitmap: PathBuf,
192        /// X coordinate.
193        x: u32,
194        /// Y coordinate.
195        y: u32,
196        /// Backtrace to the source of the error.
197        backtrace: Backtrace,
198    },
199}
200
201impl BannerImages {
202    /// Creates a new [`BannerImages`] from a bitmap and palette.
203    pub fn from_bitmap(bitmap: BannerBitmap, palette: BannerPalette) -> Self {
204        Self {
205            bitmap,
206            palette,
207            animation_bitmaps: None,
208            animation_palettes: None,
209            bitmap_path: "bitmap.png".into(),
210            palette_path: "palette.png".into(),
211        }
212    }
213
214    /// Loads the bitmap and palette
215    ///
216    /// # Errors
217    ///
218    /// This function will return an error if [`Reader::open`] or [`Reader::decode`] fails, or if the images are the wrong
219    /// size, or the bitmap has a color not present in the palette.
220    pub fn load(&mut self, path: &Path) -> Result<(), BannerImageError> {
221        let bitmap_image = ImageReader::open(path.join(&self.bitmap_path))?.decode()?;
222        if bitmap_image.width() != 32 || bitmap_image.height() != 32 {
223            return WrongSizeSnafu {
224                expected: ImageSize { width: 32, height: 32 },
225                actual: ImageSize { width: bitmap_image.width(), height: bitmap_image.height() },
226            }
227            .fail();
228        }
229
230        let palette_image = ImageReader::open(path.join(&self.palette_path))?.decode()?;
231        if palette_image.width() != 16 || palette_image.height() != 1 {
232            return WrongSizeSnafu {
233                expected: ImageSize { width: 16, height: 1 },
234                actual: ImageSize { width: palette_image.width(), height: palette_image.height() },
235            }
236            .fail();
237        }
238
239        let mut bitmap = BannerBitmap([0u8; 0x200]);
240        for (x, y, color) in bitmap_image.pixels() {
241            let alpha = color.0[3];
242            let index = if alpha == 0 {
243                0
244            } else {
245                let Some(index) = palette_image.pixels().find_map(|(i, _, c)| (color == c).then_some(i)) else {
246                    return InvalidPixelSnafu { bitmap: path.join(&self.bitmap_path), x, y }.fail();
247                };
248                index
249            };
250            bitmap.set_pixel(x as usize, y as usize, index as u8);
251        }
252
253        let mut palette = BannerPalette([0u16; 16]);
254        for (i, _, color) in palette_image.pixels() {
255            let [r, g, b, _] = color.0;
256            palette.set_color(i as usize, r, g, b);
257        }
258
259        self.bitmap = bitmap;
260        self.palette = palette;
261        Ok(())
262    }
263
264    /// Saves to a bitmap and palette file in the given path.
265    ///
266    /// # Errors
267    ///
268    /// See [`RgbImage::save`].
269    pub fn save_bitmap_file(&self, path: &Path) -> Result<(), BannerImageError> {
270        let mut bitmap_image = RgbaImage::new(32, 32);
271        for y in 0..32 {
272            for x in 0..32 {
273                let index = self.bitmap.get_pixel(x, y);
274                let color = self.palette.get_color(index);
275                bitmap_image.put_pixel(x as u32, y as u32, Rgba(color));
276            }
277        }
278
279        let mut palette_image = RgbaImage::new(16, 1);
280        for index in 0..16 {
281            let color = self.palette.get_color(index);
282            palette_image.put_pixel(index as u32, 0, Rgba(color));
283        }
284
285        bitmap_image.save(path.join(&self.bitmap_path))?;
286        palette_image.save(path.join(&self.palette_path))?;
287        Ok(())
288    }
289}
290
291/// Game title in different languages.
292#[derive(Serialize, Deserialize, Default)]
293pub struct BannerTitle {
294    /// Japanese.
295    pub japanese: String,
296    /// English.
297    pub english: String,
298    /// French.
299    pub french: String,
300    /// German.
301    pub german: String,
302    /// Italian.
303    pub italian: String,
304    /// Spanish.
305    pub spanish: String,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    /// Chinese.
308    pub chinese: Option<String>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    /// Korean.
311    pub korean: Option<String>,
312}
313
314macro_rules! copy_title {
315    ($banner:ident, $language:expr, $title:expr) => {
316        if let Some(title) = $banner.title_mut($language) {
317            *title = Unicode16Array::from($title.as_str());
318        }
319    };
320}
321
322impl BannerTitle {
323    fn copy_to_banner(&self, banner: &mut raw::Banner) {
324        copy_title!(banner, Language::Japanese, &self.japanese);
325        copy_title!(banner, Language::English, &self.english);
326        copy_title!(banner, Language::French, &self.french);
327        copy_title!(banner, Language::German, &self.german);
328        copy_title!(banner, Language::Italian, &self.italian);
329        copy_title!(banner, Language::Spanish, &self.spanish);
330        if let Some(chinese) = &self.chinese {
331            copy_title!(banner, Language::Chinese, chinese);
332        }
333        if let Some(korean) = &self.korean {
334            copy_title!(banner, Language::Korean, korean);
335        }
336    }
337}
338
339/// Keyframe for animated icon.
340#[derive(Serialize, Deserialize)]
341pub struct BannerKeyframe {
342    /// Flips the bitmap vertically.
343    pub flip_vertically: bool,
344    /// Flips the bitmap horizontally.
345    pub flip_horizontally: bool,
346    /// Palette index.
347    pub palette: usize,
348    /// Bitmap index.
349    pub bitmap: usize,
350    /// Duration in frames.
351    pub frame_duration: usize,
352}
353
354impl BannerKeyframe {
355    /// Builds a raw keyframe.
356    ///
357    /// # Panics
358    ///
359    /// Panics if the frame duration, bitmap index or palette do not fit in the raw keyframe.
360    pub fn build(&self) -> raw::BannerKeyframe {
361        raw::BannerKeyframe::new()
362            .with_frame_duration(self.frame_duration.try_into().unwrap())
363            .with_bitmap_index(self.bitmap.try_into().unwrap())
364            .with_palette_index(self.palette.try_into().unwrap())
365            .with_flip_horizontally(self.flip_horizontally)
366            .with_flip_vertically(self.flip_vertically)
367    }
368}