Skip to main content

image_extras/
icns.rs

1//! Decoding of ICNS image files
2//!
3//! The .icns (Apple Icon Image format) format is an icon format used by macOS.
4//!
5//! Similar to the `image` crate's .ico decoder, this decoder just extracts the
6//! "best" contained image in the file (largest, highest bit depth, earliest
7//! option.)
8//!
9//! Nested icns files are ignored, as may be unsupported icon image types.
10//!
11//! This implementation does not include support for decoding JPEG 2000
12//! subimages, but [IcnsDecoder::new_with_decode_func] can be used to provide
13//! your own implementation. The default image format hook registered in
14//! [crate::register] will, if the "best" image entry happens to use JPEG 2000,
15//! try to decode the subimage using the image format hook registered for JP2
16//! images, if one has been set up.
17//!
18//! # Related Links
19//! * <https://en.wikipedia.org/wiki/Apple_Icon_Image_format> - ICNS format on Wikipedia
20//! * <https://web.archive.org/web/20180618155438/https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/app-icon/> - ICNS is no longer recommended for macOS icons
21
22use std::collections::HashMap;
23use std::fmt::{self, Display};
24use std::io::{Cursor, Read, Seek, SeekFrom};
25
26use image::error::{
27    DecodingError, ImageFormatHint, LimitError, LimitErrorKind, UnsupportedError,
28    UnsupportedErrorKind,
29};
30use image::{ColorType, ImageDecoder, ImageError, ImageReader, ImageResult, LimitSupport, Limits};
31
32use icns::{Encoding, IconElement, IconType, OSType, PixelFormat};
33
34/// Errors that can occur during decoding and parsing an ICO image or one of its enclosed images.
35#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
36enum DecoderError {
37    /// The end of the image occurs at a stream position > u64::MAX
38    ImageEndAfterEndOfStream,
39    /// The icon file does not begin with b"icns"
40    NotICNS,
41    /// The icon file contains multiple image entries of a given type
42    DuplicateEntry(IconType),
43    /// The last record in the file does not end exactly at the ICNS file end point
44    IncompleteEntry,
45    /// Did not find a decodable image entry
46    NoImageFound,
47    /// The given entry type requires a corresponding mask element, but none was found
48    MissingMask(IconType),
49    /// Image entry length field impossibly short
50    BadEntryLength,
51    /// An image entry, expected to be have either PNG or Jpeg 2000 content, had neither
52    NotPNGorJP2(IconType),
53    /// An image entry with PNG contents had size inconsistent with the icon
54    BadPngSize(u32, u32, u32),
55    /// An image entry with JP2 contents had size inconsistent with the icon
56    BadJp2Size(u32, u32, u32),
57}
58
59impl From<DecoderError> for ImageError {
60    fn from(e: DecoderError) -> ImageError {
61        ImageError::Decoding(DecodingError::new(icns_format_hint(), e))
62    }
63}
64
65impl Display for DecoderError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            DecoderError::ImageEndAfterEndOfStream => {
69                f.write_str("The end of the image would have a stream position >u64::MAX")
70            }
71            DecoderError::NotICNS => f.write_str("Image file does not start with 'icns'"),
72            DecoderError::DuplicateEntry(t) => f.write_fmt(format_args!(
73                "Image file contains multiple entries of type {t:?}"
74            )),
75            DecoderError::MissingMask(t) => f.write_fmt(format_args!(
76                "Image file did not contain a mask entry for the entry of type {t:?}"
77            )),
78            DecoderError::IncompleteEntry => f.write_str(
79                "The last entry in the file would extend past the end of file indicated by the header"
80            ),
81            DecoderError::NoImageFound => f.write_str(
82                "No image entry that the decoder might support was found"
83            ),
84            DecoderError::BadEntryLength => f.write_str(
85                "Image file contained an entry with invalid length (less than 8)"
86            ),
87            DecoderError::NotPNGorJP2(t) => f.write_fmt(format_args!(
88                "Image file entry of type {t:?} contained neither PNG nor Jpeg 2000 data"
89            )),
90            DecoderError::BadPngSize(w,h,s) => f.write_fmt(format_args!(
91                "Image file entry with PNG data had size {w}x{h} instead of expected {s}x{s}"
92            )),
93            DecoderError::BadJp2Size(w,h,s) => f.write_fmt(format_args!(
94                "Image file entry with Jpeg 2000 data had size {w}x{h} instead of expected {s}x{s}"
95            )),
96        }
97    }
98}
99impl std::error::Error for DecoderError {}
100
101fn icns_format_hint() -> ImageFormatHint {
102    ImageFormatHint::Name("ICNS".into())
103}
104
105/// Adapt PNG decoding errors to ImageError, adding an ICNS format hint when possible
106fn png_to_image_error(err: png::DecodingError) -> ImageError {
107    use png::DecodingError::*;
108    match err {
109        IoError(err) => ImageError::IoError(err),
110        // The input image was not a valid PNG.
111        err @ Format(_) => ImageError::Decoding(DecodingError::new(icns_format_hint(), err)),
112        Parameter(_) => unreachable!(),
113        LimitsExceeded => {
114            ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
115        }
116    }
117}
118
119/// When possible, wrap the error passed in with an ICNS format hint.
120fn jp2_to_image_error(err: ImageError) -> ImageError {
121    match err {
122        ImageError::Decoding(e) => ImageError::Decoding(DecodingError::new(icns_format_hint(), e)),
123        ImageError::Encoding(_) => {
124            // Should not be encoding any files
125            unreachable!();
126        }
127        ImageError::Parameter(e) => ImageError::Parameter(e),
128        ImageError::Limits(e) => ImageError::Limits(e),
129        ImageError::Unsupported(e) => {
130            ImageError::Decoding(DecodingError::new(icns_format_hint(), e))
131        }
132        ImageError::IoError(e) => ImageError::IoError(e),
133    }
134}
135
136/// A function type used to decode a square embedded image.
137///
138/// Arguments:
139/// - `data: &[u8]`: complete data for the embedded image to decode
140/// - `size: u32`: the expected width and height of the image. Will be `> 0` and `<= 1024`
141/// - `buf: &mut [u8]`: array of bytes into which to write RGBA data. Will have size `4*size*size`
142/// - `allocation_limit: u64`: a soft limit on how much memory to allocate while decoding
143pub type SubformatDecodeFn = Box<dyn Fn(&[u8], u32, &mut [u8], u64) -> ImageResult<()>>;
144
145/// A function of type [SubformatDecodeFn] that decodes the PNG image in `data` into `buf`.
146///
147/// This returns an error if data is invalid or the image's dimensions do no exactly
148/// match (size, size).
149fn decode_png(data: &[u8], size: u32, buf: &mut [u8], allocation_limit: u64) -> ImageResult<()> {
150    let mut decoder = png::Decoder::new_with_limits(
151        Cursor::new(data),
152        png::Limits {
153            bytes: allocation_limit.try_into().unwrap_or(usize::MAX),
154        },
155    );
156    // Transform to produce La8 or Rgba8 output.
157    // TODO: add flag GRAY_TO_RGB when `png` implements it
158    decoder.set_transformations(png::Transformations::STRIP_16 | png::Transformations::ALPHA);
159    let info = decoder.read_header_info().map_err(png_to_image_error)?;
160
161    if info.width != size || info.height != size {
162        return Err(DecoderError::BadPngSize(info.width, info.height, size).into());
163    }
164
165    let mut reader = decoder.read_info().map_err(png_to_image_error)?;
166    let (color_type, bits) = reader.output_color_type();
167
168    assert!(bits == png::BitDepth::Eight);
169    match color_type {
170        png::ColorType::GrayscaleAlpha => {
171            // The space for this temporary vector was accounted for in `IcnsDecoder::set_limits`
172            let mut tmp = vec![0u8; (size as usize) * (size as usize) * 2];
173            reader.next_frame(&mut tmp).map_err(png_to_image_error)?;
174
175            for (ga, rgba) in tmp.chunks_exact(2).zip(buf.chunks_exact_mut(4)) {
176                rgba.copy_from_slice(&[ga[0], ga[0], ga[0], ga[1]]);
177            }
178        }
179        png::ColorType::Rgba => {
180            reader.next_frame(buf).map_err(png_to_image_error)?;
181        }
182        _ => unreachable!(),
183    }
184
185    reader.finish().map_err(png_to_image_error)?;
186
187    Ok(())
188}
189
190/// A function of type [SubformatDecodeFn] that tries to decode the jpeg 2000 image
191/// using the `image` crate's decoding hooks.
192///
193/// This will only work if a format hook for JPEG 2000 has been registered.
194pub fn decode_jpeg2000_using_hook(
195    data: &[u8],
196    size: u32,
197    buf: &mut [u8],
198    allocation_limit: u64,
199) -> ImageResult<()> {
200    // The magic bytes have already been checked by IcnsDecoder, so it is unlikely
201    // that a different format's decoder will be used be accident.
202    // TODO:  explicitly set JPEG 2000 as the format, to be certain
203    let mut reader = ImageReader::new(Cursor::new(data));
204    reader = reader.with_guessed_format()?;
205    let mut limits = Limits::no_limits();
206    limits.max_alloc = Some(allocation_limit);
207    reader.limits(limits);
208
209    let image = reader.decode().map_err(jp2_to_image_error)?;
210    if image.width() != size || image.height() != size {
211        return Err(DecoderError::BadJp2Size(image.width(), image.height(), size).into());
212    }
213
214    buf.copy_from_slice(image.to_rgba8().as_flat_samples().samples);
215
216    Ok(())
217}
218
219/// A function of type [SubformatDecodeFn] which just returns an error instead of trying
220/// to decode the provided JPEG 2000 icon data.
221pub fn unsupported_jpeg2000(
222    _data: &[u8],
223    _size: u32,
224    _buf: &mut [u8],
225    _allocation_limit: u64,
226) -> ImageResult<()> {
227    Err(ImageError::Unsupported(
228        UnsupportedError::from_format_and_kind(
229            icns_format_hint(),
230            UnsupportedErrorKind::GenericFeature("Jpeg 2000 subimage".to_string()),
231        ),
232    ))
233}
234
235#[derive(Clone, Copy)]
236struct IcnsEntry {
237    code: IconType,
238    stream_pos: u64,
239    length: u32,
240}
241
242impl IcnsEntry {
243    /// Return a lexicographically sortable score for how suitable it
244    /// is as the "best" image in the file.
245    ///
246    /// This requires that the entry contains non-mask content (does not have encoding Mask8).
247    fn score(&self) -> (u32, u8, u64) {
248        let bit_depth = match self.code.encoding() {
249            Encoding::Mask8 => {
250                panic!("Entry with color data required, not Mask8");
251            }
252            Encoding::Mono => 1,
253            Encoding::MonoA => 2,
254            // Paletted entries have an associated 1 bit mask
255            Encoding::Palette4 => 5,
256            Encoding::Palette8 => 9,
257            // RLE24 entries have an associated 8 bit mask
258            Encoding::RLE24 => 32,
259            Encoding::JP2PNG => 32,
260        };
261        (
262            self.code.pixel_width(),
263            bit_depth,
264            u64::MAX - self.stream_pos,
265        )
266    }
267}
268
269/// ICNS decoder
270pub struct IcnsDecoder<R> {
271    reader: R,
272
273    main: IcnsEntry,
274    // If a mask entry applies to the main image, its details will be indicated here
275    mask: Option<IcnsEntry>,
276
277    limits: Limits,
278    jp2: SubformatDecodeFn,
279}
280
281/// Read the data from the reader for the stream position range `start..start + len`
282fn read_vec_at<R>(reader: &mut R, start: u64, len: u32) -> Result<Vec<u8>, std::io::Error>
283where
284    R: Read + Seek,
285{
286    assert!(start.checked_add(len as u64).is_some());
287    reader.seek(SeekFrom::Start(start))?;
288
289    let mut data = vec![0; len.try_into().unwrap()];
290    reader.read_exact(&mut data)?;
291    Ok(data)
292}
293
294impl<R> IcnsDecoder<R>
295where
296    R: Read + Seek,
297{
298    /// Create a new `IcnsDecoder` and seek around the input file to locate
299    /// the "best" image. ("best" here means largest, breaking ties to prefer
300    /// higher total bit depth; if still tied the earliest image is chosen.)
301    ///
302    /// The resulting decoder does not support decoding Jpeg2000 image entries.
303    /// Use [IcnsDecoder::new_with_decode_funcs] if you'd like to supply your
304    /// own function for that.
305    pub fn new(reader: R) -> Result<IcnsDecoder<R>, ImageError> {
306        Self::new_with_decode_func(reader, Box::new(unsupported_jpeg2000))
307    }
308
309    /// Create a new `IcnsDecoder` and seek around the input file to locate
310    /// the "best" image. ("best" here means largest, breaking ties to prefer
311    /// higher total bit depth; if still tied the earliest image is chosen.)
312    ///
313    /// The ICNS format can nest PNG and JP2 images; this function accepts a
314    /// function that can be used to decode the JP2 images. See for example
315    /// [unsupported_jpeg2000], and [decode_jpeg2000_using_hook].
316    pub fn new_with_decode_func(
317        mut reader: R,
318        jp2: SubformatDecodeFn,
319    ) -> Result<IcnsDecoder<R>, ImageError> {
320        let mut header = [0u8; 8];
321        reader.read_exact(&mut header)?;
322        let (magic, file_len_field) = header.split_at(4);
323        if magic != b"icns" {
324            return Err(DecoderError::NotICNS.into());
325        }
326        let file_length = u32::from_be_bytes(file_len_field.try_into().unwrap());
327        let Some(remaining_len) = file_length.checked_sub(8) else {
328            return Err(DecoderError::BadEntryLength.into());
329        };
330
331        // Record all decodable entries. Because only the first entry of each known
332        // icon type is recorded, the map's size is bounded.
333        let mut first_entries: HashMap<IconType, IcnsEntry> = HashMap::new();
334
335        let base_pos = reader.stream_position()?;
336        let Some(end) = base_pos.checked_add(u64::from(remaining_len)) else {
337            return Err(DecoderError::ImageEndAfterEndOfStream.into());
338        };
339
340        // Loop over all entries of the file, ignoring unrecognized elements.
341        let mut cur_pos = base_pos;
342        while cur_pos < end {
343            let image_start_pos = cur_pos;
344            if cur_pos > end.saturating_sub(8) {
345                return Err(DecoderError::IncompleteEntry.into());
346            }
347
348            let mut entry = [0u8; 8];
349            reader.read_exact(&mut entry)?;
350            let (ostype_field, entry_len_field) = entry.split_at(4);
351            let ostype = OSType(ostype_field.try_into().unwrap());
352            let entry_len = u32::from_be_bytes(entry_len_field.try_into().unwrap());
353
354            let Some(data_len) = entry_len.checked_sub(8) else {
355                return Err(DecoderError::BadEntryLength.into());
356            };
357            if cur_pos > end.saturating_sub(entry_len as u64) {
358                return Err(DecoderError::IncompleteEntry.into());
359            }
360            if cur_pos < end {
361                reader.seek_relative(data_len as i64)?;
362            }
363            cur_pos += entry_len as u64;
364
365            // Identify the entry
366            let Some(code) = IconType::from_ostype(ostype) else {
367                // Silently ignore unrecognized fields; ICNS files may contain more data
368                continue;
369            };
370
371            let entry = IcnsEntry {
372                code,
373                stream_pos: image_start_pos + 8,
374                length: data_len,
375            };
376            if first_entries.insert(code, entry).is_some() {
377                return Err(DecoderError::DuplicateEntry(code).into());
378            }
379        }
380
381        let main_entry = first_entries
382            .values()
383            .filter(|entry| entry.code.encoding() != Encoding::Mask8)
384            .max_by_key(|entry| entry.score());
385
386        let Some(main) = main_entry.copied() else {
387            return Err(DecoderError::NoImageFound.into());
388        };
389        let mut mask = None;
390        if let Some(mtype) = main.code.mask_type() {
391            mask = first_entries.get(&mtype).copied();
392            if mask.is_none() {
393                return Err(DecoderError::MissingMask(main.code).into());
394            }
395        }
396        Ok(IcnsDecoder {
397            reader,
398            main,
399            mask,
400            limits: Limits::no_limits(),
401            jp2,
402        })
403    }
404}
405
406impl<R: Read + Seek> ImageDecoder for IcnsDecoder<R> {
407    fn dimensions(&self) -> (u32, u32) {
408        (self.main.code.pixel_width(), self.main.code.pixel_height())
409    }
410
411    fn color_type(&self) -> ColorType {
412        match self.main.code.encoding() {
413            Encoding::Mask8 => unreachable!(),
414            Encoding::Mono => ColorType::L8,
415            Encoding::MonoA => ColorType::La8,
416            _ => ColorType::Rgba8,
417        }
418    }
419
420    fn set_limits(&mut self, mut limits: Limits) -> ImageResult<()> {
421        limits.check_support(&LimitSupport::default())?;
422        let (width, height) = self.dimensions();
423        limits.check_dimensions(width, height)?;
424
425        let icon_size = self.main.code.pixel_width();
426
427        let main_data = self.main.length;
428        let mask_data = self.mask.map(|x| x.length).unwrap_or_default();
429
430        // Decoding non PNG/JP2 icons using `icns` can create temporary images using
431        // a maximum of about 4 + 4 bytes per pixel (up to 4 for the initial decoding
432        // result, plus up to 4 to store a conversion to RGBA.) This may change if the
433        // dependency is updated.
434        //
435        // Decoding PNG icon entries may use an additional 2 bytes per pixel (to handle
436        // grayscale images), which is also < 8.
437        //
438        // Tight memory bounds aren't critical here, because ICNS entries are at most
439        // 1024x1024 pixels.
440        assert!(icon_size <= 1024);
441        let icon_decode_space = icon_size * icon_size * 8;
442
443        let space_req = u64::from(icon_decode_space) + u64::from(main_data) + u64::from(mask_data);
444        limits.reserve(space_req)?;
445
446        self.limits = limits;
447
448        Ok(())
449    }
450
451    fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
452        assert!(self.total_bytes() == buf.len().try_into().unwrap());
453
454        let main_data = read_vec_at(&mut self.reader, self.main.stream_pos, self.main.length)?;
455
456        const PNG_SIGNATURE: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
457        const JP2_SIGNATURE: &[u8] = &[
458            0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A,
459        ];
460
461        if self.main.code.encoding() == Encoding::JP2PNG {
462            // Handle JP2 or PNG images _directly_ in this implementation, in order
463            // to implement memory limits and make it easier to keep these complicated
464            // format decoders up to date.
465            if main_data.starts_with(PNG_SIGNATURE) {
466                decode_png(
467                    &main_data,
468                    self.main.code.pixel_width(),
469                    buf,
470                    self.limits.max_alloc.unwrap_or(u64::MAX),
471                )?;
472            } else if main_data.starts_with(JP2_SIGNATURE) {
473                (self.jp2)(
474                    &main_data,
475                    self.main.code.pixel_width(),
476                    buf,
477                    self.limits.max_alloc.unwrap_or(u64::MAX),
478                )?;
479            } else {
480                return Err(DecoderError::NotPNGorJP2(self.main.code).into());
481            }
482        } else {
483            let main = IconElement::new(self.main.code.ostype(), main_data);
484
485            let img = if let Some(mask_entry) = &self.mask {
486                let mask_data =
487                    read_vec_at(&mut self.reader, mask_entry.stream_pos, mask_entry.length)?;
488                let mask = IconElement::new(mask_entry.code.ostype(), mask_data);
489
490                main.decode_image_with_mask(&mask)?
491            } else {
492                assert!(self.main.code.mask_type().is_none());
493                main.decode_image()?
494            };
495            assert!((img.width(), img.height()) == self.dimensions());
496            assert!(img.pixel_format() != PixelFormat::Alpha);
497
498            match (img.pixel_format(), self.color_type()) {
499                (PixelFormat::Gray, ColorType::L8) => {
500                    buf.copy_from_slice(img.data());
501                }
502                (PixelFormat::GrayAlpha, ColorType::La8) => {
503                    buf.copy_from_slice(img.data());
504                }
505                (PixelFormat::RGBA, ColorType::Rgba8)
506                | (PixelFormat::RGB, ColorType::Rgba8)
507                | (PixelFormat::Gray, ColorType::Rgba8)
508                | (PixelFormat::GrayAlpha, ColorType::Rgba8) => {
509                    let converted = img.convert_to(PixelFormat::RGBA);
510                    buf.copy_from_slice(converted.data());
511                }
512
513                // This format combination would be an error in the `icns` crate
514                _ => unreachable!(
515                    "icns crate produced {:?}, not compatible with {:?}",
516                    img.pixel_format(),
517                    self.color_type()
518                ),
519            };
520        }
521
522        Ok(())
523    }
524
525    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
526        (*self).read_image(buf)
527    }
528}