use alloc::vec::Vec;
use super::assemble::{MuxFrame, WebPMux};
use super::demux::{BlendMethod, DisposeMethod};
use super::error::{MuxError, MuxResult};
use crate::decoder::LoopCount;
use crate::encoder::vp8::encode_frame_lossy;
use crate::encoder::{
EncoderConfig, EncoderParams, NoProgress, PixelLayout, encode_alpha_lossless,
encode_frame_lossless,
};
use enough::Unstoppable;
#[derive(Debug, Clone)]
pub struct AnimationConfig {
pub background_color: [u8; 4],
pub loop_count: LoopCount,
pub minimize_size: bool,
}
impl Default for AnimationConfig {
fn default() -> Self {
Self {
background_color: [0, 0, 0, 0],
loop_count: LoopCount::Forever,
minimize_size: true,
}
}
}
struct PendingFrame {
mux_frame: MuxFrame,
timestamp_ms: u32,
}
pub struct AnimationEncoder {
width: u32,
height: u32,
mux: WebPMux,
pending: Option<PendingFrame>,
minimize_size: bool,
prev_canvas: Option<Vec<u8>>,
}
impl AnimationEncoder {
#[track_caller]
pub fn new(width: u32, height: u32, config: AnimationConfig) -> MuxResult<Self> {
if width == 0 || height == 0 || width > 16384 || height > 16384 {
return Err(whereat::at!(MuxError::InvalidDimensions { width, height }));
}
let mut mux = WebPMux::new(width, height);
mux.set_animation(config.background_color, config.loop_count);
Ok(Self {
width,
height,
mux,
pending: None,
minimize_size: config.minimize_size,
prev_canvas: None,
})
}
#[allow(clippy::too_many_arguments)]
fn push_encoded_frame(
&mut self,
encoded: EncodedFrame,
frame_width: u32,
frame_height: u32,
x_offset: u32,
y_offset: u32,
timestamp_ms: u32,
dispose: DisposeMethod,
blend: BlendMethod,
) -> Result<(), MuxError> {
if let Some(prev) = self.pending.take() {
let duration = timestamp_ms.saturating_sub(prev.timestamp_ms);
let mut frame = prev.mux_frame;
frame.duration_ms = duration;
self.mux.push_frame(frame).map_err(|e| e.decompose().0)?;
}
let mux_frame = MuxFrame {
x_offset,
y_offset,
width: frame_width,
height: frame_height,
duration_ms: 0, dispose,
blend,
bitstream: encoded.bitstream,
alpha_data: encoded.alpha_data,
is_lossless: encoded.is_lossless,
};
self.pending = Some(PendingFrame {
mux_frame,
timestamp_ms,
});
Ok(())
}
#[track_caller]
pub fn add_frame(
&mut self,
pixels: &[u8],
color_type: PixelLayout,
timestamp_ms: u32,
encoder_config: &EncoderConfig,
) -> MuxResult<()> {
if !self.minimize_size || color_type == PixelLayout::Yuv420 {
let result = self.add_frame_advanced(
pixels,
color_type,
self.width,
self.height,
0,
0,
timestamp_ms,
encoder_config,
DisposeMethod::None,
BlendMethod::Overwrite,
);
if color_type == PixelLayout::Yuv420 {
self.prev_canvas = None;
}
return result;
}
let rgba = to_rgba(pixels, color_type, self.width, self.height)
.expect("non-YUV420 color types always convert to RGBA");
let params = encoder_config.to_params();
if let Some(ref prev) = self.prev_canvas {
match find_diff_rect(prev, &rgba, self.width, self.height) {
None => {
let one_pixel: &[u8] = if color_type.has_alpha() {
&rgba[..4]
} else {
&rgba[..3]
};
let sub_color = if color_type.has_alpha() {
PixelLayout::Rgba8
} else {
PixelLayout::Rgb8
};
let encoded = encode_frame_data(one_pixel, 1, 1, sub_color, ¶ms)?;
self.push_encoded_frame(
encoded,
1,
1,
0,
0,
timestamp_ms,
DisposeMethod::None,
BlendMethod::Overwrite,
)?;
}
Some((x, y, w, h)) => {
let include_alpha = color_type.has_alpha();
let sub_pixels = extract_sub_rect(&rgba, self.width, x, y, w, h, include_alpha);
let sub_color = if include_alpha {
PixelLayout::Rgba8
} else {
PixelLayout::Rgb8
};
let encoded = encode_frame_data(&sub_pixels, w, h, sub_color, ¶ms)?;
self.push_encoded_frame(
encoded,
w,
h,
x,
y,
timestamp_ms,
DisposeMethod::None,
BlendMethod::Overwrite,
)?;
}
}
} else {
let encoded = encode_frame_data(pixels, self.width, self.height, color_type, ¶ms)?;
self.push_encoded_frame(
encoded,
self.width,
self.height,
0,
0,
timestamp_ms,
DisposeMethod::None,
BlendMethod::Overwrite,
)?;
}
self.prev_canvas = Some(rgba);
Ok(())
}
#[track_caller]
#[allow(clippy::too_many_arguments)]
pub fn add_frame_advanced(
&mut self,
pixels: &[u8],
color_type: PixelLayout,
frame_width: u32,
frame_height: u32,
x_offset: u32,
y_offset: u32,
timestamp_ms: u32,
encoder_config: &EncoderConfig,
dispose: DisposeMethod,
blend: BlendMethod,
) -> MuxResult<()> {
let params = encoder_config.to_params();
let encoded = encode_frame_data(pixels, frame_width, frame_height, color_type, ¶ms)?;
self.push_encoded_frame(
encoded,
frame_width,
frame_height,
x_offset,
y_offset,
timestamp_ms,
dispose,
blend,
)?;
self.prev_canvas = None;
Ok(())
}
#[track_caller]
pub fn finalize(mut self, last_frame_duration_ms: u32) -> MuxResult<Vec<u8>> {
if let Some(prev) = self.pending.take() {
let mut frame = prev.mux_frame;
frame.duration_ms = last_frame_duration_ms;
self.mux.push_frame(frame)?;
}
self.mux.downgrade_single_frame_to_static();
self.mux.assemble()
}
pub fn icc_profile(&mut self, data: Vec<u8>) {
self.mux.set_icc_profile(data);
}
pub fn exif(&mut self, data: Vec<u8>) {
self.mux.set_exif(data);
}
pub fn xmp(&mut self, data: Vec<u8>) {
self.mux.set_xmp(data);
}
}
fn to_rgba(pixels: &[u8], color_type: PixelLayout, width: u32, height: u32) -> Option<Vec<u8>> {
let npixels = (width as usize) * (height as usize);
let mut rgba = alloc::vec![0u8; npixels * 4];
match color_type {
PixelLayout::L8 => {
garb::bytes::gray_to_rgba(&pixels[..npixels], &mut rgba).ok()?;
Some(rgba)
}
PixelLayout::La8 => {
garb::bytes::gray_alpha_to_rgba(&pixels[..npixels * 2], &mut rgba).ok()?;
Some(rgba)
}
PixelLayout::Rgb8 => {
garb::bytes::rgb_to_rgba(&pixels[..npixels * 3], &mut rgba).ok()?;
Some(rgba)
}
PixelLayout::Rgba8 => Some(pixels[..npixels * 4].to_vec()),
PixelLayout::Bgr8 => {
garb::bytes::bgr_to_rgba(&pixels[..npixels * 3], &mut rgba).ok()?;
Some(rgba)
}
PixelLayout::Bgra8 => {
garb::bytes::bgra_to_rgba(&pixels[..npixels * 4], &mut rgba).ok()?;
Some(rgba)
}
PixelLayout::Argb8 => {
garb::bytes::argb_to_rgba(&pixels[..npixels * 4], &mut rgba).ok()?;
Some(rgba)
}
PixelLayout::Yuv420 => None,
}
}
fn find_diff_rect(
prev: &[u8],
curr: &[u8],
width: u32,
height: u32,
) -> Option<(u32, u32, u32, u32)> {
let w = width as usize;
let h = height as usize;
let mut min_x = w;
let mut max_x = 0usize;
let mut min_y = h;
let mut max_y = 0usize;
for y in 0..h {
let row_off = y * w * 4;
for x in 0..w {
let off = row_off + x * 4;
if prev[off..off + 4] != curr[off..off + 4] {
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
}
}
if min_x > max_x {
return None;
}
let x0 = (min_x as u32) & !1;
let y0 = (min_y as u32) & !1;
let x1 = ((max_x as u32 + 2) & !1).min(width);
let y1 = ((max_y as u32 + 2) & !1).min(height);
Some((x0, y0, x1 - x0, y1 - y0))
}
fn extract_sub_rect(
rgba: &[u8],
canvas_width: u32,
x: u32,
y: u32,
w: u32,
h: u32,
include_alpha: bool,
) -> Vec<u8> {
let cw = canvas_width as usize;
let bpp = if include_alpha { 4 } else { 3 };
let mut out = Vec::with_capacity((w as usize) * (h as usize) * bpp);
for row in 0..h as usize {
let src_y = y as usize + row;
let src_off = (src_y * cw + x as usize) * 4;
for col in 0..w as usize {
let off = src_off + col * 4;
out.push(rgba[off]);
out.push(rgba[off + 1]);
out.push(rgba[off + 2]);
if include_alpha {
out.push(rgba[off + 3]);
}
}
}
out
}
struct EncodedFrame {
bitstream: Vec<u8>,
alpha_data: Option<Vec<u8>>,
is_lossless: bool,
}
fn encode_frame_data(
pixels: &[u8],
width: u32,
height: u32,
color: PixelLayout,
params: &EncoderParams,
) -> Result<EncodedFrame, MuxError> {
let mut bitstream = Vec::new();
let lossy_with_alpha = params.use_lossy && color.has_alpha();
let stride = width as usize;
if params.use_lossy {
encode_frame_lossy(
&mut bitstream,
pixels,
width,
height,
stride,
color,
params,
&Unstoppable,
&NoProgress,
)?;
let alpha_data = if lossy_with_alpha {
let mut alpha = Vec::new();
encode_alpha_lossless(
&mut alpha,
pixels,
width,
height,
stride,
color,
params.alpha_quality,
&Unstoppable,
)?;
Some(alpha)
} else {
None
};
Ok(EncodedFrame {
bitstream,
alpha_data,
is_lossless: false,
})
} else {
encode_frame_lossless(
&mut bitstream,
pixels,
width,
height,
stride,
color,
params.clone(),
false,
&Unstoppable,
)?;
Ok(EncodedFrame {
bitstream,
alpha_data: None,
is_lossless: true,
})
}
}