///|
/// Byte offsets for lightweight GIF inspection and resumable processing.
pub(all) struct GifIndexEntry {
frame_index : Int
marker_offset : Int
descriptor_offset : Int
extension_offset : Int?
} derive(Debug)
///|
pub fn GifIndexEntry::frame_index(self : GifIndexEntry) -> Int {
self.frame_index
}
///|
pub fn GifIndexEntry::marker_offset(self : GifIndexEntry) -> Int {
self.marker_offset
}
///|
pub fn GifIndexEntry::descriptor_offset(self : GifIndexEntry) -> Int {
self.descriptor_offset
}
///|
pub fn GifIndexEntry::extension_offset(self : GifIndexEntry) -> Int? {
self.extension_offset
}
///|
pub(all) struct GifByteIndex {
bytes : Int
header_bytes : Int
trailer_offset : Int?
frames : Array[GifIndexEntry]
extension_count : Int
} derive(Debug)
///|
pub fn GifByteIndex::bytes(self : GifByteIndex) -> Int {
self.bytes
}
///|
pub fn GifByteIndex::header_bytes(self : GifByteIndex) -> Int {
self.header_bytes
}
///|
pub fn GifByteIndex::trailer_offset(self : GifByteIndex) -> Int? {
self.trailer_offset
}
///|
pub fn GifByteIndex::frames(self : GifByteIndex) -> ArrayView[GifIndexEntry] {
self.frames[:]
}
///|
pub fn GifByteIndex::extension_count(self : GifByteIndex) -> Int {
self.extension_count
}
///|
fn skip_index_sub_blocks(data : Bytes, position : Int) -> Int? {
let mut offset = position
let mut done = false
while !done && offset < data.length() {
let size = data[offset].to_int()
offset += 1
if size == 0 {
done = true
} else if offset + size <= data.length() {
offset += size
} else {
return None
}
}
if done {
Some(offset)
} else {
None
}
}
///|
/// Build a conservative frame/extension index without decoding LZW payloads.
pub fn index_gif(data : Bytes) -> GifByteIndex raise GifError {
if data.length() < 13 {
raise UnexpectedEnd(context="GIF index header")
}
if !(data[0] == b'G' && data[1] == b'I' && data[2] == b'F') {
raise InvalidSignature
}
let packed = data[10].to_int()
let global_size = if (packed & 0x80) == 0 {
0
} else {
3 * (1 << ((packed & 7) + 1))
}
let mut offset = 13 + global_size
let entries : Array[GifIndexEntry] = []
let mut extensions = 0
let mut pending_extension : Int? = None
let mut trailer : Int? = None
while offset < data.length() {
let marker_offset = offset
let marker = data[offset].to_int()
offset += 1
match marker {
0x3b => {
trailer = Some(marker_offset)
offset = data.length()
}
0x21 => {
extensions += 1
pending_extension = Some(marker_offset)
if offset >= data.length() {
raise UnexpectedEnd(context="GIF extension label")
}
let label = data[offset].to_int()
offset += 1
if label == 0xf9 || label == 0x01 {
if offset >= data.length() {
raise UnexpectedEnd(context="GIF extension block")
}
let size = data[offset].to_int()
offset += 1 + size
if offset > data.length() {
raise UnexpectedEnd(context="GIF extension payload")
}
match skip_index_sub_blocks(data, offset) {
None => raise UnexpectedEnd(context="GIF extension sub-blocks")
Some(next) => offset = next
}
} else if label == 0xff {
if offset >= data.length() {
raise UnexpectedEnd(context="GIF application extension")
}
let size = data[offset].to_int()
offset += 1 + size
match skip_index_sub_blocks(data, offset) {
None => raise UnexpectedEnd(context="GIF application payload")
Some(next) => offset = next
}
} else {
match skip_index_sub_blocks(data, offset) {
None => raise UnexpectedEnd(context="GIF extension sub-blocks")
Some(next) => offset = next
}
}
}
0x2c => {
let descriptor = marker_offset
if offset + 9 > data.length() {
raise UnexpectedEnd(context="GIF image descriptor")
}
let packed_image = data[offset + 8].to_int()
offset += 9
if (packed_image & 0x80) != 0 {
offset += 3 * (1 << ((packed_image & 7) + 1))
}
if offset >= data.length() {
raise UnexpectedEnd(context="GIF LZW code size")
}
offset += 1
match skip_index_sub_blocks(data, offset) {
None => raise UnexpectedEnd(context="GIF image sub-blocks")
Some(next) => offset = next
}
entries.push(GifIndexEntry::{
frame_index: entries.length(),
marker_offset,
descriptor_offset: descriptor,
extension_offset: pending_extension,
})
pending_extension = None
}
_ => raise InvalidBlock(context="unknown GIF marker in index")
}
}
GifByteIndex::{
bytes: data.length(),
header_bytes: 13 + global_size,
trailer_offset: trailer,
frames: entries,
extension_count: extensions,
}
}
///|
/// Check whether an index points to a complete GIF stream.
pub fn GifByteIndex::is_complete(self : GifByteIndex) -> Bool {
self.trailer_offset is Some(_) && self.frames.length() > 0
}
///|
/// Return frame offsets in decode order for diagnostic output.
pub fn GifByteIndex::frame_offsets(self : GifByteIndex) -> Array[Int] {
let result : Array[Int] = []
for entry in self.frames {
result.push(entry.marker_offset())
}
result
}