image/codecs/ico/
encoder.rs

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