///| A minimal, dependency-free BMP loader. It decodes uncompressed (BI_RGB)
///| 8-bit palette, 24-bit and 32-bit bitmaps into a grayscale `Image`.
///|
///| BMP stores rows bottom-up by default and pads each row to a 4-byte
///| boundary; both behaviours are handled here.
///|
/// Read a little-endian unsigned 16-bit value at `off`.
fn read_u16(data : Bytes, off : Int) -> Int {
data.at(off).to_int() | (data.at(off + 1).to_int() << 8)
}
///| Read a little-endian 32-bit value at `off`.
///
///| `Int` is 32-bit two's complement, so assembling the four bytes yields the
///| correct value whether the field is meant as `u32` (small magnitudes) or
///|
/// `i32` (e.g. a negative BMP height for top-down rows).
fn read_u32(data : Bytes, off : Int) -> Int {
data.at(off).to_int() |
(data.at(off + 1).to_int() << 8) |
(data.at(off + 2).to_int() << 16) |
(data.at(off + 3).to_int() << 24)
}
///| Compute the grayscale value of one pixel in a BMP row.
///
///| `row_off` points at the first byte of the (already flipped) source row and
///| `palette_offset` at the start of the 4-byte-per-entry BGR0 palette, which
///|
/// is only consulted for 8-bit images.
fn bmp_pixel_gray(
data : Bytes,
bpp : Int,
palette_offset : Int,
row_off : Int,
x : Int,
) -> Int {
if bpp == 8 {
let idx = data.at(row_off + x).to_int()
let po = palette_offset + idx * 4
rgb_to_gray(
data.at(po + 2).to_int(),
data.at(po + 1).to_int(),
data.at(po).to_int(),
)
} else if bpp == 24 {
let po = row_off + x * 3
rgb_to_gray(
data.at(po + 2).to_int(),
data.at(po + 1).to_int(),
data.at(po).to_int(),
)
} else {
// 32-bit BGRA; the alpha byte is ignored.
let po = row_off + x * 4
rgb_to_gray(
data.at(po + 2).to_int(),
data.at(po + 1).to_int(),
data.at(po).to_int(),
)
}
}
///| Parse a BMP byte stream into a grayscale `Image`.
///
///|
/// Raises `DecodeError` when the input is not a supported uncompressed BMP.
pub fn parse_bmp(data : Bytes) -> Image raise DecodeError {
if data.length() < 54 {
raise DecodeError("BMP: buffer too short for a BITMAPINFOHEADER")
}
if data.at(0) != b'B' || data.at(1) != b'M' {
raise DecodeError("BMP: missing BM magic")
}
let pixel_offset = read_u32(data, 10)
let header_size = read_u32(data, 14)
if header_size < 40 {
raise DecodeError("BMP: unsupported DIB header size")
}
let width = read_u32(data, 18)
let height = read_u32(data, 22)
let bpp = read_u16(data, 28)
let compression = read_u32(data, 30)
if compression != 0 {
raise DecodeError("BMP: only uncompressed (BI_RGB) images are supported")
}
if width <= 0 {
raise DecodeError("BMP: invalid width")
}
if bpp != 8 && bpp != 24 && bpp != 32 {
raise DecodeError("BMP: unsupported bit depth")
}
let flip = height > 0
let rows = if flip { height } else { 0 - height }
let row_size = (width * bpp + 31) / 32 * 4
let palette_offset = 14 + header_size
let pixels = Bytes::makei(width * rows, fn(i) {
let x = i % width
let y = i / width
let src_y = if flip { rows - 1 - y } else { y }
let row_off = pixel_offset + src_y * row_size
bmp_pixel_gray(data, bpp, palette_offset, row_off, x).to_byte()
})
Image::new(width, rows, pixels)
}