From 22a4ca613b9d635f2b485af91d51da4864833544 Mon Sep 17 00:00:00 2001 From: Sergio Gonzalez Martin Date: Wed, 18 Feb 2026 09:56:40 +0000 Subject: [PATCH] bmp/decoder: Add streaming/resumable BMP decoding support Port upstream image crate BMP decoder improvements that add: - Resumable streaming decoding with BmpDecoder::new_resumable() - ICC color profile extraction support - Phased metadata reading (headers, palette, ICC profile) - Row-level and RLE checkpoint resumability on UnexpectedEof - Proper state machine for decoder progress tracking - Refactored header parsing into dedicated structs - RowsDecoded enum to track decode progress - Strict/lenient spec validation (BmpSpec enum) - OS/2 BITMAPCOREHEADER2 (v2) support - RLE24 (24-bit run length encoding) decoding - BitfieldCompression enum for BI_ALPHABITFIELDS support - Lenient handling for non-conformant BMPs (planes, top-down RLE, oversized palettes, zero bitfield masks) These changes enable progressive BMP image decoding where data arrives incrementally, which is essential for network image loading. Upstream: https://github.com/image-rs/image diff --git a/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/decoder.rs b/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/decoder.rs index 5771665..00799e3 100644 --- a/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/decoder.rs +++ b/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/decoder.rs @@ -5,15 +5,23 @@ use std::iter::{repeat, Rev}; use std::slice::ChunksExactMut; use std::{error, fmt}; -use byteorder_lite::{LittleEndian, ReadBytesExt}; - use crate::color::ColorType; use crate::error::{ DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::io::free_functions::load_rect; -use crate::io::ReadExt; -use crate::{ImageDecoder, ImageDecoderRect, ImageFormat}; +use crate::{ImageDecoder, ImageFormat}; + +/// Controls how strictly the BMP decoder adheres to the specification. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum BmpSpec { + /// Strictly follow the BMP specification. + /// Rejects files that violate spec constraints (e.g., RLE with top-down). + Strict, + /// Allow some non-conformant files that violate some spec constraints + /// but still can be decoded at best effort. + #[default] + Lenient, +} const BITMAPCOREHEADER_SIZE: u32 = 12; const BITMAPINFOHEADER_SIZE: u32 = 40; @@ -22,6 +30,21 @@ const BITMAPV3HEADER_SIZE: u32 = 56; const BITMAPV4HEADER_SIZE: u32 = 108; const BITMAPV5HEADER_SIZE: u32 = 124; +const OS2_V2_MAX_HEADER_SIZE: u32 = 64; +const OS2_V2_MIN_HEADER_SIZE: u32 = 16; + +// Compression method constants +const BI_RGB: u32 = 0; +const BI_RLE8: u32 = 1; +const BI_RLE4: u32 = 2; +const BI_BITFIELDS: u32 = 3; +const BI_JPEG: u32 = 4; // Used in legacy Windows pass-through printing path (not supported) and for RLE24 +const BI_PNG: u32 = 5; // Used in legacy Windows pass-through printing path - not supported +const BI_ALPHABITFIELDS: u32 = 6; +const BI_CMYK: u32 = 11; +const BI_CMYKRLE8: u32 = 12; +const BI_CMYKRLE4: u32 = 13; + static LOOKUP_TABLE_3_BIT_TO_8_BIT: [u8; 8] = [0, 36, 73, 109, 146, 182, 219, 255]; static LOOKUP_TABLE_4_BIT_TO_8_BIT: [u8; 16] = [ 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255, @@ -61,9 +84,224 @@ const RLE_ESCAPE_EOL: u8 = 0; const RLE_ESCAPE_EOF: u8 = 1; const RLE_ESCAPE_DELTA: u8 = 2; +/// Opaque alpha channel value (fully opaque) +const ALPHA_OPAQUE: u8 = 0xFF; + /// The maximum width/height the decoder will process. const MAX_WIDTH_HEIGHT: i32 = 0xFFFF; +/// The value of the V5 header field indicating an embedded ICC profile ("MBED"). +const PROFILE_EMBEDDED: u32 = 0x4D424544; + +/// During progressive decoding, the decoder applies transforms (e.g. a vertical +/// flip for bottom-up BMP files) as it writes rows into the output buffer. +/// This enum describes which rows contain valid pixel data by indicating the +/// transform that was applied. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RowsDecoded { + /// Rows were decoded sequentially from the top of the image. + TopDown { + /// Number of top rows decoded so far. + rows: u32, + }, + /// Rows were decoded from the bottom of the image (vertical flip). + BottomUp { + /// Number of bottom rows decoded so far. + rows: u32, + }, +} + +impl RowsDecoded { + /// Returns the number of decoded rows. + #[inline] + pub fn rows(&self) -> u32 { + match *self { + RowsDecoded::TopDown { rows } | RowsDecoded::BottomUp { rows } => rows, + } + } +} + +/// Parsed BITMAPCOREHEADER fields (excludes 4-byte size field). +struct ParsedCoreHeader { + width: i32, + height: i32, + bit_count: u16, + image_type: ImageType, +} + +impl ParsedCoreHeader { + /// Parse BITMAPCOREHEADER fields from an 8-byte buffer. + fn parse(buffer: &[u8; 8], spec_strictness: BmpSpec) -> ImageResult { + let width = i32::from(u16::from_le_bytes(buffer[0..2].try_into().unwrap())); + let height = i32::from(u16::from_le_bytes(buffer[2..4].try_into().unwrap())); + + let planes = u16::from_le_bytes(buffer[4..6].try_into().unwrap()); + if spec_strictness == BmpSpec::Strict && planes != 1 { + return Err(DecoderError::MoreThanOnePlane.into()); + } + + let bit_count = u16::from_le_bytes(buffer[6..8].try_into().unwrap()); + let image_type = match bit_count { + 1 | 4 | 8 => ImageType::Palette, + 24 => ImageType::RGB24, + _ => { + return Err( + DecoderError::InvalidChannelWidth(ChannelWidthError::Rgb, bit_count).into(), + ) + } + }; + + Ok(ParsedCoreHeader { + width, + height, + bit_count, + image_type, + }) + } +} + +/// Parsed BITMAPINFOHEADER fields (excludes 4-byte size field). +struct ParsedInfoHeader { + width: i32, + height: i32, + top_down: bool, + bit_count: u16, + compression: u32, + colors_used: u32, +} + +impl ParsedInfoHeader { + /// Parse BITMAPINFOHEADER fields from a 36-byte buffer. + fn parse(buffer: &[u8; 36], spec_strictness: BmpSpec) -> ImageResult { + let width = i32::from_le_bytes(buffer[0..4].try_into().unwrap()); + let mut height = i32::from_le_bytes(buffer[4..8].try_into().unwrap()); + + // Width cannot be negative + if width < 0 { + return Err(DecoderError::NegativeWidth(width).into()); + } else if width > MAX_WIDTH_HEIGHT || height > MAX_WIDTH_HEIGHT { + return Err(DecoderError::ImageTooLarge(width, height).into()); + } + + if height == i32::MIN { + return Err(DecoderError::InvalidHeight.into()); + } + + // A negative height indicates a top-down DIB + let top_down = if height < 0 { + height = -height; + true + } else { + false + }; + + let planes = u16::from_le_bytes(buffer[8..10].try_into().unwrap()); + if spec_strictness == BmpSpec::Strict && planes != 1 { + return Err(DecoderError::MoreThanOnePlane.into()); + } + + let bit_count = u16::from_le_bytes(buffer[10..12].try_into().unwrap()); + let compression = u32::from_le_bytes(buffer[12..16].try_into().unwrap()); + + // Top-down DIBs cannot be compressed (per BMP specification). + // In lenient mode, we allow this for compatibility with other decoders. + if spec_strictness == BmpSpec::Strict + && top_down + && compression != BI_RGB + && compression != BI_BITFIELDS + && compression != BI_ALPHABITFIELDS + { + return Err(DecoderError::ImageTypeInvalidForTopDown(compression).into()); + } + + // Skip size_image (16-19), x_pix_permeter (20-23), y_pix_permeter (24-27) + let colors_used = u32::from_le_bytes(buffer[28..32].try_into().unwrap()); + // Skip important_colors (32-35) + Ok(ParsedInfoHeader { + width, + height, + top_down, + bit_count, + compression, + colors_used, + }) + } +} + +/// Parsed bitfield masks from DIB header. +struct ParsedBitfields { + r_mask: u32, + g_mask: u32, + b_mask: u32, + a_mask: u32, +} + +impl ParsedBitfields { + /// Parse bitfield masks from buffer. + /// Caller must ensure buffer has sufficient length; this method does not validate. + /// Note: Caller must ensure buffer has 12 (V2/Core) or 16 (V3/V4/V5) bytes length; this method does not validate. + #[track_caller] + fn parse(buffer: &[u8], has_alpha: bool) -> Self { + let r_mask = u32::from_le_bytes(buffer[0..4].try_into().unwrap()); + let g_mask = u32::from_le_bytes(buffer[4..8].try_into().unwrap()); + let b_mask = u32::from_le_bytes(buffer[8..12].try_into().unwrap()); + let a_mask = if has_alpha { + u32::from_le_bytes(buffer[12..16].try_into().unwrap()) + } else { + 0 + }; + + ParsedBitfields { + r_mask, + g_mask, + b_mask, + a_mask, + } + } +} + +/// Parsed ICC profile metadata from V5 header. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ParsedIccProfile { + /// Absolute file offset where the ICC profile data starts. + profile_offset: u64, + profile_size: u32, +} + +impl ParsedIccProfile { + /// Parse ICC profile metadata from V5 header buffer. + /// Returns None if no embedded ICC profile is present. + /// Note: Caller must ensure buffer has 116 bytes length; this method does not validate. + #[track_caller] + fn parse(buffer: &[u8], bmp_header_offset: u64) -> Option { + // bV5CSType is at offset 56 from header start, which is offset 52 from after the size field + let cs_type = u32::from_le_bytes(buffer[52..56].try_into().unwrap()); + + // Only embedded profiles are supported + if cs_type != PROFILE_EMBEDDED { + return None; + } + + // bV5ProfileData is at offset 112 from header start, which is offset 108 from after size field + let profile_offset_from_header = u32::from_le_bytes(buffer[108..112].try_into().unwrap()); + + // bV5ProfileSize is at offset 116 from header start, which is offset 112 from after size field + let profile_size = u32::from_le_bytes(buffer[112..116].try_into().unwrap()); + + if profile_size == 0 || profile_offset_from_header == 0 { + return None; + } + + // Compute the absolute file offset by adding the header's position to the relative offset + let profile_offset = bmp_header_offset + u64::from(profile_offset_from_header); + + Some(ParsedIccProfile { + profile_offset, + profile_size, + }) + } +} + #[derive(PartialEq, Copy, Clone)] enum ImageType { Palette, @@ -73,10 +311,87 @@ enum ImageType { RGBA32, RLE8, RLE4, + RLE24, Bitfields16, Bitfields32, } +/// Progress within the metadata reading phase. +/// +/// The metadata is split into phases: +/// 1. Headers: File header, DIB header, and bitmasks (~30-150 bytes total). +/// These are always re-read together on retry since they're small. +/// 2. Optional data: Palette (up to 1KB) and ICC profile (variable, can be several KB). +/// These are tracked separately since they can be larger. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum MetadataProgress { + /// Initial state, nothing read yet. + #[default] + NotStarted, + /// Reading main headers (file header, DIB header, bitmasks). + /// Stores the start offset for seeking on retry. + ReadingMainHeader { start_offset: u64 }, + /// Headers have been read; now reading palette. + /// Stores header offsets for subsequent phases. + ReadingPalette { offsets: HeaderOffsets }, + /// Headers and palette (if any) have been read; now reading ICC profile. + /// Stores header offsets for the ICC profile read. + ReadingIccProfile { offsets: HeaderOffsets }, + /// All metadata has been read successfully. + Complete, +} + +/// Offsets and sizes discovered during header parsing. +/// Carried through metadata phases to avoid redundant state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct HeaderOffsets { + /// Offset where palette data starts (after headers). + palette_offset: u64, + /// ICC profile metadata if present. + icc_profile: Option, +} + +/// Progress within the RLE decoding phase. +/// +/// RLE decoding checkpoints at row boundaries (after EndOfRow markers) and +/// after Delta instructions to avoid quadratic time with malformed files. +/// On UnexpectedEof, decoding resumes from the last stored checkpoint. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum RleProgress { + /// Not started yet. + #[default] + NotStarted, + /// Checkpoint at position (row, x) with stream at stream_pos. + /// On resume, decoding continues from this exact pixel position. + Checkpoint { row: u32, x: u32, stream_pos: u64 }, +} + +/// Decoder state for resumable decoding. +/// +/// This allows the decoder to recover from `UnexpectedEof` errors. +/// Decoding can resume from the last successfully decoded row or RLE symbol. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DecoderState { + /// Currently reading metadata (headers, palette, ICC profile). + ReadingMetadata { progress: MetadataProgress }, + /// Currently reading row-based (non-RLE) image data. + /// Stores the number of rows successfully decoded. + ReadingRowData { rows_decoded: u32 }, + /// Currently reading RLE-compressed data. + /// Tracks progress at symbol boundaries for resumability. + ReadingRleData { progress: RleProgress }, + /// Image data has been fully decoded. + ImageDecoded, +} + +impl Default for DecoderState { + fn default() -> Self { + DecoderState::ReadingMetadata { + progress: MetadataProgress::default(), + } + } +} + #[derive(PartialEq)] enum BMPHeaderType { Core, @@ -85,6 +400,7 @@ enum BMPHeaderType { V3, V4, V5, + Os2V2, } #[derive(PartialEq)] @@ -95,6 +411,15 @@ enum FormatFullBytes { Format888, } +/// Compression type for bitfield-based formats. +#[derive(PartialEq, Copy, Clone)] +enum BitfieldCompression { + /// BI_BITFIELDS: RGB masks only (3 masks, 12 bytes after header). + Rgb, + /// BI_ALPHABITFIELDS: RGBA masks (4 masks, 16 bytes after header). + Rgba, +} + enum Chunker<'a> { FromTop(ChunksExactMut<'a, u8>), FromBottom(Rev>), @@ -160,6 +485,9 @@ enum DecoderError { colors_used: u32, bit_count: u16, }, + + /// read_image_data was called before read_metadata completed + MetadataNotRead, } impl fmt::Display for DecoderError { @@ -199,6 +527,9 @@ impl fmt::Display for DecoderError { } => f.write_fmt(format_args!( "Palette size {colors_used} exceeds maximum size for BMP with bit count of {bit_count}" )), + DecoderError::MetadataNotRead => { + f.write_str("read_image_data called before read_metadata completed") + } } } } @@ -220,6 +551,8 @@ enum ChannelWidthError { Rle8, /// 4-bit run length encoding Rle4, + /// 24-bit run length encoding (OS/2) + Rle24, /// Bitfields (16- or 32-bit) Bitfields, } @@ -230,11 +563,33 @@ impl fmt::Display for ChannelWidthError { ChannelWidthError::Rgb => "RGB", ChannelWidthError::Rle8 => "RLE8", ChannelWidthError::Rle4 => "RLE4", + ChannelWidthError::Rle24 => "RLE24", ChannelWidthError::Bitfields => "bitfields", }) } } +/// BMP rows must be padded to a multiple of 4 bytes. +#[inline] +fn calculate_row_padding(bytes_per_row: usize) -> usize { + (4 - (bytes_per_row % 4)) % 4 +} + +/// Allocate a row buffer with OOM protection. +fn allocate_row_buffer(size: usize) -> ImageResult> { + let mut buffer = vec_try_with_capacity(size).map_err(|_| { + ImageError::Unsupported(UnsupportedError::from_format_and_kind( + ImageFormat::Bmp.into(), + UnsupportedErrorKind::GenericFeature(format!( + "Row buffer allocation ({} bytes) too large", + size + )), + )) + })?; + buffer.resize(size, 0); + Ok(buffer) +} + /// Convenience function to check if the combination of width, length and number of /// channels would result in a buffer that would overflow. fn check_for_overflow(width: i32, length: i32, channels: usize) -> ImageResult<()> { @@ -263,35 +618,58 @@ fn num_bytes(width: i32, length: i32, channels: usize) -> Option { } } -/// Call the provided function on each row of the provided buffer, returning Err if the provided -/// function returns an error, extends the buffer if it's not large enough. -fn with_rows( +/// Process rows with resumability support. +/// +/// Calls `func` for each row from `start_row` to `height`, passing the output row slice. +/// On success, returns the total number of rows (height). +/// On error, returns the number of rows successfully completed before the error. +/// +/// The caller is responsible for seeking to the correct file position before calling. +fn with_rows_resumable( buffer: &mut [u8], width: i32, height: i32, channels: usize, top_down: bool, + start_row: u32, mut func: F, -) -> io::Result<()> +) -> Result where F: FnMut(&mut [u8]) -> io::Result<()>, { // An overflow should already have been checked for when this is called, // though we check anyhow, as it somehow seems to increase performance slightly. let row_width = channels.checked_mul(width as usize).unwrap(); - let full_image_size = row_width.checked_mul(height as usize).unwrap(); - assert_eq!(buffer.len(), full_image_size); - - if !top_down { - for row in buffer.chunks_mut(row_width).rev() { - func(row)?; + let height = height as u32; + + /// Get the index of a row in the output buffer given the file row index. + /// For top-down images, row 0 in the file is row 0 in the buffer. + /// For bottom-up images, row 0 in the file is the last row in the buffer. + #[inline] + fn output_row_index(file_row: u32, height: u32, top_down: bool) -> usize { + if top_down { + file_row as usize + } else { + (height - 1 - file_row) as usize } - } else { - for row in buffer.chunks_mut(row_width) { - func(row)?; + } + + /// Get a mutable reference to a specific row in the output buffer. + #[inline] + fn get_row_mut(buf: &mut [u8], row_index: usize, row_stride: usize) -> &mut [u8] { + let start = row_index * row_stride; + &mut buf[start..][..row_stride] + } + + for file_row in start_row..height { + let out_row_idx = output_row_index(file_row, height, top_down); + let row = get_row_mut(buffer, out_row_idx, row_width); + + if let Err(e) = func(row) { + return Err((file_row, e)); } } - Ok(()) + Ok(height) } fn set_8bit_pixel_run<'a, T: Iterator>( @@ -428,6 +806,7 @@ impl Bitfield { fn read(&self, data: u32) -> u8 { let data = data >> self.shift; match self.len { + 0 => 0, 1 => ((data & 0b1) * 0xff) as u8, 2 => ((data & 0b11) * 0x55) as u8, 3 => LOOKUP_TABLE_3_BIT_TO_8_BIT[(data & 0b00_0111) as usize], @@ -456,6 +835,7 @@ impl Bitfields { b_mask: u32, a_mask: u32, max_len: u32, + spec_strictness: BmpSpec, ) -> ImageResult { let bitfields = Bitfields { r: Bitfield::from_mask(r_mask, max_len)?, @@ -463,13 +843,76 @@ impl Bitfields { b: Bitfield::from_mask(b_mask, max_len)?, a: Bitfield::from_mask(a_mask, max_len)?, }; - if bitfields.r.len == 0 || bitfields.g.len == 0 || bitfields.b.len == 0 { + // In strict mode, all RGB channels must have non-zero masks. + // In lenient mode, allow zero masks (the channel will read as 0). + if spec_strictness == BmpSpec::Strict + && (bitfields.r.len == 0 || bitfields.g.len == 0 || bitfields.b.len == 0) + { return Err(DecoderError::BitfieldMaskMissing(max_len).into()); } Ok(bitfields) } } +/// Helper to read RLE data using the already-buffered reader. +/// Avoids double-buffering since BmpDecoder already requires BufRead. +struct RleReader<'a, R> { + reader: &'a mut R, + bytes_read: u64, +} + +impl<'a, R: BufRead> RleReader<'a, R> { + fn new(reader: &'a mut R) -> Self { + Self { + reader, + bytes_read: 0, + } + } + + /// Total bytes consumed since this reader was created. + fn bytes_read(&self) -> u64 { + self.bytes_read + } + + fn read_byte(&mut self) -> io::Result { + let buf = self.reader.fill_buf()?; + if buf.is_empty() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "unexpected end of RLE data", + )); + } + let byte = buf[0]; + self.reader.consume(1); + self.bytes_read += 1; + Ok(byte) + } + + fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { + let mut remaining = buf.len(); + let mut offset = 0; + + while remaining > 0 { + let available = self.reader.fill_buf()?; + if available.is_empty() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "unexpected end of RLE data", + )); + } + + let to_read = remaining.min(available.len()); + buf[offset..offset + to_read].copy_from_slice(&available[..to_read]); + self.reader.consume(to_read); + self.bytes_read += to_read as u64; + offset += to_read; + remaining -= to_read; + } + + Ok(()) + } +} + /// A bmp decoder pub struct BmpDecoder { reader: R, @@ -483,21 +926,17 @@ pub struct BmpDecoder { top_down: bool, no_file_header: bool, add_alpha_channel: bool, - has_loaded_metadata: bool, image_type: ImageType, bit_count: u16, colors_used: u32, palette: Option>, bitfields: Option, -} + icc_profile: Option>, + spec_strictness: BmpSpec, -enum RLEInsn { - EndOfFile, - EndOfRow, - Delta(u8, u8), - Absolute(u8, Vec), - PixelRun(u8, u8), + /// Current decoder state for resumable decoding. + state: DecoderState, } impl BmpDecoder { @@ -514,13 +953,15 @@ impl BmpDecoder { top_down: false, no_file_header: false, add_alpha_channel: false, - has_loaded_metadata: false, image_type: ImageType::Palette, bit_count: 0, colors_used: 0, palette: None, bitfields: None, + icc_profile: None, + spec_strictness: BmpSpec::default(), + state: DecoderState::default(), } } @@ -531,6 +972,60 @@ impl BmpDecoder { Ok(decoder) } + /// Create a new decoder that decodes from the stream `r` without reading + /// metadata immediately. This allows for resumable decoding when the + /// underlying reader may return `UnexpectedEof`. + /// + /// After creating the decoder, call `read_metadata()` to read the BMP + /// headers. If it returns an `UnexpectedEof` error, you can retry on the + /// same decoder instance after more data becomes available. + /// + /// Once metadata is read, call `read_image_data()` to read the pixel data. + /// This also supports retrying on `UnexpectedEof`. + /// + /// # Example + /// + /// ```ignore + /// use image::codecs::bmp::BmpDecoder; + /// use image::error::ImageError; + /// use image::ImageDecoder; + /// use std::io; + /// + /// fn is_unexpected_eof(err: &ImageError) -> bool { + /// matches!(err, ImageError::IoError(e) if e.kind() == io::ErrorKind::UnexpectedEof) + /// } + /// + /// let mut decoder = BmpDecoder::new_resumable(reader); + /// + /// // Phase 1: Read metadata (with retry on UnexpectedEof) + /// loop { + /// match decoder.read_metadata() { + /// Ok(()) => break, + /// Err(ref e) if is_unexpected_eof(e) => { + /// // Wait for more data and retry on same decoder + /// continue; + /// } + /// Err(e) => return Err(e), + /// } + /// } + /// + /// // Phase 2: Read image data (with retry on UnexpectedEof) + /// let mut buf = vec![0u8; decoder.total_bytes() as usize]; + /// loop { + /// match decoder.read_image_data(&mut buf) { + /// Ok(()) => break, + /// Err(ref e) if is_unexpected_eof(e) => { + /// // Wait for more data and retry on same decoder + /// continue; + /// } + /// Err(e) => return Err(e), + /// } + /// } + /// ``` + pub fn new_resumable(reader: R) -> BmpDecoder { + Self::new_decoder(reader) + } + /// Create a new decoder that decodes from the stream ```r``` without first /// reading a BITMAPFILEHEADER. This is useful for decoding the `CF_DIB` format /// directly from the Windows clipboard. @@ -563,51 +1058,144 @@ impl BmpDecoder { if self.no_file_header { return Ok(()); } - let mut signature = [0; 2]; - self.reader.read_exact(&mut signature)?; - if signature != b"BM"[..] { + // Read entire 14-byte file header + const FILE_HEADER_SIZE: usize = 14; + let mut buffer = [0u8; FILE_HEADER_SIZE]; + self.reader.read_exact(&mut buffer)?; + + // Check signature + if &buffer[0..2] != b"BM" { return Err(DecoderError::BmpSignatureInvalid.into()); } - // The next 8 bytes represent file size, followed the 4 reserved bytes - // We're not interesting these values - self.reader.read_u32::()?; - self.reader.read_u32::()?; - - self.data_offset = u64::from(self.reader.read_u32::()?); + // Skip file size (4 bytes) and reserved (4 bytes) at offsets 2-9 + // Extract data_offset from bytes 10-13 + let data_offset = u32::from_le_bytes([buffer[10], buffer[11], buffer[12], buffer[13]]); + self.data_offset = u64::from(data_offset); Ok(()) } + /// Determine the image type from the compression method, bit count, and header type. + fn image_type_from_compression( + compression: u32, + bit_count: u16, + add_alpha_channel: bool, + header_type: &BMPHeaderType, + ) -> ImageResult { + match compression { + BI_RGB => match bit_count { + 1 | 2 | 4 | 8 => Ok(ImageType::Palette), + 16 => Ok(ImageType::RGB16), + 24 => Ok(ImageType::RGB24), + 32 if add_alpha_channel => Ok(ImageType::RGBA32), + 32 => Ok(ImageType::RGB32), + _ => { + Err(DecoderError::InvalidChannelWidth(ChannelWidthError::Rgb, bit_count).into()) + } + }, + BI_RLE8 => match bit_count { + 8 => Ok(ImageType::RLE8), + _ => Err( + DecoderError::InvalidChannelWidth(ChannelWidthError::Rle8, bit_count).into(), + ), + }, + BI_RLE4 => match bit_count { + 4 => Ok(ImageType::RLE4), + _ => Err( + DecoderError::InvalidChannelWidth(ChannelWidthError::Rle4, bit_count).into(), + ), + }, + BI_BITFIELDS | BI_ALPHABITFIELDS => match bit_count { + 16 => Ok(ImageType::Bitfields16), + 32 => Ok(ImageType::Bitfields32), + _ => Err(DecoderError::InvalidChannelWidth( + ChannelWidthError::Bitfields, + bit_count, + ) + .into()), + }, + BI_JPEG if *header_type == BMPHeaderType::Os2V2 && bit_count == 24 => { + Ok(ImageType::RLE24) + } + BI_JPEG if *header_type == BMPHeaderType::Os2V2 => { + Err(DecoderError::InvalidChannelWidth(ChannelWidthError::Rle24, bit_count).into()) + } + BI_JPEG => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Bmp.into(), + UnsupportedErrorKind::GenericFeature("JPEG compression".to_owned()), + ), + )), + BI_PNG => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Bmp.into(), + UnsupportedErrorKind::GenericFeature("PNG compression".to_owned()), + ), + )), + BI_CMYK | BI_CMYKRLE4 | BI_CMYKRLE8 => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Bmp.into(), + UnsupportedErrorKind::GenericFeature("CMYK format".to_owned()), + ), + )), + _ => Err(DecoderError::ImageTypeUnknown(compression).into()), + } + } + /// Read BITMAPCOREHEADER /// /// returns Err if any of the values are invalid. fn read_bitmap_core_header(&mut self) -> ImageResult<()> { - // As height/width values in BMP files with core headers are only 16 bits long, - // they won't be larger than `MAX_WIDTH_HEIGHT`. - self.width = i32::from(self.reader.read_u16::()?); - self.height = i32::from(self.reader.read_u16::()?); + // Core header (after size field): width(2), height(2), planes(2), bitcount(2) = 8 bytes + let mut buffer = [0u8; 8]; + self.reader.read_exact(&mut buffer)?; + + let parsed = ParsedCoreHeader::parse(&buffer, self.spec_strictness)?; + + self.width = parsed.width; + self.height = parsed.height; + self.bit_count = parsed.bit_count; + self.image_type = parsed.image_type; check_for_overflow(self.width, self.height, self.num_channels())?; - // Number of planes (format specifies that this should be 1). - if self.reader.read_u16::()? != 1 { - return Err(DecoderError::MoreThanOnePlane.into()); + Ok(()) + } + + /// Read OS/2 BITMAPCOREHEADER2 (variable size 16-64 bytes, layout-compatible + /// with BITMAPINFOHEADER). Fields beyond the header size default to 0. + fn read_bitmap_os2v2_header(&mut self, header_size: u32) -> ImageResult<()> { + let remaining = (header_size - 4) as usize; + + // Zero-pad to 36 bytes for ParsedInfoHeader::parse. + let mut buffer = [0u8; 36]; + let to_read = remaining.min(36); + self.reader.read_exact(&mut buffer[..to_read])?; + + // Skip OS/2-specific fields beyond the BITMAPINFOHEADER portion (max 28 bytes). + if remaining > 36 { + let skip = remaining - 36; + let mut discard = [0u8; 28]; + self.reader.read_exact(&mut discard[..skip])?; } - self.bit_count = self.reader.read_u16::()?; - self.image_type = match self.bit_count { - 1 | 4 | 8 => ImageType::Palette, - 24 => ImageType::RGB24, - _ => { - return Err(DecoderError::InvalidChannelWidth( - ChannelWidthError::Rgb, - self.bit_count, - ) - .into()) - } - }; + let parsed = ParsedInfoHeader::parse(&buffer, self.spec_strictness)?; + + self.width = parsed.width; + self.height = parsed.height; + self.top_down = parsed.top_down; + self.bit_count = parsed.bit_count; + self.colors_used = parsed.colors_used; + self.image_type = Self::image_type_from_compression( + parsed.compression, + parsed.bit_count, + self.add_alpha_channel, + &self.bmp_header_type, + )?; + + check_for_overflow(self.width, self.height, self.num_channels())?; Ok(()) } @@ -615,251 +1203,302 @@ impl BmpDecoder { /// Read BITMAPINFOHEADER /// or BITMAPV{2|3|4|5}HEADER. /// - /// returns Err if any of the values are invalid. - fn read_bitmap_info_header(&mut self) -> ImageResult<()> { - self.width = self.reader.read_i32::()?; - self.height = self.reader.read_i32::()?; - - // Width can not be negative - if self.width < 0 { - return Err(DecoderError::NegativeWidth(self.width).into()); - } else if self.width > MAX_WIDTH_HEIGHT || self.height > MAX_WIDTH_HEIGHT { - // Limit very large image sizes to avoid OOM issues. Images with these sizes are - // unlikely to be valid anyhow. - return Err(DecoderError::ImageTooLarge(self.width, self.height).into()); - } - - if self.height == i32::MIN { - return Err(DecoderError::InvalidHeight.into()); - } - - // A negative height indicates a top-down DIB. - if self.height < 0 { - self.height *= -1; - self.top_down = true; - } + /// Returns the bitfield compression type or Err if any of the values are invalid. + fn read_bitmap_info_header(&mut self) -> ImageResult { + // Info header (after size field): 36 bytes minimum + let mut buffer = [0u8; 36]; + self.reader.read_exact(&mut buffer)?; + + let parsed = ParsedInfoHeader::parse(&buffer, self.spec_strictness)?; + + self.width = parsed.width; + self.height = parsed.height; + self.top_down = parsed.top_down; + self.bit_count = parsed.bit_count; + self.colors_used = parsed.colors_used; + self.image_type = Self::image_type_from_compression( + parsed.compression, + parsed.bit_count, + self.add_alpha_channel, + &self.bmp_header_type, + )?; check_for_overflow(self.width, self.height, self.num_channels())?; - // Number of planes (format specifies that this should be 1). - if self.reader.read_u16::()? != 1 { - return Err(DecoderError::MoreThanOnePlane.into()); - } - - self.bit_count = self.reader.read_u16::()?; - let image_type_u32 = self.reader.read_u32::()?; - - // Top-down dibs can not be compressed. - if self.top_down && image_type_u32 != 0 && image_type_u32 != 3 { - return Err(DecoderError::ImageTypeInvalidForTopDown(image_type_u32).into()); - } - self.image_type = match image_type_u32 { - 0 => match self.bit_count { - 1 | 2 | 4 | 8 => ImageType::Palette, - 16 => ImageType::RGB16, - 24 => ImageType::RGB24, - 32 if self.add_alpha_channel => ImageType::RGBA32, - 32 => ImageType::RGB32, - _ => { - return Err(DecoderError::InvalidChannelWidth( - ChannelWidthError::Rgb, - self.bit_count, - ) - .into()) - } - }, - 1 => match self.bit_count { - 8 => ImageType::RLE8, - _ => { - return Err(DecoderError::InvalidChannelWidth( - ChannelWidthError::Rle8, - self.bit_count, - ) - .into()) - } - }, - 2 => match self.bit_count { - 4 => ImageType::RLE4, - _ => { - return Err(DecoderError::InvalidChannelWidth( - ChannelWidthError::Rle4, - self.bit_count, - ) - .into()) - } - }, - 3 => match self.bit_count { - 16 => ImageType::Bitfields16, - 32 => ImageType::Bitfields32, - _ => { - return Err(DecoderError::InvalidChannelWidth( - ChannelWidthError::Bitfields, - self.bit_count, - ) - .into()) - } - }, - 4 => { - // JPEG compression is not implemented yet. - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Bmp.into(), - UnsupportedErrorKind::GenericFeature("JPEG compression".to_owned()), - ), - )); - } - 5 => { - // PNG compression is not implemented yet. - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Bmp.into(), - UnsupportedErrorKind::GenericFeature("PNG compression".to_owned()), - ), - )); - } - 11..=13 => { - // CMYK types are not implemented yet. - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Bmp.into(), - UnsupportedErrorKind::GenericFeature("CMYK format".to_owned()), - ), - )); - } - _ => { - // Unknown compression type. - return Err(DecoderError::ImageTypeUnknown(image_type_u32).into()); - } + let compression = match parsed.compression { + BI_ALPHABITFIELDS => BitfieldCompression::Rgba, + _ => BitfieldCompression::Rgb, }; - - // The next 12 bytes represent data array size in bytes, - // followed the horizontal and vertical printing resolutions - // We will calculate the pixel array size using width & height of image - // We're not interesting the horz or vert printing resolutions - self.reader.read_u32::()?; - self.reader.read_u32::()?; - self.reader.read_u32::()?; - - self.colors_used = self.reader.read_u32::()?; - - // The next 4 bytes represent number of "important" colors - // We're not interested in this value, so we'll skip it - self.reader.read_u32::()?; - - Ok(()) + Ok(compression) } - fn read_bitmasks(&mut self) -> ImageResult<()> { - let r_mask = self.reader.read_u32::()?; - let g_mask = self.reader.read_u32::()?; - let b_mask = self.reader.read_u32::()?; + fn read_bitmasks(&mut self, compression: BitfieldCompression) -> ImageResult<()> { + // Determine if we need to read alpha mask: + // - V3/V4/V5 headers have the alpha mask embedded in the header + // - BI_ALPHABITFIELDS compression has a 4th mask after the header + let has_alpha = matches!( + self.bmp_header_type, + BMPHeaderType::V3 | BMPHeaderType::V4 | BMPHeaderType::V5 + ) || compression == BitfieldCompression::Rgba; - let a_mask = match self.bmp_header_type { - BMPHeaderType::V3 | BMPHeaderType::V4 | BMPHeaderType::V5 => { - self.reader.read_u32::()? - } - _ => 0, - }; + // Read bitfield masks into buffer + let buffer_size = if has_alpha { 16 } else { 12 }; + let mut buffer = vec![0u8; buffer_size]; + self.reader.read_exact(&mut buffer)?; + // Parse masks using shared logic + let parsed = ParsedBitfields::parse(&buffer, has_alpha); + + // Create Bitfields from parsed masks self.bitfields = match self.image_type { - ImageType::Bitfields16 => { - Some(Bitfields::from_mask(r_mask, g_mask, b_mask, a_mask, 16)?) - } - ImageType::Bitfields32 => { - Some(Bitfields::from_mask(r_mask, g_mask, b_mask, a_mask, 32)?) + ImageType::Bitfields16 | ImageType::Bitfields32 => { + let max_len = match self.image_type { + ImageType::Bitfields16 => 16, + ImageType::Bitfields32 => 32, + _ => unreachable!(), + }; + Some(Bitfields::from_mask( + parsed.r_mask, + parsed.g_mask, + parsed.b_mask, + parsed.a_mask, + max_len, + self.spec_strictness, + )?) } _ => None, }; - if self.bitfields.is_some() && a_mask != 0 { + if self.bitfields.is_some() && parsed.a_mask != 0 { self.add_alpha_channel = true; } Ok(()) } - fn read_metadata(&mut self) -> ImageResult<()> { - if !self.has_loaded_metadata { - self.read_file_header()?; - let bmp_header_offset = self.reader.stream_position()?; - let bmp_header_size = self.reader.read_u32::()?; - let bmp_header_end = bmp_header_offset + u64::from(bmp_header_size); - - self.bmp_header_type = match bmp_header_size { - BITMAPCOREHEADER_SIZE => BMPHeaderType::Core, - BITMAPINFOHEADER_SIZE => BMPHeaderType::Info, - BITMAPV2HEADER_SIZE => BMPHeaderType::V2, - BITMAPV3HEADER_SIZE => BMPHeaderType::V3, - BITMAPV4HEADER_SIZE => BMPHeaderType::V4, - BITMAPV5HEADER_SIZE => BMPHeaderType::V5, - _ if bmp_header_size < BITMAPCOREHEADER_SIZE => { - // Size of any valid header types won't be smaller than core header type. - return Err(DecoderError::HeaderTooSmall(bmp_header_size).into()); - } - _ => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Bmp.into(), - UnsupportedErrorKind::GenericFeature(format!( - "Unknown bitmap header type (size={bmp_header_size})" - )), - ), - )) - } - }; - - match self.bmp_header_type { - BMPHeaderType::Core => { - self.read_bitmap_core_header()?; - } - BMPHeaderType::Info - | BMPHeaderType::V2 - | BMPHeaderType::V3 - | BMPHeaderType::V4 - | BMPHeaderType::V5 => { - self.read_bitmap_info_header()?; - } - } - - let mut bitmask_bytes_offset = 0; - if self.image_type == ImageType::Bitfields16 - || self.image_type == ImageType::Bitfields32 - { - self.read_bitmasks()?; - - // Per https://learn.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types, bitmaps - // using the `BITMAPINFOHEADER`, `BITMAPV4HEADER`, or `BITMAPV5HEADER` structures with - // an image type of `BI_BITFIELD` contain RGB bitfield masks immediately after the header. - // - // `read_bitmasks` correctly reads these from earlier in the header itself but we must - // ensure the reader starts on the image data itself, not these extra mask bytes. - if matches!( - self.bmp_header_type, - BMPHeaderType::Info | BMPHeaderType::V4 | BMPHeaderType::V5 - ) { - // This is `size_of::() * 3` (a red, green, and blue mask), but with less noise. - bitmask_bytes_offset = 12; - } - }; - - self.reader - .seek(SeekFrom::Start(bmp_header_end + bitmask_bytes_offset))?; - - match self.image_type { - ImageType::Palette | ImageType::RLE4 | ImageType::RLE8 => self.read_palette()?, - _ => {} - } - - if self.no_file_header { - // Use the offset of the end of metadata instead of reading a BMP file header. - self.data_offset = self.reader.stream_position()?; - } - - self.has_loaded_metadata = true; - } + /// Read ICC profile data from the file. + fn read_icc_profile(&mut self, icc: &ParsedIccProfile) -> ImageResult<()> { + self.reader.seek(SeekFrom::Start(icc.profile_offset))?; + let mut profile_data = vec![0u8; icc.profile_size as usize]; + self.reader.read_exact(&mut profile_data)?; + self.icc_profile = Some(profile_data); Ok(()) } + /// Read BMP metadata (headers, palette, etc.). + /// + /// On `UnexpectedEof`, the decoder can be retried - the implementation tracks + /// progress and resumes from where it left off. Once successful, subsequent + /// calls are no-ops. + /// + /// Metadata reading is divided into phases: + /// 1. Headers: File header, DIB header, and bitmasks (~30-150 bytes). + /// These are re-read together on retry since they're small. + /// 2. Palette: Up to 1KB for indexed color images. + /// 3. ICC profile: Variable size, can be several KB (V5 headers only). + pub fn read_metadata(&mut self) -> ImageResult<()> { + // Check if we're in a metadata reading state + let DecoderState::ReadingMetadata { progress } = self.state else { + return Ok(()); // Already past metadata phase + }; + + match self.read_metadata_impl(progress) { + Ok(()) => { + // Transition directly to the appropriate image reading state + self.state = if self.is_rle() { + DecoderState::ReadingRleData { + progress: RleProgress::NotStarted, + } + } else { + DecoderState::ReadingRowData { rows_decoded: 0 } + }; + Ok(()) + } + Err(e) => Err(e), + } + } + + /// Internal implementation of metadata reading with phased resumability. + /// + /// Uses recursive calls to progress through phases. Each phase either: + /// - Succeeds and calls the next phase + /// - Fails with an error (which may be retryable like UnexpectedEof) + /// + /// Recursion depth is bounded (max 4): NotStarted → ReadingMainHeader → ReadingPalette → ReadingIccProfile → Complete + fn read_metadata_impl(&mut self, progress: MetadataProgress) -> ImageResult<()> { + match progress { + MetadataProgress::NotStarted => { + // Record current position and transition to ReadingMainHeader + let start_offset = self.reader.stream_position()?; + let next = MetadataProgress::ReadingMainHeader { start_offset }; + self.state = DecoderState::ReadingMetadata { progress: next }; + self.read_metadata_impl(next) + } + MetadataProgress::ReadingMainHeader { start_offset } => { + // Seek to start position (for retry support) + self.reader.seek(SeekFrom::Start(start_offset))?; + + // Read headers and get offsets for subsequent phases + let offsets = self.read_headers()?; + + // Always progress to ReadingPalette next + let next = MetadataProgress::ReadingPalette { offsets }; + self.state = DecoderState::ReadingMetadata { progress: next }; + self.read_metadata_impl(next) + } + MetadataProgress::ReadingPalette { offsets } => { + // Always seek to palette position (this is also where image data starts + // for non-palette formats) + self.reader.seek(SeekFrom::Start(offsets.palette_offset))?; + + // Read palette if needed for this image type + if matches!( + self.image_type, + ImageType::Palette | ImageType::RLE4 | ImageType::RLE8 + ) { + self.read_palette()?; + } + + // For no_file_header mode, capture data_offset now (after palette read) + // before ICC profile reading potentially changes reader position + if self.no_file_header { + self.data_offset = self.reader.stream_position()?; + } + + // Always progress to ReadingIccProfile next + let next = MetadataProgress::ReadingIccProfile { offsets }; + self.state = DecoderState::ReadingMetadata { progress: next }; + self.read_metadata_impl(next) + } + MetadataProgress::ReadingIccProfile { offsets } => { + // Read ICC profile if present + if let Some(ref icc) = offsets.icc_profile { + self.read_icc_profile(icc)?; + } + + // Always progress to Complete next + self.state = DecoderState::ReadingMetadata { + progress: MetadataProgress::Complete, + }; + self.read_metadata_impl(MetadataProgress::Complete) + } + MetadataProgress::Complete => Ok(()), + } + } + + /// Read headers phase: file header, DIB header, and bitmasks. + /// Returns HeaderOffsets containing positions for subsequent phases. + fn read_headers(&mut self) -> ImageResult { + self.read_file_header()?; + let bmp_header_offset = self.reader.stream_position()?; + + // Read header size into buffer for consistency with buffer-based pattern + let mut size_buffer = [0u8; 4]; + self.reader.read_exact(&mut size_buffer)?; + let bmp_header_size = u32::from_le_bytes(size_buffer); + + let bmp_header_end = bmp_header_offset + u64::from(bmp_header_size); + + self.bmp_header_type = match bmp_header_size { + BITMAPCOREHEADER_SIZE => BMPHeaderType::Core, + BITMAPINFOHEADER_SIZE => BMPHeaderType::Info, + BITMAPV2HEADER_SIZE => BMPHeaderType::V2, + BITMAPV3HEADER_SIZE => BMPHeaderType::V3, + BITMAPV4HEADER_SIZE => BMPHeaderType::V4, + BITMAPV5HEADER_SIZE => BMPHeaderType::V5, + _ if bmp_header_size < BITMAPCOREHEADER_SIZE => { + // Size of any valid header types won't be smaller than core header type. + return Err(DecoderError::HeaderTooSmall(bmp_header_size).into()); + } + // OS/2 BITMAPCOREHEADER2 (OS22XBITMAPHEADER): 16-64 bytes, 4-byte aligned + // (plus special sizes 42 and 46). Sizes 40/52/56 are caught by exact arms + // above and decoded as Windows headers (layout-compatible, so this is fine; + // the only difference is that a 40-byte OS/2 header with RLE24 won't trigger + // the Os2V2 path, but that combination is effectively nonexistent). + _ if (OS2_V2_MIN_HEADER_SIZE..=OS2_V2_MAX_HEADER_SIZE).contains(&bmp_header_size) + && (bmp_header_size % 4 == 0 || bmp_header_size == 42 || bmp_header_size == 46) => + { + BMPHeaderType::Os2V2 + } + _ => { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Bmp.into(), + UnsupportedErrorKind::GenericFeature(format!( + "Unknown bitmap header type (size={bmp_header_size})" + )), + ), + )) + } + }; + + let bitfield_compression = match self.bmp_header_type { + BMPHeaderType::Core => { + self.read_bitmap_core_header()?; + BitfieldCompression::Rgb + } + BMPHeaderType::Os2V2 => { + self.read_bitmap_os2v2_header(bmp_header_size)?; + BitfieldCompression::Rgb + } + BMPHeaderType::Info + | BMPHeaderType::V2 + | BMPHeaderType::V3 + | BMPHeaderType::V4 + | BMPHeaderType::V5 => self.read_bitmap_info_header()?, + }; + + let mut bitmask_bytes_offset = 0; + if matches!( + self.image_type, + ImageType::Bitfields16 | ImageType::Bitfields32 + ) { + self.read_bitmasks(bitfield_compression)?; + + // Per https://learn.microsoft.com/en-us/windows/win32/gdi/bitmap-header-types, bitmaps + // using the `BITMAPINFOHEADER`, `BITMAPV4HEADER`, or `BITMAPV5HEADER` structures with + // an image type of `BI_BITFIELD` or `BI_ALPHABITFIELDS` contain bitfield masks + // immediately after the header. + // + // `read_bitmasks` correctly reads these from earlier in the header itself but we must + // ensure the reader starts on the image data itself, not these extra mask bytes. + if matches!( + self.bmp_header_type, + BMPHeaderType::Info | BMPHeaderType::V4 | BMPHeaderType::V5 + ) { + bitmask_bytes_offset = match bitfield_compression { + BitfieldCompression::Rgba => 16, // 4 masks * 4 bytes + BitfieldCompression::Rgb => 12, // 3 masks * 4 bytes + }; + } + }; + + // Parse ICC profile metadata from V5 header (but don't read the profile data yet) + let mut icc_profile = None; + if bmp_header_size >= BITMAPV5HEADER_SIZE { + // Read the full V5 header into a buffer for ICC profile metadata parsing + // V5 header is 124 bytes total, minus 4-byte size field = 120 bytes + let mut header_buffer = vec![0u8; (bmp_header_size - 4) as usize]; + let current_pos = self.reader.stream_position()?; + self.reader.seek(SeekFrom::Start(bmp_header_offset + 4))?; + self.reader.read_exact(&mut header_buffer)?; + + // Extract ICC profile metadata for later reading + icc_profile = ParsedIccProfile::parse(&header_buffer, bmp_header_offset); + + // Seek back to where we were + self.reader.seek(SeekFrom::Start(current_pos))?; + } + + // Calculate palette offset (position after headers) + let palette_offset = bmp_header_end + bitmask_bytes_offset; + + Ok(HeaderOffsets { + palette_offset, + icc_profile, + }) + } + #[cfg(feature = "ico")] #[doc(hidden)] pub fn read_metadata_in_ico_format(&mut self) -> ImageResult<()> { @@ -877,14 +1516,17 @@ impl BmpDecoder { match self.colors_used { 0 => Ok(1 << self.bit_count), _ => { - if self.colors_used > 1 << self.bit_count { + if self.spec_strictness == BmpSpec::Strict && self.colors_used > 1 << self.bit_count + { return Err(DecoderError::PaletteSizeExceeded { colors_used: self.colors_used, bit_count: self.bit_count, } .into()); } - Ok(self.colors_used as usize) + // In lenient mode, clamp to max palette size for the bit depth + let max_size = 1usize << self.bit_count; + Ok((self.colors_used as usize).min(max_size)) } } } @@ -971,22 +1613,32 @@ impl BmpDecoder { let mut indices = vec![0; row_byte_length]; let palette = self.palette.as_ref().unwrap(); let bit_count = self.bit_count; - let reader = &mut self.reader; let width = self.width as usize; let skip_palette = self.indexed_color; - reader.seek(SeekFrom::Start(self.data_offset))?; + let rows_decoded = self.rows_decoded(); + let start_row = rows_decoded.rows(); + let top_down = matches!(rows_decoded, RowsDecoded::TopDown { .. }); + + let file_offset = self.data_offset + (start_row as u64 * row_byte_length as u64); + self.reader.seek(SeekFrom::Start(file_offset))?; - if num_channels == 4 { - buf.chunks_exact_mut(4).for_each(|c| c[3] = 0xFF); + // Set alpha to opaque for all pixels if needed (only on first call) + if start_row == 0 && num_channels == 4 { + buf.as_chunks_mut::<4>() + .0 + .iter_mut() + .for_each(|c| c[3] = ALPHA_OPAQUE); } - with_rows( + let reader = &mut self.reader; + let result = with_rows_resumable( buf, self.width, self.height, num_channels, - self.top_down, + top_down, + start_row, |row| { reader.read_exact(&mut indices)?; if skip_palette { @@ -1011,9 +1663,9 @@ impl BmpDecoder { } Ok(()) }, - )?; + ); - Ok(()) + self.finish_row_decode(result) } fn read_16_bit_pixel_data( @@ -1022,79 +1674,106 @@ impl BmpDecoder { bitfields: Option<&Bitfields>, ) -> ImageResult<()> { let num_channels = self.num_channels(); - let row_padding_len = self.width as usize % 2 * 2; - let row_padding = &mut [0; 2][..row_padding_len]; let bitfields = match bitfields { Some(b) => b, None => self.bitfields.as_ref().unwrap(), }; - let reader = &mut self.reader; - reader.seek(SeekFrom::Start(self.data_offset))?; + let row_data_len = self.width as usize * 2; + let row_padding_len = calculate_row_padding(row_data_len); + let total_row_len = row_data_len + row_padding_len; + + let rows_decoded = self.rows_decoded(); + let start_row = rows_decoded.rows(); + let top_down = matches!(rows_decoded, RowsDecoded::TopDown { .. }); + let width = self.width; + let height = self.height; + + let file_offset = self.data_offset + (start_row as u64 * total_row_len as u64); + self.reader.seek(SeekFrom::Start(file_offset))?; - with_rows( + let mut row_buffer = allocate_row_buffer(total_row_len)?; + + let reader = &mut self.reader; + let result = with_rows_resumable( buf, - self.width, - self.height, + width, + height, num_channels, - self.top_down, + top_down, + start_row, |row| { - for pixel in row.chunks_mut(num_channels) { - let data = u32::from(reader.read_u16::()?); - + reader.read_exact(&mut row_buffer)?; + let row_buffer_chunks = row_buffer.as_chunks::<2>().0.iter(); + for (&row_data, pixel) in row_buffer_chunks.zip(row.chunks_exact_mut(num_channels)) + { + let data = u32::from(u16::from_le_bytes(row_data)); pixel[0] = bitfields.r.read(data); pixel[1] = bitfields.g.read(data); pixel[2] = bitfields.b.read(data); if num_channels == 4 { - if bitfields.a.len != 0 { - pixel[3] = bitfields.a.read(data); + pixel[3] = if bitfields.a.len != 0 { + bitfields.a.read(data) } else { - pixel[3] = 0xFF; - } + ALPHA_OPAQUE + }; } } - reader.read_exact(row_padding) + Ok(()) }, - )?; + ); - Ok(()) + self.finish_row_decode(result) } /// Read image data from a reader in 32-bit formats that use bitfields. fn read_32_bit_pixel_data(&mut self, buf: &mut [u8]) -> ImageResult<()> { let num_channels = self.num_channels(); - let bitfields = self.bitfields.as_ref().unwrap(); - let reader = &mut self.reader; - reader.seek(SeekFrom::Start(self.data_offset))?; + let row_data_len = self.width as usize * 4; + + let rows_decoded = self.rows_decoded(); + let start_row = rows_decoded.rows(); + let top_down = matches!(rows_decoded, RowsDecoded::TopDown { .. }); + let width = self.width; + let height = self.height; - with_rows( + let file_offset = self.data_offset + (start_row as u64 * row_data_len as u64); + self.reader.seek(SeekFrom::Start(file_offset))?; + + let mut row_buffer = allocate_row_buffer(row_data_len)?; + + let reader = &mut self.reader; + let result = with_rows_resumable( buf, - self.width, - self.height, + width, + height, num_channels, - self.top_down, + top_down, + start_row, |row| { - for pixel in row.chunks_mut(num_channels) { - let data = reader.read_u32::()?; - + reader.read_exact(&mut row_buffer)?; + let row_buffer_chunks = row_buffer.as_chunks::<4>().0.iter(); + for (&row_data, pixel) in row_buffer_chunks.zip(row.chunks_exact_mut(num_channels)) + { + let data = u32::from_le_bytes(row_data); pixel[0] = bitfields.r.read(data); pixel[1] = bitfields.g.read(data); pixel[2] = bitfields.b.read(data); if num_channels == 4 { - if bitfields.a.len != 0 { - pixel[3] = bitfields.a.read(data); + pixel[3] = if bitfields.a.len != 0 { + bitfields.a.read(data) } else { - pixel[3] = 0xff; - } + ALPHA_OPAQUE + }; } } Ok(()) }, - )?; + ); - Ok(()) + self.finish_row_decode(result) } /// Read image data from a reader where the colours are stored as 8-bit values (24 or 32-bit). @@ -1104,220 +1783,345 @@ impl BmpDecoder { format: &FormatFullBytes, ) -> ImageResult<()> { let num_channels = self.num_channels(); + let row_data_len = match *format { + FormatFullBytes::RGB24 => self.width as usize * 3, + FormatFullBytes::Format888 => self.width as usize * 4, + FormatFullBytes::RGB32 | FormatFullBytes::RGBA32 => self.width as usize * 4, + }; let row_padding_len = match *format { - FormatFullBytes::RGB24 => (4 - (self.width as usize * 3) % 4) % 4, + FormatFullBytes::RGB24 => calculate_row_padding(row_data_len), _ => 0, }; - let row_padding = &mut [0; 4][..row_padding_len]; + let total_row_len = row_data_len + row_padding_len; - self.reader.seek(SeekFrom::Start(self.data_offset))?; + let rows_decoded = self.rows_decoded(); + let start_row = rows_decoded.rows(); + let top_down = matches!(rows_decoded, RowsDecoded::TopDown { .. }); + let width = self.width; + let height = self.height; - let reader = &mut self.reader; + let file_offset = self.data_offset + (start_row as u64 * total_row_len as u64); + self.reader.seek(SeekFrom::Start(file_offset))?; + + let mut row_buffer = allocate_row_buffer(total_row_len)?; - with_rows( + let reader = &mut self.reader; + let result = with_rows_resumable( buf, - self.width, - self.height, + width, + height, num_channels, - self.top_down, + top_down, + start_row, |row| { - for pixel in row.chunks_mut(num_channels) { - if *format == FormatFullBytes::Format888 { - reader.read_u8()?; - } + reader.read_exact(&mut row_buffer)?; - // Read the colour values (b, g, r). - // Reading 3 bytes and reversing them is significantly faster than reading one - // at a time. - reader.read_exact(&mut pixel[0..3])?; - pixel[0..3].reverse(); + for (i, pixel) in row.chunks_mut(num_channels).enumerate() { + let offset = match *format { + FormatFullBytes::Format888 => i * 4 + 1, // Skip first byte + _ => { + i * match *format { + FormatFullBytes::RGB24 => 3, + _ => 4, + } + } + }; - if *format == FormatFullBytes::RGB32 { - reader.read_u8()?; - } + // Read the colour values (b, g, r) and reverse to (r, g, b) + pixel[0..3].copy_from_slice(&row_buffer[offset..offset + 3]); + pixel[0..3].reverse(); // Read the alpha channel if present if *format == FormatFullBytes::RGBA32 { - reader.read_exact(&mut pixel[3..4])?; + pixel[3] = row_buffer[offset + 3]; } else if num_channels == 4 { - pixel[3] = 0xFF; + pixel[3] = ALPHA_OPAQUE; } } - reader.read_exact(row_padding) + Ok(()) }, - )?; + ); - Ok(()) + self.finish_row_decode(result) } fn read_rle_data(&mut self, buf: &mut [u8], image_type: ImageType) -> ImageResult<()> { - // Seek to the start of the actual image data. - self.reader.seek(SeekFrom::Start(self.data_offset))?; + let (start_row, start_x, start_pos) = match self.state { + DecoderState::ReadingRleData { + progress: RleProgress::NotStarted, + } => (0u32, 0u32, self.data_offset), + DecoderState::ReadingRleData { + progress: RleProgress::Checkpoint { row, x, stream_pos }, + } => (row, x, stream_pos), + _ => unreachable!("read_rle_data called in unexpected state: {:?}", self.state), + }; + + self.reader.seek(SeekFrom::Start(start_pos))?; let num_channels = self.num_channels(); - let p = self.palette.as_ref().unwrap(); + let p = if image_type != ImageType::RLE24 { + Some(self.palette.as_ref().unwrap()) + } else { + None + }; - // Handling deltas in the RLE scheme means that we need to manually - // iterate through rows and pixels. Even if we didn't have to handle - // deltas, we have to ensure that a single runlength doesn't straddle - // two rows. - let mut row_iter = self.rows(buf); + let mut row_iter = self.rows(buf).skip(start_row as usize); + let mut current_row = start_row; + let mut first_row_iteration = true; + + // Pre-allocate buffer for RLE4/8 absolute mode (max 256 bytes). + // RLE24 reads inline BGR triples directly, so this buffer is unused. + let mut rle_indices_buffer = [0u8; 256]; + + let mut rle_reader = RleReader::new(&mut self.reader); while let Some(row) = row_iter.next() { let mut pixel_iter = row.chunks_exact_mut(num_channels); - let mut x = 0; + // When resuming mid-row, skip to the saved x position on the first row. + let mut x = if first_row_iteration && start_x > 0 { + pixel_iter.nth(start_x as usize - 1); // nth(n) consumes n+1 elements + start_x + } else { + 0 + }; + first_row_iteration = false; + loop { - let instruction = { - let control_byte = self.reader.read_u8()?; - match control_byte { - RLE_ESCAPE => { - let op = self.reader.read_u8()?; - - match op { - RLE_ESCAPE_EOL => RLEInsn::EndOfRow, - RLE_ESCAPE_EOF => RLEInsn::EndOfFile, - RLE_ESCAPE_DELTA => { - let xdelta = self.reader.read_u8()?; - let ydelta = self.reader.read_u8()?; - RLEInsn::Delta(xdelta, ydelta) - } - _ => { - let mut length = op as usize; - if self.image_type == ImageType::RLE4 { - length = length.div_ceil(2); - } - length += length & 1; - let mut buffer = Vec::new(); - self.reader.read_exact_vec(&mut buffer, length)?; - RLEInsn::Absolute(op, buffer) - } + let control_byte = rle_reader.read_byte()?; + + match control_byte { + RLE_ESCAPE => { + let op = rle_reader.read_byte()?; + match op { + RLE_ESCAPE_EOL => { + pixel_iter.for_each(|p| p.fill(0)); + current_row += 1; + x = 0; + break; } - } - _ => { - let palette_index = self.reader.read_u8()?; - RLEInsn::PixelRun(control_byte, palette_index) - } - } - }; - - match instruction { - RLEInsn::EndOfFile => { - pixel_iter.for_each(|p| p.fill(0)); - row_iter.for_each(|r| r.fill(0)); - return Ok(()); - } - RLEInsn::EndOfRow => { - pixel_iter.for_each(|p| p.fill(0)); - break; - } - RLEInsn::Delta(x_delta, y_delta) => { - // The msdn site on bitmap compression doesn't specify - // what happens to the values skipped when encountering - // a delta code, however IE and the windows image - // preview seems to replace them with black pixels, - // so we stick to that. - - if y_delta > 0 { - // Zero out the remainder of the current row. - pixel_iter.for_each(|p| p.fill(0)); - - // If any full rows are skipped, zero them out. - for _ in 1..y_delta { - let row = row_iter.next().ok_or(DecoderError::CorruptRleData)?; - row.fill(0); + RLE_ESCAPE_EOF => { + pixel_iter.for_each(|p| p.fill(0)); + row_iter.for_each(|r| r.fill(0)); + return Ok(()); } + RLE_ESCAPE_DELTA => { + let x_delta = rle_reader.read_byte()?; + let y_delta = rle_reader.read_byte()?; + + // IE and Windows image preview replace skipped pixels + // with black, so we stick to that. + if y_delta > 0 { + pixel_iter.for_each(|p| p.fill(0)); + + for _ in 1..y_delta { + let row = + row_iter.next().ok_or(DecoderError::CorruptRleData)?; + row.fill(0); + } - // Set the pixel iterator to the start of the next row. - pixel_iter = row_iter - .next() - .ok_or(DecoderError::CorruptRleData)? - .chunks_exact_mut(num_channels); - - // Zero out the pixels up to the current point in the row. - for _ in 0..x { - pixel_iter - .next() - .ok_or(DecoderError::CorruptRleData)? - .fill(0); - } - } + current_row += y_delta as u32; - for _ in 0..x_delta { - let pixel = pixel_iter.next().ok_or(DecoderError::CorruptRleData)?; - pixel.fill(0); - } - x += x_delta as usize; - } - RLEInsn::Absolute(length, indices) => { - // Absolute mode cannot span rows, so if we run - // out of pixels to process, we should stop - // processing the image. - match image_type { - ImageType::RLE8 => { - if !set_8bit_pixel_run( - &mut pixel_iter, - p, - indices.iter(), - length as usize, - ) { - return Err(DecoderError::CorruptRleData.into()); + pixel_iter = row_iter + .next() + .ok_or(DecoderError::CorruptRleData)? + .chunks_exact_mut(num_channels); + + for _ in 0..x { + pixel_iter + .next() + .ok_or(DecoderError::CorruptRleData)? + .fill(0); + } + } + + for _ in 0..x_delta { + let pixel = + pixel_iter.next().ok_or(DecoderError::CorruptRleData)?; + pixel.fill(0); } + x += x_delta as u32; } - ImageType::RLE4 => { - if !set_4bit_pixel_run( - &mut pixel_iter, - p, - indices.iter(), - length as usize, - ) { - return Err(DecoderError::CorruptRleData.into()); + _ => { + // Absolute mode: pixel data differs by RLE type. + let count = op as usize; + match image_type { + ImageType::RLE8 => { + let mut length = count; + length += length & 1; + rle_reader.read_exact(&mut rle_indices_buffer[..length])?; + if !set_8bit_pixel_run( + &mut pixel_iter, + p.unwrap(), + rle_indices_buffer[..length].iter(), + count, + ) { + return Err(DecoderError::CorruptRleData.into()); + } + } + ImageType::RLE4 => { + let mut length = count.div_ceil(2); + length += length & 1; + rle_reader.read_exact(&mut rle_indices_buffer[..length])?; + if !set_4bit_pixel_run( + &mut pixel_iter, + p.unwrap(), + rle_indices_buffer[..length].iter(), + count, + ) { + return Err(DecoderError::CorruptRleData.into()); + } + } + ImageType::RLE24 => { + for _ in 0..count { + let b = rle_reader.read_byte()?; + let g = rle_reader.read_byte()?; + let r = rle_reader.read_byte()?; + if let Some(pixel) = pixel_iter.next() { + pixel[0] = r; + pixel[1] = g; + pixel[2] = b; + } + } + // RLE24 absolute mode pads to word (2-byte) boundary. + if !(count * 3).is_multiple_of(2) { + rle_reader.read_byte()?; + } + } + _ => unreachable!(), } + x += count as u32; } - _ => unreachable!(), } - x += length as usize; } - RLEInsn::PixelRun(n_pixels, palette_index) => { + _ => { + // Encoded run: pixel data differs by RLE type. + let n_pixels = control_byte as usize; match image_type { ImageType::RLE8 => { - // A pixel run isn't allowed to span rows. - // imagemagick produces invalid images where n_pixels exceeds row length, - // so we clamp n_pixels to the row length to display them properly: + // Clamp to row length for compat with imagemagick: // https://github.com/image-rs/image/issues/2321 - // - // This is like set_8bit_pixel_run() but doesn't fail when `n_pixels` is too large - let repeat_pixel: [u8; 3] = p[palette_index as usize]; - (&mut pixel_iter).take(n_pixels as usize).for_each(|p| { - p[2] = repeat_pixel[2]; - p[1] = repeat_pixel[1]; + let palette_index = rle_reader.read_byte()?; + let repeat_pixel: [u8; 3] = p.unwrap()[palette_index as usize]; + (&mut pixel_iter).take(n_pixels).for_each(|p| { p[0] = repeat_pixel[0]; + p[1] = repeat_pixel[1]; + p[2] = repeat_pixel[2]; }); } ImageType::RLE4 => { + let palette_index = rle_reader.read_byte()?; if !set_4bit_pixel_run( &mut pixel_iter, - p, + p.unwrap(), repeat(&palette_index), - n_pixels as usize, + n_pixels, ) { return Err(DecoderError::CorruptRleData.into()); } } + ImageType::RLE24 => { + let b = rle_reader.read_byte()?; + let g = rle_reader.read_byte()?; + let r = rle_reader.read_byte()?; + for _ in 0..n_pixels { + if let Some(pixel) = pixel_iter.next() { + pixel[0] = r; + pixel[1] = g; + pixel[2] = b; + } + } + } _ => unreachable!(), } - x += n_pixels as usize; + x += n_pixels as u32; } } + + // Checkpoint after every instruction to avoid potential quadratic + // time complexity when the decoder is given data one byte at a time. + self.state = DecoderState::ReadingRleData { + progress: RleProgress::Checkpoint { + row: current_row, + x, + stream_pos: start_pos + rle_reader.bytes_read(), + }, + }; } + + // Checkpoint after EndOfRow (which breaks out of the inner loop). + self.state = DecoderState::ReadingRleData { + progress: RleProgress::Checkpoint { + row: current_row, + x, + stream_pos: start_pos + rle_reader.bytes_read(), + }, + }; } Ok(()) } - /// Read the actual data of the image. This function is deliberately not public because it - /// cannot be called multiple times without seeking back the underlying reader in between. - pub(crate) fn read_image_data(&mut self, buf: &mut [u8]) -> ImageResult<()> { + /// Determine if the current image type is RLE-compressed. + fn is_rle(&self) -> bool { + matches!( + self.image_type, + ImageType::RLE4 | ImageType::RLE8 | ImageType::RLE24 + ) + } + + /// Returns which rows in the output buffer contain valid decoded pixel data. + /// + /// See [`RowsDecoded`] for details on how to interpret the result. + pub fn rows_decoded(&self) -> RowsDecoded { + let rows = match self.state { + DecoderState::ReadingRowData { rows_decoded } => rows_decoded, + DecoderState::ReadingRleData { progress } => match progress { + RleProgress::NotStarted => 0, + // row is 0-indexed current row; rows 0..row are complete + RleProgress::Checkpoint { row, .. } => row, + }, + DecoderState::ImageDecoded => self.height as u32, + DecoderState::ReadingMetadata { .. } => 0, + }; + if self.top_down { + RowsDecoded::TopDown { rows } + } else { + RowsDecoded::BottomUp { rows } + } + } + + /// Handle the result of a row-based decode operation, updating state accordingly. + fn finish_row_decode(&mut self, result: Result) -> ImageResult<()> { + let (Ok(rows) | Err((rows, _))) = result; + self.state = DecoderState::ReadingRowData { rows_decoded: rows }; + match result { + Ok(_) => Ok(()), + Err((_, e)) => Err(e)?, + } + } + + /// Read the actual pixel data of the image. + /// + /// Must be called after `read_metadata()` succeeds. On `UnexpectedEof`, the decoder + /// can be retried: + /// + /// - For non-RLE formats: decoding resumes from the last successfully decoded row. + /// Already-decoded rows are preserved in `buf`. + /// - For RLE formats: decoding resumes from the last checkpoint (completed instruction symbol). + /// Rows and pixels completed before the error are preserved in `buf`. + pub fn read_image_data(&mut self, buf: &mut [u8]) -> ImageResult<()> { + match self.state { + DecoderState::ImageDecoded => Ok(()), + DecoderState::ReadingRowData { .. } | DecoderState::ReadingRleData { .. } => self + .read_image_data_impl(buf) + .map(|()| self.state = DecoderState::ImageDecoded), + DecoderState::ReadingMetadata { .. } => Err(DecoderError::MetadataNotRead.into()), + } + } + + /// Internal implementation of image data reading. + fn read_image_data_impl(&mut self, buf: &mut [u8]) -> ImageResult<()> { match self.image_type { ImageType::Palette => self.read_palettized_pixel_data(buf), ImageType::RGB16 => self.read_16_bit_pixel_data(buf, Some(&R5_G5_B5_COLOR_MASK)), @@ -1326,6 +2130,7 @@ impl BmpDecoder { ImageType::RGBA32 => self.read_full_byte_pixel_data(buf, &FormatFullBytes::RGBA32), ImageType::RLE8 => self.read_rle_data(buf, ImageType::RLE8), ImageType::RLE4 => self.read_rle_data(buf, ImageType::RLE4), + ImageType::RLE24 => self.read_rle_data(buf, ImageType::RLE24), ImageType::Bitfields16 => match self.bitfields { Some(_) => self.read_16_bit_pixel_data(buf, None), None => Err(DecoderError::BitfieldMasksMissing(16).into()), @@ -1359,6 +2164,10 @@ impl ImageDecoder for BmpDecoder { } } + fn icc_profile(&mut self) -> ImageResult>> { + Ok(self.icc_profile.clone()) + } + fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); self.read_image_data(buf) @@ -1369,37 +2178,9 @@ impl ImageDecoder for BmpDecoder { } } -impl ImageDecoderRect for BmpDecoder { - fn read_rect( - &mut self, - x: u32, - y: u32, - width: u32, - height: u32, - buf: &mut [u8], - row_pitch: usize, - ) -> ImageResult<()> { - let start = self.reader.stream_position()?; - load_rect( - x, - y, - width, - height, - buf, - row_pitch, - self, - self.total_bytes() as usize, - |_, _| Ok(()), - |s, buf| s.read_image_data(buf), - )?; - self.reader.seek(SeekFrom::Start(start))?; - Ok(()) - } -} - #[cfg(test)] mod test { - use std::io::{BufReader, Cursor}; + use std::io::{BufRead, BufReader, Cursor, Seek}; use super::*; @@ -1418,16 +2199,6 @@ mod test { } } - #[test] - fn read_rect() { - let f = - BufReader::new(std::fs::File::open("tests/images/bmp/images/Core_8_Bit.bmp").unwrap()); - let mut decoder = BmpDecoder::new(f).unwrap(); - - let mut buf: Vec = vec![0; 8 * 8 * 3]; - decoder.read_rect(0, 0, 8, 8, &mut buf, 8 * 3).unwrap(); - } - #[test] fn read_rle_too_short() { let data = vec![ @@ -1477,4 +2248,474 @@ mod test { assert_eq!(ref_img, no_hdr_img); } } + + /// Validates that the given ICC profile data can be parsed by moxcms and contains + /// the expected properties for an RGB display profile. + fn validate_icc_profile( + profile_data: &[u8], + source_file: &str, + expected_color_space: moxcms::DataColorSpace, + expected_profile_class: moxcms::ProfileClass, + ) { + let parsed_profile = moxcms::ColorProfile::new_from_slice(profile_data); + assert!( + parsed_profile.is_ok(), + "ICC profile from {} should be parseable by moxcms: {:?}", + source_file, + parsed_profile.err() + ); + let parsed_profile = parsed_profile.unwrap(); + assert_eq!( + parsed_profile.color_space, expected_color_space, + "ICC profile from {} should have RGB color space", + source_file + ); + assert_eq!( + parsed_profile.profile_class, expected_profile_class, + "ICC profile from {} should be a display/monitor profile", + source_file + ); + } + + #[test] + fn test_icc_profile() { + // V5 header file without embedded ICC profile + let f = + BufReader::new(std::fs::File::open("tests/images/bmp/images/V5_24_Bit.bmp").unwrap()); + let mut decoder = BmpDecoder::new(f).unwrap(); + let profile = decoder.icc_profile().unwrap(); + assert!(profile.is_none()); + + // Test files with embedded ICC profiles + let f = + BufReader::new(std::fs::File::open("tests/images/bmp/images/rgb24prof.bmp").unwrap()); + let mut decoder = BmpDecoder::new(f).unwrap(); + let profile = decoder.icc_profile().unwrap(); + assert!(profile.is_some()); + let profile_data = profile.unwrap(); + assert_eq!(profile_data.len(), 3048); + validate_icc_profile( + &profile_data, + "rgb24prof.bmp", + moxcms::DataColorSpace::Rgb, + moxcms::ProfileClass::DisplayDevice, + ); + + let f = + BufReader::new(std::fs::File::open("tests/images/bmp/images/rgb24prof2.bmp").unwrap()); + let mut decoder = BmpDecoder::new(f).unwrap(); + let profile = decoder.icc_profile().unwrap(); + assert!(profile.is_some()); + let profile_data = profile.unwrap(); + assert_eq!(profile_data.len(), 540); + validate_icc_profile( + &profile_data, + "rgb24prof2.bmp", + moxcms::DataColorSpace::Rgb, + moxcms::ProfileClass::DisplayDevice, + ); + } + + /// A reader that simulates partial data availability for testing resumable decoding. + /// It wraps a byte slice and limits how many bytes can be read before returning UnexpectedEof. + struct PartialReader { + data: Vec, + position: u64, + available_bytes: usize, + } + + impl PartialReader { + fn new(data: Vec) -> Self { + Self { + data, + position: 0, + available_bytes: 0, + } + } + + /// Set the number of bytes available for reading (absolute, not additive). + fn set_available(&mut self, bytes: usize) { + self.available_bytes = bytes.min(self.data.len()); + } + } + + impl io::Read for PartialReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.position as usize >= self.available_bytes { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "simulated partial data", + )); + } + + let available = self.available_bytes - self.position as usize; + let to_read = buf.len().min(available); + let start = self.position as usize; + buf[..to_read].copy_from_slice(&self.data[start..start + to_read]); + self.position += to_read as u64; + Ok(to_read) + } + } + + impl BufRead for PartialReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + if self.position as usize >= self.available_bytes { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "simulated partial data", + )); + } + + let start = self.position as usize; + Ok(&self.data[start..self.available_bytes]) + } + + fn consume(&mut self, amt: usize) { + self.position += amt as u64; + } + } + + impl Seek for PartialReader { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + let new_pos = match pos { + SeekFrom::Start(offset) => offset as i64, + SeekFrom::End(offset) => self.data.len() as i64 + offset, + SeekFrom::Current(offset) => self.position as i64 + offset, + }; + + if new_pos < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "seek to negative position", + )); + } + + self.position = new_pos as u64; + Ok(self.position) + } + } + + /// Helper to check if an error is UnexpectedEof + fn is_unexpected_eof(err: &ImageError) -> bool { + matches!(err, ImageError::IoError(e) if e.kind() == io::ErrorKind::UnexpectedEof) + } + + /// Test resumable decoding with various BMP formats. + /// Verifies that read_metadata() and read_image_data() can be retried after + /// UnexpectedEof and produce identical results to normal decoding. + /// Also verifies metadata phase progress and row-level progress for non-RLE formats. + #[test] + fn test_resumable_decoding() { + use crate::ImageDecoder; + + struct TestCase { + path: &'static str, + is_rle: bool, + has_palette: bool, + has_icc_profile: bool, + top_down: bool, + } + + // Test multiple BMP formats to ensure resumable decoding works across variants + let test_files = [ + TestCase { + path: "tests/images/bmp/images/Info_R8_G8_B8.bmp", + is_rle: false, + has_palette: false, + has_icc_profile: false, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/Info_A8_R8_G8_B8.bmp", + is_rle: false, + has_palette: false, + has_icc_profile: false, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/Info_A8_R8_G8_B8_Top_Down.bmp", + is_rle: false, + has_palette: false, + has_icc_profile: false, + top_down: true, + }, + TestCase { + path: "tests/images/bmp/images/Info_8_Bit.bmp", + is_rle: false, + has_palette: true, + has_icc_profile: false, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/Core_8_Bit.bmp", + is_rle: false, + has_palette: true, + has_icc_profile: false, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/pal8rle.bmp", + is_rle: true, + has_palette: true, + has_icc_profile: false, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/pal4rle.bmp", + is_rle: true, + has_palette: true, + has_icc_profile: false, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/rgb24prof.bmp", + is_rle: false, + has_palette: false, + has_icc_profile: true, + top_down: false, + }, + TestCase { + path: "tests/images/bmp/images/rgb24rle24.bmp", + is_rle: true, + has_palette: false, + has_icc_profile: false, + top_down: false, + }, + ]; + + for TestCase { + path, + is_rle, + has_palette, + has_icc_profile, + top_down, + } in test_files + { + let data = std::fs::read(path).unwrap(); + let file_size = data.len(); + + // Get reference result from normal decoding + let mut ref_decoder = BmpDecoder::new(Cursor::new(data.clone())).unwrap(); + let expected_bytes = ref_decoder.total_bytes() as usize; + let mut ref_buf = vec![0u8; expected_bytes]; + let ref_icc_len = ref_decoder.icc_profile().unwrap().map(|p| p.len()); + ref_decoder.read_image(&mut ref_buf).unwrap(); + + // Test resumable decoding with simulated streaming + let reader = PartialReader::new(data); + let mut decoder = BmpDecoder::new_resumable(reader); + + // Track metadata phase transitions + let mut saw_reading_palette = false; + let mut saw_reading_icc = false; + + // Phase 1: Stream bytes until metadata succeeds + let mut bytes_available = 0; + loop { + decoder.reader.set_available(bytes_available); + match decoder.read_metadata() { + Ok(()) => break, + Err(ref e) if is_unexpected_eof(e) => { + if let DecoderState::ReadingMetadata { progress } = decoder.state { + match progress { + MetadataProgress::ReadingPalette { .. } => { + saw_reading_palette = true + } + MetadataProgress::ReadingIccProfile { .. } => { + saw_reading_icc = true + } + _ => {} + } + } + + // Simulate more data arriving (add 10 bytes at a time, capped at file size) + bytes_available = (bytes_available + 10).min(file_size); + assert!( + bytes_available <= file_size, + "{path}: metadata should succeed before EOF" + ); + } + Err(e) => panic!("{path}: unexpected error during metadata: {e:?}"), + } + } + + // Verify metadata phase transitions occurred as expected + if has_palette { + assert!( + saw_reading_palette, + "{path}: should have seen ReadingPalette phase" + ); + } + if has_icc_profile { + assert!( + saw_reading_icc, + "{path}: should have seen ReadingIccProfile phase" + ); + let icc = decoder.icc_profile().unwrap(); + assert_eq!( + icc.map(|p| p.len()), + ref_icc_len, + "{path}: ICC profile length mismatch" + ); + } + + // Verify dimensions are available after metadata + let (width, height) = decoder.dimensions(); + assert!(width > 0 && height > 0, "{path}: invalid dimensions"); + assert_eq!( + decoder.total_bytes() as usize, + expected_bytes, + "{path}: total_bytes mismatch" + ); + + // Phase 2: Stream bytes until image data succeeds + let mut buf = vec![0u8; expected_bytes]; + let mut prev_decoded_rows = 0u32; + loop { + decoder.reader.set_available(bytes_available); + match decoder.read_image_data(&mut buf) { + Ok(()) => { + // After successful decode, rows_decoded() should return full height + let progress = decoder.rows_decoded(); + assert_eq!( + progress.rows(), + height, + "{path}: rows_decoded() should equal height after complete decode" + ); + if top_down { + assert!( + matches!(progress, RowsDecoded::TopDown { .. }), + "{path}: top-down file should produce TopDown, got {progress:?}" + ); + } else { + assert!( + matches!(progress, RowsDecoded::BottomUp { .. }), + "{path}: bottom-up file should produce BottomUp, got {progress:?}" + ); + } + break; + } + Err(ref e) if is_unexpected_eof(e) => { + // Validate rows_decoded() returns correct count and variant + let progress = decoder.rows_decoded(); + let decoded_rows = progress.rows(); + assert!( + decoded_rows <= height, + "{path}: rows_decoded() {decoded_rows} exceeds height {height}" + ); + assert!(decoded_rows >= prev_decoded_rows, "{path}: rows_decoded() decreased from {prev_decoded_rows} to {decoded_rows}"); + prev_decoded_rows = decoded_rows; + + // Verify state tracks progress appropriately + match decoder.state { + DecoderState::ReadingRowData { rows_decoded } => { + assert!(!is_rle, "{path}: expected ReadingRleData for RLE format"); + assert!( + rows_decoded < height, + "{path}: rows_decoded {rows_decoded} >= height {height}" + ); + assert_eq!( + decoded_rows, rows_decoded, + "{path}: rows_decoded() mismatch" + ); + } + DecoderState::ReadingRleData { progress } => { + assert!( + is_rle, + "{path}: expected ReadingRowData for non-RLE format" + ); + match progress { + RleProgress::NotStarted => { + assert_eq!( + decoded_rows, 0, + "{path}: should be 0 for NotStarted" + ); + } + RleProgress::Checkpoint { row, .. } => { + assert!( + row < height, + "{path}: RLE row {row} >= height {height}" + ); + assert_eq!( + decoded_rows, row, + "{path}: rows_decoded() mismatch with RLE row" + ); + } + } + } + _ => panic!("{path}: unexpected state: {:?}", decoder.state), + } + + bytes_available += 100; + assert!( + bytes_available <= file_size + 100, + "{path}: image data should succeed before EOF" + ); + } + Err(e) => panic!("{path}: unexpected error during image data: {e:?}"), + } + } + + // Verify decoded data matches reference + assert_eq!(buf, ref_buf, "{path}: decoded data mismatch"); + } + } + + /// Test that BMP files with known spec violations are accepted by the + /// decoder (which defaults to lenient mode), and that strict mode still + /// detects the violations internally. + /// + /// These files come from the Chromium BMP test suite ("bad/" category): + /// - `rletopdown`: RLE compression with top-down orientation (spec forbids this) + /// - `badplanes`: planes field != 1 (spec requires exactly 1) + /// - `badpalettesize`: colors_used exceeds max for the bit depth + /// - `pal8oversizepal`: 8-bit palette with colors_used=300 (max is 256) + /// - `rgb16-880`: 16-bit bitfields with 8-8-0 channel widths (blue mask is zero) + #[test] + fn test_strict_vs_lenient_spec_validation() { + let questionable_files = [ + ( + "tests/images/bmp/images/lenient/rletopdown.bmp", + "rletopdown: RLE with top-down should be rejected in strict mode", + ), + ( + "tests/images/bmp/images/lenient/badplanes.bmp", + "badplanes: planes != 1 should be rejected in strict mode", + ), + ( + "tests/images/bmp/images/lenient/badpalettesize.bmp", + "badpalettesize: palette size exceeding bit depth should be rejected in strict mode", + ), + ( + "tests/images/bmp/images/lenient/pal8oversizepal.bmp", + "pal8oversizepal: colors_used=300 exceeds max 256 for 8-bit", + ), + ( + "tests/images/bmp/images/lenient/rgb16-880.bmp", + "rgb16-880: zero blue mask should be rejected in strict mode", + ), + ]; + + for (path, description) in &questionable_files { + let data = std::fs::read(path) + .unwrap_or_else(|e| panic!("{description}: failed to read {path}: {e}")); + + // Default (lenient) mode: these files should be accepted + let decoder = BmpDecoder::new(Cursor::new(&data)).unwrap_or_else(|e| { + panic!("{description}: decoding failed: {e:?}"); + }); + let mut buf = vec![0u8; decoder.total_bytes() as usize]; + decoder.read_image(buf.as_mut_slice()).unwrap_or_else(|e| { + panic!("{description}: read_image failed: {e:?}"); + }); + + // Strict mode (internal): these files should be rejected + let mut strict_decoder = BmpDecoder::new_resumable(Cursor::new(&data)); + strict_decoder.spec_strictness = BmpSpec::Strict; + assert!( + strict_decoder.read_metadata().is_err(), + "{description}: expected error in strict mode, but got Ok" + ); + } + } } diff --git a/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/mod.rs b/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/mod.rs --- a/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/mod.rs +++ b/third_party/rust/chromium_crates_io/vendor/image-v0_25/src/codecs/bmp/mod.rs @@ -7,6 +7,7 @@ //! * pub use self::decoder::BmpDecoder; +pub use self::decoder::RowsDecoded; pub use self::encoder::BmpEncoder; mod decoder; -- 2.34.1