///|
/// Encode this pixmap as a deterministic 8-bit RGBA PNG image.
///
/// The encoder writes filter-0 scanlines into uncompressed DEFLATE blocks, so
/// the output is dependency-free and stable for headless render fixtures.
pub fn Pixmap::to_png_bytes(self : Pixmap) -> Result[Bytes, PixmapEncodeError] {
guard self.width > 0 && self.height > 0 else {
return Err(InvalidPngDimensions(self.width, self.height))
}
let out : Array[Byte] = []
append_png_signature(out)
let ihdr : Array[Byte] = []
append_png_u32_be(ihdr, self.width.reinterpret_as_uint())
append_png_u32_be(ihdr, self.height.reinterpret_as_uint())
ihdr.push(8) // bit depth
ihdr.push(6) // RGBA truecolor with alpha
ihdr.push(0) // compression method
ihdr.push(0) // filter method
ihdr.push(0) // no interlace
append_png_chunk(out, [73, 72, 68, 82], ihdr) // IHDR
let scanlines = png_rgba_filter0_scanlines(self)
let idat = png_zlib_stored_stream(scanlines)
append_png_chunk(out, [73, 68, 65, 84], idat) // IDAT
append_png_chunk(out, [73, 69, 78, 68], []) // IEND
Ok(Bytes::from_array(out))
}
///|
/// Decode an 8-bit RGBA PNG image into a pixmap.
///
/// This decoder supports non-interlaced PNGs with RGBA color, standard
/// scanline filters, and zlib-wrapped DEFLATE image data. It is
/// intentionally paired with `to_png_bytes` for portable fixture roundtrips.
pub fn Pixmap::from_png_bytes(
data : Bytes,
) -> Result[Pixmap, PixmapDecodeError] {
match decode_png_info(data) {
Err(error) => Err(error)
Ok(info) =>
match png_zlib_stream_decode(info.idat) {
Err(error) => Err(error)
Ok(scanlines) => decode_png_rgba_pixmap(info, scanlines)
}
}
}
///|
fn append_png_signature(out : Array[Byte]) -> Unit {
let signature : Array[Byte] = [137, 80, 78, 71, 13, 10, 26, 10]
append_png_bytes(out, signature)
}
///|
fn append_png_chunk(
out : Array[Byte],
chunk_type : Array[Byte],
data : Array[Byte],
) -> Unit {
append_png_u32_be(out, data.length().reinterpret_as_uint())
append_png_bytes(out, chunk_type)
append_png_bytes(out, data)
append_png_u32_be(out, png_crc32_chunk(chunk_type, data))
}
///|
fn append_png_bytes(out : Array[Byte], bytes : Array[Byte]) -> Unit {
for byte in bytes {
out.push(byte)
}
}
///|
fn append_png_u16_le(out : Array[Byte], value : Int) -> Unit {
out.push(value.land(0xFF).to_byte())
out.push((value >> 8).land(0xFF).to_byte())
}
///|
fn append_png_u32_be(out : Array[Byte], value : UInt) -> Unit {
out.push((value >> 24).to_byte())
out.push((value >> 16).to_byte())
out.push((value >> 8).to_byte())
out.push(value.to_byte())
}
///|
fn png_rgba_filter0_scanlines(pixmap : Pixmap) -> Array[Byte] {
let out : Array[Byte] = []
for y in 0.. Array[Byte] {
let out : Array[Byte] = []
out.push(0x78)
out.push(0x01)
if data.length() == 0 {
out.push(1)
append_png_u16_le(out, 0)
append_png_u16_le(out, 0xFFFF)
append_png_u32_be(out, png_adler32(data))
return out
}
let mut offset = 0
while offset < data.length() {
let remaining = data.length() - offset
let block_len = remaining.min(65535)
let is_final = offset + block_len >= data.length()
out.push(if is_final { 1 } else { 0 })
append_png_u16_le(out, block_len)
append_png_u16_le(out, 0xFFFF - block_len)
for i in 0.. UInt {
let mod_adler = 65521
let mut s1 = 1
let mut s2 = 0
for byte in data {
s1 = (s1 + byte.to_int()) % mod_adler
s2 = (s2 + s1) % mod_adler
}
(s2.reinterpret_as_uint() << 16) | s1.reinterpret_as_uint()
}
///|
fn png_crc32_chunk(chunk_type : Array[Byte], data : Array[Byte]) -> UInt {
let mut crc : UInt = 0xFFFFFFFF
for byte in chunk_type {
crc = png_crc32_update(crc, byte)
}
for byte in data {
crc = png_crc32_update(crc, byte)
}
crc ^ 0xFFFFFFFF
}
///|
fn png_crc32_update(crc : UInt, byte : Byte) -> UInt {
let mut c = crc ^ byte.to_uint()
for _ in 0..<8 {
c = if (c & 1) != 0 { (c >> 1) ^ 0xEDB88320 } else { c >> 1 }
}
c
}
///|
struct PngInfo {
width : Int
height : Int
idat : Array[Byte]
} derive(Debug)
///|
pub extend PngInfo with Debug::{to_repr}
///|
struct PngHeader {
width : Int
height : Int
} derive(Debug)
///|
pub extend PngHeader with Debug::{to_repr}
///|
struct PngDeflateBitReader {
data : Array[Byte]
stream_end : Int
mut byte_offset : Int
mut bit_offset : Int
} derive(Debug)
///|
pub extend PngDeflateBitReader with Debug::{to_repr}
///|
struct PngDeflateHuffmanEntry {
symbol : Int
length : Int
code : Int
} derive(Debug)
///|
pub extend PngDeflateHuffmanEntry with Debug::{to_repr}
///|
struct PngDeflateHuffman {
entries : Array[PngDeflateHuffmanEntry]
max_bits : Int
} derive(Debug)
///|
pub extend PngDeflateHuffman with Debug::{to_repr}
///|
struct PngDeflateTables {
literal_lengths : PngDeflateHuffman
distances : PngDeflateHuffman
} derive(Debug)
///|
pub extend PngDeflateTables with Debug::{to_repr}
///|
fn decode_png_info(data : Bytes) -> Result[PngInfo, PixmapDecodeError] {
match require_png_available(data, 0, 8) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard has_png_signature(data) else { return Err(InvalidPngSignature) }
let mut offset = 8
let mut seen_ihdr = false
let mut seen_iend = false
let mut width = 0
let mut height = 0
let idat : Array[Byte] = []
while offset < data.length() && !seen_iend {
match require_png_available(data, offset, 8) {
Err(error) => return Err(error)
Ok(_) => ()
}
let length = read_png_u32_be_int_at(data, offset)
let type_offset = offset + 4
let data_offset = offset + 8
let crc_offset = data_offset + length
match require_png_available(data, data_offset, length + 4) {
Err(error) => return Err(error)
Ok(_) => ()
}
let expected_crc = read_png_u32_be_uint_at(data, crc_offset)
let actual_crc = png_crc32_chunk_at(data, type_offset, data_offset, length)
guard expected_crc == actual_crc else {
return Err(PngCrcMismatch(expected_crc, actual_crc))
}
if png_chunk_type_is(data, type_offset, 73, 72, 68, 82) {
match decode_png_header(data, data_offset, length) {
Err(error) => return Err(error)
Ok(header) => {
seen_ihdr = true
width = header.width
height = header.height
}
}
} else if !seen_ihdr {
return Err(MissingPngImageHeader)
} else if png_chunk_type_is(data, type_offset, 73, 68, 65, 84) {
append_png_chunk_data(idat, data, data_offset, length)
} else if png_chunk_type_is(data, type_offset, 73, 69, 78, 68) {
seen_iend = true
} else if png_chunk_is_unsupported_critical(data, type_offset) {
return Err(
UnsupportedPngChunk(
data[type_offset].to_int(),
data[type_offset + 1].to_int(),
data[type_offset + 2].to_int(),
data[type_offset + 3].to_int(),
),
)
}
offset = crc_offset + 4
}
guard seen_ihdr else { return Err(MissingPngImageHeader) }
guard seen_iend else { return Err(MissingPngImageEnd) }
guard idat.length() > 0 else { return Err(MissingPngImageData) }
Ok({ width, height, idat, })
}
///|
fn decode_png_header(
data : Bytes,
offset : Int,
length : Int,
) -> Result[PngHeader, PixmapDecodeError] {
guard length == 13 else { return Err(UnsupportedPngHeaderSize(length)) }
let width = read_png_u32_be_int_at(data, offset)
let height = read_png_u32_be_int_at(data, offset + 4)
guard width > 0 && height > 0 else {
return Err(InvalidPngDimensions(width, height))
}
let bit_depth = data[offset + 8].to_int()
let color_type = data[offset + 9].to_int()
let compression = data[offset + 10].to_int()
let filter = data[offset + 11].to_int()
let interlace = data[offset + 12].to_int()
guard bit_depth == 8 else { return Err(UnsupportedPngBitDepth(bit_depth)) }
guard color_type == 6 else { return Err(UnsupportedPngColorType(color_type)) }
guard compression == 0 else {
return Err(UnsupportedPngCompression(compression))
}
guard filter == 0 else { return Err(UnsupportedPngFilter(filter)) }
guard interlace == 0 else { return Err(UnsupportedPngInterlace(interlace)) }
Ok({ width, height, })
}
///|
fn png_zlib_stream_decode(
data : Array[Byte],
) -> Result[Array[Byte], PixmapDecodeError] {
match require_png_array_available(data, 0, 6) {
Err(error) => return Err(error)
Ok(_) => ()
}
let cmf = data[0].to_int()
let flg = data[1].to_int()
guard (cmf * 256 + flg) % 31 == 0 else {
return Err(InvalidPngZlibHeader(cmf, flg))
}
let compression = cmf.land(0x0F)
guard compression == 8 else {
return Err(UnsupportedPngDeflateCompression(compression))
}
guard flg.land(0x20) == 0 else {
return Err(UnsupportedPngDeflatePresetDictionary)
}
let stream_end = data.length() - 4
let out : Array[Byte] = []
let reader : PngDeflateBitReader = {
data,
stream_end,
byte_offset: 2,
bit_offset: 0,
}
let mut is_final = false
while !is_final {
match png_deflate_read_bits(reader, 1) {
Err(error) => return Err(error)
Ok(bit) => is_final = bit != 0
}
let block_type = match png_deflate_read_bits(reader, 2) {
Err(error) => return Err(error)
Ok(bits) => bits
}
match block_type {
0 =>
match png_deflate_decode_stored_block(reader, out) {
Err(error) => return Err(error)
Ok(_) => ()
}
1 => {
let tables = png_deflate_fixed_tables()
match
png_deflate_decode_compressed_block(
reader,
out,
tables.literal_lengths,
tables.distances,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
2 =>
match png_deflate_decode_dynamic_tables(reader) {
Err(error) => return Err(error)
Ok(tables) =>
match
png_deflate_decode_compressed_block(
reader,
out,
tables.literal_lengths,
tables.distances,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
_ => return Err(UnsupportedPngDeflateBlockType(block_type))
}
}
png_deflate_align_to_byte(reader)
guard reader.byte_offset == stream_end else {
return Err(PngInflateLengthMismatch(stream_end, reader.byte_offset))
}
let expected_adler = read_png_u32_be_uint_array_at(data, stream_end)
let actual_adler = png_adler32(out)
guard expected_adler == actual_adler else {
return Err(PngAdlerMismatch(expected_adler, actual_adler))
}
Ok(out)
}
///|
fn png_deflate_read_bits(
reader : PngDeflateBitReader,
count : Int,
) -> Result[Int, PixmapDecodeError] {
let mut value = 0
for shift in 0..> reader.bit_offset).land(
1,
)
value = value | (bit << shift)
reader.bit_offset = reader.bit_offset + 1
if reader.bit_offset == 8 {
reader.bit_offset = 0
reader.byte_offset = reader.byte_offset + 1
}
}
Ok(value)
}
///|
fn png_deflate_align_to_byte(reader : PngDeflateBitReader) -> Unit {
if reader.bit_offset != 0 {
reader.bit_offset = 0
reader.byte_offset = reader.byte_offset + 1
}
}
///|
fn png_deflate_decode_stored_block(
reader : PngDeflateBitReader,
out : Array[Byte],
) -> Result[Unit, PixmapDecodeError] {
png_deflate_align_to_byte(reader)
match require_png_array_available(reader.data, reader.byte_offset, 4) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard reader.byte_offset + 4 <= reader.stream_end else {
return Err(UnexpectedEndOfData(reader.byte_offset, 4, reader.data.length()))
}
let block_len = read_png_u16_le_array_at(reader.data, reader.byte_offset)
let inverse_len = read_png_u16_le_array_at(
reader.data,
reader.byte_offset + 2,
)
reader.byte_offset = reader.byte_offset + 4
guard block_len + inverse_len == 0xFFFF else {
return Err(PngStoredBlockLengthMismatch(block_len, inverse_len))
}
guard reader.byte_offset + block_len <= reader.stream_end else {
return Err(
UnexpectedEndOfData(reader.byte_offset, block_len, reader.data.length()),
)
}
for i in 0.. PngDeflateTables {
let literal_lengths : Array[Int] = []
for symbol in 0..<288 {
let length = if symbol < 144 {
8
} else if symbol < 256 {
9
} else if symbol < 280 {
7
} else {
8
}
literal_lengths.push(length)
}
let distance_lengths : Array[Int] = []
for _ in 0..<32 {
distance_lengths.push(5)
}
{
literal_lengths: png_deflate_build_huffman(literal_lengths),
distances: png_deflate_build_huffman(distance_lengths),
}
}
///|
fn png_deflate_decode_dynamic_tables(
reader : PngDeflateBitReader,
) -> Result[PngDeflateTables, PixmapDecodeError] {
let hlit = match png_deflate_read_bits(reader, 5) {
Err(error) => return Err(error)
Ok(bits) => bits + 257
}
let hdist = match png_deflate_read_bits(reader, 5) {
Err(error) => return Err(error)
Ok(bits) => bits + 1
}
let hclen = match png_deflate_read_bits(reader, 4) {
Err(error) => return Err(error)
Ok(bits) => bits + 4
}
let code_length_order : Array[Int] = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
]
let code_length_lengths : Array[Int] = []
for _ in 0..<19 {
code_length_lengths.push(0)
}
for i in 0.. return Err(error)
Ok(length) => code_length_lengths[code_length_order[i]] = length
}
}
let code_length_huffman = png_deflate_build_huffman(code_length_lengths)
let total_lengths = hlit + hdist
let lengths : Array[Int] = []
let mut previous = 0
while lengths.length() < total_lengths {
let symbol = match png_deflate_decode_huffman(reader, code_length_huffman) {
Err(error) => return Err(error)
Ok(symbol) => symbol
}
if symbol <= 15 {
lengths.push(symbol)
previous = symbol
} else if symbol == 16 {
guard lengths.length() > 0 else { return Err(InvalidPngDeflateCode) }
let repeat = match png_deflate_read_bits(reader, 2) {
Err(error) => return Err(error)
Ok(bits) => bits + 3
}
for _ in 0.. return Err(error)
Ok(bits) => bits + 3
}
for _ in 0.. return Err(error)
Ok(bits) => bits + 11
}
for _ in 0.. PngDeflateHuffman {
let bit_counts = FixedArray::make(16, 0)
let mut max_bits = 0
for length in code_lengths {
if length > 0 {
bit_counts[length] = bit_counts[length] + 1
max_bits = max_bits.max(length)
}
}
let next_codes = FixedArray::make(16, 0)
let mut code = 0
for bits in 1..<=15 {
code = (code + bit_counts[bits - 1]) << 1
next_codes[bits] = code
}
let entries : Array[PngDeflateHuffmanEntry] = []
for symbol in 0.. 0 {
let canonical = next_codes[length]
next_codes[length] = canonical + 1
entries.push({
symbol,
length,
code: png_deflate_reverse_bits(canonical, length),
})
}
}
{ entries, max_bits, }
}
///|
fn png_deflate_reverse_bits(value : Int, length : Int) -> Int {
let mut input = value
let mut output = 0
for _ in 0..> 1
}
output
}
///|
fn png_deflate_decode_huffman(
reader : PngDeflateBitReader,
huffman : PngDeflateHuffman,
) -> Result[Int, PixmapDecodeError] {
guard huffman.max_bits > 0 else { return Err(InvalidPngDeflateCode) }
let mut code = 0
for length in 1..<=huffman.max_bits {
let bit = match png_deflate_read_bits(reader, 1) {
Err(error) => return Err(error)
Ok(bit) => bit
}
code = code | (bit << (length - 1))
for entry in huffman.entries {
if entry.length == length && entry.code == code {
return Ok(entry.symbol)
}
}
}
Err(InvalidPngDeflateCode)
}
///|
fn png_deflate_decode_compressed_block(
reader : PngDeflateBitReader,
out : Array[Byte],
literal_lengths : PngDeflateHuffman,
distances : PngDeflateHuffman,
) -> Result[Unit, PixmapDecodeError] {
let length_bases : Array[Int] = [
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67,
83, 99, 115, 131, 163, 195, 227, 258,
]
let length_extra_bits : Array[Int] = [
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5,
5, 5, 0,
]
let distance_bases : Array[Int] = [
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577,
]
let distance_extra_bits : Array[Int] = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11,
11, 12, 12, 13, 13,
]
let mut reached_end = false
while !reached_end {
let symbol = match png_deflate_decode_huffman(reader, literal_lengths) {
Err(error) => return Err(error)
Ok(symbol) => symbol
}
if symbol < 256 {
out.push(symbol.to_byte())
} else if symbol == 256 {
reached_end = true
} else {
let length_index = symbol - 257
guard length_index >= 0 && length_index < length_bases.length() else {
return Err(InvalidPngDeflateCode)
}
let length_extra = match
png_deflate_read_bits(reader, length_extra_bits[length_index]) {
Err(error) => return Err(error)
Ok(bits) => bits
}
let length = length_bases[length_index] + length_extra
let distance_symbol = match
png_deflate_decode_huffman(reader, distances) {
Err(error) => return Err(error)
Ok(symbol) => symbol
}
guard distance_symbol >= 0 && distance_symbol < distance_bases.length() else {
return Err(InvalidPngDeflateCode)
}
let distance_extra = match
png_deflate_read_bits(reader, distance_extra_bits[distance_symbol]) {
Err(error) => return Err(error)
Ok(bits) => bits
}
let distance = distance_bases[distance_symbol] + distance_extra
guard distance > 0 && distance <= out.length() else {
return Err(InvalidPngDeflateCode)
}
for _ in 0.. Result[Pixmap, PixmapDecodeError] {
let row_bytes = info.width * 4
let source_row_bytes = row_bytes + 1
let expected_len = source_row_bytes * info.height
guard scanlines.length() == expected_len else {
return Err(PngScanlineLengthMismatch(expected_len, scanlines.length()))
}
let pixmap = Pixmap::new(info.width, info.height)
let dst = pixmap.get_pixels()
for y in 0..= 0 && filter_type <= 4 else {
return Err(UnsupportedPngFilterType(filter_type))
}
let dst_row = y * row_bytes
for i in 0..= 4 { dst[dst_row + i - 4].to_int() } else { 0 }
let up = if y > 0 { dst[dst_row - row_bytes + i].to_int() } else { 0 }
let upper_left = if y > 0 && i >= 4 {
dst[dst_row - row_bytes + i - 4].to_int()
} else {
0
}
let predictor = match filter_type {
0 => 0
1 => left
2 => up
3 => (left + up) / 2
4 => png_paeth_predictor(left, up, upper_left)
_ => 0
}
dst[dst_row + i] = (scanlines[source_row + 1 + i].to_int() + predictor)
.land(0xFF)
.to_byte()
}
}
Ok(pixmap)
}
///|
fn png_paeth_predictor(a : Int, b : Int, c : Int) -> Int {
let p = a + b - c
let pa = (p - a).abs()
let pb = (p - b).abs()
let pc = (p - c).abs()
if pa <= pb && pa <= pc {
a
} else if pb <= pc {
b
} else {
c
}
}
///|
fn has_png_signature(data : Bytes) -> Bool {
data[0] == 137 &&
data[1] == 80 &&
data[2] == 78 &&
data[3] == 71 &&
data[4] == 13 &&
data[5] == 10 &&
data[6] == 26 &&
data[7] == 10
}
///|
fn append_png_chunk_data(
out : Array[Byte],
data : Bytes,
offset : Int,
length : Int,
) -> Unit {
for i in 0.. Bool {
data[offset].to_int() == b0 &&
data[offset + 1].to_int() == b1 &&
data[offset + 2].to_int() == b2 &&
data[offset + 3].to_int() == b3
}
///|
fn png_chunk_is_unsupported_critical(data : Bytes, offset : Int) -> Bool {
let is_critical = data[offset].to_int().land(0x20) == 0
is_critical && !png_chunk_type_is(data, offset, 80, 76, 84, 69)
}
///|
fn png_crc32_chunk_at(
data : Bytes,
type_offset : Int,
data_offset : Int,
length : Int,
) -> UInt {
let mut crc : UInt = 0xFFFFFFFF
for i in 0..<4 {
crc = png_crc32_update(crc, data[type_offset + i])
}
for i in 0.. Result[Unit, PixmapDecodeError] {
guard offset >= 0 && length >= 0 && offset + length <= data.length() else {
return Err(UnexpectedEndOfData(offset, length, data.length()))
}
Ok(())
}
///|
fn require_png_array_available(
data : Array[Byte],
offset : Int,
length : Int,
) -> Result[Unit, PixmapDecodeError] {
guard offset >= 0 && length >= 0 && offset + length <= data.length() else {
return Err(UnexpectedEndOfData(offset, length, data.length()))
}
Ok(())
}
///|
fn read_png_u16_le_array_at(data : Array[Byte], offset : Int) -> Int {
data[offset].to_int() + data[offset + 1].to_int() * 256
}
///|
fn read_png_u32_be_int_at(data : Bytes, offset : Int) -> Int {
data[offset].to_int() * 16777216 +
data[offset + 1].to_int() * 65536 +
data[offset + 2].to_int() * 256 +
data[offset + 3].to_int()
}
///|
fn read_png_u32_be_uint_at(data : Bytes, offset : Int) -> UInt {
let b0 = data[offset].to_uint()
let b1 = data[offset + 1].to_uint()
let b2 = data[offset + 2].to_uint()
let b3 = data[offset + 3].to_uint()
(b0 << 24) | (b1 << 16) | (b2 << 8) | b3
}
///|
fn read_png_u32_be_uint_array_at(data : Array[Byte], offset : Int) -> UInt {
let b0 = data[offset].to_uint()
let b1 = data[offset + 1].to_uint()
let b2 = data[offset + 2].to_uint()
let b3 = data[offset + 3].to_uint()
(b0 << 24) | (b1 << 16) | (b2 << 8) | b3
}