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 | }; |
7 | use crate::image::ImageEncoder; |
8 | use crate::{ExtendedColorType, 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<'a, W: 'a> { |
16 | writer: &'a mut W, |
17 | } |
18 | |
19 | impl<'a, W: Write + 'a> BmpEncoder<'a, W> { |
20 | /// Create a new encoder that writes its output to ```w```. |
21 | pub fn new(w: &'a mut W) -> Self { |
22 | BmpEncoder { writer: w } |
23 | } |
24 | |
25 | /// Encodes the image `image` that has dimensions `width` and `height` and `ExtendedColorType` `c`. |
26 | /// |
27 | /// # Panics |
28 | /// |
29 | /// Panics if `width * height * c.bytes_per_pixel() != image.len()`. |
30 | #[track_caller ] |
31 | pub fn encode( |
32 | &mut self, |
33 | image: &[u8], |
34 | width: u32, |
35 | height: u32, |
36 | c: ExtendedColorType, |
37 | ) -> ImageResult<()> { |
38 | self.encode_with_palette(image, width, height, c, None) |
39 | } |
40 | |
41 | /// Same as `encode`, but allow a palette to be passed in. The `palette` is ignored for color |
42 | /// types other than Luma/Luma-with-alpha. |
43 | /// |
44 | /// # Panics |
45 | /// |
46 | /// Panics if `width * height * c.bytes_per_pixel() != image.len()`. |
47 | #[track_caller ] |
48 | pub fn encode_with_palette( |
49 | &mut self, |
50 | image: &[u8], |
51 | width: u32, |
52 | height: u32, |
53 | c: ExtendedColorType, |
54 | palette: Option<&[[u8; 3]]>, |
55 | ) -> ImageResult<()> { |
56 | if palette.is_some() && c != ExtendedColorType::L8 && c != ExtendedColorType::La8 { |
57 | return Err(ImageError::IoError(io::Error::new( |
58 | io::ErrorKind::InvalidInput, |
59 | format!( |
60 | "Unsupported color type {c:?} when using a non-empty palette. Supported types: Gray(8), GrayA(8)." |
61 | ), |
62 | ))); |
63 | } |
64 | |
65 | let expected_buffer_len = c.buffer_size(width, height); |
66 | assert_eq!( |
67 | expected_buffer_len, |
68 | image.len() as u64, |
69 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x {height} image" , |
70 | image.len(), |
71 | ); |
72 | |
73 | let bmp_header_size = BITMAPFILEHEADER_SIZE; |
74 | |
75 | let (dib_header_size, written_pixel_size, palette_color_count) = |
76 | get_pixel_info(c, palette)?; |
77 | let row_pad_size = (4 - (width * written_pixel_size) % 4) % 4; // each row must be padded to a multiple of 4 bytes |
78 | let image_size = width |
79 | .checked_mul(height) |
80 | .and_then(|v| v.checked_mul(written_pixel_size)) |
81 | .and_then(|v| v.checked_add(height * row_pad_size)) |
82 | .ok_or_else(|| { |
83 | ImageError::Parameter(ParameterError::from_kind( |
84 | ParameterErrorKind::DimensionMismatch, |
85 | )) |
86 | })?; |
87 | let palette_size = palette_color_count * 4; // all palette colors are BGRA |
88 | let file_size = bmp_header_size |
89 | .checked_add(dib_header_size) |
90 | .and_then(|v| v.checked_add(palette_size)) |
91 | .and_then(|v| v.checked_add(image_size)) |
92 | .ok_or_else(|| { |
93 | ImageError::Encoding(EncodingError::new( |
94 | ImageFormatHint::Exact(ImageFormat::Bmp), |
95 | "calculated BMP header size larger than 2^32" , |
96 | )) |
97 | })?; |
98 | |
99 | // write BMP header |
100 | self.writer.write_u8(b'B' )?; |
101 | self.writer.write_u8(b'M' )?; |
102 | self.writer.write_u32::<LittleEndian>(file_size)?; // file size |
103 | self.writer.write_u16::<LittleEndian>(0)?; // reserved 1 |
104 | self.writer.write_u16::<LittleEndian>(0)?; // reserved 2 |
105 | self.writer |
106 | .write_u32::<LittleEndian>(bmp_header_size + dib_header_size + palette_size)?; // image data offset |
107 | |
108 | // write DIB header |
109 | self.writer.write_u32::<LittleEndian>(dib_header_size)?; |
110 | self.writer.write_i32::<LittleEndian>(width as i32)?; |
111 | self.writer.write_i32::<LittleEndian>(height as i32)?; |
112 | self.writer.write_u16::<LittleEndian>(1)?; // color planes |
113 | self.writer |
114 | .write_u16::<LittleEndian>((written_pixel_size * 8) as u16)?; // bits per pixel |
115 | if dib_header_size >= BITMAPV4HEADER_SIZE { |
116 | // Assume BGRA32 |
117 | self.writer.write_u32::<LittleEndian>(3)?; // compression method - bitfields |
118 | } else { |
119 | self.writer.write_u32::<LittleEndian>(0)?; // compression method - no compression |
120 | } |
121 | self.writer.write_u32::<LittleEndian>(image_size)?; |
122 | self.writer.write_i32::<LittleEndian>(0)?; // horizontal ppm |
123 | self.writer.write_i32::<LittleEndian>(0)?; // vertical ppm |
124 | self.writer.write_u32::<LittleEndian>(palette_color_count)?; |
125 | self.writer.write_u32::<LittleEndian>(0)?; // all colors are important |
126 | if dib_header_size >= BITMAPV4HEADER_SIZE { |
127 | // Assume BGRA32 |
128 | self.writer.write_u32::<LittleEndian>(0xff << 16)?; // red mask |
129 | self.writer.write_u32::<LittleEndian>(0xff << 8)?; // green mask |
130 | self.writer.write_u32::<LittleEndian>(0xff)?; // blue mask |
131 | self.writer.write_u32::<LittleEndian>(0xff << 24)?; // alpha mask |
132 | self.writer.write_u32::<LittleEndian>(0x7352_4742)?; // colorspace - sRGB |
133 | |
134 | // endpoints (3x3) and gamma (3) |
135 | for _ in 0..12 { |
136 | self.writer.write_u32::<LittleEndian>(0)?; |
137 | } |
138 | } |
139 | |
140 | // write image data |
141 | match c { |
142 | ExtendedColorType::Rgb8 => self.encode_rgb(image, width, height, row_pad_size, 3)?, |
143 | ExtendedColorType::Rgba8 => self.encode_rgba(image, width, height, row_pad_size, 4)?, |
144 | ExtendedColorType::L8 => { |
145 | self.encode_gray(image, width, height, row_pad_size, 1, palette)?; |
146 | } |
147 | ExtendedColorType::La8 => { |
148 | self.encode_gray(image, width, height, row_pad_size, 2, palette)?; |
149 | } |
150 | _ => { |
151 | return Err(ImageError::IoError(io::Error::new( |
152 | io::ErrorKind::InvalidInput, |
153 | &get_unsupported_error_message(c)[..], |
154 | ))) |
155 | } |
156 | } |
157 | |
158 | Ok(()) |
159 | } |
160 | |
161 | fn encode_rgb( |
162 | &mut self, |
163 | image: &[u8], |
164 | width: u32, |
165 | height: u32, |
166 | row_pad_size: u32, |
167 | bytes_per_pixel: u32, |
168 | ) -> io::Result<()> { |
169 | let width = width as usize; |
170 | let height = height as usize; |
171 | let x_stride = bytes_per_pixel as usize; |
172 | let y_stride = width * x_stride; |
173 | for row in (0..height).rev() { |
174 | // from the bottom up |
175 | let row_start = row * y_stride; |
176 | for px in image[row_start..][..y_stride].chunks_exact(x_stride) { |
177 | let r = px[0]; |
178 | let g = px[1]; |
179 | let b = px[2]; |
180 | // written as BGR |
181 | self.writer.write_all(&[b, g, r])?; |
182 | } |
183 | self.write_row_pad(row_pad_size)?; |
184 | } |
185 | |
186 | Ok(()) |
187 | } |
188 | |
189 | fn encode_rgba( |
190 | &mut self, |
191 | image: &[u8], |
192 | width: u32, |
193 | height: u32, |
194 | row_pad_size: u32, |
195 | bytes_per_pixel: u32, |
196 | ) -> io::Result<()> { |
197 | let width = width as usize; |
198 | let height = height as usize; |
199 | let x_stride = bytes_per_pixel as usize; |
200 | let y_stride = width * x_stride; |
201 | for row in (0..height).rev() { |
202 | // from the bottom up |
203 | let row_start = row * y_stride; |
204 | for px in image[row_start..][..y_stride].chunks_exact(x_stride) { |
205 | let r = px[0]; |
206 | let g = px[1]; |
207 | let b = px[2]; |
208 | let a = px[3]; |
209 | // written as BGRA |
210 | self.writer.write_all(&[b, g, r, a])?; |
211 | } |
212 | self.write_row_pad(row_pad_size)?; |
213 | } |
214 | |
215 | Ok(()) |
216 | } |
217 | |
218 | fn encode_gray( |
219 | &mut self, |
220 | image: &[u8], |
221 | width: u32, |
222 | height: u32, |
223 | row_pad_size: u32, |
224 | bytes_per_pixel: u32, |
225 | palette: Option<&[[u8; 3]]>, |
226 | ) -> io::Result<()> { |
227 | // write grayscale palette |
228 | if let Some(palette) = palette { |
229 | for item in palette { |
230 | // each color is written as BGRA, where A is always 0 |
231 | self.writer.write_all(&[item[2], item[1], item[0], 0])?; |
232 | } |
233 | } else { |
234 | for val in 0u8..=255 { |
235 | // each color is written as BGRA, where A is always 0 and since only grayscale is being written, B = G = R = index |
236 | self.writer.write_all(&[val, val, val, 0])?; |
237 | } |
238 | } |
239 | |
240 | // write image data |
241 | let x_stride = bytes_per_pixel; |
242 | let y_stride = width * x_stride; |
243 | for row in (0..height).rev() { |
244 | // from the bottom up |
245 | let row_start = row * y_stride; |
246 | |
247 | // color value is equal to the palette index |
248 | if x_stride == 1 { |
249 | // improve performance by writing the whole row at once |
250 | self.writer |
251 | .write_all(&image[row_start as usize..][..y_stride as usize])?; |
252 | } else { |
253 | for col in 0..width { |
254 | let pixel_start = (row_start + (col * x_stride)) as usize; |
255 | self.writer.write_u8(image[pixel_start])?; |
256 | // alpha is never written as it's not widely supported |
257 | } |
258 | } |
259 | |
260 | self.write_row_pad(row_pad_size)?; |
261 | } |
262 | |
263 | Ok(()) |
264 | } |
265 | |
266 | fn write_row_pad(&mut self, row_pad_size: u32) -> io::Result<()> { |
267 | for _ in 0..row_pad_size { |
268 | self.writer.write_u8(0)?; |
269 | } |
270 | |
271 | Ok(()) |
272 | } |
273 | } |
274 | |
275 | impl<W: Write> ImageEncoder for BmpEncoder<'_, W> { |
276 | #[track_caller ] |
277 | fn write_image( |
278 | mut self, |
279 | buf: &[u8], |
280 | width: u32, |
281 | height: u32, |
282 | color_type: ExtendedColorType, |
283 | ) -> ImageResult<()> { |
284 | self.encode(image:buf, width, height, c:color_type) |
285 | } |
286 | } |
287 | |
288 | fn get_unsupported_error_message(c: ExtendedColorType) -> String { |
289 | format!("Unsupported color type {c:?}. Supported types: RGB(8), RGBA(8), Gray(8), GrayA(8)." ) |
290 | } |
291 | |
292 | /// Returns a tuple representing: (dib header size, written pixel size, palette color count). |
293 | fn get_pixel_info( |
294 | c: ExtendedColorType, |
295 | palette: Option<&[[u8; 3]]>, |
296 | ) -> io::Result<(u32, u32, u32)> { |
297 | let sizes: (u32, u32, u32) = match c { |
298 | ExtendedColorType::Rgb8 => (BITMAPINFOHEADER_SIZE, 3, 0), |
299 | ExtendedColorType::Rgba8 => (BITMAPV4HEADER_SIZE, 4, 0), |
300 | ExtendedColorType::L8 => ( |
301 | BITMAPINFOHEADER_SIZE, |
302 | 1, |
303 | palette.map(|p| p.len()).unwrap_or(default:256) as u32, |
304 | ), |
305 | ExtendedColorType::La8 => ( |
306 | BITMAPINFOHEADER_SIZE, |
307 | 1, |
308 | palette.map(|p| p.len()).unwrap_or(default:256) as u32, |
309 | ), |
310 | _ => { |
311 | return Err(io::Error::new( |
312 | kind:io::ErrorKind::InvalidInput, |
313 | &get_unsupported_error_message(c)[..], |
314 | )) |
315 | } |
316 | }; |
317 | |
318 | Ok(sizes) |
319 | } |
320 | |
321 | #[cfg (test)] |
322 | mod tests { |
323 | use super::super::BmpDecoder; |
324 | use super::BmpEncoder; |
325 | |
326 | use crate::image::ImageDecoder; |
327 | use crate::ExtendedColorType; |
328 | use std::io::Cursor; |
329 | |
330 | fn round_trip_image(image: &[u8], width: u32, height: u32, c: ExtendedColorType) -> Vec<u8> { |
331 | let mut encoded_data = Vec::new(); |
332 | { |
333 | let mut encoder = BmpEncoder::new(&mut encoded_data); |
334 | encoder |
335 | .encode(image, width, height, c) |
336 | .expect("could not encode image" ); |
337 | } |
338 | |
339 | let decoder = BmpDecoder::new(Cursor::new(&encoded_data)).expect("failed to decode" ); |
340 | |
341 | let mut buf = vec![0; decoder.total_bytes() as usize]; |
342 | decoder.read_image(&mut buf).expect("failed to decode" ); |
343 | buf |
344 | } |
345 | |
346 | #[test ] |
347 | fn round_trip_single_pixel_rgb() { |
348 | let image = [255u8, 0, 0]; // single red pixel |
349 | let decoded = round_trip_image(&image, 1, 1, ExtendedColorType::Rgb8); |
350 | assert_eq!(3, decoded.len()); |
351 | assert_eq!(255, decoded[0]); |
352 | assert_eq!(0, decoded[1]); |
353 | assert_eq!(0, decoded[2]); |
354 | } |
355 | |
356 | #[test ] |
357 | #[cfg (target_pointer_width = "64" )] |
358 | fn huge_files_return_error() { |
359 | let mut encoded_data = Vec::new(); |
360 | let image = vec![0u8; 3 * 40_000 * 40_000]; // 40_000x40_000 pixels, 3 bytes per pixel, allocated on the heap |
361 | let mut encoder = BmpEncoder::new(&mut encoded_data); |
362 | let result = encoder.encode(&image, 40_000, 40_000, ExtendedColorType::Rgb8); |
363 | assert!(result.is_err()); |
364 | } |
365 | |
366 | #[test ] |
367 | fn round_trip_single_pixel_rgba() { |
368 | let image = [1, 2, 3, 4]; |
369 | let decoded = round_trip_image(&image, 1, 1, ExtendedColorType::Rgba8); |
370 | assert_eq!(&decoded[..], &image[..]); |
371 | } |
372 | |
373 | #[test ] |
374 | fn round_trip_3px_rgb() { |
375 | let image = [0u8; 3 * 3 * 3]; // 3x3 pixels, 3 bytes per pixel |
376 | let _decoded = round_trip_image(&image, 3, 3, ExtendedColorType::Rgb8); |
377 | } |
378 | |
379 | #[test ] |
380 | fn round_trip_gray() { |
381 | let image = [0u8, 1, 2]; // 3 pixels |
382 | let decoded = round_trip_image(&image, 3, 1, ExtendedColorType::L8); |
383 | // should be read back as 3 RGB pixels |
384 | assert_eq!(9, decoded.len()); |
385 | assert_eq!(0, decoded[0]); |
386 | assert_eq!(0, decoded[1]); |
387 | assert_eq!(0, decoded[2]); |
388 | assert_eq!(1, decoded[3]); |
389 | assert_eq!(1, decoded[4]); |
390 | assert_eq!(1, decoded[5]); |
391 | assert_eq!(2, decoded[6]); |
392 | assert_eq!(2, decoded[7]); |
393 | assert_eq!(2, decoded[8]); |
394 | } |
395 | |
396 | #[test ] |
397 | fn round_trip_graya() { |
398 | let image = [0u8, 0, 1, 0, 2, 0]; // 3 pixels, each with an alpha channel |
399 | let decoded = round_trip_image(&image, 1, 3, ExtendedColorType::La8); |
400 | // should be read back as 3 RGB pixels |
401 | assert_eq!(9, decoded.len()); |
402 | assert_eq!(0, decoded[0]); |
403 | assert_eq!(0, decoded[1]); |
404 | assert_eq!(0, decoded[2]); |
405 | assert_eq!(1, decoded[3]); |
406 | assert_eq!(1, decoded[4]); |
407 | assert_eq!(1, decoded[5]); |
408 | assert_eq!(2, decoded[6]); |
409 | assert_eq!(2, decoded[7]); |
410 | assert_eq!(2, decoded[8]); |
411 | } |
412 | } |
413 | |