ds_rom/rom/
banner.rs

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