///|
/// Metadata extension kinds that can be inserted into a GIF89a stream.
pub(all) enum GifMetadataKind {
Comment
Application(identifier~ : String, authentication~ : String)
} derive(Eq, Debug)
///|
/// A metadata record stored between the logical screen and image blocks.
pub(all) struct GifMetadata {
kind : GifMetadataKind
payload : Bytes
} derive(Debug)
///|
pub fn GifMetadata::comment(text : String) -> GifMetadata {
{ kind: Comment, payload: @utf8.encode(text) }
}
///|
pub fn GifMetadata::application(
identifier : String,
authentication : String,
payload : Bytes,
) -> GifMetadata {
{ kind: Application(identifier~, authentication~), payload }
}
///|
pub fn GifMetadata::kind(self : GifMetadata) -> GifMetadataKind {
self.kind
}
///|
pub fn GifMetadata::payload(self : GifMetadata) -> Bytes {
self.payload
}
///|
fn metadata_ascii(value : String, width : Int) -> Array[Byte] {
let result : Array[Byte] = []
for byte in @utf8.encode(value) {
if result.length() < width {
result.push(byte)
}
}
while result.length() < width {
result.push(b' ')
}
result
}
///|
fn metadata_sub_blocks(payload : Bytes) -> Array[Byte] {
let result : Array[Byte] = []
let mut offset = 0
while offset < payload.length() {
let remaining = payload.length() - offset
let size = if remaining > 255 { 255 } else { remaining }
result.push(size.to_byte())
for i in 0.. Array[Byte] {
let result : Array[Byte] = []
match metadata.kind {
Comment => {
result.push(0x21)
result.push(0xfe)
for byte in metadata_sub_blocks(metadata.payload) {
result.push(byte)
}
}
Application(identifier~, authentication~) => {
result.push(0x21)
result.push(0xff)
result.push(0x0b)
for byte in metadata_ascii(identifier, 8) {
result.push(byte)
}
for byte in metadata_ascii(authentication, 3) {
result.push(byte)
}
for byte in metadata_sub_blocks(metadata.payload) {
result.push(byte)
}
}
}
result
}
///|
fn insert_metadata(bytes : Bytes, metadata : ArrayView[GifMetadata]) -> Bytes {
let trailer = if bytes.length() == 0 { 0 } else { bytes.length() - 1 }
let result : Array[Byte] = []
for i in 0.. 0 {
result.push(bytes[trailer])
}
Bytes::from_array(result[:])
}
///|
/// Encode an animation and insert comment/application extensions before the trailer.
pub fn encode_with_metadata(
animation : Animation,
metadata : ArrayView[GifMetadata],
options? : EncodeOptions = EncodeOptions::default(),
) -> Bytes raise GifError {
let encoded = encode_with_options(animation, options)
insert_metadata(encoded, metadata)
}
///|
/// Add a UTF-8 comment to a freshly encoded animation.
pub fn encode_with_comment(
animation : Animation,
comment : String,
options? : EncodeOptions = EncodeOptions::default(),
) -> Bytes raise GifError {
let item = GifMetadata::comment(comment)
encode_with_metadata(animation, [item][:], options~)
}
///|
/// Add a Netscape-compatible application payload without changing loop timing.
pub fn encode_with_application(
animation : Animation,
identifier : String,
authentication : String,
payload : Bytes,
options? : EncodeOptions = EncodeOptions::default(),
) -> Bytes raise GifError {
let item = GifMetadata::application(identifier, authentication, payload)
encode_with_metadata(animation, [item][:], options~)
}
///|
/// Read all comment extension payloads without decoding pixel data.
pub fn extract_comments(data : Bytes) -> Array[String] raise GifError {
let result : Array[String] = []
let mut i = 0
while i + 1 < data.length() {
if data[i] == 0x21 && data[i + 1] == 0xfe {
let payload : Array[Byte] = []
i += 2
while i < data.length() {
let size = data[i].to_int()
i += 1
if size == 0 {
break
}
if i + size > data.length() {
raise UnexpectedEnd(context="comment extension")
}
for j in 0.. raise InvalidBlock(context="comment is not valid UTF-8")
}
} else {
i += 1
}
}
result
}
///|
/// Return the number of extension bytes added by a metadata record.
pub fn GifMetadata::encoded_length(self : GifMetadata) -> Int {
metadata_extension(self).length()
}
///|
/// Validate the identifier widths required by an application extension.
pub fn GifMetadata::is_valid(self : GifMetadata) -> Bool {
match self.kind {
Comment => true
Application(identifier~, authentication~) =>
@utf8.encode(identifier).length() <= 8 &&
@utf8.encode(authentication).length() <= 3
}
}