image/codecs/ico/
encoder.rs

1use byteorder_lite::{LittleEndian, WriteBytesExt};
2use std::borrow::Cow;
3use std::io::{self, Write};
4
5use crate::codecs::png::PngEncoder;
6use crate::error::{ImageError, ImageResult, ParameterError, ParameterErrorKind};
7use crate::{ExtendedColorType, ImageEncoder};
8
9// Enum value indicating an ICO image (as opposed to a CUR image):
10const ICO_IMAGE_TYPE: u16 = 1;
11// The length of an ICO file ICONDIR structure, in bytes:
12const ICO_ICONDIR_SIZE: u32 = 6;
13// The length of an ICO file DIRENTRY structure, in bytes:
14const ICO_DIRENTRY_SIZE: u32 = 16;
15
16/// ICO encoder
17pub struct IcoEncoder<W: Write> {
18    w: W,
19}
20
21/// An ICO image entry
22pub struct IcoFrame<'a> {
23    // Pre-encoded PNG or BMP
24    encoded_image: Cow<'a, [u8]>,
25    // Stored as `0 => 256, n => n`
26    width: u8,
27    // Stored as `0 => 256, n => n`
28    height: u8,
29    color_type: ExtendedColorType,
30}
31
32impl<'a> IcoFrame<'a> {
33    /// Construct a new `IcoFrame` using a pre-encoded PNG or BMP
34    ///
35    /// The `width` and `height` must be between 1 and 256 (inclusive).
36    pub fn with_encoded(
37        encoded_image: impl Into<Cow<'a, [u8]>>,
38        width: u32,
39        height: u32,
40        color_type: ExtendedColorType,
41    ) -> ImageResult<Self> {
42        let encoded_image = encoded_image.into();
43
44        if !(1..=256).contains(&width) {
45            return Err(ImageError::Parameter(ParameterError::from_kind(
46                ParameterErrorKind::Generic(format!(
47                    "the image width must be `1..=256`, instead width {width} was provided",
48                )),
49            )));
50        }
51
52        if !(1..=256).contains(&height) {
53            return Err(ImageError::Parameter(ParameterError::from_kind(
54                ParameterErrorKind::Generic(format!(
55                    "the image height must be `1..=256`, instead height {height} was provided",
56                )),
57            )));
58        }
59
60        Ok(Self {
61            encoded_image,
62            width: width as u8,
63            height: height as u8,
64            color_type,
65        })
66    }
67
68    /// Construct a new `IcoFrame` by encoding `buf` as a PNG
69    ///
70    /// The `width` and `height` must be between 1 and 256 (inclusive)
71    pub fn as_png(
72        buf: &[u8],
73        width: u32,
74        height: u32,
75        color_type: ExtendedColorType,
76    ) -> ImageResult<Self> {
77        let mut image_data: Vec<u8> = Vec::new();
78        PngEncoder::new(&mut image_data).write_image(buf, width, height, color_type)?;
79
80        let frame = Self::with_encoded(image_data, width, height, color_type)?;
81        Ok(frame)
82    }
83}
84
85impl<W: Write> IcoEncoder<W> {
86    /// Create a new encoder that writes its output to ```w```.
87    pub fn new(w: W) -> IcoEncoder<W> {
88        IcoEncoder { w }
89    }
90
91    /// Takes some [`IcoFrame`]s and encodes them into an ICO.
92    ///
93    /// `images` is a list of images, usually ordered by dimension, which
94    /// must be between 1 and 65535 (inclusive) in length.
95    pub fn encode_images(mut self, images: &[IcoFrame<'_>]) -> ImageResult<()> {
96        if !(1..=usize::from(u16::MAX)).contains(&images.len()) {
97            return Err(ImageError::Parameter(ParameterError::from_kind(
98                ParameterErrorKind::Generic(format!(
99                    "the number of images must be `1..=u16::MAX`, instead {} images were provided",
100                    images.len(),
101                )),
102            )));
103        }
104        let num_images = images.len() as u16;
105
106        let mut offset = ICO_ICONDIR_SIZE + (ICO_DIRENTRY_SIZE * (images.len() as u32));
107        write_icondir(&mut self.w, num_images)?;
108        for image in images {
109            write_direntry(
110                &mut self.w,
111                image.width,
112                image.height,
113                image.color_type,
114                offset,
115                image.encoded_image.len() as u32,
116            )?;
117
118            offset += image.encoded_image.len() as u32;
119        }
120        for image in images {
121            self.w.write_all(&image.encoded_image)?;
122        }
123        Ok(())
124    }
125}
126
127impl<W: Write> ImageEncoder for IcoEncoder<W> {
128    /// Write an ICO image with the specified width, height, and color type.
129    ///
130    /// For color types with 16-bit per channel or larger, the contents of `buf` should be in
131    /// native endian.
132    ///
133    /// WARNING: In image 0.23.14 and earlier this method erroneously expected buf to be in big endian.
134    #[track_caller]
135    fn write_image(
136        self,
137        buf: &[u8],
138        width: u32,
139        height: u32,
140        color_type: ExtendedColorType,
141    ) -> ImageResult<()> {
142        let expected_buffer_len = color_type.buffer_size(width, height);
143        assert_eq!(
144            expected_buffer_len,
145            buf.len() as u64,
146            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
147            buf.len(),
148        );
149
150        let image = IcoFrame::as_png(buf, width, height, color_type)?;
151        self.encode_images(&[image])
152    }
153}
154
155fn write_icondir<W: Write>(w: &mut W, num_images: u16) -> io::Result<()> {
156    // Reserved field (must be zero):
157    w.write_u16::<LittleEndian>(0)?;
158    // Image type (ICO or CUR):
159    w.write_u16::<LittleEndian>(ICO_IMAGE_TYPE)?;
160    // Number of images in the file:
161    w.write_u16::<LittleEndian>(num_images)?;
162    Ok(())
163}
164
165fn write_direntry<W: Write>(
166    w: &mut W,
167    width: u8,
168    height: u8,
169    color: ExtendedColorType,
170    data_start: u32,
171    data_size: u32,
172) -> io::Result<()> {
173    // Image dimensions:
174    w.write_u8(width)?;
175    w.write_u8(height)?;
176    // Number of colors in palette (or zero for no palette):
177    w.write_u8(0)?;
178    // Reserved field (must be zero):
179    w.write_u8(0)?;
180    // Color planes:
181    w.write_u16::<LittleEndian>(0)?;
182    // Bits per pixel:
183    w.write_u16::<LittleEndian>(color.bits_per_pixel())?;
184    // Image data size, in bytes:
185    w.write_u32::<LittleEndian>(data_size)?;
186    // Image data offset, in bytes:
187    w.write_u32::<LittleEndian>(data_start)?;
188    Ok(())
189}