# BMP Streaming Decoder API Proposal
## Executive Summary
This proposal introduces a streaming/resumable decoding API for the BMP decoder in the `image` crate. The API enables incremental decoding of BMP images without requiring all data to be available upfront, which is essential for network streaming, progressive rendering, and memory-constrained environments.
## Motivation
### Current Limitations
The existing BMP decoder requires:
1. **Complete decoding upfront**: All image data must be decoded into a complete output buffer before any pixels are accessible
2. **Blocking I/O**: Uses `read_exact()` which blocks until all requested data is available
3. **Memory overhead**: Large images require allocating the entire decoded buffer upfront (width × height × channels bytes)
**Note**: While the input data must be available via a `BufRead + Seek` reader (which may buffer network data), the primary limitation is that decoding happens all-at-once rather than incrementally. The streaming API enables row-by-row decoding, reducing peak memory usage from O(image_size) to O(row_width).
### Use Cases Requiring Streaming
1. **Network Streaming**: Decode BMP images as they download over HTTP/network connections
2. **Progressive Rendering**: Display image rows as they become available for better UX
3. **Memory-Constrained Devices**: Process images row-by-row to minimize memory footprint
4. **Large Image Processing**: Handle multi-gigabyte BMPs without exhausting RAM
5. **Embedded Systems**: Real-time image processing with limited buffering
### Real-World Example: Chromium's Blink decoders
Chromium's Blink rendering engine implements incremental BMP decoding in `BmpImageReader`. The decoder supports:
- Incremental metadata reading
- Row-by-row pixel decoding
- Resumable state after partial reads
- Progressive rendering as data arrives over network
This proposal brings similar capabilities to Rust's `image` crate.
## Design Principles
### 1. Backward Compatibility
- **Zero breaking changes**: All existing APIs remain unchanged
- **Opt-in**: Streaming mode is explicitly enabled, not default
- **Drop-in replacement**: Existing code continues working without modifications
### 2. Minimal API Surface
- Introduce only essential new methods
- Leverage existing `BufRead + Seek` trait bounds
- Avoid complexity where possible
### 3. Composability
- Methods work independently but compose well
- Support both imperative (`try_read_row()`) and iterator (`into_rows()`) patterns
- Enable custom processing pipelines
### 4. Error Handling Philosophy
- **"Needs more data" has its specific error**: Define a new `ImageError::InsufficientData` to report the case that more data is needed to complete decoding.
- **Avoids `UnexpectedEof` overloading**: Don't reuse existing error types for normal streaming states
- **Clear semantics**: `Result<Option<T>>` pattern cleanly separates three outcomes:
- `Ok(Some(value))` - operation succeeded with data
- `Ok(None)` - operation succeeded but no more data to decode
- `Err(ImageError::InsufficientData)` - there is missing data to complete decoding. This is a recoverable error
- `Err(error)` - operation failed due to invalid data or I/O error
### 5. Async and Coroutine Compatibility
- **Non-blocking API contract**: The `Result<Option<T>>` return type allows callers to:
- Call the method when data is available
- Get (newly defined) `ImageError::InsufficientData` when more data is needed
- Yield control back to event loop/async runtime
- Resume later when more data arrives
- **Event-driven friendly**: No assumptions about data availability; caller controls when to retry
- **Future async support**: API signature is compatible with async/await patterns:
- Can be wrapped in async functions that `await` on data availability
- No assigning double significate to existing IO errors by defining a new ImageError enum value for the case in which we are missing data to continue decoding.
- No breaking changes needed when adding async reader support
### 6. BMP-specific API
- Designing a format-agnostic API is explicitly a non-goal
- Rationale: row-by-row BMP decoding requires a different API than progressive JPG/JXL decoding (or streaming decoding of _interlaced_ PNG images).
- In the future (once we gain experience and confidence in format-specific APIs) we may want to revisit this and consider designing a format-agnostic API (if one is possible)
## API Design
### 1. State Management
#### New Field: `streaming_state`
```rust
pub struct BmpDecoder<R: BufRead + Seek> {
// ... existing fields ...
streaming_state: Option<DecoderState>,
}
```
**Rationale:**
- `Option<DecoderState>` allows backward compatibility (None = traditional mode)
- Explicitly tracks decode progress through all phases
- Enables resumption after partial reads
#### New Enum: `DecoderState` (internal)
```rust
#[derive(Clone, Debug)]
enum DecoderState { // Note: not pub - internal implementation detail
Initial,
ReadingFileHeader { bytes_read: usize, buffer: Vec<u8> },
ReadingDibHeaderSize { bytes_read: usize, buffer: Vec<u8> },
ReadingDibHeader { header_size: u32, bytes_read: usize, buffer: Vec<u8> },
ReadingIccProfile { offset: u64, size: u32, bytes_read: usize, buffer: Vec<u8> },
ReadingPalette { entries_read: u32, total_entries: u32, bytes_per_color: usize, buffer: Vec<u8> },
ReadingBitmasks { bytes_read: usize, total_bytes: usize, buffer: Vec<u8> },
ReadyForPixelData { current_row: i32, row_byte_length: usize },
ReadingPixelRow { row_index: i32, bytes_read: usize, row_buffer: Vec<u8> },
Complete,
}
```
**Visibility:**
- `DecoderState` is private (internal implementation detail)
- Users never directly access or construct state values
- State is managed automatically by public methods (`try_read_metadata()`, `read_row()`)
- Keeps API surface minimal and allows internal state machine changes without breaking compatibility
**Rationale:**
- **Explicit states**: Each decode phase is clearly represented
- **Resumable**: Stores partial progress (bytes_read, buffers) to resume after data arrival
- **Type-safe**: Impossible to be in invalid state combinations
- **Buffer storage**: Each state owns necessary buffers, avoiding separate allocations
#### New Constructor: `new_streaming()`
```rust
pub fn new_streaming(reader: R) -> ImageResult<BmpDecoder<R>>
```
**Returns:**
- `Ok(BmpDecoder)` - Decoder initialized in streaming mode, ready for `try_read_metadata()`
- `Err(_)` - Reader initialization failed
**Rationale:**
- **Explicit opt-in**: Different constructor makes streaming mode choice visible at call site
- **No automatic metadata loading**: Unlike `new()`, doesn't call `read_metadata()` internally
- **Initialized state**: Sets `streaming_state = Some(DecoderState::Initial)` automatically
- **Same reader requirements**: Takes same `R: BufRead + Seek` as traditional constructor
- **Avoiding breaking changes**: Clients using old constructors expect blocking IO, but the new constructor shouldn't block.
#### Enabling Streaming Mode
To use the streaming API, create a decoder with `new_streaming()` instead of `new()`:
```rust
// Traditional mode (auto-loads metadata on construction)
let decoder = BmpDecoder::new(reader)?;
// Streaming mode (opt-in, manual metadata loading)
let mut decoder = BmpDecoder::new_streaming(reader)?;
// Now use streaming methods
decoder.try_read_metadata()?;
decoder.read_row(&mut buffer)?;
```
**Rationale:**
- **Explicit opt-in**: Streaming mode must be explicitly enabled via constructor choice
- **Backward compatible**: Existing `new()` continues to work unchanged
- **Clear separation**: Different constructor signals different behavior and expectations
- **No manual state setup**: Constructor handles `streaming_state` initialization internally
**State Transitions:**
```
Initial
→ ReadingFileHeader (14 bytes)
→ ReadingDibHeaderSize (4 bytes)
→ ReadingDibHeader (variable size, extracts ICC metadata if V5)
→ ReadingBitmasks? (optional 12-16 bytes, carries ICC metadata)
→ ReadingPalette? (optional, variable size, carries ICC metadata)
→ ReadyForPixelData (reads pixel rows, carries ICC metadata)
→ ReadingIccProfile? (optional, after ALL pixel data, for V5 with embedded profiles)
→ Complete
```
### 2. Metadata Reading
#### New Method: `try_read_metadata()`
```rust
pub fn try_read_metadata(&mut self) -> ImageResult<Option<()>>
```
**Returns:**
- `Ok(Some(()))` - Metadata fully loaded, ready for pixel reading
- `Err(ImageError::InsufficientData)` - there is missing data to complete decoding. This is a recoverable error
- `Err(_)` - Actual decode error (invalid format, I/O failure, etc.)
**Rationale:**
- **Non-blocking pattern**: `Option<T>` is idiomatic Rust for "may need more attempts"
- **Composable**: Easy to integrate into async loops or event-driven systems
- **Clear contract**: Three distinct outcomes, no ambiguity
- **No error variant pollution**: "Needs more data" returns its specific error `ImageError::InsufficientData` instead of giving a new meaning to an already existing error.
**Why `Result<Option<()>>` instead of just `Result<bool>` or a new error variant?**
- `Option<()>` is more idiomatic than `bool` for "incomplete/complete" semantics
- Matches patterns in other Rust libraries (e.g., `Iterator::next()`)
- Clearer intent: `Some(())` = "got something", `None` = "nothing yet"
- Avoids polluting error enums with non-error states
- Reserves `Err(_)` exclusively for actual failures
**Example Usage:**
```rust
// Event-driven processing
loop {
match decoder.try_read_metadata()? {
Some(()) => break, // Metadata complete
None => {
// Wait for more data
wait_for_data(&mut reader)?;
}
}
}
```
### 3. Pixel Data Reading
#### New Method: `try_read_row()`
```rust
pub fn try_read_row(&mut self, buf: &mut [u8]) -> ImageResult<Option<usize>>
```
**Parameters:**
- `buf`: Pre-allocated buffer of size `width * channels`
- Use existing `ImageDecoder` trait methods to determine size:
- `dimensions()` returns `(width, height)`
- `color_type()` or derived methods provide channel count
- Row size = `width * num_channels`
**Returns:**
- `Ok(Some(row_index))` - Row successfully decoded, returns which row (0-based)
- `Ok(None)` - operation succeeded but no more data to decode
- `Err(ImageError::InsufficientData)` - there is missing data to complete decoding. This is a recoverable error
- `Err(_)` - Decode error
**Note**: In the case that all rows have already been decoded and try_next_row is called again, it is expected to return a DecoderError as the caller shouldn't call for more rows than the image has.
**Row Ordering:**
- **Streaming mode**: Rows returned in file storage order without automatic flipping
- Bottom-up BMPs: row 0 is bottom of image, incrementing upward
- Top-down BMPs: row 0 is top of image, incrementing downward
- Use `is_top_down()` to determine orientation and flip if needed for display
- **Important difference from traditional decoder**: The non-streaming `BmpDecoder::new()` automatically flips bottom-up images to top-down order. The streaming API returns rows as stored in the file to enable incremental processing.
**Rationale:**
- **Memory efficiency**: Caller controls allocation, can reuse same buffer
- **Progress tracking**: Returns row index for progress bars/status updates
- **Explicit termination**: `None` clearly indicates completion
- **Buffer validation**: Checks size matches expected row width
- **Streaming-compatible**: Cannot pre-flip all rows; must return as read from file
**Why require pre-allocated buffer?**
- Allows buffer reuse across rows (zero-allocation loop)
- Caller can use stack allocation for small images
- More flexible than forcing heap allocation
**Example Usage:**
```rust
use image::codecs::bmp::BmpDecoder;
use std::fs::File;
use std::io::BufReader;
let file = File::open("image.bmp")?;
let reader = BufReader::new(file);
let mut decoder = BmpDecoder::new_streaming(reader)?;
// Load metadata first
decoder.try_read_metadata()?;
// Get dimensions to allocate buffer
let (width, height) = decoder.dimensions();
let channels = 3; // RGB - or use color_type() to determine
let row_size = width as usize * channels;
let mut row_buffer = vec![0u8; row_size];
// Read all rows
while let Some(row_idx) = decoder.try_read_row(&mut row_buffer)? {
// Process row (your custom logic here)
process_row(row_idx, &row_buffer);
// row_buffer is reused, no allocations in loop
}
```
#### New Method: `is_top_down()`
```rust
pub fn is_top_down(&self) -> bool
```
**Returns:**
- `true` if rows are stored top-down in the file (negative height in BMP header)
- `false` if rows are stored bottom-up (standard BMP orientation).
**Behavior difference:**
- **In streaming mode** (`new_streaming()`): Returns the actual file storage orientation
- **In traditional mode** (`new()`): Always returns `true` because decoder normalizes to top-down during `new()`
**Rationale:**
- **Essential for streaming**: Callers need to know orientation to correctly display or process rows
- **Enables correct rendering**: Caller can flip rows for display if needed
- **Documents behavioral difference**: Makes explicit that streaming mode preserves file order
#### New Method: `into_rows()` (Not required for Chromium integration)
```rust
pub fn into_rows(&mut self) -> BmpRowIterator<'_, R>
```
**Returns:** Iterator yielding `ImageResult<Vec<u8>>`
**Rationale:**
- **Ergonomic**: Natural Rust idiom for sequential processing
- **Composable**: Works with iterator adapters (`map`, `filter`, `take`, etc.)
- **Convenience**: Handles allocation automatically for simple cases
- **Fallible iteration**: Errors propagate naturally through iterator
**Why allocate internally?**
- Many use cases don't need buffer reuse
- Simpler API for common case
- Users needing zero-copy can use `try_read_row()` directly
**Why provide both `try_read_row()` and `into_rows()`?**
- **`try_read_row()`**: Minimal API for performance-critical code (zero allocations, caller-controlled buffers)
- **`into_rows()`**: Ergonomic API for typical usage (iterator pattern, automatic allocation)
- **Different use cases**: Performance vs convenience tradeoff left to caller
- **Precedent**: Similar to `BufRead::read_line()` vs `BufRead::lines()` in std
**Example Usage:**
```rust
// Simple iteration
for row in decoder.into_rows() {
let pixels = row?;
display_row(&pixels);
}
// With iterator adapters
let processed: Vec<_> = decoder.into_rows()
.map(|r| r.map(|pixels| apply_filter(&pixels)))
.collect::<ImageResult<_>>()?;
```
### 4. Supporting Type
#### New Struct: `BmpRowIterator` (Not required for Chromium integration)
```rust
pub struct BmpRowIterator<'a, R: BufRead + Seek> {
decoder: &'a mut BmpDecoder<R>,
row_buffer: Vec<u8>,
}
impl<'a, R: BufRead + Seek> Iterator for BmpRowIterator<'a, R> {
type Item = ImageResult<Vec<u8>>;
// ...
}
```
**Rationale:**
- **Standard trait**: Implements `Iterator` for Rust ecosystem compatibility
- **Owned buffers**: Each iteration returns owned `Vec<u8>` for safety
- **Lazy evaluation**: Rows decoded on-demand, not all upfront
- **Error handling**: Yields `Result` to handle I/O errors gracefully