Coverage Report

Created: 2026-05-30 07:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}