///|
/// Safe boundary helpers for applications that prefer explicit status values.
pub(all) struct EncodeOutcome {
bytes : Bytes?
error_code : String?
error_message : String?
} derive(Debug)
///|
pub fn EncodeOutcome::bytes(self : EncodeOutcome) -> Bytes? {
self.bytes
}
///|
pub fn EncodeOutcome::error_code(self : EncodeOutcome) -> String? {
self.error_code
}
///|
pub fn EncodeOutcome::error_message(self : EncodeOutcome) -> String? {
self.error_message
}
///|
pub fn EncodeOutcome::ok(self : EncodeOutcome) -> Bool {
self.bytes is Some(_)
}
///|
fn gif_error_code(error : GifError) -> (String, String) {
match error {
InvalidSignature => ("INVALID_SIGNATURE", "input is not a GIF stream")
UnexpectedEnd(_) => ("UNEXPECTED_END", "input ends before a complete block")
InvalidDimensions =>
("INVALID_DIMENSIONS", "image dimensions are outside the GIF range")
InvalidBlock(_) => ("INVALID_BLOCK", "GIF block structure is malformed")
UnsupportedFeature(_) =>
("UNSUPPORTED_FEATURE", "GIF feature is not supported")
InvalidLzw(_) => ("INVALID_LZW", "LZW payload cannot be decoded")
InvalidPalette(_) => ("INVALID_PALETTE", "palette entry is invalid")
}
}
///|
/// Encode without throwing across a foreign-function boundary.
pub fn encode_checked(
animation : Animation,
options : EncodeOptions,
) -> EncodeOutcome {
try
EncodeOutcome::{
bytes: Some(encode_with_options(animation, options)),
error_code: None,
error_message: None,
}
catch {
error => {
let (code, message) = gif_error_code(error)
EncodeOutcome::{
bytes: None,
error_code: Some(code),
error_message: Some(message),
}
}
} noraise {
_ =>
EncodeOutcome::{
bytes: None,
error_code: Some("UNKNOWN"),
error_message: Some("unknown GIF error"),
}
}
}
///|
/// Decode without throwing across an application callback boundary.
pub fn decode_checked(data : Bytes) -> Animation? {
try Some(decode(data)) catch {
_ => None
} noraise {
_ => None
}
}
///|
/// Return a one-frame animation for a canvas with validated timing.
pub fn single_frame_animation(canvas : Canvas, delay_cs : Int) -> Animation {
Animation::new(width=canvas.width, height=canvas.height).add_frame(
Frame::new(canvas~, delay_cs=if delay_cs < 0 { 0 } else { delay_cs }),
)
}
///|
/// Encode a canvas with a single call and a safe palette budget.
pub fn encode_canvas_checked(
canvas : Canvas,
colors : Int,
delay_cs : Int,
) -> EncodeOutcome {
let options = EncodeOptions::new(
max_colors=clamp_palette_size(colors),
default_delay_cs=delay_cs,
)
encode_checked(single_frame_animation(canvas, delay_cs), options)
}
///|
/// Decode the first visible frame, if a stream contains one.
pub fn decode_first_frame(data : Bytes) -> Frame? {
match decode_checked(data) {
None => None
Some(animation) =>
if animation.frame_count() == 0 {
None
} else {
Some(animation.frames[0])
}
}
}
///|
/// Return a human-readable message for an invalid buffer.
pub fn explain_gif_error(data : Bytes) -> String {
match validate_gif_bytes(data).first_error() {
None => "valid GIF stream"
Some(code) => "GIF validation failed: \{code}"
}
}
///|
/// Test whether a decoded animation can be re-encoded losslessly in shape.
pub fn can_roundtrip(data : Bytes) -> Bool {
match decode_checked(data) {
None => false
Some(animation) => encode_checked(animation, EncodeOptions::default()).ok()
}
}
///|
/// Return a transparent placeholder for an empty input canvas.
pub fn normalize_empty_canvas(canvas : Canvas) -> Canvas {
if canvas.width == 0 || canvas.height == 0 {
Canvas::new(width=1, height=1, fill=rgba(0, 0, 0, 0))
} else {
canvas
}
}