Coverage Report

Created: 2026-06-18 07:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/codecs/ico/encoder.rs
Line
Count
Source
1
use byteorder_lite::{LittleEndian, WriteBytesExt};
2
use std::borrow::Cow;
3
use std::io::{self, Write};
4
5
use crate::codecs::png::PngEncoder;
6
use crate::error::{EncodingError, ImageError, ImageResult, ParameterError, ParameterErrorKind};
7
use crate::{ExtendedColorType, ImageEncoder, ImageFormat};
8
9
// Enum value indicating an ICO image (as opposed to a CUR image):
10
const ICO_IMAGE_TYPE: u16 = 1;
11
// The length of an ICO file ICONDIR structure, in bytes:
12
const ICO_ICONDIR_SIZE: u32 = 6;
13
// The length of an ICO file DIRENTRY structure, in bytes:
14
const ICO_DIRENTRY_SIZE: u32 = 16;
15
16
/// ICO encoder
17
pub struct IcoEncoder<W: Write> {
18
    w: W,
19
}
20
21
/// An ICO image entry
22
pub 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
32
impl<'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
0
    pub fn with_encoded(
37
0
        encoded_image: impl Into<Cow<'a, [u8]>>,
38
0
        width: u32,
39
0
        height: u32,
40
0
        color_type: ExtendedColorType,
41
0
    ) -> ImageResult<Self> {
42
0
        let encoded_image = encoded_image.into();
43
44
0
        if !(1..=256).contains(&width) {
45
0
            return Err(ImageError::Parameter(ParameterError::from_kind(
46
0
                ParameterErrorKind::Generic(format!(
47
0
                    "the image width must be `1..=256`, instead width {width} was provided",
48
0
                )),
49
0
            )));
50
0
        }
51
52
0
        if !(1..=256).contains(&height) {
53
0
            return Err(ImageError::Parameter(ParameterError::from_kind(
54
0
                ParameterErrorKind::Generic(format!(
55
0
                    "the image height must be `1..=256`, instead height {height} was provided",
56
0
                )),
57
0
            )));
58
0
        }
59
60
0
        Ok(Self {
61
0
            encoded_image,
62
0
            width: width as u8,
63
0
            height: height as u8,
64
0
            color_type,
65
0
        })
66
0
    }
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
0
    pub fn as_png(
72
0
        buf: &[u8],
73
0
        width: u32,
74
0
        height: u32,
75
0
        color_type: ExtendedColorType,
76
0
    ) -> ImageResult<Self> {
77
0
        let mut image_data: Vec<u8> = Vec::new();
78
0
        PngEncoder::new(&mut image_data).write_image(buf, width, height, color_type)?;
79
80
0
        let frame = Self::with_encoded(image_data, width, height, color_type)?;
81
0
        Ok(frame)
82
0
    }
