/src/image/src/codecs/bmp/encoder.rs
Line | Count | Source |
1 | | use byteorder_lite::{LittleEndian, WriteBytesExt}; |
2 | | use std::io::{self, Write}; |
3 | | |
4 | | use crate::error::{ |
5 | | EncodingError, ImageError, ImageFormatHint, ImageResult, ParameterError, ParameterErrorKind, |
6 | | UnsupportedError, UnsupportedErrorKind, |
7 | | }; |
8 | | use crate::{DynamicImage, ExtendedColorType, ImageEncoder, ImageFormat}; |
9 | | |
10 | | const BITMAPFILEHEADER_SIZE: u32 = 14; |
11 | | const BITMAPINFOHEADER_SIZE: u32 = 40; |
12 | | const BITMAPV4HEADER_SIZE: u32 = 108; |
13 | | |
14 | | /// The representation of a BMP encoder. |
15 | | pub struct BmpEncoder<W> { |
16 | | writer: W, |
17 | | } |
18 | | |
19 | | impl<W: Write> BmpEncoder<W> { |
20 | | /// Create a new encoder that writes its output to ```w```. |
21 | 0 | pub fn new(w: W) -> Self { |
22 | 0 | BmpEncoder { writer: w } |
23 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::new Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new |
24 | | |
25 | | /// Encodes the image `image` that has dimensions `width` and `height` and `ExtendedColorType` `c`. |
26 | | /// |
27 | | /// # Panics |
28 | | /// |
29 | | /// Panics if the buffer does not hold exactly the number of bytes required for the given |
30 | | /// `width`, `height`, and color type, accounting for rows padded to whole bytes for |
31 | | /// sub-byte color types: `height * ((width * color_type.bits_per_pixel() as u32 + 7) / 8)`. |
32 | | #[track_caller] |
33 | 0 | pub fn encode( |
34 | 0 | &mut self, |
35 | 0 | image: &[u8], |
36 | 0 | width: u32, |
37 | 0 | height: u32, |
38 | 0 | c: ExtendedColorType, |
39 | 0 | ) -> ImageResult<()> { |
40 | 0 | self.encode_with_palette(image, width, height, c, None) |
41 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode |
42 | | |
43 | | /// Same as `encode`, but allow a palette to be passed in. The `palette` is ignored for color |
44 | | /// types other than Luma/Luma-with-alpha. |
45 | | /// |
46 | | /// # Panics |
47 | | /// |
48 | | /// Panics if the buffer does not hold exactly the number of bytes required for the given |
49 | | /// `width`, `height`, and color type, accounting for rows padded to whole bytes for |
50 | | /// sub-byte color types: `height * ((width * color_type.bits_per_pixel() as u32 + 7) / 8)`. |
51 | | #[track_caller] |
52 | 0 | pub fn encode_with_palette( |
53 | 0 | &mut self, |
54 | 0 | image: &[u8], |
55 | 0 | width: u32, |
56 | 0 | height: u32, |
57 | 0 | color_type: ExtendedColorType, |
58 | 0 | palette: Option<&[[u8; 3]]>, |
59 | 0 | ) -> ImageResult<()> { |
60 | 0 | if palette.is_some() |
61 | 0 | && color_type != ExtendedColorType::L1 |
62 | 0 | && color_type != ExtendedColorType::L8 |
63 | 0 | && color_type != ExtendedColorType::La8 |
64 | | { |
65 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
66 | 0 | ParameterErrorKind::Generic( |
67 | 0 | "Palette given which must only be used with L1, L8 or La8 color types" |
68 | 0 | .to_string(), |
69 | 0 | ), |
70 | 0 | ))); |
71 | 0 | } |
72 | | |
73 | | // width and height must be representable by a *signed* 32-bit integer |
74 | 0 | if width > i32::MAX as u32 || height > i32::MAX as u32 { |
75 | 0 | return Err(ImageError::Limits(crate::error::LimitError::from_kind( |
76 | 0 | crate::error::LimitErrorKind::DimensionError, |
77 | 0 | ))); |
78 | 0 | } |
79 | | |
80 | 0 | let expected_buffer_len = color_type.buffer_size(width, height); |
81 | 0 | assert_eq!( |
82 | | expected_buffer_len, |
83 | 0 | image.len() as u64, |
84 | 0 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image", |
85 | 0 | image.len(), |
86 | | ); |
87 | | |
88 | 0 | let bmp_header_size = BITMAPFILEHEADER_SIZE; |
89 | | |
90 | 0 | let (dib_header_size, bits_per_pixel, palette_color_count) = |
91 | 0 | written_pixel_info(color_type, palette)?; |
92 | | |
93 | 0 | let row_bytes = (width as u64 * bits_per_pixel as u64).div_ceil(8); |
94 | 0 | let padded_row = row_bytes.next_multiple_of(4); // Each row must be padded to a multiple of 4 bytes |
95 | 0 | let row_padding = (padded_row - row_bytes) as u32; |
96 | | |
97 | 0 | let image_size = padded_row |
98 | 0 | .checked_mul(height as u64) |
99 | 0 | .and_then(|size| u32::try_from(size).ok()) Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#0}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#0} |
100 | 0 | .ok_or_else(|| { |
101 | 0 | ImageError::Parameter(ParameterError::from_kind( |
102 | 0 | ParameterErrorKind::DimensionMismatch, |
103 | 0 | )) |
104 | 0 | })?; Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#1}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#1} |
105 | | |
106 | | // all palette colors are BGRA |
107 | 0 | let palette_size = palette_color_count.checked_mul(4).ok_or_else(|| { |
108 | 0 | ImageError::Encoding(EncodingError::new( |
109 | 0 | ImageFormatHint::Exact(ImageFormat::Bmp), |
110 | 0 | "calculated palette size larger than 2^32", |
111 | 0 | )) |
112 | 0 | })?; Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#2}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#2} |
113 | | |
114 | 0 | let file_size = bmp_header_size |
115 | 0 | .checked_add(dib_header_size) |
116 | 0 | .and_then(|v| v.checked_add(palette_size)) Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#3}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#3} |
117 | 0 | .and_then(|v| v.checked_add(image_size)) Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#4}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#4} |
118 | 0 | .ok_or_else(|| { |
119 | 0 | ImageError::Encoding(EncodingError::new( |
120 | 0 | ImageFormatHint::Exact(ImageFormat::Bmp), |
121 | 0 | "calculated BMP header size larger than 2^32", |
122 | 0 | )) |
123 | 0 | })?; Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#5}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#5} |
124 | | |
125 | 0 | let image_data_offset = bmp_header_size |
126 | 0 | .checked_add(dib_header_size) |
127 | 0 | .and_then(|v| v.checked_add(palette_size)) Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#6}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#6} |
128 | 0 | .ok_or_else(|| { |
129 | 0 | ImageError::Encoding(EncodingError::new( |
130 | 0 | ImageFormatHint::Exact(ImageFormat::Bmp), |
131 | 0 | "calculated BMP size larger than 2^32", |
132 | 0 | )) |
133 | 0 | })?; Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette::{closure#7}Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette::{closure#7} |
134 | | |
135 | | // write BMP header |
136 | 0 | self.writer.write_u8(b'B')?; |
137 | 0 | self.writer.write_u8(b'M')?; |
138 | 0 | self.writer.write_u32::<LittleEndian>(file_size)?; // file size |
139 | 0 | self.writer.write_u16::<LittleEndian>(0)?; // reserved 1 |
140 | 0 | self.writer.write_u16::<LittleEndian>(0)?; // reserved 2 |
141 | 0 | self.writer.write_u32::<LittleEndian>(image_data_offset)?; // image data offset |
142 | | |
143 | | // write DIB header |
144 | 0 | self.writer.write_u32::<LittleEndian>(dib_header_size)?; |
145 | 0 | self.writer.write_i32::<LittleEndian>(width as i32)?; |
146 | 0 | self.writer.write_i32::<LittleEndian>(height as i32)?; |
147 | 0 | self.writer.write_u16::<LittleEndian>(1)?; // color planes |
148 | 0 | self.writer.write_u16::<LittleEndian>(bits_per_pixel)?; // bits per pixel |
149 | 0 | if dib_header_size >= BITMAPV4HEADER_SIZE { |
150 | | // Assume BGRA32 |
151 | 0 | self.writer.write_u32::<LittleEndian>(3)?; // compression method - bitfields |
152 | | } else { |
153 | 0 | self.writer.write_u32::<LittleEndian>(0)?; // compression method - no compression |
154 | | } |
155 | 0 | self.writer.write_u32::<LittleEndian>(image_size)?; |
156 | 0 | self.writer.write_i32::<LittleEndian>(0)?; // horizontal ppm |
157 | 0 | self.writer.write_i32::<LittleEndian>(0)?; // vertical ppm |
158 | 0 | self.writer.write_u32::<LittleEndian>(palette_color_count)?; |
159 | 0 | self.writer.write_u32::<LittleEndian>(0)?; // all colors are important |
160 | 0 | if dib_header_size >= BITMAPV4HEADER_SIZE { |
161 | | // Assume BGRA32 |
162 | 0 | self.writer.write_u32::<LittleEndian>(0xff << 16)?; // red mask |
163 | 0 | self.writer.write_u32::<LittleEndian>(0xff << 8)?; // green mask |
164 | 0 | self.writer.write_u32::<LittleEndian>(0xff)?; // blue mask |
165 | 0 | self.writer.write_u32::<LittleEndian>(0xff << 24)?; // alpha mask |
166 | 0 | self.writer.write_u32::<LittleEndian>(0x7352_4742)?; // colorspace - sRGB |
167 | | |
168 | | // endpoints (3x3) and gamma (3) |
169 | 0 | for _ in 0..12 { |
170 | 0 | self.writer.write_u32::<LittleEndian>(0)?; |
171 | | } |
172 | 0 | } |
173 | | |
174 | | // done for empty images |
175 | 0 | if width == 0 || height == 0 { |
176 | 0 | return Ok(()); |
177 | 0 | } |
178 | | |
179 | | // write image data |
180 | 0 | match color_type { |
181 | 0 | ExtendedColorType::Rgb8 => self.encode_rgb(image, width, height, row_padding, 3)?, |
182 | 0 | ExtendedColorType::Rgba8 => self.encode_rgba(image, width, height, row_padding, 4)?, |
183 | | ExtendedColorType::L1 => { |
184 | 0 | self.encode_1bit_palette(image, width, height, row_padding, palette)?; |
185 | | } |
186 | | ExtendedColorType::L8 => { |
187 | 0 | self.encode_gray(image, width, height, row_padding, 1, palette)?; |
188 | | } |
189 | | ExtendedColorType::La8 => { |
190 | 0 | self.encode_gray(image, width, height, row_padding, 2, palette)?; |
191 | | } |
192 | | _ => { |
193 | 0 | return Err(ImageError::Unsupported( |
194 | 0 | UnsupportedError::from_format_and_kind( |
195 | 0 | ImageFormat::Bmp.into(), |
196 | 0 | UnsupportedErrorKind::Color(color_type), |
197 | 0 | ), |
198 | 0 | )); |
199 | | } |
200 | | } |
201 | | |
202 | 0 | Ok(()) |
203 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_with_palette Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_with_palette |
204 | | |
205 | 0 | fn encode_rgb( |
206 | 0 | &mut self, |
207 | 0 | image: &[u8], |
208 | 0 | width: u32, |
209 | 0 | height: u32, |
210 | 0 | row_padding: u32, |
211 | 0 | bytes_per_pixel: u32, |
212 | 0 | ) -> io::Result<()> { |
213 | 0 | let width = width as usize; |
214 | 0 | let height = height as usize; |
215 | 0 | let x_stride = bytes_per_pixel as usize; |
216 | 0 | let y_stride = width * x_stride; |
217 | 0 | for row in (0..height).rev() { |
218 | | // from the bottom up |
219 | 0 | let row_start = row * y_stride; |
220 | 0 | for px in image[row_start..][..y_stride].chunks_exact(x_stride) { |
221 | 0 | let r = px[0]; |
222 | 0 | let g = px[1]; |
223 | 0 | let b = px[2]; |
224 | | // written as BGR |
225 | 0 | self.writer.write_all(&[b, g, r])?; |
226 | | } |
227 | 0 | self.write_row_pad(row_padding)?; |
228 | | } |
229 | | |
230 | 0 | Ok(()) |
231 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_rgb Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_rgb |
232 | | |
233 | 0 | fn encode_rgba( |
234 | 0 | &mut self, |
235 | 0 | image: &[u8], |
236 | 0 | width: u32, |
237 | 0 | height: u32, |
238 | 0 | row_padding: u32, |
239 | 0 | bytes_per_pixel: u32, |
240 | 0 | ) -> io::Result<()> { |
241 | 0 | let width = width as usize; |
242 | 0 | let height = height as usize; |
243 | 0 | let x_stride = bytes_per_pixel as usize; |
244 | 0 | let y_stride = width * x_stride; |
245 | 0 | for row in (0..height).rev() { |
246 | | // from the bottom up |
247 | 0 | let row_start = row * y_stride; |
248 | 0 | for px in image[row_start..][..y_stride].chunks_exact(x_stride) { |
249 | 0 | let r = px[0]; |
250 | 0 | let g = px[1]; |
251 | 0 | let b = px[2]; |
252 | 0 | let a = px[3]; |
253 | | // written as BGRA |
254 | 0 | self.writer.write_all(&[b, g, r, a])?; |
255 | | } |
256 | 0 | self.write_row_pad(row_padding)?; |
257 | | } |
258 | | |
259 | 0 | Ok(()) |
260 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_rgba Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_rgba |
261 | | |
262 | 0 | fn encode_gray( |
263 | 0 | &mut self, |
264 | 0 | image: &[u8], |
265 | 0 | width: u32, |
266 | 0 | height: u32, |
267 | 0 | row_padding: u32, |
268 | 0 | bytes_per_pixel: u32, |
269 | 0 | palette: Option<&[[u8; 3]]>, |
270 | 0 | ) -> io::Result<()> { |
271 | | // write grayscale palette |
272 | 0 | if let Some(palette) = palette { |
273 | 0 | for item in palette { |
274 | | // each color is written as BGRA, where A is always 0 |
275 | 0 | self.writer.write_all(&[item[2], item[1], item[0], 0])?; |
276 | | } |
277 | | } else { |
278 | 0 | for val in 0u8..=255 { |
279 | | // each color is written as BGRA, where A is always 0 and since only grayscale is being written, B = G = R = index |
280 | 0 | self.writer.write_all(&[val, val, val, 0])?; |
281 | | } |
282 | | } |
283 | | |
284 | | // write image data |
285 | 0 | let x_stride = bytes_per_pixel; |
286 | 0 | let y_stride = width * x_stride; |
287 | 0 | for row in (0..height).rev() { |
288 | | // from the bottom up |
289 | 0 | let row_start = row * y_stride; |
290 | | |
291 | | // color value is equal to the palette index |
292 | 0 | if x_stride == 1 { |
293 | | // improve performance by writing the whole row at once |
294 | 0 | self.writer |
295 | 0 | .write_all(&image[row_start as usize..][..y_stride as usize])?; |
296 | | } else { |
297 | 0 | for col in 0..width { |
298 | 0 | let pixel_start = (row_start + (col * x_stride)) as usize; |
299 | 0 | self.writer.write_u8(image[pixel_start])?; |
300 | | // alpha is never written as it's not widely supported |
301 | | } |
302 | | } |
303 | | |
304 | 0 | self.write_row_pad(row_padding)?; |
305 | | } |
306 | | |
307 | 0 | Ok(()) |
308 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_gray Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_gray |
309 | | |
310 | 0 | fn encode_1bit_palette( |
311 | 0 | &mut self, |
312 | 0 | image: &[u8], |
313 | 0 | width: u32, |
314 | 0 | _height: u32, |
315 | 0 | row_padding: u32, |
316 | 0 | palette: Option<&[[u8; 3]]>, |
317 | 0 | ) -> io::Result<()> { |
318 | | // write 2-color palette (1-bit images have exactly 2 colors) |
319 | 0 | if let Some(palette) = palette { |
320 | | // Use custom palette (should have exactly 2 colors) |
321 | 0 | for item in palette.iter().take(2) { |
322 | | // each color is written as BGRA, where A is always 0 |
323 | 0 | self.writer.write_all(&[item[2], item[1], item[0], 0])?; |
324 | | } |
325 | | // If palette has less than 2 colors, pad with black |
326 | 0 | for _ in palette.len()..2 { |
327 | 0 | self.writer.write_all(&[0, 0, 0, 0])?; |
328 | | } |
329 | | } else { |
330 | | // Default palette: black and white |
331 | 0 | self.writer.write_all(&[0, 0, 0, 0])?; // color 0: black |
332 | 0 | self.writer.write_all(&[255, 255, 255, 0])?; // color 1: white |
333 | | } |
334 | | |
335 | | // write image data |
336 | | // Input is already packed: 8 pixels per byte, MSB first |
337 | | // Bit 7 = pixel 0, Bit 6 = pixel 1, ..., Bit 0 = pixel 7 |
338 | 0 | let bytes_per_row = width.div_ceil(8) as usize; |
339 | 0 | for row in image.chunks_exact(bytes_per_row).rev() { |
340 | | // from the bottom up |
341 | 0 | self.writer.write_all(row)?; |
342 | 0 | self.write_row_pad(row_padding)?; |
343 | | } |
344 | | |
345 | 0 | Ok(()) |
346 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::encode_1bit_palette Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_1bit_palette |
347 | | |
348 | 0 | fn write_row_pad(&mut self, row_pad_size: u32) -> io::Result<()> { |
349 | 0 | for _ in 0..row_pad_size { |
350 | 0 | self.writer.write_u8(0)?; |
351 | | } |
352 | | |
353 | 0 | Ok(()) |
354 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_>>::write_row_pad Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::write_row_pad |
355 | | } |
356 | | |
357 | | impl<W: Write> ImageEncoder for BmpEncoder<W> { |
358 | | #[track_caller] |
359 | 0 | fn write_image( |
360 | 0 | mut self, |
361 | 0 | buf: &[u8], |
362 | 0 | width: u32, |
363 | 0 | height: u32, |
364 | 0 | color_type: ExtendedColorType, |
365 | 0 | ) -> ImageResult<()> { |
366 | 0 | self.encode(buf, width, height, color_type) |
367 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_> as image::io::encoder::ImageEncoder>::write_image Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image |
368 | | |
369 | 0 | fn make_compatible_img( |
370 | 0 | &self, |
371 | 0 | _: crate::io::encoder::MethodSealedToImage, |
372 | 0 | img: &DynamicImage, |
373 | 0 | ) -> Option<DynamicImage> { |
374 | 0 | crate::io::encoder::dynimage_conversion_8bit(img) |
375 | 0 | } Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<_> as image::io::encoder::ImageEncoder>::make_compatible_img Unexecuted instantiation: <image::codecs::bmp::encoder::BmpEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::make_compatible_img |
376 | | } |
377 | | |
378 | | /// Returns a tuple representing: (dib header size, bits per pixel, palette color count). |
379 | 0 | fn written_pixel_info( |
380 | 0 | c: ExtendedColorType, |
381 | 0 | palette: Option<&[[u8; 3]]>, |
382 | 0 | ) -> Result<(u32, u16, u32), ImageError> { |
383 | 0 | let (header, bits_per_pixel, palette_count) = match c { |
384 | 0 | ExtendedColorType::Rgb8 => (BITMAPINFOHEADER_SIZE, 24, Some(0)), |
385 | 0 | ExtendedColorType::Rgba8 => (BITMAPV4HEADER_SIZE, 32, Some(0)), |
386 | | ExtendedColorType::L1 => ( |
387 | | BITMAPINFOHEADER_SIZE, |
388 | | 1, |
389 | 0 | u32::try_from(palette.map(|p| p.len()).unwrap_or(2)).ok(), |
390 | | ), |
391 | | ExtendedColorType::L8 => ( |
392 | | BITMAPINFOHEADER_SIZE, |
393 | | 8, |
394 | 0 | u32::try_from(palette.map(|p| p.len()).unwrap_or(256)).ok(), |
395 | | ), |
396 | | ExtendedColorType::La8 => ( |
397 | | BITMAPINFOHEADER_SIZE, |
398 | | 8, |
399 | 0 | u32::try_from(palette.map(|p| p.len()).unwrap_or(256)).ok(), |
400 | | ), |
401 | | _ => { |
402 | 0 | return Err(ImageError::Unsupported( |
403 | 0 | UnsupportedError::from_format_and_kind( |
404 | 0 | ImageFormat::Bmp.into(), |
405 | 0 | UnsupportedErrorKind::Color(c), |
406 | 0 | ), |
407 | 0 | )); |
408 | | } |
409 | | }; |
410 | | |
411 | 0 | let palette_count = palette_count.ok_or_else(|| { |
412 | 0 | ImageError::Encoding(EncodingError::new( |
413 | 0 | ImageFormatHint::Exact(ImageFormat::Bmp), |
414 | 0 | "calculated palette size larger than 2^32", |
415 | 0 | )) |
416 | 0 | })?; |
417 | | |
418 | 0 | Ok((header, bits_per_pixel, palette_count)) |
419 | 0 | } |
420 | | |
421 | | #[cfg(test)] |
422 | | mod tests { |
423 | | use super::super::BmpDecoder; |
424 | | use super::BmpEncoder; |
425 | | |
426 | | use crate::ExtendedColorType; |
427 | | use crate::ImageDecoder as _; |
428 | | use std::io::Cursor; |
429 | | |
430 | | fn round_trip_image(image: &[u8], width: u32, height: u32, c: ExtendedColorType) -> Vec<u8> { |
431 | | let mut encoded_data = Vec::new(); |
432 | | { |
433 | | let mut encoder = BmpEncoder::new(&mut encoded_data); |
434 | | encoder |
435 | | .encode(image, width, height, c) |
436 | | .expect("could not encode image"); |
437 | | } |
438 | | |
439 | | let mut decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); |
440 | | let layout = decoder.prepare_image().unwrap(); |
441 | | let mut buf = vec![0; layout.total_bytes() as usize]; |
442 | | decoder.read_image(&mut buf).expect("failed to decode"); |
443 | | buf |
444 | | } |
445 | | |
446 | | #[test] |
447 | | fn round_trip_single_pixel_rgb() { |
448 | | let image = [255u8, 0, 0]; // single red pixel |
449 | | let decoded = round_trip_image(&image, 1, 1, ExtendedColorType::Rgb8); |
450 | | assert_eq!(3, decoded.len()); |
451 | | assert_eq!(255, decoded[0]); |
452 | | assert_eq!(0, decoded[1]); |
453 | | assert_eq!(0, decoded[2]); |
454 | | } |
455 | | |
456 | | #[test] |
457 | | #[cfg(target_pointer_width = "64")] |
458 | | fn huge_files_return_error() { |
459 | | let mut encoded_data = Vec::new(); |
460 | | let image = vec![0u8; 3 * 40_000 * 40_000]; // 40_000x40_000 pixels, 3 bytes per pixel, allocated on the heap |
461 | | let mut encoder = BmpEncoder::new(&mut encoded_data); |
462 | | let result = encoder.encode(&image, 40_000, 40_000, ExtendedColorType::Rgb8); |
463 | | assert!(result.is_err()); |
464 | | } |
465 | | |
466 | | #[test] |
467 | | fn round_trip_single_pixel_rgba() { |
468 | | let image = [1, 2, 3, 4]; |
469 | | let decoded = round_trip_image(&image, 1, 1, ExtendedColorType::Rgba8); |
470 | | assert_eq!(&decoded[..], &image[..]); |
471 | | } |
472 | | |
473 | | #[test] |
474 | | fn round_trip_3px_rgb() { |
475 | | let image = [0u8; 3 * 3 * 3]; // 3x3 pixels, 3 bytes per pixel |
476 | | let _decoded = round_trip_image(&image, 3, 3, ExtendedColorType::Rgb8); |
477 | | } |
478 | | |
479 | | #[test] |
480 | | fn round_trip_gray() { |
481 | | let image = [0u8, 1, 2]; // 3 pixels |
482 | | let decoded = round_trip_image(&image, 3, 1, ExtendedColorType::L8); |
483 | | // should be read back as 3 RGB pixels |
484 | | assert_eq!(9, decoded.len()); |
485 | | assert_eq!(0, decoded[0]); |
486 | | assert_eq!(0, decoded[1]); |
487 | | assert_eq!(0, decoded[2]); |
488 | | assert_eq!(1, decoded[3]); |
489 | | assert_eq!(1, decoded[4]); |
490 | | assert_eq!(1, decoded[5]); |
491 | | assert_eq!(2, decoded[6]); |
492 | | assert_eq!(2, decoded[7]); |
493 | | assert_eq!(2, decoded[8]); |
494 | | } |
495 | | |
496 | | #[test] |
497 | | fn round_trip_graya() { |
498 | | let image = [0u8, 0, 1, 0, 2, 0]; // 3 pixels, each with an alpha channel |
499 | | let decoded = round_trip_image(&image, 1, 3, ExtendedColorType::La8); |
500 | | // should be read back as 3 RGB pixels |
501 | | assert_eq!(9, decoded.len()); |
502 | | assert_eq!(0, decoded[0]); |
503 | | assert_eq!(0, decoded[1]); |
504 | | assert_eq!(0, decoded[2]); |
505 | | assert_eq!(1, decoded[3]); |
506 | | assert_eq!(1, decoded[4]); |
507 | | assert_eq!(1, decoded[5]); |
508 | | assert_eq!(2, decoded[6]); |
509 | | assert_eq!(2, decoded[7]); |
510 | | assert_eq!(2, decoded[8]); |
511 | | } |
512 | | |
513 | | #[test] |
514 | | fn regression_issue_2604() { |
515 | | let mut image = vec![]; |
516 | | let mut encoder = BmpEncoder::new(&mut image); |
517 | | encoder |
518 | | .encode(&[], 1 << 31, 0, ExtendedColorType::Rgb8) |
519 | | .unwrap_err(); |
520 | | } |
521 | | |
522 | | #[test] |
523 | | fn round_trip_1bit() { |
524 | | // 8x2 image with alternating pattern |
525 | | // Row 1: [0,1,0,1,0,1,0,1] = 0b01010101 = 0x55 |
526 | | // Row 2: [1,0,1,0,1,0,1,0] = 0b10101010 = 0xAA |
527 | | // Packed format: MSB = pixel 0, LSB = pixel 7 |
528 | | let image = vec![0x55, 0xAA]; |
529 | | let decoded = round_trip_image(&image, 8, 2, ExtendedColorType::L1); |
530 | | // Decoder expands to RGB |
531 | | assert_eq!(8 * 2 * 3, decoded.len()); |
532 | | |
533 | | // Check first row (0,1,0,1,0,1,0,1) |
534 | | assert_eq!(&decoded[0..3], &[0, 0, 0]); // black |
535 | | assert_eq!(&decoded[3..6], &[255, 255, 255]); // white |
536 | | assert_eq!(&decoded[6..9], &[0, 0, 0]); // black |
537 | | assert_eq!(&decoded[9..12], &[255, 255, 255]); // white |
538 | | } |
539 | | |
540 | | #[test] |
541 | | fn round_trip_1bit_non_multiple_of_8() { |
542 | | // 7x1 image (width not divisible by 8) |
543 | | // Pixels: [1,0,1,0,1,0,1] packed into bits 7-1 (bit 0 unused) |
544 | | // Binary: 0b10101010 = 0xAA |
545 | | let image = vec![0xAA]; |
546 | | let decoded = round_trip_image(&image, 7, 1, ExtendedColorType::L1); |
547 | | assert_eq!(7 * 3, decoded.len()); |
548 | | |
549 | | // Check pattern |
550 | | assert_eq!(&decoded[0..3], &[255, 255, 255]); // white |
551 | | assert_eq!(&decoded[3..6], &[0, 0, 0]); // black |
552 | | assert_eq!(&decoded[6..9], &[255, 255, 255]); // white |
553 | | } |
554 | | |
555 | | #[test] |
556 | | fn round_trip_1bit_single_pixel() { |
557 | | // Single white pixel: bit 7 = 1, rest unused |
558 | | // Binary: 0b10000000 = 0x80 |
559 | | let image = vec![0x80]; |
560 | | let decoded = round_trip_image(&image, 1, 1, ExtendedColorType::L1); |
561 | | assert_eq!(3, decoded.len()); |
562 | | assert_eq!(&decoded[..], &[255, 255, 255]); |
563 | | |
564 | | // Single black pixel: all bits 0 |
565 | | let image = vec![0x00]; |
566 | | let decoded = round_trip_image(&image, 1, 1, ExtendedColorType::L1); |
567 | | assert_eq!(3, decoded.len()); |
568 | | assert_eq!(&decoded[..], &[0, 0, 0]); |
569 | | } |
570 | | |
571 | | #[test] |
572 | | fn round_trip_1bit_9px() { |
573 | | // 9 pixels (tests packing across byte boundary) |
574 | | // Byte 1: [1,1,1,1,1,1,1,1] = 0b11111111 = 0xFF |
575 | | // Byte 2: [0,_,_,_,_,_,_,_] = 0b00000000 = 0x00 (pixel 8=0, rest unused) |
576 | | let image = vec![0xFF, 0x00]; |
577 | | let decoded = round_trip_image(&image, 9, 1, ExtendedColorType::L1); |
578 | | assert_eq!(9 * 3, decoded.len()); |
579 | | |
580 | | // First 8 should be white |
581 | | for i in 0..8 { |
582 | | assert_eq!(&decoded[i * 3..(i + 1) * 3], &[255, 255, 255]); |
583 | | } |
584 | | // Last one should be black |
585 | | assert_eq!(&decoded[24..27], &[0, 0, 0]); |
586 | | } |
587 | | |
588 | | #[test] |
589 | | fn round_trip_1bit_with_custom_palette() { |
590 | | // Test custom palette encoding |
591 | | // 4 pixels: [0,1,0,1] packed into bits 7-4 |
592 | | // Binary: 0b01010000 = 0x50 |
593 | | let image = vec![0x50]; |
594 | | let palette = vec![ |
595 | | [255, 0, 0], // red for 0 |
596 | | [0, 0, 255], // blue for 1 |
597 | | ]; |
598 | | |
599 | | let mut encoded_data = Vec::new(); |
600 | | { |
601 | | let mut encoder = BmpEncoder::new(&mut encoded_data); |
602 | | encoder |
603 | | .encode_with_palette(&image, 4, 1, ExtendedColorType::L1, Some(&palette)) |
604 | | .expect("could not encode image with custom palette"); |
605 | | } |
606 | | |
607 | | // Decode and verify |
608 | | let mut decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode"); |
609 | | let layout = decoder.prepare_image().unwrap(); |
610 | | let mut buf = vec![0; layout.total_bytes() as usize]; |
611 | | decoder.read_image(&mut buf).expect("failed to decode"); |
612 | | |
613 | | // Should be decoded as RGB with custom colors |
614 | | assert_eq!(12, buf.len()); // 4 pixels * 3 bytes |
615 | | assert_eq!(&buf[0..3], &[255, 0, 0]); // red |
616 | | assert_eq!(&buf[3..6], &[0, 0, 255]); // blue |
617 | | assert_eq!(&buf[6..9], &[255, 0, 0]); // red |
618 | | assert_eq!(&buf[9..12], &[0, 0, 255]); // blue |
619 | | } |
620 | | |
621 | | #[test] |
622 | | fn round_trip_1bit_various_widths() { |
623 | | // Test various widths to ensure padding works correctly |
624 | | // Generate packed data for all-white pixels |
625 | | for width in 1..=17 { |
626 | | let mut image = Vec::new(); |
627 | | let mut remaining = width; |
628 | | while remaining > 0 { |
629 | | let bits_in_byte = remaining.min(8); |
630 | | // Create byte with 'bits_in_byte' MSBs set to 1 |
631 | | let byte = if bits_in_byte == 8 { |
632 | | 0xFF |
633 | | } else { |
634 | | 0xFF << (8 - bits_in_byte) |
635 | | }; |
636 | | image.push(byte); |
637 | | remaining -= bits_in_byte; |
638 | | } |
639 | | |
640 | | let decoded = round_trip_image(&image, width, 1, ExtendedColorType::L1); |
641 | | assert_eq!(width as usize * 3, decoded.len()); |
642 | | // All pixels should be white |
643 | | for chunk in decoded.chunks(3) { |
644 | | assert_eq!(chunk, &[255, 255, 255]); |
645 | | } |
646 | | } |
647 | | } |
648 | | |
649 | | #[test] |
650 | | fn encode_1bit_invalid_palette_type() { |
651 | | // Palette should only work with L1, L8, La8 |
652 | | let image = vec![255, 0, 0]; // RGB pixel |
653 | | let palette = vec![[0, 0, 0], [255, 255, 255]]; |
654 | | let mut encoded_data = Vec::new(); |
655 | | let mut encoder = BmpEncoder::new(&mut encoded_data); |
656 | | |
657 | | let result = |
658 | | encoder.encode_with_palette(&image, 1, 1, ExtendedColorType::Rgb8, Some(&palette)); |
659 | | |
660 | | assert!(result.is_err()); |
661 | | } |
662 | | |
663 | | #[test] |
664 | | fn round_trip_1bit_checkerboard() { |
665 | | // 8x8 checkerboard pattern (packed format) |
666 | | // Row 0: [0,1,0,1,0,1,0,1] = 0x55 |
667 | | // Row 1: [1,0,1,0,1,0,1,0] = 0xAA |
668 | | // Pattern repeats... |
669 | | let mut image = Vec::new(); |
670 | | for y in 0..8 { |
671 | | let mut byte = 0u8; |
672 | | for x in 0..8 { |
673 | | let bit_val = if (x + y) % 2 == 0 { 0 } else { 1 }; |
674 | | byte |= bit_val << (7 - x); |
675 | | } |
676 | | image.push(byte); |
677 | | } |
678 | | |
679 | | let decoded = round_trip_image(&image, 8, 8, ExtendedColorType::L1); |
680 | | assert_eq!(8 * 8 * 3, decoded.len()); |
681 | | |
682 | | // Verify checkerboard pattern |
683 | | for y in 0..8 { |
684 | | for x in 0..8 { |
685 | | let idx = (y * 8 + x) * 3; |
686 | | let expected = if (x + y) % 2 == 0 { |
687 | | [0, 0, 0] |
688 | | } else { |
689 | | [255, 255, 255] |
690 | | }; |
691 | | assert_eq!(&decoded[idx..idx + 3], &expected); |
692 | | } |
693 | | } |
694 | | } |
695 | | |
696 | | #[test] |
697 | | fn round_trip_1bit_13x3() { |
698 | | // Create different patterns per row to verify correct encoding/decoding |
699 | | // Row 0: [1,0,1,0,1,0,1,0, 1,1,1,1,1] = 0xAA, 0xF8 |
700 | | // Row 1: [0,1,0,1,0,1,0,1, 0,0,0,0,0] = 0x55, 0x00 |
701 | | // Row 2: [1,1,1,1,1,1,1,1, 1,0,1,0,1] = 0xFF, 0xA8 |
702 | | let image = vec![ |
703 | | 0xAA, 0xF8, // Row 0 |
704 | | 0x55, 0x00, // Row 1 |
705 | | 0xFF, 0xA8, // Row 2 |
706 | | ]; |
707 | | |
708 | | let decoded = round_trip_image(&image, 13, 3, ExtendedColorType::L1); |
709 | | assert_eq!(13 * 3 * 3, decoded.len()); |
710 | | |
711 | | // Row 0: [1,0,1,0,1,0,1,0, 1,1,1,1,1] |
712 | | let row0_expected = [ |
713 | | 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, // pixels 0-3 |
714 | | 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, // pixels 4-7 |
715 | | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, |
716 | | 255, // pixels 8-12 |
717 | | ]; |
718 | | assert_eq!(&decoded[0..39], &row0_expected); |
719 | | |
720 | | // Row 1: [0,1,0,1,0,1,0,1, 0,0,0,0,0] |
721 | | let row1_expected = [ |
722 | | 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, // pixels 0-3 |
723 | | 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, // pixels 4-7 |
724 | | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // pixels 8-12 |
725 | | ]; |
726 | | assert_eq!(&decoded[39..78], &row1_expected); |
727 | | |
728 | | // Row 2: [1,1,1,1,1,1,1,1, 1,0,1,0,1] |
729 | | let row2_expected = [ |
730 | | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // pixels 0-3 |
731 | | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // pixels 4-7 |
732 | | 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, // pixels 8-12 |
733 | | ]; |
734 | | assert_eq!(&decoded[78..117], &row2_expected); |
735 | | } |
736 | | |
737 | | // Test that dimensions must be representable by i32 |
738 | | #[test] |
739 | | fn dimensions_i32() { |
740 | | // start with an image that can be encoded |
741 | | BmpEncoder::new(std::io::sink()) |
742 | | .encode(&[], i32::MAX as u32, 0, ExtendedColorType::Rgb8) |
743 | | .expect("width is representable by i32"); |
744 | | |
745 | | // now one that cannot be represented |
746 | | BmpEncoder::new(std::io::sink()) |
747 | | .encode(&[], (i32::MAX as u32) + 1, 0, ExtendedColorType::Rgb8) |
748 | | .expect_err("width is not representable by i32"); |
749 | | } |
750 | | } |