///|
/// BMP codec for uncompressed 24-bit (BGR) and 32-bit (BGRA) images:
/// 14-byte file header + 40-byte BITMAPINFOHEADER, bottom-up row order and
/// 4-byte row padding for 24-bit data, exactly as Windows expects.

///|
fn push_u16_le(buf : Array[Byte], v : Int) -> Unit {
  push_u8(buf, v)
  push_u8(buf, v / 256)
}

///|
fn push_u32_le(buf : Array[Byte], v : Int) -> Unit {
  push_u8(buf, v)
  push_u8(buf, v / 256)
  push_u8(buf, v / 65536)
  push_u8(buf, v / 16777216)
}

///|
fn read_u16_le(data : Array[Byte], off : Int) -> Int {
  data[off].to_int() + data[off + 1].to_int() * 256
}

///|
fn read_u32_le(data : Array[Byte], off : Int) -> Int {
  data[off].to_int() +
  data[off + 1].to_int() * 256 +
  data[off + 2].to_int() * 65536 +
  data[off + 3].to_int() * 16777216
}

///|
/// Reads a signed 32-bit little-endian value without overflowing `Int`
/// (BMP height may legitimately be negative for top-down bitmaps).
fn read_i32_le(data : Array[Byte], off : Int) -> Int {
  let b0 = data[off].to_int()
  let b1 = data[off + 1].to_int()
  let b2 = data[off + 2].to_int()
  let b3 = data[off + 3].to_int()
  if b3 < 128 {
    b3 * 16777216 + b2 * 65536 + b1 * 256 + b0
  } else {
    -((255 - b3) * 16777216 +
    (255 - b2) * 65536 +
    (255 - b1) * 256 +
    (255 - b0) +
    1)
  }
}

///|
/// Encodes an image as an uncompressed BMP. `with_alpha = true` writes a
/// 32-bit BGRA bitmap; otherwise a 24-bit BGR bitmap with padded rows.
pub fn bmp_encode(img : Image, with_alpha : Bool) -> Array[Byte] {
  let w = img.width
  let h = img.height
  let bpp = if with_alpha { 32 } else { 24 }
  let stride = if with_alpha { w * 4 } else { (w * 3 + 3) / 4 * 4 }
  let data_size = stride * h
  let out : Array[Byte] = []
  // BITMAPFILEHEADER (14 bytes).
  push_u8(out, 66) // 'B'
  push_u8(out, 77) // 'M'
  push_u32_le(out, 54 + data_size)
  push_u32_le(out, 0)
  push_u32_le(out, 54) // pixel data offset
  // BITMAPINFOHEADER (40 bytes).
  push_u32_le(out, 40)
  push_u32_le(out, w)
  push_u32_le(out, h) // positive height = bottom-up rows
  push_u16_le(out, 1) // planes
  push_u16_le(out, bpp)
  push_u32_le(out, 0) // BI_RGB, no compression
  push_u32_le(out, data_size)
  push_u32_le(out, 2835) // 72 DPI in pixels/metre
  push_u32_le(out, 2835)
  push_u32_le(out, 0)
  push_u32_le(out, 0)
  for row in 0.. Image? {
  if data.length() < 54 {
    return None
  }
  if data[0].to_int() != 66 || data[1].to_int() != 77 {
    return None
  }
  let offset = read_u32_le(data, 10)
  let header_size = read_u32_le(data, 14)
  if header_size < 40 {
    return None
  }
  let width = read_i32_le(data, 18)
  let raw_height = read_i32_le(data, 22)
  let top_down = raw_height < 0
  let height = if top_down { -raw_height } else { raw_height }
  let bpp = read_u16_le(data, 28)
  let compression = read_u32_le(data, 30)
  if width <= 0 || height <= 0 || width * height > MAX_IMAGE_PIXELS {
    return None
  }
  if (bpp != 24 && bpp != 32) || compression != 0 {
    return None
  }
  let bytes_per_px = bpp / 8
  let stride = (width * bytes_per_px + 3) / 4 * 4
  if offset < 54 || offset + stride * height > data.length() {
    return None
  }
  let img = Image::new(width, height)
  for row in 0..