83
}
84
85
impl<W: Write> IcoEncoder<W> {
86
    /// Create a new encoder that writes its output to ```w```.
87
0
    pub fn new(w: W) -> IcoEncoder<W> {
88
0
        IcoEncoder { w }
89
0
    }
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<_>>::new
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new
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
0
    pub fn encode_images(mut self, images: &[IcoFrame<'_>]) -> ImageResult<()> {
96
0
        if !(1..=usize::from(u16::MAX)).contains(&images.len()) {
97
0
            return Err(ImageError::Parameter(ParameterError::from_kind(
98
0
                ParameterErrorKind::Generic(format!(
99
0
                    "the number of images must be `1..=u16::MAX`, instead {} images were provided",
100
0
                    images.len(),
101
0
                )),
102
0
            )));
103
0
        }
104
105
0
        write_icondir(&mut self.w, images.len() as u16)?;
106
107
0
        let mut offset = ICO_ICONDIR_SIZE + ICO_DIRENTRY_SIZE * images.len() as u32;
108
0
        for (i, image) in images.iter().enumerate() {
109
0
            let Ok(data_size) = u32::try_from(image.encoded_image.len()) else {
110
0
                return Err(ImageError::Encoding(EncodingError::new(
111
0
                    ImageFormat::Ico.into(),
112
0
                    "the encoded image data must be at most 4 GiB",
113
0
                )));
114
            };
115
116
0
            write_direntry(
117
0
                &mut self.w,
118
0
                image.width,
119
0
                image.height,
120
0
                image.color_type,
121
0
                offset,
122
0
                data_size,
123
0
            )?;
124
125
            // The offset is always calculated for the next frame. So we want
126
            // to skip it on the last frame since there is no next frame.
127
            // This has the effect of allowing the last frame's content to go
128
            // beyond the 4 GiB in the underlying writer.
129
0
            if i == images.len() - 1 {
130
0
                break;
131
0
            }
132
133
0
            offset = offset.checked_add(data_size).ok_or_else(|| {
134
0
                ImageError::Encoding(EncodingError::new(
135
0
                    ImageFormat::Ico.into(),
136
0
                    "the total size of the ICO file must be at most 4 GiB",
137
0
                ))
138
0
            })?;
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<_>>::encode_images::{closure#0}
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_images::{closure#0}
139
        }
140
0
        for image in images {
141
0
            self.w.write_all(&image.encoded_image)?;
142
        }
143
0
        Ok(())
144
0
    }
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<_>>::encode_images
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_images
145
}
146
147
impl<W: Write> ImageEncoder for IcoEncoder<W> {
148
    /// Write an ICO image with the specified width, height, and color type.
149
    ///
150
    /// For color types with 16-bit per channel or larger, the contents of `buf` should be in
151
    /// native endian.
152
    ///
153
    /// WARNING: In image 0.23.14 and earlier this method erroneously expected buf to be in big endian.
154
    #[track_caller]
155
0
    fn write_image(
156
0
        self,
157
0
        buf: &[u8],
158
0
        width: u32,
159
0
        height: u32,
160
0
        color_type: ExtendedColorType,
161
0
    ) -> ImageResult<()> {
162
0
        let expected_buffer_len = color_type.buffer_size(width, height);
163
0
        assert_eq!(
164
            expected_buffer_len,
165
0
            buf.len() as u64,
166
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
167
0
            buf.len(),
168
        );
169
170
0
        let image = IcoFrame::as_png(buf, width, height, color_type)?;
171
0
        self.encode_images(&[image])
172
0
    }
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<_> as image::io::encoder::ImageEncoder>::write_image
Unexecuted instantiation: <image::codecs::ico::encoder::IcoEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image
173
}
174
175
0
fn write_icondir<W: Write>(w: &mut W, num_images: u16) -> io::Result<()> {
176
    // Reserved field (must be zero):
177
0
    w.write_u16::<LittleEndian>(0)?;
178
    // Image type (ICO or CUR):
179
0
    w.write_u16::<LittleEndian>(ICO_IMAGE_TYPE)?;
180
    // Number of images in the file:
181
0
    w.write_u16::<LittleEndian>(num_images)?;
182
0
    Ok(())
183
0
}
Unexecuted instantiation: image::codecs::ico::encoder::write_icondir::<_>
Unexecuted instantiation: image::codecs::ico::encoder::write_icondir::<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>
184
185
0
fn write_direntry<W: Write>(
186
0
    w: &mut W,
187
0
    width: u8,
188
0
    height: u8,
189
0
    color: ExtendedColorType,
190
0
    data_start: u32,
191
0
    data_size: u32,
192
0
) -> io::Result<()> {
193
    // Image dimensions:
194
0
    w.write_u8(width)?;
195
0
    w.write_u8(height)?;
196
    // Number of colors in palette (or zero for no palette):
197
0
    w.write_u8(0)?;
198
    // Reserved field (must be zero):
199
0
    w.write_u8(0)?;
200
    // Color planes: 1 is correct, 0 is merely accepted in most circumstances.
201
0
    w.write_u16::<LittleEndian>(1)?;
202
    // Bits per pixel:
203
0
    w.write_u16::<LittleEndian>(color.bits_per_pixel())?;
204
    // Image data size, in bytes:
205
0
    w.write_u32::<LittleEndian>(data_size)?;
206
    // Image data offset, in bytes:
207
0
    w.write_u32::<LittleEndian>(data_start)?;
208
0
    Ok(())
209
0
}
Unexecuted instantiation: image::codecs::ico::encoder::write_direntry::<_>
Unexecuted instantiation: image::codecs::ico::encoder::write_direntry::<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>
210
211
#[cfg(test)]
212
mod test {
213
    use super::*;
214
215
    // Test that the encoder allows image where all frames have offsets < 4GiB
216
    // (even if the total file size might be larger than 4 GiB), but disallows
217
    // image where any frame has an offset >= 4 GiB.
218
    #[test]
219
    fn ico_larger_than_4_gib() {
220
        // Allocate a 1 MiB ""image"" and make 4096 frames with it.
221
        // The last frame will peek beyond the 4 GiB mark, since the header also takes a bit of memory.
222
        let data = vec![0; 1024 * 1024];
223
        let create_frame =
224
            || IcoFrame::with_encoded(data.as_slice(), 256, 256, ExtendedColorType::Rgba8).unwrap();
225
226
        let mut frames: Vec<IcoFrame> = (0..4096).map(|_| create_frame()).collect();
227
228
        let encoder = IcoEncoder::new(io::sink());
229
        let res = encoder.encode_images(&frames);
230
        assert!(res.is_ok());
231
232
        // adding just one more frame will cause the offset of the last frame to go beyond 4 GiB, which should cause an error.
233
        frames.push(create_frame());
234
        let encoder = IcoEncoder::new(io::sink());
235
        let res = encoder.encode_images(&frames);
236
        assert!(res.is_err());
237
    }
238
}