///|
/// Header-level information collected without expanding image pixels.
pub(all) struct GifInfo {
version : String
width : Int
height : Int
frame_count : Int
has_global_palette : Bool
uses_local_palettes : Bool
uses_transparency : Bool
loop_count : Int?
compressed_bytes : Int
} derive(Debug)
///|
pub fn GifInfo::version(self : GifInfo) -> String {
self.version
}
///|
pub fn GifInfo::width(self : GifInfo) -> Int {
self.width
}
///|
pub fn GifInfo::height(self : GifInfo) -> Int {
self.height
}
///|
pub fn GifInfo::frame_count(self : GifInfo) -> Int {
self.frame_count
}
///|
pub fn GifInfo::has_global_palette(self : GifInfo) -> Bool {
self.has_global_palette
}
///|
pub fn GifInfo::uses_local_palettes(self : GifInfo) -> Bool {
self.uses_local_palettes
}
///|
pub fn GifInfo::uses_transparency(self : GifInfo) -> Bool {
self.uses_transparency
}
///|
pub fn GifInfo::loop_count(self : GifInfo) -> Int? {
self.loop_count
}
///|
pub fn GifInfo::compressed_bytes(self : GifInfo) -> Int {
self.compressed_bytes
}
///|
fn inspect_extension(
cursor : GifCursor,
label : Int,
loop_count : Ref[Int?],
) -> (Bool, Bool, Int) raise GifError {
match label {
0xf9 => {
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()
ignore(cursor.skip(2, context="graphic control extension delay"))
ignore(cursor.byte(context="graphic control extension transparent index"))
if cursor.byte(context="graphic control extension terminator") != 0 {
raise InvalidBlock(context="graphic control extension terminator")
}
((packed & 1) != 0, false, 0)
}
0xff => {
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))
}
(false, false, payload.length())
}
0x01 | 0xfe => {
let size = cursor.byte(context="extension block size").to_int()
cursor.skip(size, context="extension block")
let payload = read_sub_blocks(cursor, context="extension sub-blocks")
(false, false, payload.length())
}
_ => {
let payload = read_sub_blocks(cursor, context="extension sub-blocks")
(false, false, payload.length())
}
}
}
///|
/// Inspect GIF structure while leaving compressed image data untouched.
pub fn inspect(data : Bytes) -> GifInfo raise GifError {
if data.length() < 13 {
raise UnexpectedEnd(context="GIF header")
}
let is_87a = data[3] == b'8' && data[4] == b'7' && data[5] == b'a'
let is_89a = data[3] == b'8' && data[4] == b'9' && data[5] == b'a'
if !(data[0] == b'G' &&
data[1] == b'I' &&
data[2] == b'F' &&
(is_87a || is_89a)) {
raise InvalidSignature
}
let cursor = GifCursor::new(data)
ignore(cursor.take(6, context="GIF signature"))
let version = if is_87a { "GIF87a" } else { "GIF89a" }
let screen = parse_logical_screen(cursor)
let loop_count : Ref[Int?] = Ref(None)
let mut frame_count = 0
let mut uses_local = false
let mut uses_transparency = false
let mut compressed_bytes = 0
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()
let (transparent, _, _) = inspect_extension(cursor, label, loop_count)
if transparent {
uses_transparency = true
}
}
0x2c => {
ignore(cursor.u16(context="image left"))
ignore(cursor.u16(context="image top"))
let width = cursor.u16(context="image width")
let height = cursor.u16(context="image height")
if width <= 0 || height <= 0 {
raise InvalidDimensions
}
let packed = cursor.byte(context="image descriptor").to_int()
if (packed & 0x80) != 0 {
uses_local = true
ignore(read_palette(cursor, packed_table_size(packed)))
} else if screen.global_palette is None {
raise InvalidPalette(context="image has no color table")
}
ignore(cursor.byte(context="image LZW minimum code size"))
let payload = read_sub_blocks(cursor, context="image data sub-blocks")
compressed_bytes += payload.length()
frame_count += 1
}
_ => raise InvalidBlock(context="unknown GIF block marker")
}
}
if !done || frame_count == 0 {
raise InvalidBlock(context="GIF has no complete image frames")
}
GifInfo::{
version,
width: screen.width,
height: screen.height,
frame_count,
has_global_palette: screen.global_palette is Some(_),
uses_local_palettes: uses_local,
uses_transparency,
loop_count: loop_count.val,
compressed_bytes,
}
}
///|
/// Return false instead of raising for callers that only need a quick probe.
pub fn is_valid(data : Bytes) -> Bool {
try {
ignore(inspect(data))
true
} catch {
_ => false
} noraise {
_ => true
}
}