///|
/// Compatibility helpers for common GIF producer and consumer conventions.
pub(all) enum DisposalHint {
Preserve
ClearToBackground
RestorePrevious
} derive(Eq, Debug)
///|
pub fn DisposalHint::to_disposal(self : DisposalHint) -> Disposal {
match self {
Preserve => Keep
ClearToBackground => Background
RestorePrevious => Previous
}
}
///|
pub fn disposal_hint(disposal : Disposal) -> DisposalHint {
match disposal {
Keep => Preserve
Background => ClearToBackground
Previous => RestorePrevious
}
}
///|
/// Normalize a frame for viewers that reject zero-sized or negative offsets.
pub fn normalize_frame(
frame : Frame,
screen_width : Int,
screen_height : Int,
) -> Frame? {
let width = if screen_width < 1 { 1 } else { screen_width }
let height = if screen_height < 1 { 1 } else { screen_height }
let canvas = frame.canvas()
let left = if frame.left < 0 { 0 } else { frame.left }
let top = if frame.top < 0 { 0 } else { frame.top }
let available_width = width - left
let available_height = height - top
if canvas.width <= 0 ||
canvas.height <= 0 ||
available_width <= 0 ||
available_height <= 0 {
None
} else {
let output_width = if canvas.width > available_width {
available_width
} else {
canvas.width
}
let output_height = if canvas.height > available_height {
available_height
} else {
canvas.height
}
Some(
frame
.with_canvas(
canvas.crop(x=0, y=0, width=output_width, height=output_height),
)
.with_offset(left~, top~),
)
}
}
///|
/// Normalize all frames and preserve a useful placeholder when input is empty.
pub fn normalize_animation(animation : Animation) -> Animation {
let result = Animation::new(
width=if animation.width < 1 { 1 } else { animation.width },
height=if animation.height < 1 { 1 } else { animation.height },
)
.with_loop_count(loop_count=animation.loop_count)
.with_background(background=animation.background)
for frame in animation.frames {
match normalize_frame(frame, result.width, result.height) {
None => ()
Some(value) => ignore(result.add_frame(value))
}
}
if result.frame_count() == 0 {
ignore(
result.add_frame(
Frame::new(
canvas=Canvas::new(
width=result.width,
height=result.height,
fill=animation.background,
),
delay_cs=0,
),
),
)
}
result
}
///|
/// Construct a GIF89a-compatible animation with a safe default delay.
pub fn compatible_animation(
width : Int,
height : Int,
frames : ArrayView[Canvas],
delay_cs : Int,
) -> Animation {
let result = Animation::new(
width=if width < 1 { 1 } else { width },
height=if height < 1 { 1 } else { height },
)
let delay = if delay_cs < 0 {
0
} else if delay_cs > 65535 {
65535
} else {
delay_cs
}
for canvas in frames {
ignore(result.add_frame(Frame::new(canvas~, delay_cs=delay)))
}
normalize_animation(result)
}
///|
/// Return whether all frames use metadata accepted by the encoder.
pub fn Animation::uses_compatible_metadata(self : Animation) -> Bool {
for frame in self.frames {
let (left, top) = frame.offset()
if left < 0 ||
top < 0 ||
frame.canvas().width <= 0 ||
frame.canvas().height <= 0 {
return false
}
}
true
}
///|
/// Convert centiseconds to a display-friendly millisecond duration.
pub fn centiseconds_to_milliseconds(value : Int) -> Int {
if value < 0 {
0
} else {
value * 10
}
}
///|
/// Convert milliseconds to centiseconds using nearest integer rounding.
pub fn milliseconds_to_centiseconds(value : Int) -> Int {
if value < 0 {
0
} else {
(value + 5) / 10
}
}