///|
/// Error returned when decoding pixmap bytes.
pub(all) enum PixmapDecodeError {
UnexpectedEndOfData(Int, Int, Int)
InvalidRgbaDimensions(Int, Int)
RgbaByteLengthMismatch(Int, Int)
InvalidBmpSignature(Byte, Byte)
UnsupportedBmpHeaderSize(Int)
UnsupportedBmpColorPlanes(Int)
UnsupportedBmpBitsPerPixel(Int)
UnsupportedBmpCompression(Int)
InvalidBmpDimensions(Int, Int)
BmpPixelDataOutOfBounds(Int, Int, Int)
InvalidPngSignature
MissingPngImageHeader
MissingPngImageData
MissingPngImageEnd
UnsupportedPngHeaderSize(Int)
UnsupportedPngBitDepth(Int)
UnsupportedPngColorType(Int)
UnsupportedPngCompression(Int)
UnsupportedPngFilter(Int)
UnsupportedPngInterlace(Int)
UnsupportedPngChunk(Int, Int, Int, Int)
UnsupportedPngDeflateCompression(Int)
UnsupportedPngDeflatePresetDictionary
UnsupportedPngDeflateBlockType(Int)
InvalidPngDeflateCode
UnsupportedPngFilterType(Int)
InvalidPngDimensions(Int, Int)
InvalidPngZlibHeader(Int, Int)
PngCrcMismatch(UInt, UInt)
PngAdlerMismatch(UInt, UInt)
PngStoredBlockLengthMismatch(Int, Int)
PngInflateLengthMismatch(Int, Int)
PngScanlineLengthMismatch(Int, Int)
} derive(Eq, Debug)
///|
pub extend PixmapDecodeError with Debug::{to_repr}
///|
pub extend PixmapDecodeError with Eq::{not_equal, equal}
///|
/// Error returned when encoding pixmap bytes.
pub(all) enum PixmapEncodeError {
InvalidPngDimensions(Int, Int)
} derive(Eq, Debug)
///|
pub extend PixmapEncodeError with Debug::{to_repr}
///|
pub extend PixmapEncodeError with Eq::{not_equal, equal}
///|
/// Return an isolated copy of the raw RGBA pixel bytes in row-major order.
pub fn Pixmap::to_rgba_bytes(self : Pixmap) -> Bytes {
rgba_bytes_from_pixels(self.pixels)
}
///|
/// Encode this pixmap as a binary PPM (P6) image, dropping the alpha channel.
/// The result is a deterministic, dependency-free fixture format for headless
/// render tests and simple CPU-rendered pixel dumps.
pub fn Pixmap::to_ppm_bytes(self : Pixmap) -> Bytes {
let out : Array[Byte] = []
append_ppm_header(out, self.width, self.height)
for y in 0.. Bytes {
let out : Array[Byte] = []
append_bmp_header(out, self.width, self.height)
for y_offset in 0.. Result[Pixmap, PixmapDecodeError] {
guard width >= 0 && height >= 0 else {
return Err(InvalidRgbaDimensions(width, height))
}
let expected_len = width * height * 4
guard data.length() == expected_len else {
return Err(RgbaByteLengthMismatch(expected_len, data.length()))
}
let pixmap = Pixmap::new(width, height)
let pixels = pixmap.get_pixels()
for i in 0.. Result[Pixmap, PixmapDecodeError] {
match decode_bmp_info(data) {
Err(error) => Err(error)
Ok(info) => Ok(decode_bmp_pixmap(data, info))
}
}
///|
fn rgba_bytes_from_pixels(pixels : FixedArray[Byte]) -> Bytes {
let out : Array[Byte] = []
for i in 0.. Unit {
append_ppm_byte(out, 80) // P
append_ppm_byte(out, 54) // 6
append_ppm_byte(out, 10)
append_ppm_decimal(out, width)
append_ppm_byte(out, 32)
append_ppm_decimal(out, height)
append_ppm_byte(out, 10)
append_ppm_byte(out, 50) // 2
append_ppm_byte(out, 53) // 5
append_ppm_byte(out, 53) // 5
append_ppm_byte(out, 10)
}
///|
fn append_ppm_decimal(out : Array[Byte], value : Int) -> Unit {
let mut n = value
if n == 0 {
append_ppm_byte(out, 48)
} else {
if n < 0 {
append_ppm_byte(out, 45)
n = 0 - n
}
let digits : Array[Byte] = []
while n > 0 {
digits.push((48 + n % 10).to_byte())
n = n / 10
}
for i in 0.. Unit {
out.push(value.land(0xFF).to_byte())
}
///|
fn append_bmp_header(out : Array[Byte], width : Int, height : Int) -> Unit {
let pixel_byte_count = width * height * 4
append_bmp_u16_le(out, 0x4D42) // BM
append_bmp_u32_le(out, 54 + pixel_byte_count)
append_bmp_u16_le(out, 0)
append_bmp_u16_le(out, 0)
append_bmp_u32_le(out, 54)
append_bmp_u32_le(out, 40)
append_bmp_u32_le(out, width)
append_bmp_u32_le(out, height)
append_bmp_u16_le(out, 1)
append_bmp_u16_le(out, 32)
append_bmp_u32_le(out, 0)
append_bmp_u32_le(out, pixel_byte_count)
append_bmp_u32_le(out, 0)
append_bmp_u32_le(out, 0)
append_bmp_u32_le(out, 0)
append_bmp_u32_le(out, 0)
}
///|
fn append_bmp_u16_le(out : Array[Byte], value : Int) -> Unit {
out.push(value.land(0xFF).to_byte())
out.push((value / 256).land(0xFF).to_byte())
}
///|
fn append_bmp_u32_le(out : Array[Byte], value : Int) -> Unit {
out.push(value.land(0xFF).to_byte())
out.push((value / 256).land(0xFF).to_byte())
out.push((value / 65536).land(0xFF).to_byte())
out.push((value / 16777216).land(0xFF).to_byte())
}
///|
struct BmpInfo {
pixel_offset : Int
width : Int
height : Int
is_top_down : Bool
bytes_per_pixel : Int
source_row_bytes : Int
} derive(Debug)
///|
pub extend BmpInfo with Debug::{to_repr}
///|
fn decode_bmp_info(data : Bytes) -> Result[BmpInfo, PixmapDecodeError] {
guard data.length() >= 54 else {
return Err(UnexpectedEndOfData(0, 54, data.length()))
}
guard data[0] == 66 && data[1] == 77 else {
return Err(InvalidBmpSignature(data[0], data[1]))
}
let pixel_offset = read_u32_le_at(data, 10)
let header_size = read_u32_le_at(data, 14)
guard header_size >= 40 else {
return Err(UnsupportedBmpHeaderSize(header_size))
}
let width = read_i32_le_at(data, 18)
let raw_height = read_i32_le_at(data, 22)
guard width > 0 && raw_height != 0 else {
return Err(InvalidBmpDimensions(width, raw_height))
}
let color_planes = read_u16_le_at(data, 26)
guard color_planes == 1 else {
return Err(UnsupportedBmpColorPlanes(color_planes))
}
let bits_per_pixel = read_u16_le_at(data, 28)
guard bits_per_pixel == 24 || bits_per_pixel == 32 else {
return Err(UnsupportedBmpBitsPerPixel(bits_per_pixel))
}
let compression = read_u32_le_at(data, 30)
guard compression == 0 else {
return Err(UnsupportedBmpCompression(compression))
}
let height = raw_height.abs()
let bytes_per_pixel = bits_per_pixel / 8
let source_row_bytes = (width * bits_per_pixel + 31) / 32 * 4
let required_len = pixel_offset + source_row_bytes * height
guard data.length() >= required_len else {
return Err(
BmpPixelDataOutOfBounds(pixel_offset, required_len, data.length()),
)
}
Ok({
pixel_offset,
width,
height,
is_top_down: raw_height < 0,
bytes_per_pixel,
source_row_bytes,
})
}
///|
fn decode_bmp_pixmap(data : Bytes, info : BmpInfo) -> Pixmap {
let pixmap = Pixmap::new(info.width, info.height)
let dst = pixmap.get_pixels()
let dst_row_bytes = info.width * 4
for y in 0.. Int {
data[offset].to_int() + data[offset + 1].to_int() * 256
}
///|
fn read_u32_le_at(data : Bytes, offset : Int) -> Int {
data[offset].to_int() +
data[offset + 1].to_int() * 256 +
data[offset + 2].to_int() * 65536 +
data[offset + 3].to_int() * 16777216
}
///|
fn read_i32_le_at(data : Bytes, offset : Int) -> Int {
let b0 = data[offset].to_int()
let b1 = data[offset + 1].to_int()
let b2 = data[offset + 2].to_int()
let b3 = data[offset + 3].to_int()
let signed_b3 = if b3 >= 128 { b3 - 256 } else { b3 }
b0 + b1 * 256 + b2 * 65536 + signed_b3 * 16777216
}