///|
priv struct GceState {
delay_cs : Int
disposal : Disposal
transparent_index : Int?
}
///|
fn default_gce() -> GceState {
{ delay_cs: 0, disposal: Keep, transparent_index: None }
}
///|
priv struct LogicalScreen {
width : Int
height : Int
background : Rgba
global_palette : Array[Rgba]?
}
///|
fn parse_logical_screen(cursor : GifCursor) -> LogicalScreen raise GifError {
let width = cursor.u16(context="logical screen width")
let height = cursor.u16(context="logical screen height")
if width <= 0 || height <= 0 {
raise InvalidDimensions
}
let packed = cursor.byte(context="logical screen descriptor").to_int()
let background_index = cursor.byte(context="background color index").to_int()
ignore(cursor.byte(context="pixel aspect ratio"))
let global_palette = if (packed & 0x80) != 0 {
Some(read_palette(cursor, packed_table_size(packed)))
} else {
None
}
let background = match global_palette {
None => rgb(0, 0, 0)
Some(colors) =>
if background_index < colors.length() {
colors[background_index]
} else {
rgb(0, 0, 0)
}
}
LogicalScreen::{ width, height, background, global_palette }
}
///|
fn parse_gce(cursor : GifCursor) -> GceState raise GifError {
let size = cursor.byte(context="graphic control extension size").to_int()
if size != 4 {
raise InvalidBlock(context="graphic control extension")
}
let packed = cursor.byte(context="graphic control extension").to_int()
let delay_cs = cursor.u16(context="graphic control extension delay")
let transparent_index = cursor
.byte(context="graphic control extension")
.to_int()
let terminator = cursor.byte(context="graphic control extension terminator")
if terminator != 0 {
raise InvalidBlock(context="graphic control extension terminator")
}
let disposal = match (packed >> 2) & 0x07 {
2 => Background
3 => Previous
_ => Keep
}
{
delay_cs,
disposal,
transparent_index: if (packed & 1) != 0 {
Some(transparent_index)
} else {
None
},
}
}
///|
fn parse_application_extension(
cursor : GifCursor,
loop_count : Ref[Int?],
) -> Unit raise GifError {
let size = cursor
.byte(context="application extension identifier size")
.to_int()
let identifier = cursor.take(size, context="application extension identifier")
let payload = read_sub_blocks(cursor, context="application extension data")
if is_bytes(identifier, [
b'N', b'E', b'T', b'S', b'C', b'A', b'P', b'E', b'2', b'.', b'0',
]) &&
payload.length() >= 3 &&
payload[0] == 1 {
loop_count.val = Some(payload[1].to_int() | (payload[2].to_int() << 8))
}
}
///|
fn parse_image(
cursor : GifCursor,
screen : LogicalScreen,
gce : GceState,
) -> Frame raise GifError {
let left = cursor.u16(context="image left")
let top = cursor.u16(context="image top")
let width = cursor.u16(context="image width")
let height = cursor.u16(context="image height")
if width <= 0 ||
height <= 0 ||
left + width > screen.width ||
top + height > screen.height {
raise InvalidDimensions
}
let packed = cursor.byte(context="image descriptor").to_int()
let palette = if (packed & 0x80) != 0 {
read_palette(cursor, packed_table_size(packed))
} else {
match screen.global_palette {
None =>
raise InvalidPalette(context="image has no global or local color table")
Some(colors) => colors
}
}
let min_code_size = cursor
.byte(context="image LZW minimum code size")
.to_int()
let compressed = read_sub_blocks(cursor, context="image data sub-blocks")
let indices = lzw_decode(compressed, min_code_size, width * height)
let canvas = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
let transparent_color = match gce.transparent_index {
None => None
Some(index) =>
if index < palette.length() {
let color = palette[index]
Some(rgba(color.red(), color.green(), color.blue(), 0))
} else {
None
}
}
for y in 0.. c
None => rgba(0, 0, 0, 0)
},
),
)
} else {
if index >= palette.length() {
raise InvalidPalette(context="image index is outside its color table")
}
ignore(canvas.set(x=target_x, y=target_y, color=palette[index]))
}
}
}
let frame = Frame::new(canvas~, delay_cs=gce.delay_cs)
.with_disposal(disposal=gce.disposal)
.with_offset(left~, top~)
match transparent_color {
None => frame
Some(color) => frame.with_transparent(color~)
}
}
///|
/// Decode a GIF87a/GIF89a stream into an animation.
pub fn decode(data : Bytes) -> Animation raise GifError {
if data.length() < 13 {
raise UnexpectedEnd(context="GIF header")
}
if !(data[0] == b'G' &&
data[1] == b'I' &&
data[2] == b'F' &&
data[3] == b'8' &&
(data[4] == b'7' || data[4] == b'9') &&
data[5] == b'a') {
raise InvalidSignature
}
let cursor = GifCursor::new(data)
ignore(cursor.take(6, context="GIF signature"))
let screen = parse_logical_screen(cursor)
let loop_count : Ref[Int?] = Ref(None)
let mut animation = Animation::new(width=screen.width, height=screen.height).with_background(
background=screen.background,
)
let mut current_gce = default_gce()
let mut done = false
while !done && cursor.remaining() > 0 {
let marker = cursor.byte(context="GIF block marker").to_int()
match marker {
0x3b => done = true
0x21 => {
let label = cursor.byte(context="extension label").to_int()
match label {
0xf9 => current_gce = parse_gce(cursor)
0xff => parse_application_extension(cursor, loop_count)
0x01 | 0xfe => {
let size = cursor.byte(context="extension block size").to_int()
cursor.skip(size, context="extension block")
ignore(read_sub_blocks(cursor, context="extension sub-blocks"))
}
_ => ignore(read_sub_blocks(cursor, context="extension sub-blocks"))
}
}
0x2c => {
let frame = parse_image(cursor, screen, current_gce)
animation = animation.add_frame(frame)
current_gce = default_gce()
}
_ => raise InvalidBlock(context="unknown GIF block marker")
}
}
if !done || animation.frames.length() == 0 {
raise InvalidBlock(context="GIF has no complete image frames")
}
animation.with_loop_count(loop_count=loop_count.val)
}