// Copyright (c) 2025 lws
// GIF (Graphics Interchange Format) decoder
//
// Supports GIF87a and GIF89a formats with:
// - LZW decompression with variable-length codes (up to 12 bits)
// - Global and local color tables
// - 4-pass interlacing
// - Transparency via Graphic Control Extension
// - Single-frame decoding (returns first image)
//
// Performance optimizations:
// - LZW state inlined into mutable locals (no per-code struct allocation)
// - Color tables pre-computed as RGBA byte quads (no per-pixel .to_byte())
// - Non-interlaced fast path (no row_map indirection)
// - Stack array allocated once and reused
// - Bulk byte reading when bit-aligned
//-----------------------------------------------------------------------------
// LZW Decompressor for GIF (inlined, zero struct allocation)
//-----------------------------------------------------------------------------
///|
/// Decode LZW stream into raw pixel indices
/// Uses mutable locals for LZW state to eliminate struct allocation overhead.
/// For a 256x256 image, this avoids ~65,000 struct allocations.
fn lzw_decompress(
data : Bytes,
min_code_size : Int,
pixel_count : Int,
) -> Array[Byte] raise Failure {
// LZW constants
let max_table = 4096
let clear_code = 1 << min_code_size
let eoi_code = clear_code + 1
// String table (mutated in place)
let table_prefix = Array::make(max_table, 0)
let table_suffix = Array::make(max_table, 0)
// Decode stack reused across all calls (sized to max possible depth)
let stack = Array::make(max_table, b'\x00')
// Mutable LZW state (was LzwDecoder struct)
let mut pos : Int = 0
let mut bit_pos : Int = 0
let mut code_size : Int = min_code_size + 1
let mut next_code : Int = eoi_code + 1
let mut max_code : Int = (1 << code_size) - 1
let pixels = Array::make(pixel_count, b'\x00')
let mut pi = 0
// --- read first code ---
let (p1, bp1, code1) = lzw_read_code(data, pos, bit_pos, code_size)
pos = p1
bit_pos = bp1
if code1 == eoi_code {
return pixels
}
if code1 == clear_code {
// Reset and read next as root
next_code = eoi_code + 1
code_size = min_code_size + 1
max_code = (1 << code_size) - 1
let (p2, bp2, code2) = lzw_read_code(data, pos, bit_pos, code_size)
pos = p2
bit_pos = bp2
if code2 == eoi_code {
return pixels
}
if pi < pixel_count {
pixels[pi] = code2.to_byte()
pi = pi + 1
}
let mut prev : Int = code2
while pi < pixel_count {
let (p3, bp3, code3) = lzw_read_code(data, pos, bit_pos, code_size)
pos = p3
bit_pos = bp3
if code3 == eoi_code {
break
}
if code3 == clear_code {
// Reset table
next_code = eoi_code + 1
code_size = min_code_size + 1
max_code = (1 << code_size) - 1
let (p4, bp4, code4) = lzw_read_code(data, pos, bit_pos, code_size)
pos = p4
bit_pos = bp4
if code4 == eoi_code {
break
}
if pi < pixel_count {
pixels[pi] = code4.to_byte()
pi = pi + 1
}
prev = code4
continue
}
// decode_and_add inlined
let (cs5, nc5, mc5, out_len) = lzw_decode_string(
table_prefix, table_suffix, stack, code3, prev, eoi_code, code_size, next_code,
max_code,
)
code_size = cs5
next_code = nc5
max_code = mc5
// Copy from stack (which now contains output in reverse) to pixels
for i = 0; i < out_len && pi < pixel_count; i = i + 1 {
pixels[pi] = stack[out_len - 1 - i]
pi = pi + 1
}
prev = code3
}
return pixels
}
// Normal path: first code is a root code
if pi < pixel_count {
pixels[pi] = code1.to_byte()
pi = pi + 1
}
let mut prev : Int = code1
// --- main decode loop ---
while pi < pixel_count {
let (p2, bp2, code2) = lzw_read_code(data, pos, bit_pos, code_size)
pos = p2
bit_pos = bp2
if code2 == eoi_code {
break
}
if code2 == clear_code {
// Reset table
next_code = eoi_code + 1
code_size = min_code_size + 1
max_code = (1 << code_size) - 1
let (p3, bp3, code3) = lzw_read_code(data, pos, bit_pos, code_size)
pos = p3
bit_pos = bp3
if code3 == eoi_code {
break
}
if pi < pixel_count {
pixels[pi] = code3.to_byte()
pi = pi + 1
}
prev = code3
continue
}
// decode_and_add inlined
let (cs3, nc3, mc3, out_len) = lzw_decode_string(
table_prefix, table_suffix, stack, code2, prev, eoi_code, code_size, next_code,
max_code,
)
code_size = cs3
next_code = nc3
max_code = mc3
// Copy from stack (output in reverse order) to pixels
for i = 0; i < out_len && pi < pixel_count; i = i + 1 {
pixels[pi] = stack[out_len - 1 - i]
pi = pi + 1
}
prev = code2
}
pixels
}
///|
/// Read the next variable-length code from the LZW bitstream (LSB-first).
/// Returns (new_pos, new_bit_pos, code).
/// Uses bulk byte reading when byte-aligned for codes >= 8 bits.
fn lzw_read_code(
data : Bytes,
pos : Int,
bit_pos : Int,
code_size : Int,
) -> (Int, Int, Int) raise Failure {
let mut byte_pos = pos
let mut bp = bit_pos
let mut result = 0
let mut bit_count = 0
// Fast path: byte-aligned and reading 8+ bits -> read two bytes at once
if bp == 0 && code_size >= 8 && byte_pos + 1 < data.length() {
let lo = data[byte_pos].to_int()
let hi = data[byte_pos + 1].to_int()
let word = lo | (hi << 8)
result = word & ((1 << code_size) - 1)
let bits_read = if code_size <= 16 { code_size } else { 16 }
byte_pos = byte_pos + bits_read / 8
bp = bits_read % 8
return (byte_pos, bp, result)
}
// General path: bit-by-bit
while bit_count < code_size {
if byte_pos >= data.length() {
raise Failure::Failure("GIF: unexpected end of LZW data")
}
let byte = data[byte_pos].to_int()
let bits_left = 8 - bp
let needed = code_size - bit_count
let take = if needed < bits_left { needed } else { bits_left }
let val = (byte >> bp) & ((1 << take) - 1)
result = result | (val << bit_count)
bit_count = bit_count + take
if take == bits_left {
byte_pos = byte_pos + 1
bp = 0
} else {
bp = bp + take
}
}
(byte_pos, bp, result)
}
///|
/// Decode a single LZW code's output string, building it on the stack.
/// Returns (new_code_size, new_next_code, new_max_code, output_length).
/// The output is placed on `stack` in reverse order (pop from end to get forward order).
/// This function handles both the normal case and the KwKwK edge case correctly:
/// when `code == next_code`, output = string(prev_code) + first_char(string(prev_code)).
fn lzw_decode_string(
table_prefix : Array[Int],
table_suffix : Array[Int],
stack : Array[Byte],
code : Int,
prev_code : Int,
eoi_code : Int,
code_size : Int,
next_code : Int,
max_code : Int,
) -> (Int, Int, Int, Int) raise Failure {
let max_table = table_prefix.length()
let mut c = code
let mut sp = 0
// KwKwK case: code not yet in table.
// Output = string(prev_code) + first_char(string(prev_code)).
// We must trace prev_code to find its first character, then push it.
if c == next_code {
// Find first character of prev_code's string by tracing to root
let mut t = prev_code
while t > eoi_code {
if t >= max_table {
raise Failure::Failure("GIF: invalid LZW prefix chain")
}
t = table_prefix[t]
}
stack[sp] = t.to_byte()
sp = sp + 1
c = prev_code
}
// Walk prefix chain: push suffix bytes onto stack
while c > eoi_code {
if c >= next_code || c >= max_table {
raise Failure::Failure("GIF: invalid LZW code \{c}")
}
stack[sp] = table_suffix[c].to_byte()
sp = sp + 1
c = table_prefix[c]
}
// Root code (c <= eoi_code, the root value is the actual pixel index)
stack[sp] = c.to_byte()
sp = sp + 1
// First byte of output (bottom of stack = first character of decoded string)
let first = stack[sp - 1].to_int()
// Add new entry: prev_code + first_byte
let mut new_next = next_code
let mut new_cs = code_size
let mut new_max = max_code
if next_code < max_table {
table_prefix[next_code] = prev_code
table_suffix[next_code] = first
new_next = next_code + 1
// Grow code size if needed
if new_next > max_code && code_size < 12 {
new_cs = code_size + 1
new_max = (1 << new_cs) - 1
}
}
(new_cs, new_next, new_max, sp)
}
//-----------------------------------------------------------------------------
// GIF Decoder
//-----------------------------------------------------------------------------
///|
/// Decode a GIF image from raw bytes (returns the first frame)
pub fn decode_gif(data : Bytes) -> Image raise Failure {
let decoder = GifDecoder::new(data)
decoder.decode()
}
///|
/// GIF decoder state
priv struct GifDecoder {
data : Bytes
pos : Int
width : Int
height : Int
global_ct : Array[(Int, Int, Int)] // (R, G, B) tuples
bg_color : Int
}
///|
fn GifDecoder::new(data : Bytes) -> GifDecoder raise Failure {
// Check signature
if data.length() < 13 {
raise Failure::Failure("GIF: file too small")
}
// "GIF" signature
if data[0] != b'G' || data[1] != b'I' || data[2] != b'F' {
raise Failure::Failure("GIF: invalid signature")
}
// Version: "87a" or "89a"
if data[3] != b'8' || (data[4] != b'7' && data[4] != b'9') || data[5] != b'a' {
raise Failure::Failure("GIF: unsupported version")
}
// Logical Screen Descriptor
let w = read_u16_le(data, 6)
let h = read_u16_le(data, 8)
let packed = data[10].to_int()
let bg = data[11].to_int()
let _aspect = data[12].to_int()
if w <= 0 || h <= 0 {
raise Failure::Failure("GIF: invalid dimensions")
}
let has_gct = packed >> 7 != 0
let gct_size = 1 << ((packed & 7) + 1)
// Read Global Color Table if present
let mut gct = Array::make(0, (0, 0, 0))
let mut pos = 13
if has_gct {
gct = Array::make(gct_size, (0, 0, 0))
for i = 0; i < gct_size; i = i + 1 {
let r = data[pos].to_int()
let g = data[pos + 1].to_int()
let b = data[pos + 2].to_int()
gct[i] = (r, g, b)
pos = pos + 3
}
}
{ data, pos, width: w, height: h, global_ct: gct, bg_color: bg }
}
///|
/// Read a sub-block chain (GIF data sub-blocks)
/// Each sub-block: 1-byte count (0-255) followed by count bytes of data
/// Terminated by a 0-byte count
fn read_sub_blocks(data : Bytes, start_pos : Int) -> (Bytes, Int) raise Failure {
let buf = Buffer()
let mut pos = start_pos
while pos < data.length() {
let count = data[pos].to_int()
pos = pos + 1
if count == 0 {
break
}
if pos + count > data.length() {
raise Failure::Failure("GIF: truncated sub-block")
}
buf.write_bytes(data[pos:pos + count].to_owned())
pos = pos + count
}
(buf.to_bytes(), pos)
}
///|
/// Build interlace row mapping for 4-pass interlacing
/// Returns an array mapping source row index to destination row
fn build_interlace_map(img_h : Int) -> Array[Int] {
let row_map = Array::make(img_h, 0)
let mut ri = 0
// Pass 1: every 8th row starting at 0
let mut r = 0
while r < img_h {
row_map[ri] = r
ri = ri + 1
r = r + 8
}
// Pass 2: every 8th row starting at 4
r = 4
while r < img_h {
row_map[ri] = r
ri = ri + 1
r = r + 8
}
// Pass 3: every 4th row starting at 2
r = 2
while r < img_h {
row_map[ri] = r
ri = ri + 1
r = r + 4
}
// Pass 4: every 2nd row starting at 1
r = 1
while r < img_h {
row_map[ri] = r
ri = ri + 1
r = r + 2
}
row_map
}
///|
/// Pre-compute color table as RGBA byte quads for fast pixel writes.
/// Converts (Int,Int,Int) tuples to [r_byte, g_byte, b_byte, a_byte] arrays,
/// eliminating per-pixel .to_byte() calls and tuple destructuring.
fn ct_to_rgba(ct : Array[(Int, Int, Int)]) -> Array[Array[Byte]] {
let n = ct.length()
let result = Array::make(n, [b'\x00', b'\x00', b'\x00', b'\xFF'])
for i = 0; i < n; i = i + 1 {
let (r, g, b) = ct[i]
result[i] = [r.to_byte(), g.to_byte(), b.to_byte(), b'\xFF']
}
result
}
///|
/// Decode a single GIF frame (returns first frame found)
fn GifDecoder::decode(self : GifDecoder) -> Image raise Failure {
let mut pos = self.pos
// GCE state tracked as mutable locals
let mut has_transparency = false
let mut transparent_idx = -1
let mut _delay = 0
while pos < self.data.length() {
let block_type = self.data[pos].to_int()
pos = pos + 1
// Extension block
if block_type == 0x21 {
if pos >= self.data.length() {
raise Failure::Failure("GIF: truncated extension")
}
let ext_type = self.data[pos].to_int()
pos = pos + 1
if ext_type == 0xF9 {
// Graphic Control Extension
if pos + 5 > self.data.length() {
raise Failure::Failure("GIF: truncated GCE")
}
let _block_size = self.data[pos].to_int() // should be 4
pos = pos + 1
let gce_packed = self.data[pos].to_int()
pos = pos + 1
_delay = read_u16_le(self.data, pos)
pos = pos + 2
let trans_idx = self.data[pos].to_int()
pos = pos + 1
let _term = self.data[pos].to_int() // block terminator 0x00
pos = pos + 1
has_transparency = (gce_packed & 1) != 0
transparent_idx = if has_transparency { trans_idx } else { -1 }
} else {
// Skip other extensions (comment, plain text, application)
let (_bs, np) = read_sub_blocks(self.data, pos)
pos = np
}
continue
}
// Image Descriptor
if block_type == 0x2C {
// Parse image descriptor
if pos + 9 > self.data.length() {
raise Failure::Failure("GIF: truncated image descriptor")
}
let _img_left = read_u16_le(self.data, pos)
let _img_top = read_u16_le(self.data, pos + 2)
let img_w = read_u16_le(self.data, pos + 4)
let img_h = read_u16_le(self.data, pos + 6)
let img_packed = self.data[pos + 8].to_int()
pos = pos + 9
let has_lct = img_packed >> 7 != 0
let is_interlaced = ((img_packed >> 6) & 1) != 0
let lct_size = 1 << ((img_packed & 7) + 1)
// Read Local Color Table if present
let mut active_ct = self.global_ct
if has_lct {
active_ct = Array::make(lct_size, (0, 0, 0))
for i = 0; i < lct_size; i = i + 1 {
let r = self.data[pos].to_int()
let g = self.data[pos + 1].to_int()
let b = self.data[pos + 2].to_int()
active_ct[i] = (r, g, b)
pos = pos + 3
}
}
// Pre-compute RGBA byte palette (eliminates per-pixel .to_byte() calls)
let palette_rgba = ct_to_rgba(active_ct)
let pal_len = palette_rgba.length()
// Read LZW minimum code size
if pos >= self.data.length() {
raise Failure::Failure("GIF: no LZW code size")
}
let min_code_size = self.data[pos].to_int()
if min_code_size < 2 {
raise Failure::Failure("GIF: invalid LZW code size")
}
pos = pos + 1
// Read sub-blocks containing LZW data
let (lzw_data, np) = read_sub_blocks(self.data, pos)
pos = np
// Decompress LZW data
let pixel_count = img_w * img_h
let indices = lzw_decompress(lzw_data, min_code_size, pixel_count)
// Build output image
let out_w = self.width
let out_h = self.height
let out_size = out_w * out_h * 4
let out = Array::make(out_size, b'\x00')
if is_interlaced {
// Interlaced path: build row_map and use indirect addressing
let row_map = build_interlace_map(img_h)
for src_row = 0; src_row < img_h; src_row = src_row + 1 {
let dst_row = row_map[src_row]
let src_base = src_row * img_w
let dst_base = dst_row * out_w * 4
for x = 0; x < img_w; x = x + 1 {
let idx = indices[src_base + x].to_int()
let di = dst_base + x * 4
if has_transparency && idx == transparent_idx {
// Transparent: alpha=0 (bytes already zero from init)
continue
}
if idx < pal_len {
let c = palette_rgba[idx]
out[di] = c[0]
out[di + 1] = c[1]
out[di + 2] = c[2]
out[di + 3] = c[3]
} else {
out[di + 3] = b'\xFF' // Opaque black for out-of-bounds
}
}
}
} else {
// Non-interlaced fast path: no row_map indirection
for y = 0; y < img_h; y = y + 1 {
let src_base = y * img_w
let dst_base = y * out_w * 4
for x = 0; x < img_w; x = x + 1 {
let idx = indices[src_base + x].to_int()
let di = dst_base + x * 4
if has_transparency && idx == transparent_idx {
// Transparent: alpha=0 (bytes already zero from init)
continue
}
if idx < pal_len {
let c = palette_rgba[idx]
out[di] = c[0]
out[di + 1] = c[1]
out[di + 2] = c[2]
out[di + 3] = c[3]
} else {
out[di + 3] = b'\xFF' // Opaque black for out-of-bounds
}
}
}
}
return Image::new(
out_w,
out_h,
PixelFormat::RGBA8,
Bytes::from_array(out),
)
}
// Trailer: end of file
if block_type == 0x3B {
raise Failure::Failure("GIF: no image found")
}
raise Failure::Failure("GIF: unknown block type 0x\{block_type.to_byte()}")
}
raise Failure::Failure("GIF: no image found")
}
///|
/// Decode all frames from an animated GIF, returning an AnimatedImage.
/// Parses Graphic Control Extensions for frame delays and transparency,
/// and the Netscape Application Extension for loop count.
/// Frames are composited onto the full canvas respecting disposal methods.
pub fn decode_gif_all(data : Bytes) -> AnimatedImage raise Failure {
let decoder = GifDecoder::new(data)
decoder.decode_all()
}
///|
fn GifDecoder::decode_all(self : GifDecoder) -> AnimatedImage raise Failure {
let mut pos = self.pos
let mut loop_count = 0 // 0 = infinite loop (default for GIF)
// Collect frames
let frames_buf = Buffer()
let delays_buf = Buffer()
// GCE state for the next frame
let mut has_transparency = false
let mut transparent_idx = -1
let mut delay = 0
let mut disposal = 0 // 0=unspecified, 1=leave, 2=background, 3=restore
// Canvas state
let canvas_w = self.width
let canvas_h = self.height
let canvas_size = canvas_w * canvas_h * 4
let canvas = Array::make(canvas_size, b'\x00')
// Pre-compute background color from global color table if available
let bg_rgba = if self.bg_color >= 0 && self.bg_color < self.global_ct.length() {
let (br, bg, bb) = self.global_ct[self.bg_color]
[br.to_byte(), bg.to_byte(), bb.to_byte(), b'\xFF']
} else {
[b'\x00', b'\x00', b'\x00', b'\x00']
}
while pos < self.data.length() {
let block_type = self.data[pos].to_int()
pos = pos + 1
// Extension block
if block_type == 0x21 {
if pos >= self.data.length() {
raise Failure::Failure("GIF: truncated extension")
}
let ext_type = self.data[pos].to_int()
pos = pos + 1
if ext_type == 0xF9 {
// Graphic Control Extension
if pos + 5 > self.data.length() {
raise Failure::Failure("GIF: truncated GCE")
}
let _block_size = self.data[pos].to_int()
pos = pos + 1
let gce_packed = self.data[pos].to_int()
pos = pos + 1
delay = read_u16_le(self.data, pos)
pos = pos + 2
let trans_idx = self.data[pos].to_int()
pos = pos + 1
let _term = self.data[pos].to_int()
pos = pos + 1
has_transparency = (gce_packed & 1) != 0
transparent_idx = if has_transparency { trans_idx } else { -1 }
disposal = (gce_packed >> 2) & 7
} else if ext_type == 0xFF {
// Application Extension
let (app_data, np) = read_sub_blocks(self.data, pos)
pos = np
// Check for Netscape Application Extension (loop count)
if app_data.length() >= 15 &&
app_data[0] == b'N' &&
app_data[1] == b'E' &&
app_data[2] == b'T' &&
app_data[3] == b'S' &&
app_data[4] == b'C' &&
app_data[5] == b'A' &&
app_data[6] == b'P' &&
app_data[7] == b'E' &&
app_data[8] == b'2' &&
app_data[9] == b'.' &&
app_data[10] == b'0' {
// Sub-block data: 3 bytes (1 byte sub-block ID, 2 bytes loop count LE)
if app_data.length() >= 14 {
loop_count = app_data[12].to_int() | (app_data[13].to_int() << 8)
}
}
} else {
// Skip other extensions
let (_bs, np) = read_sub_blocks(self.data, pos)
pos = np
}
continue
}
// Image Descriptor
if block_type == 0x2C {
if pos + 9 > self.data.length() {
raise Failure::Failure("GIF: truncated image descriptor")
}
let img_left = read_u16_le(self.data, pos)
let img_top = read_u16_le(self.data, pos + 2)
let img_w = read_u16_le(self.data, pos + 4)
let img_h = read_u16_le(self.data, pos + 6)
let img_packed = self.data[pos + 8].to_int()
pos = pos + 9
let has_lct = img_packed >> 7 != 0
let is_interlaced = ((img_packed >> 6) & 1) != 0
let lct_size = 1 << ((img_packed & 7) + 1)
// Read Local Color Table if present
let mut active_ct = self.global_ct
if has_lct {
active_ct = Array::make(lct_size, (0, 0, 0))
for i = 0; i < lct_size; i = i + 1 {
let r = self.data[pos].to_int()
let g = self.data[pos + 1].to_int()
let b = self.data[pos + 2].to_int()
active_ct[i] = (r, g, b)
pos = pos + 3
}
}
let palette_rgba = ct_to_rgba(active_ct)
let pal_len = palette_rgba.length()
// Read LZW minimum code size
if pos >= self.data.length() {
raise Failure::Failure("GIF: no LZW code size")
}
let min_code_size = self.data[pos].to_int()
if min_code_size < 2 {
raise Failure::Failure("GIF: invalid LZW code size")
}
pos = pos + 1
// Read sub-blocks containing LZW data
let (lzw_data, np) = read_sub_blocks(self.data, pos)
pos = np
// Decompress LZW data
let pixel_count = img_w * img_h
let indices = lzw_decompress(lzw_data, min_code_size, pixel_count)
// Decode frame into RGBA buffer
let frame_buf = Array::make(canvas_w * canvas_h * 4, b'\x00')
// Handle disposal method 2: restore to background before drawing
if disposal == 2 {
for i = 0; i < canvas_size; i = i + 1 {
canvas[i] = bg_rgba[i % 4]
}
}
if is_interlaced {
let row_map = build_interlace_map(img_h)
for src_row = 0; src_row < img_h; src_row = src_row + 1 {
let dst_row = row_map[src_row]
let src_base = src_row * img_w
for x = 0; x < img_w; x = x + 1 {
let idx = indices[src_base + x].to_int()
let ox = img_left + x
let oy = img_top + dst_row
if ox >= 0 && ox < canvas_w && oy >= 0 && oy < canvas_h {
let di = (oy * canvas_w + ox) * 4
if has_transparency && idx == transparent_idx {
// Transparent: leave existing canvas pixel
continue
}
if idx < pal_len {
let c = palette_rgba[idx]
frame_buf[di] = c[0]
frame_buf[di + 1] = c[1]
frame_buf[di + 2] = c[2]
frame_buf[di + 3] = c[3]
} else {
frame_buf[di + 3] = b'\xFF'
}
}
}
}
} else {
for y = 0; y < img_h; y = y + 1 {
let src_base = y * img_w
for x = 0; x < img_w; x = x + 1 {
let idx = indices[src_base + x].to_int()
let ox = img_left + x
let oy = img_top + y
if ox >= 0 && ox < canvas_w && oy >= 0 && oy < canvas_h {
let di = (oy * canvas_w + ox) * 4
if has_transparency && idx == transparent_idx {
continue
}
if idx < pal_len {
let c = palette_rgba[idx]
frame_buf[di] = c[0]
frame_buf[di + 1] = c[1]
frame_buf[di + 2] = c[2]
frame_buf[di + 3] = c[3]
} else {
frame_buf[di + 3] = b'\xFF'
}
}
}
}
}
// Composite frame onto canvas so transparent pixels show the background
// (whether it's the original canvas, cleared to bg, or previous content)
for i = 0; i < canvas_size; i = i + 4 {
if frame_buf[i + 3] != b'\x00' {
canvas[i] = frame_buf[i]
canvas[i + 1] = frame_buf[i + 1]
canvas[i + 2] = frame_buf[i + 2]
canvas[i + 3] = b'\xFF'
}
}
// Save the composited canvas as the frame
let frame_bytes = Bytes::from_array(
{
let copy = Array::make(canvas_size, b'\x00')
for i = 0; i < canvas_size; i = i + 1 {
copy[i] = canvas[i]
}
copy
},
)
frames_buf.write_bytes(frame_bytes)
delays_buf.write_bytes(
Bytes::from_array([delay.to_byte(), (delay >> 8).to_byte()]),
)
continue
}
// Trailer: end of file
if block_type == 0x3B {
break
}
raise Failure::Failure("GIF: unknown block type 0x\{block_type.to_byte()}")
}
// Build result
let frame_data = frames_buf.to_bytes()
let delay_data = delays_buf.to_bytes()
let num_frames = delay_data.length() / 2
if num_frames == 0 {
raise Failure::Failure("GIF: no frames found")
}
let empty_bytes = Bytes::from_array(Array::make(4, b'\x00'))
let dummy_img = Image::new(1, 1, PixelFormat::RGBA8, empty_bytes)
let frames = Array::make(num_frames, dummy_img)
let delays = Array::make(num_frames, 0)
for i = 0; i < num_frames; i = i + 1 {
let fstart = i * canvas_size
let arr = Array::make(canvas_size, b'\x00')
for j = 0; j < canvas_size; j = j + 1 {
arr[j] = frame_data[fstart + j]
}
frames[i] = Image::new(
canvas_w,
canvas_h,
PixelFormat::RGBA8,
Bytes::from_array(arr),
)
delays[i] = delay_data[i * 2].to_int() |
(delay_data[i * 2 + 1].to_int() << 8)
}
AnimatedImage::new(frames, delays, canvas_w, canvas_h, loop_count)
}