Skip to main content

ai_image/codecs/ico/
decoder.rs

1use alloc::{boxed::Box, vec::Vec};
2use byteorder_lite::{LittleEndian, ReadBytesExt};
3use core::{error, fmt};
4use no_std_io::io::{BufRead, Read, Seek, SeekFrom};
5
6use crate::color::ColorType;
7use crate::error::{
8    DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind,
9};
10use crate::{ImageDecoder, ImageFormat};
11
12use self::InnerDecoder::*;
13use crate::codecs::bmp::BmpDecoder;
14use crate::codecs::png::{PngDecoder, PNG_SIGNATURE};
15
16/// Errors that can occur during decoding and parsing an ICO image or one of its enclosed images.
17#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
18enum DecoderError {
19    /// The ICO directory is empty
20    NoEntries,
21    /// The number of color planes (0 or 1), or the horizontal coordinate of the hotspot for CUR files too big.
22    IcoEntryTooManyPlanesOrHotspot,
23    /// The bit depth (may be 0 meaning unspecified), or the vertical coordinate of the hotspot for CUR files too big.
24    IcoEntryTooManyBitsPerPixelOrHotspot,
25
26    /// The entry is in PNG format and specified a length that is shorter than PNG header.
27    PngShorterThanHeader,
28    /// The enclosed PNG is not in RGBA, which is invalid: <https://blogs.msdn.microsoft.com/oldnewthing/20101022-00/?p=12473>/.
29    PngNotRgba,
30
31    /// The entry is in BMP format and specified a data size that is not correct for the image and optional mask data.
32    InvalidDataSize,
33
34    /// The dimensions specified by the entry does not match the dimensions in the header of the enclosed image.
35    ImageEntryDimensionMismatch {
36        /// The mismatched subimage's type
37        format: IcoEntryImageFormat,
38        /// The dimensions specified by the entry
39        entry: (u16, u16),
40        /// The dimensions of the image itself
41        image: (u32, u32),
42    },
43}
44
45impl fmt::Display for DecoderError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            DecoderError::NoEntries => f.write_str("ICO directory contains no image"),
49            DecoderError::IcoEntryTooManyPlanesOrHotspot => {
50                f.write_str("ICO image entry has too many color planes or too large hotspot value")
51            }
52            DecoderError::IcoEntryTooManyBitsPerPixelOrHotspot => f.write_str(
53                "ICO image entry has too many bits per pixel or too large hotspot value",
54            ),
55            DecoderError::PngShorterThanHeader => {
56                f.write_str("Entry specified a length that is shorter than PNG header!")
57            }
58            DecoderError::PngNotRgba => f.write_str("The PNG is not in RGBA format!"),
59            DecoderError::InvalidDataSize => {
60                f.write_str("ICO image data size did not match expected size")
61            }
62            DecoderError::ImageEntryDimensionMismatch {
63                format,
64                entry,
65                image,
66            } => f.write_fmt(format_args!(
67                "Entry{entry:?} and {format}{image:?} dimensions do not match!"
68            )),
69        }
70    }
71}
72
73impl From<DecoderError> for ImageError {
74    fn from(e: DecoderError) -> ImageError {
75        ImageError::Decoding(DecodingError::new(ImageFormat::Ico.into(), e))
76    }
77}
78
79impl error::Error for DecoderError {}
80
81/// The image formats an ICO may contain
82#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
83enum IcoEntryImageFormat {
84    /// PNG in ARGB
85    Png,
86    /// BMP with optional alpha mask
87    Bmp,
88}
89
90impl fmt::Display for IcoEntryImageFormat {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_str(match self {
93            IcoEntryImageFormat::Png => "PNG",
94            IcoEntryImageFormat::Bmp => "BMP",
95        })
96    }
97}
98
99impl From<IcoEntryImageFormat> for ImageFormat {
100    fn from(val: IcoEntryImageFormat) -> Self {
101        match val {
102            IcoEntryImageFormat::Png => ImageFormat::Png,
103            IcoEntryImageFormat::Bmp => ImageFormat::Bmp,
104        }
105    }
106}
107
108/// An ico decoder
109pub struct IcoDecoder<R: BufRead + Seek> {
110    selected_entry: DirEntry,
111    inner_decoder: InnerDecoder<R>,
112}
113
114enum InnerDecoder<R: BufRead + Seek> {
115    Bmp(BmpDecoder<R>),
116    Png(Box<PngDecoder<R>>),
117}
118
119#[derive(Clone, Copy, Default)]
120struct DirEntry {
121    width: u8,
122    height: u8,
123    // We ignore some header fields as they will be replicated in the PNG, BMP and they are not
124    // necessary for determining the best_entry.
125    #[allow(unused)]
126    color_count: u8,
127    // Wikipedia has this to say:
128    // Although Microsoft's technical documentation states that this value must be zero, the icon
129    // encoder built into .NET (System.Drawing.Icon.Save) sets this value to 255. It appears that
130    // the operating system ignores this value altogether.
131    #[allow(unused)]
132    reserved: u8,
133
134    // We ignore some header fields as they will be replicated in the PNG, BMP and they are not
135    // necessary for determining the best_entry.
136    #[allow(unused)]
137    num_color_planes: u16,
138    bits_per_pixel: u16,
139
140    image_length: u32,
141    image_offset: u32,
142}
143
144impl<R: BufRead + Seek> IcoDecoder<R> {
145    /// Create a new decoder that decodes from the stream ```r```
146    pub fn new(mut r: R) -> ImageResult<IcoDecoder<R>> {
147        let entries = read_entries(&mut r)?;
148        let entry = best_entry(entries)?;
149        let decoder = entry.decoder(r)?;
150
151        Ok(IcoDecoder {
152            selected_entry: entry,
153            inner_decoder: decoder,
154        })
155    }
156}
157
158fn read_entries<R: Read>(r: &mut R) -> ImageResult<Vec<DirEntry>> {
159    let _reserved = r.read_u16::<LittleEndian>()?;
160    let _type = r.read_u16::<LittleEndian>()?;
161    let count = r.read_u16::<LittleEndian>()?;
162    (0..count).map(|_| read_entry(r)).collect()
163}
164
165fn read_entry<R: Read>(r: &mut R) -> ImageResult<DirEntry> {
166    Ok(DirEntry {
167        width: r.read_u8()?,
168        height: r.read_u8()?,
169        color_count: r.read_u8()?,
170        reserved: r.read_u8()?,
171        num_color_planes: {
172            // This may be either the number of color planes (0 or 1), or the horizontal coordinate
173            // of the hotspot for CUR files.
174            let num = r.read_u16::<LittleEndian>()?;
175            if num > 256 {
176                return Err(DecoderError::IcoEntryTooManyPlanesOrHotspot.into());
177            }
178            num
179        },
180        bits_per_pixel: {
181            // This may be either the bit depth (may be 0 meaning unspecified),
182            // or the vertical coordinate of the hotspot for CUR files.
183            let num = r.read_u16::<LittleEndian>()?;
184            if num > 256 {
185                return Err(DecoderError::IcoEntryTooManyBitsPerPixelOrHotspot.into());
186            }
187            num
188        },
189        image_length: r.read_u32::<LittleEndian>()?,
190        image_offset: r.read_u32::<LittleEndian>()?,
191    })
192}
193
194/// Find the entry with the highest (color depth, size).
195fn best_entry(mut entries: Vec<DirEntry>) -> ImageResult<DirEntry> {
196    let mut best = entries.pop().ok_or(DecoderError::NoEntries)?;
197
198    let mut best_score = (
199        best.bits_per_pixel,
200        u32::from(best.real_width()) * u32::from(best.real_height()),
201    );
202
203    for entry in entries {
204        let score = (
205            entry.bits_per_pixel,
206            u32::from(entry.real_width()) * u32::from(entry.real_height()),
207        );
208        if score > best_score {
209            best = entry;
210            best_score = score;
211        }
212    }
213    Ok(best)
214}
215
216impl DirEntry {
217    fn real_width(&self) -> u16 {
218        match self.width {
219            0 => 256,
220            w => u16::from(w),
221        }
222    }
223
224    fn real_height(&self) -> u16 {
225        match self.height {
226            0 => 256,
227            h => u16::from(h),
228        }
229    }
230
231    fn matches_dimensions(&self, width: u32, height: u32) -> bool {
232        u32::from(self.real_width()) == width.min(256)
233            && u32::from(self.real_height()) == height.min(256)
234    }
235
236    fn seek_to_start<R: Read + Seek>(&self, r: &mut R) -> ImageResult<()> {
237        r.seek(SeekFrom::Start(u64::from(self.image_offset)))?;
238        Ok(())
239    }
240
241    fn is_png<R: Read + Seek>(&self, r: &mut R) -> ImageResult<bool> {
242        self.seek_to_start(r)?;
243
244        // Read the first 8 bytes to sniff the image.
245        let mut signature = [0u8; 8];
246        r.read_exact(&mut signature)?;
247
248        Ok(signature == PNG_SIGNATURE)
249    }
250
251    fn decoder<R: BufRead + Seek>(&self, mut r: R) -> ImageResult<InnerDecoder<R>> {
252        let is_png = self.is_png(&mut r)?;
253        self.seek_to_start(&mut r)?;
254
255        if is_png {
256            Ok(Png(Box::new(PngDecoder::new(r)?)))
257        } else {
258            Ok(Bmp(BmpDecoder::new_with_ico_format(r)?))
259        }
260    }
261}
262
263impl<R: BufRead + Seek> ImageDecoder for IcoDecoder<R> {
264    fn dimensions(&self) -> (u32, u32) {
265        match self.inner_decoder {
266            Bmp(ref decoder) => decoder.dimensions(),
267            Png(ref decoder) => decoder.dimensions(),
268        }
269    }
270
271    fn color_type(&self) -> ColorType {
272        match self.inner_decoder {
273            Bmp(ref decoder) => decoder.color_type(),
274            Png(ref decoder) => decoder.color_type(),
275        }
276    }
277
278    fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
279        assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes()));
280        match self.inner_decoder {
281            Png(decoder) => {
282                if self.selected_entry.image_length < PNG_SIGNATURE.len() as u32 {
283                    return Err(DecoderError::PngShorterThanHeader.into());
284                }
285
286                // Check if the image dimensions match the ones in the image data.
287                let (width, height) = decoder.dimensions();
288                if !self.selected_entry.matches_dimensions(width, height) {
289                    return Err(DecoderError::ImageEntryDimensionMismatch {
290                        format: IcoEntryImageFormat::Png,
291                        entry: (
292                            self.selected_entry.real_width(),
293                            self.selected_entry.real_height(),
294                        ),
295                        image: (width, height),
296                    }
297                    .into());
298                }
299
300                // Embedded PNG images can only be of the 32BPP RGBA format.
301                // https://blogs.msdn.microsoft.com/oldnewthing/20101022-00/?p=12473/
302                if decoder.color_type() != ColorType::Rgba8 {
303                    return Err(DecoderError::PngNotRgba.into());
304                }
305
306                decoder.read_image(buf)
307            }
308            Bmp(mut decoder) => {
309                let (width, height) = decoder.dimensions();
310                if !self.selected_entry.matches_dimensions(width, height) {
311                    return Err(DecoderError::ImageEntryDimensionMismatch {
312                        format: IcoEntryImageFormat::Bmp,
313                        entry: (
314                            self.selected_entry.real_width(),
315                            self.selected_entry.real_height(),
316                        ),
317                        image: (width, height),
318                    }
319                    .into());
320                }
321
322                // The ICO decoder needs an alpha channel to apply the AND mask.
323                if decoder.color_type() != ColorType::Rgba8 {
324                    return Err(ImageError::Unsupported(
325                        UnsupportedError::from_format_and_kind(
326                            ImageFormat::Bmp.into(),
327                            UnsupportedErrorKind::Color(decoder.color_type().into()),
328                        ),
329                    ));
330                }
331
332                decoder.read_image_data(buf)?;
333
334                let r = decoder.reader();
335                // no_std_io::Seek doesn't provide stream_position()
336                #[allow(clippy::seek_from_current)]
337                let image_end = r.seek(SeekFrom::Current(0))?;
338                let data_end = u64::from(self.selected_entry.image_offset)
339                    + u64::from(self.selected_entry.image_length);
340
341                let mask_row_bytes = width.div_ceil(32) * 4;
342                let mask_length = u64::from(mask_row_bytes) * u64::from(height);
343
344                // data_end should be image_end + the mask length (mask_row_bytes * height).
345                // According to
346                // https://devblogs.microsoft.com/oldnewthing/20101021-00/?p=12483
347                // the mask is required, but according to Wikipedia
348                // https://en.wikipedia.org/wiki/ICO_(file_format)
349                // the mask is not required. Unfortunately, Wikipedia does not have a citation
350                // for that claim, so we can't be sure which is correct.
351                if data_end >= image_end + mask_length {
352                    // If there's an AND mask following the image, read and apply it.
353                    for y in 0..height {
354                        let mut x = 0;
355                        for _ in 0..mask_row_bytes {
356                            // Apply the bits of each byte until we reach the end of the row.
357                            let mask_byte = r.read_u8()?;
358                            for bit in (0..8).rev() {
359                                if x >= width {
360                                    break;
361                                }
362                                if mask_byte & (1 << bit) != 0 {
363                                    // Set alpha channel to transparent.
364                                    buf[((height - y - 1) * width + x) as usize * 4 + 3] = 0;
365                                }
366                                x += 1;
367                            }
368                        }
369                    }
370
371                    Ok(())
372                } else if data_end == image_end {
373                    // accept images with no mask data
374                    Ok(())
375                } else {
376                    Err(DecoderError::InvalidDataSize.into())
377                }
378            }
379        }
380    }
381
382    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
383        (*self).read_image(buf)
384    }
385}
386
387#[cfg(test)]
388mod test {
389    use super::*;
390
391    // Test if BMP images without alpha channel inside ICOs don't panic.
392    // Because the test data is invalid decoding should produce an error.
393    #[test]
394    fn bmp_16_with_missing_alpha_channel() {
395        let data = vec![
396            0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0e, 0x04, 0xc3, 0x7e, 0x00, 0x00, 0x00, 0x00,
397            0x7c, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0x01, 0x00,
398            0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
399            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
400            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
401            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
402            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
403            0x00, 0x00, 0x00, 0x8f, 0xf6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
404            0x20, 0x66, 0x74, 0x83, 0x70, 0x61, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
405            0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xeb, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x00,
406            0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0x47, 0x0d,
407            0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x62, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
408            0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c,
409            0x00, 0x00, 0x00, 0xc3, 0x3f, 0x94, 0x61, 0xaa, 0x17, 0x4d, 0x8d, 0x79, 0x1d, 0x8b,
410            0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x2e, 0x28, 0x40, 0xe5, 0x9f,
411            0x4b, 0x4d, 0xe9, 0x87, 0xd3, 0xda, 0xd6, 0x89, 0x81, 0xc5, 0xa4, 0xa1, 0x60, 0x98,
412            0x31, 0xc7, 0x1d, 0xb6, 0x8f, 0x20, 0xc8, 0x3e, 0xee, 0xd8, 0xe4, 0x8f, 0xee, 0x7b,
413            0x48, 0x9b, 0x88, 0x25, 0x13, 0xda, 0xa4, 0x13, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x40,
414            0x16, 0x01, 0xff, 0xff, 0xff, 0xff, 0xe9, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
415            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
416            0x00, 0x00, 0xa3, 0x66, 0x64, 0x41, 0x54, 0xa3, 0xa3, 0x00, 0x00, 0x00, 0xb8, 0x00,
417            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
418            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x66, 0x64, 0x41, 0x54, 0xa3, 0xa3,
419            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
420            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
421            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8f, 0xf6, 0xff, 0xff,
422            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x83, 0x70, 0x61, 0x76,
423            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff,
424            0xeb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
425            0x00, 0x00, 0x00, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x62, 0x49,
426            0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
427            0x00, 0x00, 0x00, 0xff, 0xff, 0x94, 0xc8, 0x00, 0x02, 0x0c, 0x00, 0xff, 0xff, 0xc6,
428            0x84, 0x00, 0x2a, 0x75, 0x03, 0xa3, 0x05, 0xfb, 0xe1, 0x6e, 0xe8, 0x27, 0xd6, 0xd3,
429            0x96, 0xc1, 0xe4, 0x30, 0x0c, 0x05, 0xb9, 0xa3, 0x8b, 0x29, 0xda, 0xa4, 0xf1, 0x4d,
430            0xf3, 0xb2, 0x98, 0x2b, 0xe6, 0x93, 0x07, 0xf9, 0xca, 0x2b, 0xc2, 0x39, 0x20, 0xba,
431            0x7c, 0xa0, 0xb1, 0x43, 0xe6, 0xf9, 0xdc, 0xd1, 0xc2, 0x52, 0xdc, 0x41, 0xc1, 0x2f,
432            0x29, 0xf7, 0x46, 0x32, 0xda, 0x1b, 0x72, 0x8c, 0xe6, 0x2b, 0x01, 0xe5, 0x49, 0x21,
433            0x89, 0x89, 0xe4, 0x3d, 0xa1, 0xdb, 0x3b, 0x4a, 0x0b, 0x52, 0x86, 0x52, 0x33, 0x9d,
434            0xb2, 0xcf, 0x4a, 0x86, 0x53, 0xd7, 0xa9, 0x4b, 0xaf, 0x62, 0x06, 0x49, 0x53, 0x00,
435            0xc3, 0x3f, 0x94, 0x61, 0xaa, 0x17, 0x4d, 0x8d, 0x79, 0x1d, 0x8b, 0x10, 0x00, 0x00,
436            0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x2e, 0x28, 0x40, 0xe5, 0x9f, 0x4b, 0x4d, 0xe9,
437            0x87, 0xd3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe7, 0xc5, 0x00,
438            0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0b, 0x00, 0x50, 0x31, 0x00, 0x00, 0x00, 0x00,
439            0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x76, 0x76, 0x01, 0x00, 0x00, 0x00, 0x76, 0x00,
440            0x00, 0x23, 0x3f, 0x52, 0x41, 0x44, 0x49, 0x41, 0x4e, 0x43, 0x45, 0x61, 0x50, 0x35,
441            0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x4d, 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x05,
442            0x50, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x37, 0x61,
443        ];
444
445        let decoder = IcoDecoder::new(no_std_io::io::Cursor::new(&data)).unwrap();
446        let mut buf = vec![0; usize::try_from(decoder.total_bytes()).unwrap()];
447        assert!(decoder.read_image(&mut buf).is_err());
448    }
449}