///|
/// GIF decoder (first frame) in pure MoonBit: GIF87a/GIF89a signatures,
/// logical screen descriptor, global and local color tables, the graphic
/// control extension (transparent index), variable-width LSB-first LZW
/// decompression and 4-pass interlacing. Returns `None` on malformed input.
///|
/// Reads the little-endian u16 at `off`.
fn gif_u16(data : Array[Byte], off : Int) -> Int {
data[off].to_int() + data[off + 1].to_int() * 256
}
///|
/// Concatenates the LZW data sub-blocks starting at `pos`. Returns the
/// payload and the position just past the block terminator, or `None`
/// when the stream is truncated.
fn gif_subblocks(data : Array[Byte], pos : Int) -> (Array[Byte], Int)? {
let out : Array[Byte] = []
let mut p = pos
while true {
if p >= data.length() {
return None
}
let len = data[p].to_int()
p = p + 1
if len == 0 {
break
}
if p + len > data.length() {
return None
}
for i in 0.. Array[Int]? {
if min_code_size < 2 || min_code_size > 8 {
return None
}
let clear = 1 << min_code_size
let end = clear + 1
// Dictionary entries as (prefix index, tail byte); roots have prefix -1.
let prefix : Array[Int] = Array::make(4096, -1)
let tail : Array[Int] = Array::make(4096, 0)
let out : Array[Int] = []
let mut next = end + 1
let mut width = min_code_size + 1
let mut prev = -1
let mut acc = 0
let mut nbits = 0
let mut pos = 0
// Expands one dictionary entry (walking the prefix chain) into `out`.
fn expand(code : Int) -> Int {
// Collect the chain, then emit in reverse (chains are stored tail-first).
let chain : Array[Int] = []
let mut c = code
while c >= 0 {
if c < clear {
chain.push(c)
break
}
chain.push(tail[c])
c = prefix[c]
}
for i = chain.length() - 1; i >= 0; i = i - 1 {
out.push(chain[i])
}
chain[chain.length() - 1] // first index of the expanded string
}
while true {
// Pull the next variable-width code from the LSB-first bit stream.
while nbits < width {
if pos >= bytes.length() {
return None
}
acc = acc + bytes[pos].to_int() * (1 << nbits)
nbits = nbits + 8
pos = pos + 1
}
let code = acc % (1 << width)
acc = acc / (1 << width)
nbits = nbits - width
if code == clear {
next = end + 1
width = min_code_size + 1
prev = -1
continue
}
if code == end {
break
}
if prev < 0 {
// First code after a clear must be a root.
if code >= clear {
return None
}
out.push(code)
prev = code
} else {
let first = if code < next {
expand(code)
} else if code == next {
// The KwKwK case: emit string(prev) followed by its first index.
let f = expand(prev)
out.push(f)
f
} else {
return None
}
if next < 4096 {
prefix[next] = prev
tail[next] = first
next = next + 1
if next < 4096 && next == 1 << width {
width = width + 1
}
}
prev = code
}
if out.length() >= max_pixels {
break
}
}
Some(out)
}
///|
/// A decoded GIF image descriptor. `image` contains the descriptor's own
/// pixels (without compositing against the logical screen); `left`/`top`
/// locate it in that screen. `delay_cs` is the Graphic Control Extension
/// delay in hundredths of a second. `disposal` is the GIF disposal method
/// (0 when unspecified), and `transparent_index` is -1 when transparency is
/// not enabled for this frame.
pub(all) struct GifFrame {
image : Image
left : Int
top : Int
delay_cs : Int
disposal : Int
transparent_index : Int
}
///|
/// Decodes every image descriptor in a GIF into independent RGBA frames.
///
/// Frames are intentionally not composited: callers can use `left`, `top`,
/// and `disposal` to implement the animation policy they need while keeping
/// this decoder deterministic and allocation-bounded. Returns `None` for
/// malformed input, including truncated extensions, invalid color tables,
/// invalid LZW data, and files without an image descriptor.
pub fn gif_decode_all(data : Array[Byte]) -> Array[GifFrame]? {
if data.length() < 13 {
return None
}
if data[0].to_int() != 71 || data[1].to_int() != 73 || data[2].to_int() != 70 {
return None
}
if data[3].to_int() != 56 ||
(data[4].to_int() != 55 && data[4].to_int() != 57) ||
data[5].to_int() != 97 {
return None
}
let screen_w = gif_u16(data, 6)
let screen_h = gif_u16(data, 8)
if screen_w <= 0 || screen_h <= 0 || screen_w > MAX_IMAGE_PIXELS / screen_h {
return None
}
let packed = data[10].to_int()
let has_gct = packed / 128 == 1
let gct_size = 2 << (packed % 8)
let mut pos = 13
let mut gct_at = -1
if has_gct {
gct_at = pos
pos = pos + gct_size * 3
if pos > data.length() {
return None
}
}
let frames : Array[GifFrame] = []
let mut delay_cs = 0
let mut disposal = 0
let mut transparent = -1
while true {
if pos >= data.length() {
return None
}
let block = data[pos].to_int()
pos = pos + 1
if block == 0x3B {
if frames.length() == 0 {
return None
}
return Some(frames)
} else if block == 0x21 {
if pos >= data.length() {
return None
}
let label = data[pos].to_int()
pos = pos + 1
if label == 0xF9 {
// Graphic Control Extension: fixed-size payload, then terminator.
if pos + 6 > data.length() || data[pos].to_int() != 4 {
return None
}
let flags = data[pos + 1].to_int()
disposal = flags / 4 % 8
delay_cs = gif_u16(data, pos + 2)
transparent = if flags % 2 == 1 { data[pos + 4].to_int() } else { -1 }
}
match gif_subblocks(data, pos) {
Some((_, p2)) => pos = p2
None => return None
}
} else if block == 0x2C {
if pos + 9 > data.length() {
return None
}
let left = gif_u16(data, pos)
let top = gif_u16(data, pos + 2)
let width = gif_u16(data, pos + 4)
let height = gif_u16(data, pos + 6)
let ipacked = data[pos + 8].to_int()
pos = pos + 9
if width <= 0 || height <= 0 || width > MAX_IMAGE_PIXELS / height {
return None
}
if left > screen_w ||
top > screen_h ||
width > screen_w - left ||
height > screen_h - top {
return None
}
let interlaced = ipacked / 64 % 2 == 1
let mut table_at = gct_at
let mut table_len = gct_size
if ipacked / 128 == 1 {
table_at = pos
table_len = 2 << (ipacked % 8)
pos = pos + table_len * 3
if pos > data.length() {
return None
}
}
if table_at < 0 || pos >= data.length() {
return None
}
let min_code = data[pos].to_int()
pos = pos + 1
let lzw = match gif_subblocks(data, pos) {
Some((bytes, p2)) => {
pos = p2
bytes
}
None => return None
}
let indices = match gif_lzw(lzw, min_code, width * height) {
Some(ix) => ix
None => return None
}
if indices.length() < width * height {
return None
}
let rows : Array[Int] = []
if interlaced {
for y = 0; y < height; y = y + 8 {
rows.push(y)
}
for y = 4; y < height; y = y + 8 {
rows.push(y)
}
for y = 2; y < height; y = y + 4 {
rows.push(y)
}
for y = 1; y < height; y = y + 2 {
rows.push(y)
}
} else {
for y in 0..= table_len {
return None
}
let c = table_at + idx * 3
let base = (y * width + x) * 4
img.data[base] = data[c]
img.data[base + 1] = data[c + 1]
img.data[base + 2] = data[c + 2]
img.data[base + 3] = if idx == transparent {
b'\x00'
} else {
b'\xFF'
}
}
}
frames.push({
image: img,
left,
top,
delay_cs,
disposal,
transparent_index: transparent,
})
// GCE applies to one image descriptor only.
delay_cs = 0
disposal = 0
transparent = -1
} else {
return None
}
} nobreak {
None
}
}
///|
/// Decodes the first frame of a GIF87a/GIF89a file into an RGBA image.
/// Supports global/local color tables, interlacing and the transparent
/// index from the graphic control extension. Returns `None` on anything
/// malformed (bad signature, truncated blocks, invalid LZW data).
pub fn gif_decode(data : Array[Byte]) -> Image? {
// "GIF87a" or "GIF89a".
if data.length() < 13 {
return None
}
if data[0].to_int() != 71 || data[1].to_int() != 73 || data[2].to_int() != 70 {
return None
}
if data[3].to_int() != 56 ||
(data[4].to_int() != 55 && data[4].to_int() != 57) ||
data[5].to_int() != 97 {
return None
}
let packed = data[10].to_int()
let has_gct = packed / 128 == 1
let gct_size = 2 << (packed % 8) // 2^(N+1) entries
let mut pos = 13
let mut gct_at = -1
if has_gct {
gct_at = pos
pos = pos + gct_size * 3
if pos > data.length() {
return None
}
}
let mut transparent = -1
while true {
if pos >= data.length() {
return None
}
let block = data[pos].to_int()
pos = pos + 1
if block == 0x3B {
return None // trailer before any image
} else if block == 0x21 {
// Extension: remember the transparent index from GCE, skip the rest.
if pos >= data.length() {
return None
}
let label = data[pos].to_int()
pos = pos + 1
if label == 0xF9 {
if pos + 6 > data.length() {
return None
}
let flags = data[pos + 1].to_int()
if flags % 2 == 1 {
transparent = data[pos + 4].to_int()
}
}
match gif_subblocks(data, pos) {
Some((_, p2)) => pos = p2
None => return None
}
} else if block == 0x2C {
// Image descriptor: the frame we decode.
if pos + 9 > data.length() {
return None
}
let width = gif_u16(data, pos + 4)
let height = gif_u16(data, pos + 6)
let ipacked = data[pos + 8].to_int()
pos = pos + 9
if width <= 0 || height <= 0 || width * height > MAX_IMAGE_PIXELS {
return None
}
let interlaced = ipacked / 64 % 2 == 1
let mut table_at = gct_at
let mut table_len = gct_size
if ipacked / 128 == 1 {
table_at = pos
table_len = 2 << (ipacked % 8)
pos = pos + table_len * 3
if pos > data.length() {
return None
}
}
if table_at < 0 {
return None // no color table at all
}
if pos >= data.length() {
return None
}
let min_code = data[pos].to_int()
pos = pos + 1
let (lzw, _) = match gif_subblocks(data, pos) {
Some(r) => r
None => return None
}
let indices = match gif_lzw(lzw, min_code, width * height) {
Some(ix) => ix
None => return None
}
if indices.length() < width * height {
return None
}
// Row order: sequential, or the 4 interlace passes.
let rows : Array[Int] = []
if interlaced {
for y = 0; y < height; y = y + 8 {
rows.push(y)
}
for y = 4; y < height; y = y + 8 {
rows.push(y)
}
for y = 2; y < height; y = y + 4 {
rows.push(y)
}
for y = 1; y < height; y = y + 2 {
rows.push(y)
}
} else {
for y in 0..= table_len {
return None
}
let c = table_at + idx * 3
let base = (y * width + x) * 4
img.data[base] = data[c]
img.data[base + 1] = data[c + 1]
img.data[base + 2] = data[c + 2]
img.data[base + 3] = if idx == transparent {
b'\x00'
} else {
b'\xFF'
}
}
}
return Some(img)
} else {
return None // unknown block type
}
}
None
}