Skip to main content

ds_rom/rom/
logo.rs

1use std::{fmt::Display, io, path::Path};
2
3use image::{GenericImageView, GrayImage, ImageError, ImageReader, Luma};
4use snafu::{Backtrace, Snafu};
5
6use crate::compress::huffman::{NibbleHuffman, NibbleHuffmanCode};
7
8/// Huffman codes for every combination of 4 pixels
9const HUFFMAN: NibbleHuffman = NibbleHuffman {
10    codes: [
11        /* 0000 */ NibbleHuffmanCode { length: 1, bits: 0b1 },
12        /* 0001 */ NibbleHuffmanCode { length: 4, bits: 0b0110 },
13        /* 0010 */ NibbleHuffmanCode { length: 5, bits: 0b01010 },
14        /* 0011 */ NibbleHuffmanCode { length: 4, bits: 0b0100 },
15        /* 0100 */ NibbleHuffmanCode { length: 5, bits: 0b00010 },
16        /* 0101 */ NibbleHuffmanCode { length: 6, bits: 0b011110 },
17        /* 0110 */ NibbleHuffmanCode { length: 6, bits: 0b010110 },
18        /* 0111 */ NibbleHuffmanCode { length: 6, bits: 0b000110 },
19        /* 1000 */ NibbleHuffmanCode { length: 5, bits: 0b00110 },
20        /* 1001 */ NibbleHuffmanCode { length: 6, bits: 0b011111 },
21        /* 1010 */ NibbleHuffmanCode { length: 6, bits: 0b010111 },
22        /* 1011 */ NibbleHuffmanCode { length: 6, bits: 0b000111 },
23        /* 1100 */ NibbleHuffmanCode { length: 4, bits: 0b0010 },
24        /* 1101 */ NibbleHuffmanCode { length: 5, bits: 0b01110 },
25        /* 1110 */ NibbleHuffmanCode { length: 5, bits: 0b00111 },
26        /* 1111 */ NibbleHuffmanCode { length: 4, bits: 0b0000 },
27    ],
28};
29
30const WIDTH: usize = 104;
31const HEIGHT: usize = 16;
32const SIZE: usize = WIDTH * HEIGHT / 8;
33
34const LOGO_HEADER: u32 = 0x0000d082;
35const LOGO_FOOTER: u32 = 0xfff4c307;
36
37/// Header logo.
38pub struct Logo {
39    pixels: [u8; SIZE],
40}
41
42impl Default for Logo {
43    fn default() -> Self {
44        Self { pixels: [0u8; SIZE] }
45    }
46}
47
48/// Errors related to [`Logo`].
49#[derive(Snafu, Debug)]
50pub enum LogoError {
51    /// Occurs when decompressing a logo with an invalid header.
52    #[snafu(display("invalid logo header, expected {expected:08x} but got {actual:08x}:\n{backtrace}"))]
53    InvalidHeader {
54        /// Expected header.
55        expected: u32,
56        /// Actual input header.
57        actual: u32,
58        /// Backtrace to the source of the error.
59        backtrace: Backtrace,
60    },
61    /// Occurs when decompressing a logo with an invalid footer.
62    #[snafu(display("invalid logo footer, expected {expected:08x} but got {actual:08x}:\n{backtrace}"))]
63    InvalidFooter {
64        /// Expected footer.
65        expected: u32,
66        /// Actual input footer.
67        actual: u32,
68        /// Backtrace to the source of the error.
69        backtrace: Backtrace,
70    },
71    /// Occurs when decompressing a logo which doesn't yield the correct bitmap size.
72    #[snafu(display("wrong logo size, expected {expected} bytes but got {actual} bytes:\n{backtrace}"))]
73    WrongSize {
74        /// Expected size.
75        expected: usize,
76        /// Actual input size.
77        actual: usize,
78        /// Backtrace to the source of the error.
79        backtrace: Backtrace,
80    },
81}
82
83/// Errors when loading a [`Logo`].
84#[derive(Snafu, Debug)]
85pub enum LogoLoadError {
86    /// See [`io::Error`].
87    #[snafu(transparent)]
88    Io {
89        /// Source error.
90        source: io::Error,
91    },
92    /// See [`ImageError`].
93    #[snafu(transparent)]
94    Image {
95        /// Source error.
96        source: ImageError,
97    },
98    /// Occurs when the input image has a pixel which isn't white or black.
99    #[snafu(display("logo image contains a pixel at {x},{y} which isn't white or black:\n{backtrace}"))]
100    InvalidColor {
101        /// X coordinate.
102        x: u32,
103        /// Y coordinate.
104        y: u32,
105        /// Backtrace to the source of the error.
106        backtrace: Backtrace,
107    },
108    /// Occurs when the input image has the wrong size.
109    #[snafu(display("logo image must be {expected} pixels but got {actual} pixels:\n{backtrace}"))]
110    ImageSize {
111        /// Expected size.
112        expected: ImageSize,
113        /// Actual input size.
114        actual: ImageSize,
115        /// Backtrace to the source of the error.
116        backtrace: Backtrace,
117    },
118}
119
120/// Errors when saving a [`Logo`].
121#[derive(Snafu, Debug)]
122pub enum LogoSaveError {
123    /// See [`ImageError`].
124    #[snafu(transparent)]
125    Image {
126        /// Source error.
127        source: ImageError,
128    },
129}
130
131/// Size of an image.
132#[derive(Debug)]
133pub struct ImageSize {
134    /// Image width.
135    pub width: u32,
136    /// Image height.
137    pub height: u32,
138}
139
140impl Display for ImageSize {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "{}x{}", self.width, self.height)
143    }
144}
145
146fn reverse32(data: &mut [u8]) {
147    for i in (0..data.len() & !3).step_by(4) {
148        let value = u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]);
149        let swapped = value.swap_bytes().to_le_bytes();
150        data[i..i + 4].copy_from_slice(&swapped);
151    }
152}
153
154impl Logo {
155    /// Saves this [`Logo`] to a PNG image.
156    ///
157    /// # Errors
158    ///
159    /// This function will return an error if [`GrayImage::save`] fails.
160    pub fn save_png<P: AsRef<Path>>(&self, path: P) -> Result<(), LogoSaveError> {
161        let mut image = GrayImage::new(WIDTH as u32, HEIGHT as u32);
162        for y in 0..HEIGHT {
163            for x in 0..WIDTH {
164                let luma = if self.get_pixel(x, y) { 0x00 } else { 0xff };
165                image.put_pixel(x as u32, y as u32, Luma([luma]));
166            }
167        }
168        image.save(path)?;
169        Ok(())
170    }
171
172    /// Loads a [`Logo`] from a PNG image.
173    ///
174    /// # Errors
175    ///
176    /// This function will return an error if it failed to open or decode the image, or the image has the wrong size or colors.
177    pub fn from_png<P: AsRef<Path>>(path: P) -> Result<Self, LogoLoadError> {
178        let image = ImageReader::open(path)?.decode()?;
179        if image.width() != WIDTH as u32 || image.height() != HEIGHT as u32 {
180            ImageSizeSnafu {
181                expected: ImageSize { width: WIDTH as u32, height: HEIGHT as u32 },
182                actual: ImageSize { width: image.width(), height: image.height() },
183            }
184            .fail()?;
185        }
186
187        let mut logo = Logo { pixels: [0; SIZE] };
188        for (x, y, color) in image.pixels() {
189            let [r, g, b, _] = color.0;
190            if (r != 0xff && r != 0x00) || g != r || b != r {
191                return InvalidColorSnafu { x, y }.fail();
192            }
193            logo.set_pixel(x as usize, y as usize, r == 0x00);
194        }
195        Ok(logo)
196    }
197
198    /// Decompresses a [`Logo`] from a compressed logo in the ROM header.
199    ///
200    /// # Errors
201    ///
202    /// This function will return an error if the compressed logo yields an invalid header, footer or bitmap size.
203    pub fn decompress(data: &[u8]) -> Result<Self, LogoError> {
204        let data = {
205            let mut swapped_data = data.to_owned();
206            reverse32(&mut swapped_data);
207            swapped_data.into_boxed_slice()
208        };
209
210        let mut bytes = [0u8; SIZE + 8];
211        HUFFMAN.decompress_to_slice(&data, &mut bytes);
212
213        let header = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
214        if header != LOGO_HEADER {
215            InvalidHeaderSnafu { expected: LOGO_HEADER, actual: header }.fail()?;
216        }
217
218        let footer = u32::from_le_bytes([
219            bytes[bytes.len() - 4],
220            bytes[bytes.len() - 3],
221            bytes[bytes.len() - 2],
222            bytes[bytes.len() - 1],
223        ]);
224        if footer != LOGO_FOOTER {
225            InvalidFooterSnafu { expected: LOGO_FOOTER, actual: footer }.fail()?;
226        }
227
228        let len = bytes.len();
229        let diff = &mut bytes[4..len - 4];
230        if diff.len() != SIZE {
231            WrongSizeSnafu { expected: SIZE, actual: diff.len() }.fail()?;
232        }
233        HUFFMAN.diff16_to_data(diff);
234
235        let mut logo = Logo::default();
236        logo.load_tiles(diff);
237        Ok(logo)
238    }
239
240    /// Compresses this [`Logo`] to put into a ROM header.
241    pub fn compress(&self) -> [u8; 0x9c] {
242        let mut diff = [0u8; SIZE + 8];
243        self.store_tiles(&mut diff[4..SIZE + 4]);
244        HUFFMAN.data_to_diff16(&mut diff[4..SIZE + 4]);
245
246        diff[0..4].copy_from_slice(&LOGO_HEADER.to_le_bytes());
247        diff[SIZE + 4..SIZE + 8].copy_from_slice(&LOGO_FOOTER.to_le_bytes());
248
249        let mut bytes = [0u8; 0x9c];
250        HUFFMAN.compress_to_slice(&diff, &mut bytes);
251        reverse32(&mut bytes);
252        bytes
253    }
254
255    fn load_tiles(&mut self, data: &[u8]) {
256        for y in 0..HEIGHT {
257            for x in 0..WIDTH {
258                let index = (y / 8) * WIDTH + (x / 8) * 8 + y % 8;
259                let value = if index >= data.len() {
260                    false
261                } else {
262                    let offset = x & 7;
263                    (data[index] & (1 << offset)) != 0
264                };
265                self.set_pixel(x, y, value);
266            }
267        }
268    }
269
270    fn store_tiles(&self, data: &mut [u8]) {
271        for y in 0..HEIGHT {
272            for x in 0..WIDTH {
273                let bit = 1 << (x & 7);
274                let value = self.get_pixel_value(x, y, bit);
275                let index = (y / 8) * WIDTH + (x / 8) * 8 + y % 8;
276                if index < data.len() {
277                    data[index] = (data[index] & !bit) | value
278                };
279            }
280        }
281    }
282
283    /// Returns the pixel value at the given coordinates.
284    pub fn get_pixel(&self, x: usize, y: usize) -> bool {
285        let index = (y * WIDTH + x) / 8;
286        if index >= self.pixels.len() {
287            false
288        } else {
289            let offset = !x & 7;
290            (self.pixels[index] & (1 << offset)) != 0
291        }
292    }
293
294    /// Sets the pixel value at the given coordinates.
295    pub fn set_pixel(&mut self, x: usize, y: usize, value: bool) {
296        let index = (y * WIDTH + x) / 8;
297        if index < self.pixels.len() {
298            let bit = 1 << (!x & 7);
299            let value = if value { bit } else { 0 };
300            self.pixels[index] = (self.pixels[index] & !bit) | value;
301        }
302    }
303
304    fn get_pixel_value(&self, x: usize, y: usize, value: u8) -> u8 {
305        if self.get_pixel(x, y) {
306            value
307        } else {
308            0
309        }
310    }
311
312    fn get_braille_index(&self, x: usize, y: usize) -> u8 {
313        let value = self.get_pixel_value(x, y, 0x80)
314            | self.get_pixel_value(x + 1, y, 0x40)
315            | self.get_pixel_value(x, y + 1, 0x20)
316            | self.get_pixel_value(x + 1, y + 1, 0x10)
317            | self.get_pixel_value(x, y + 2, 0x8)
318            | self.get_pixel_value(x + 1, y + 2, 0x4)
319            | self.get_pixel_value(x, y + 3, 0x2)
320            | self.get_pixel_value(x + 1, y + 3, 0x1);
321        !value
322    }
323}
324
325/// Braille patterns indexed as binary representations of 0-255. Bit positions:  
326/// 6 7  
327/// 4 5  
328/// 3 2  
329/// 1 0
330const BRAILLE: &[char; 256] = &[
331    '⠀', '⢀', '⡀', '⣀', '⠠', '⢠', '⡠', '⣠', '⠄', '⢄', '⡄', '⣄', '⠤', '⢤', '⡤', '⣤', '⠐', '⢐', '⡐', '⣐', '⠰', '⢰', '⡰', '⣰',
332    '⠔', '⢔', '⡔', '⣔', '⠴', '⢴', '⡴', '⣴', '⠂', '⢂', '⡂', '⣂', '⠢', '⢢', '⡢', '⣢', '⠆', '⢆', '⡆', '⣆', '⠦', '⢦', '⡦', '⣦',
333    '⠒', '⢒', '⡒', '⣒', '⠲', '⢲', '⡲', '⣲', '⠖', '⢖', '⡖', '⣖', '⠶', '⢶', '⡶', '⣶', '⠈', '⢈', '⡈', '⣈', '⠨', '⢨', '⡨', '⣨',
334    '⠌', '⢌', '⡌', '⣌', '⠬', '⢬', '⡬', '⣬', '⠘', '⢘', '⡘', '⣘', '⠸', '⢸', '⡸', '⣸', '⠜', '⢜', '⡜', '⣜', '⠼', '⢼', '⡼', '⣼',
335    '⠊', '⢊', '⡊', '⣊', '⠪', '⢪', '⡪', '⣪', '⠎', '⢎', '⡎', '⣎', '⠮', '⢮', '⡮', '⣮', '⠚', '⢚', '⡚', '⣚', '⠺', '⢺', '⡺', '⣺',
336    '⠞', '⢞', '⡞', '⣞', '⠾', '⢾', '⡾', '⣾', '⠁', '⢁', '⡁', '⣁', '⠡', '⢡', '⡡', '⣡', '⠅', '⢅', '⡅', '⣅', '⠥', '⢥', '⡥', '⣥',
337    '⠑', '⢑', '⡑', '⣑', '⠱', '⢱', '⡱', '⣱', '⠕', '⢕', '⡕', '⣕', '⠵', '⢵', '⡵', '⣵', '⠃', '⢃', '⡃', '⣃', '⠣', '⢣', '⡣', '⣣',
338    '⠇', '⢇', '⡇', '⣇', '⠧', '⢧', '⡧', '⣧', '⠓', '⢓', '⡓', '⣓', '⠳', '⢳', '⡳', '⣳', '⠗', '⢗', '⡗', '⣗', '⠷', '⢷', '⡷', '⣷',
339    '⠉', '⢉', '⡉', '⣉', '⠩', '⢩', '⡩', '⣩', '⠍', '⢍', '⡍', '⣍', '⠭', '⢭', '⡭', '⣭', '⠙', '⢙', '⡙', '⣙', '⠹', '⢹', '⡹', '⣹',
340    '⠝', '⢝', '⡝', '⣝', '⠽', '⢽', '⡽', '⣽', '⠋', '⢋', '⡋', '⣋', '⠫', '⢫', '⡫', '⣫', '⠏', '⢏', '⡏', '⣏', '⠯', '⢯', '⡯', '⣯',
341    '⠛', '⢛', '⡛', '⣛', '⠻', '⢻', '⡻', '⣻', '⠟', '⢟', '⡟', '⣟', '⠿', '⢿', '⡿', '⣿',
342];
343
344impl Display for Logo {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        for y in (0..HEIGHT).step_by(4) {
347            if y > 0 {
348                writeln!(f)?;
349            }
350            for x in (0..WIDTH).step_by(2) {
351                let index = self.get_braille_index(x, y) as usize;
352                let ch = BRAILLE.get(index).unwrap_or(&' ');
353                write!(f, "{ch}")?;
354            }
355        }
356
357        Ok(())
358    }
359}