use core::marker::PhantomData;
use crate::Effort;
use crate::container::anim::{AnmfFlags, AnmfHeader};
use crate::container::fourcc::FourCc;
use crate::container::vp8x::{Vp8xFlags, Vp8xInfo};
use crate::container::writer::{push_chunk, riff_envelope};
use crate::error::{Error, Result};
use crate::image::{self, Dimensions, ImageRef, Metadata, PixelLayout};
use crate::lossless::animation::{BlendMode, DisposalMode, FrameMeta};
use crate::lossless::prelude::*;
use crate::lossless::vp8l;
pub(crate) fn encode_payload(effort: Effort, width: u32, height: u32, argb: &[u32]) -> Vec<u8> {
match effort {
Effort::Fast => vp8l::encode::encode_with(width, height, argb, false),
Effort::Balanced => vp8l::encode::encode(width, height, argb),
Effort::Best => vp8l::encode::encode_best(width, height, argb),
}
}
pub use crate::image::MetadataPolicy;
#[derive(Clone, Debug, Default)]
pub struct EncoderConfig {
pub(crate) effort: Effort,
pub(crate) metadata: Metadata,
pub(crate) policy: MetadataPolicy,
}
impl EncoderConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_effort(mut self, effort: Effort) -> Self {
self.effort = effort;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub const fn with_metadata_policy(mut self, policy: MetadataPolicy) -> Self {
self.policy = policy;
self
}
pub(crate) fn resolve_metadata(&self, inherited: &Metadata) -> Metadata {
self.metadata.resolve(inherited, self.policy)
}
}
pub enum Empty {}
pub enum HasFrames {}
pub struct AnimationEncoder<S = Empty> {
canvas: Dimensions,
background: u32,
loop_count: u16,
effort: Effort,
frames: Vec<u8>,
frame_count: u32,
has_alpha: bool,
_state: PhantomData<S>,
}
impl<S> core::fmt::Debug for AnimationEncoder<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AnimationEncoder")
.field("canvas", &self.canvas)
.field("frame_count", &self.frame_count)
.field("loop_count", &self.loop_count)
.field("effort", &self.effort)
.finish_non_exhaustive()
}
}
impl AnimationEncoder<Empty> {
#[must_use]
pub const fn new(canvas: Dimensions) -> Self {
Self {
canvas,
background: 0,
loop_count: 0,
effort: Effort::Balanced,
frames: Vec::new(),
frame_count: 0,
has_alpha: false,
_state: PhantomData,
}
}
}
impl<S> AnimationEncoder<S> {
#[must_use]
pub const fn with_loop_count(mut self, loop_count: u16) -> Self {
self.loop_count = loop_count;
self
}
#[must_use]
pub const fn with_background(mut self, rgba: [u8; 4]) -> Self {
self.background = PixelLayout::Rgba8.unpack(rgba);
self
}
#[must_use]
pub const fn with_effort(mut self, effort: Effort) -> Self {
self.effort = effort;
self
}
#[must_use]
pub const fn frame_count(&self) -> u32 {
self.frame_count
}
pub fn add_frame(
self,
image: ImageRef<'_>,
meta: FrameMeta,
) -> Result<AnimationEncoder<HasFrames>> {
let dims = image.dimensions();
let fits = meta.x.is_multiple_of(2)
&& meta.y.is_multiple_of(2)
&& dims == meta.dimensions
&& meta.duration_ms < (1 << 24)
&& u64::from(meta.x) + u64::from(dims.width()) <= u64::from(self.canvas.width())
&& u64::from(meta.y) + u64::from(dims.height()) <= u64::from(self.canvas.height());
if !fits {
return Err(Error::InvalidDimensions);
}
let argb = image::unpack_pixels(image.layout(), image.as_bytes());
let frame_has_alpha = image::argb_has_alpha(&argb);
let payload = encode_payload(self.effort, dims.width(), dims.height(), &argb);
let flags = AnmfFlags::from_parts(
matches!(meta.blend, BlendMode::Overwrite),
matches!(meta.dispose, DisposalMode::Background),
);
let header = AnmfHeader {
x: meta.x,
y: meta.y,
dims,
duration_ms: meta.duration_ms,
flags,
};
let mut frame_body = header.build().to_vec();
push_chunk(&mut frame_body, FourCc::VP8L, &payload);
let mut frames = self.frames;
push_chunk(&mut frames, FourCc::ANMF, &frame_body);
Ok(AnimationEncoder {
canvas: self.canvas,
background: self.background,
loop_count: self.loop_count,
effort: self.effort,
frames,
frame_count: self.frame_count + 1,
has_alpha: self.has_alpha || frame_has_alpha,
_state: PhantomData,
})
}
}
impl AnimationEncoder<HasFrames> {
#[must_use]
pub fn finish(self) -> Vec<u8> {
let flags = Vp8xFlags::for_output(&Metadata::none(), self.has_alpha).with_animation();
let mut body = Vec::new();
push_chunk(
&mut body,
FourCc::VP8X,
&Vp8xInfo::build(flags, self.canvas),
);
push_chunk(
&mut body,
FourCc::ANIM,
&crate::container::anim::AnimChunk {
background: self.background,
loop_count: self.loop_count,
}
.build(),
);
body.extend_from_slice(&self.frames);
riff_envelope(&body)
}
#[cfg(feature = "std")]
pub fn finish_to<W: std::io::Write>(self, mut writer: W) -> Result<()> {
writer.write_all(&self.finish())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{AnimationEncoder, EncoderConfig, MetadataPolicy, encode_payload};
use crate::Effort;
use crate::error::Error;
use crate::image::{self, Dimensions, ImageRef, Metadata, PixelLayout};
use crate::lossless::animation::{BlendMode, DisposalMode, FrameMeta, decode_frames};
#[test]
fn default_is_balanced_no_metadata() {
let config = EncoderConfig::new();
assert_eq!(config.effort, Effort::Balanced);
assert!(config.metadata.is_empty());
}
#[test]
fn effort_routes_to_the_expected_encoder() {
let argb = [0xff10_2030u32, 0xff40_5060, 0xff70_8090, 0xffa0_b0c0];
assert_eq!(
encode_payload(Effort::Fast, 2, 2, &argb),
crate::lossless::vp8l::encode::encode_with(2, 2, &argb, false),
);
assert_eq!(
encode_payload(Effort::Balanced, 2, 2, &argb),
crate::lossless::vp8l::encode::encode(2, 2, &argb),
);
assert_eq!(
encode_payload(Effort::Best, 2, 2, &argb),
crate::lossless::vp8l::encode::encode_best(2, 2, &argb),
);
assert!(
encode_payload(Effort::Best, 2, 2, &argb).len()
<= encode_payload(Effort::Balanced, 2, 2, &argb).len()
);
}
#[test]
fn builder_sets_fields() {
let config = EncoderConfig::new()
.with_effort(Effort::Fast)
.with_metadata(Metadata {
exif: Some(vec![1, 2, 3]),
..Metadata::none()
});
assert_eq!(config.effort, Effort::Fast);
assert_eq!(config.metadata.exif.as_deref(), Some(&[1, 2, 3][..]));
}
#[test]
fn default_policy_is_preserve() {
assert_eq!(EncoderConfig::new().policy, MetadataPolicy::Preserve);
}
#[test]
fn resolve_metadata_delegates_to_core() {
let inherited = Metadata {
icc_profile: Some(vec![1]),
exif: Some(vec![2]),
xmp: Some(vec![3]),
};
let override_meta = Metadata {
exif: Some(vec![9]),
..Metadata::none()
};
let config = EncoderConfig::new()
.with_metadata(override_meta.clone())
.with_metadata_policy(MetadataPolicy::StripPrivate);
assert_eq!(
config.resolve_metadata(&inherited),
override_meta.resolve(&inherited, MetadataPolicy::StripPrivate),
);
}
fn meta(dims: Dimensions, x: u32, y: u32, duration: u32) -> FrameMeta {
FrameMeta {
x,
y,
dimensions: dims,
duration_ms: duration,
blend: BlendMode::Blend,
dispose: DisposalMode::Keep,
}
}
fn frame_bytes(dims: Dimensions, argb: u32) -> Vec<u8> {
let pixels = vec![argb; usize::try_from(dims.pixel_count()).unwrap()];
image::pack_pixels(PixelLayout::Rgba8, &pixels)
}
#[test]
fn animation_round_trips_through_decode_frames() {
let canvas = Dimensions::new(4, 4).unwrap();
let rgba0 = frame_bytes(canvas, 0xFF00_00FF);
let rgba1 = frame_bytes(canvas, 0xFF00_FF00);
let file = AnimationEncoder::new(canvas)
.with_loop_count(3)
.with_background([1, 2, 3, 255])
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &rgba0).unwrap(),
meta(canvas, 0, 0, 100),
)
.unwrap()
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &rgba1).unwrap(),
meta(canvas, 0, 0, 50),
)
.unwrap()
.finish();
let frames = decode_frames(&file).unwrap();
let info = frames.anim_info();
assert_eq!(info.canvas, canvas);
assert_eq!(info.loop_count, 3);
assert_eq!(info.background_rgba, [1, 2, 3, 255]);
let decoded: Vec<_> = frames.map(Result::unwrap).collect();
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].image().as_bytes(), rgba0);
assert_eq!(decoded[0].meta().duration_ms, 100);
assert_eq!(decoded[1].image().as_bytes(), rgba1);
assert_eq!(decoded[1].meta().duration_ms, 50);
}
#[test]
fn frame_count_tracks_added_frames() {
let canvas = Dimensions::new(2, 2).unwrap();
let rgba = frame_bytes(canvas, 0xFF00_0000);
let enc = AnimationEncoder::new(canvas);
assert_eq!(enc.frame_count(), 0);
let enc = enc
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &rgba).unwrap(),
meta(canvas, 0, 0, 40),
)
.unwrap();
assert_eq!(enc.frame_count(), 1);
}
#[test]
fn add_frame_rejects_odd_offset() {
let canvas = Dimensions::new(8, 8).unwrap();
let tile = Dimensions::new(2, 2).unwrap();
let rgba = frame_bytes(tile, 0xFF00_0000);
let err = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(tile, PixelLayout::Rgba8, &rgba).unwrap(),
meta(tile, 1, 0, 40), )
.unwrap_err();
assert_eq!(err, Error::InvalidDimensions);
}
#[test]
fn add_frame_rejects_out_of_bounds() {
let canvas = Dimensions::new(4, 4).unwrap();
let tile = Dimensions::new(4, 4).unwrap();
let rgba = frame_bytes(tile, 0xFF00_0000);
let err = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(tile, PixelLayout::Rgba8, &rgba).unwrap(),
meta(tile, 2, 2, 40), )
.unwrap_err();
assert_eq!(err, Error::InvalidDimensions);
}
#[test]
fn add_frame_rejects_dims_mismatch_and_huge_duration() {
let canvas = Dimensions::new(4, 4).unwrap();
let two = Dimensions::new(2, 2).unwrap();
let rgba = frame_bytes(two, 0xFF00_0000);
let err = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(two, PixelLayout::Rgba8, &rgba).unwrap(),
meta(canvas, 0, 0, 40),
)
.unwrap_err();
assert_eq!(err, Error::InvalidDimensions);
let err = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(two, PixelLayout::Rgba8, &rgba).unwrap(),
meta(two, 0, 0, 1 << 24),
)
.unwrap_err();
assert_eq!(err, Error::InvalidDimensions);
}
#[test]
fn debug_impl_renders_the_struct_fields() {
let canvas = Dimensions::new(4, 4).unwrap();
let enc = AnimationEncoder::new(canvas)
.with_loop_count(7)
.with_effort(Effort::Fast);
let rendered = format!("{enc:?}");
assert!(rendered.contains("AnimationEncoder"), "got: {rendered}");
assert!(rendered.contains("canvas"), "got: {rendered}");
assert!(rendered.contains("frame_count"), "got: {rendered}");
assert!(rendered.contains("loop_count: 7"), "got: {rendered}");
assert!(rendered.contains("Fast"), "got: {rendered}");
}
#[test]
fn add_frame_x_bound_uses_sum_not_product() {
let canvas = Dimensions::new(6, 2).unwrap();
let tile = Dimensions::new(4, 2).unwrap();
let rgba = frame_bytes(tile, 0xFF00_0000);
let enc = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(tile, PixelLayout::Rgba8, &rgba).unwrap(),
meta(tile, 2, 0, 40),
)
.expect("x+width == canvas width must fit");
assert_eq!(enc.frame_count(), 1);
}
#[test]
fn add_frame_y_bound_uses_sum_not_product() {
let canvas = Dimensions::new(2, 6).unwrap();
let tile = Dimensions::new(2, 4).unwrap();
let rgba = frame_bytes(tile, 0xFF00_0000);
let enc = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(tile, PixelLayout::Rgba8, &rgba).unwrap(),
meta(tile, 0, 2, 40),
)
.expect("y+height == canvas height must fit");
assert_eq!(enc.frame_count(), 1);
}
#[test]
fn has_alpha_flag_is_the_or_of_frames() {
let canvas = Dimensions::new(2, 2).unwrap();
let translucent = frame_bytes(canvas, 0x8000_00FF); let file = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &translucent).unwrap(),
meta(canvas, 0, 0, 40),
)
.unwrap()
.finish();
assert_eq!(&file[12..16], b"VP8X");
assert_ne!(
file[20] & 0x10,
0,
"ALPHA flag must be set for a translucent frame"
);
let opaque = frame_bytes(canvas, 0xFF00_00FF);
let file2 = AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &opaque).unwrap(),
meta(canvas, 0, 0, 40),
)
.unwrap()
.finish();
assert_eq!(
file2[20] & 0x10,
0,
"ALPHA flag must be clear for an opaque frame"
);
}
#[cfg(feature = "std")]
#[test]
fn finish_to_writes_the_same_bytes_as_finish() {
let canvas = Dimensions::new(2, 2).unwrap();
let rgba = frame_bytes(canvas, 0xFF00_00FF);
let make = || {
AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &rgba).unwrap(),
meta(canvas, 0, 0, 40),
)
.unwrap()
};
let expected = make().finish();
assert!(!expected.is_empty());
let mut buf = Vec::new();
make().finish_to(&mut buf).unwrap();
assert_eq!(buf, expected);
}
}