use crate::error::{Result, WebpError as Error};
use crate::riff::WebpMetadata;
use crate::vp8l::encode_vp8l_argb;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum AnimFrameMode {
Lossless,
Lossy,
#[default]
Auto,
Delta(DeltaConfig),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DownsampleKernel {
Box,
Gaussian,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DeltaConfig {
pub block_size: u32,
pub threshold: u32,
pub max_bbox_fraction: f32,
pub max_components_override: Option<u32>,
pub auto_inner_threshold_bytes: Option<u32>,
pub auto_inner_quality: f32,
pub enable_ssim_cost: bool,
pub ssim_threshold: u32,
pub enable_msssim_cost: bool,
pub msssim_threshold: u32,
pub msssim_downsample_kernel: DownsampleKernel,
}
impl Default for DeltaConfig {
fn default() -> Self {
Self {
block_size: 8,
threshold: 32,
max_bbox_fraction: 0.8,
max_components_override: None,
auto_inner_threshold_bytes: Some(4096),
auto_inner_quality: 75.0,
enable_ssim_cost: false,
ssim_threshold: 50,
enable_msssim_cost: false,
msssim_threshold: 50,
msssim_downsample_kernel: DownsampleKernel::Box,
}
}
}
impl DeltaConfig {
#[must_use]
pub fn max_components_override(mut self, n: u32) -> Self {
self.max_components_override = Some(n);
self
}
#[must_use]
pub fn auto_inner_threshold_bytes(mut self, bytes: Option<u32>) -> Self {
self.auto_inner_threshold_bytes = bytes;
self
}
#[must_use]
pub fn enable_ssim_cost(mut self, on: bool) -> Self {
self.enable_ssim_cost = on;
self
}
#[must_use]
pub fn ssim_threshold(mut self, t: u32) -> Self {
self.ssim_threshold = t;
self
}
#[must_use]
pub fn enable_msssim_cost(mut self, on: bool) -> Self {
self.enable_msssim_cost = on;
self
}
#[must_use]
pub fn msssim_threshold(mut self, t: u32) -> Self {
self.msssim_threshold = t;
self
}
#[must_use]
pub fn msssim_downsample_kernel(mut self, kernel: DownsampleKernel) -> Self {
self.msssim_downsample_kernel = kernel;
self
}
}
fn adaptive_max_components(density: f32) -> u32 {
let d = density.clamp(0.0, 1.0);
const LO_DENSITY: f32 = 0.05;
const HI_DENSITY: f32 = 0.30;
const LO_BUDGET: f32 = 16.0;
const HI_BUDGET: f32 = 4.0;
if d <= LO_DENSITY {
return LO_BUDGET as u32;
}
if d >= HI_DENSITY {
return HI_BUDGET as u32;
}
let t = (d - LO_DENSITY) / (HI_DENSITY - LO_DENSITY);
let budget = LO_BUDGET + t * (HI_BUDGET - LO_BUDGET);
budget.round() as u32
}
#[doc(hidden)]
pub fn debug_adaptive_max_components(density: f32) -> u32 {
adaptive_max_components(density)
}
#[doc(hidden)]
pub fn debug_cluster_density(
prev: &[u8],
curr: &[u8],
canvas_w: u32,
canvas_h: u32,
cfg: &DeltaConfig,
) -> f32 {
let max_pixels = (canvas_w as u64).saturating_mul(canvas_h as u64);
if max_pixels == 0 {
return 0.0;
}
let (_, raw_pixels) =
changed_block_components_with_density(prev, curr, canvas_w, canvas_h, cfg, u32::MAX);
(raw_pixels as f32) / (max_pixels as f32)
}
#[derive(Clone, Debug)]
pub struct AnimEncoderOptions<'a> {
pub mode: AnimFrameMode,
pub lossy_quality: f32,
pub metadata: WebpMetadata<'a>,
}
impl<'a> Default for AnimEncoderOptions<'a> {
fn default() -> Self {
Self {
mode: AnimFrameMode::default(),
lossy_quality: 75.0,
metadata: WebpMetadata::default(),
}
}
}
#[derive(Clone)]
pub struct AnimFrame<'a> {
pub width: u32,
pub height: u32,
pub x_offset: u32,
pub y_offset: u32,
pub duration_ms: u32,
pub blend: bool,
pub dispose_to_background: bool,
pub rgba: &'a [u8],
}
pub fn build_animated_webp(
canvas_w: u32,
canvas_h: u32,
background_bgra: [u8; 4],
loop_count: u16,
frames: &[AnimFrame<'_>],
) -> Result<Vec<u8>> {
build_animated_webp_with_options(
canvas_w,
canvas_h,
background_bgra,
loop_count,
frames,
AnimEncoderOptions {
mode: AnimFrameMode::Lossless,
..AnimEncoderOptions::default()
},
)
}
pub fn build_animated_webp_with_options(
canvas_w: u32,
canvas_h: u32,
background_bgra: [u8; 4],
loop_count: u16,
frames: &[AnimFrame<'_>],
options: AnimEncoderOptions<'_>,
) -> Result<Vec<u8>> {
if canvas_w == 0 || canvas_h == 0 {
return Err(Error::invalid("animated WebP: zero canvas size"));
}
if canvas_w > 16384 || canvas_h > 16384 {
return Err(Error::invalid("animated WebP: canvas exceeds 16384 px"));
}
if frames.is_empty() {
return Err(Error::invalid("animated WebP: needs at least one frame"));
}
if let AnimFrameMode::Delta(cfg) = options.mode {
return build_animated_webp_delta(
canvas_w,
canvas_h,
background_bgra,
loop_count,
frames,
&options,
cfg,
);
}
let mut any_frame_has_alpha = false;
let mut anmf_payloads: Vec<Vec<u8>> = Vec::with_capacity(frames.len());
for f in frames {
if f.width == 0 || f.height == 0 {
return Err(Error::invalid("animated WebP: zero frame size"));
}
if f.x_offset
.checked_add(f.width)
.map(|r| r > canvas_w)
.unwrap_or(true)
|| f.y_offset
.checked_add(f.height)
.map(|r| r > canvas_h)
.unwrap_or(true)
{
return Err(Error::invalid(
"animated WebP: frame bbox extends past canvas",
));
}
if f.rgba.len() != (f.width as usize) * (f.height as usize) * 4 {
return Err(Error::invalid(
"animated WebP: frame rgba length mismatch frame_w*frame_h*4",
));
}
if f.duration_ms > 0x00FF_FFFF {
return Err(Error::invalid(
"animated WebP: duration_ms exceeds 24-bit field",
));
}
if !any_frame_has_alpha && f.rgba.chunks_exact(4).any(|px| px[3] != 0xff) {
any_frame_has_alpha = true;
}
let chosen = encode_one_anmf_image(f, &options)?;
let nested_capacity = chosen.iter().map(|c| 8 + c.payload.len()).sum::<usize>();
let mut payload = Vec::with_capacity(16 + nested_capacity);
write_u24_le(&mut payload, (f.x_offset / 2) & 0x00FF_FFFF);
write_u24_le(&mut payload, (f.y_offset / 2) & 0x00FF_FFFF);
write_u24_le(&mut payload, (f.width - 1) & 0x00FF_FFFF);
write_u24_le(&mut payload, (f.height - 1) & 0x00FF_FFFF);
write_u24_le(&mut payload, f.duration_ms & 0x00FF_FFFF);
let mut flags: u8 = 0;
if !f.blend {
flags |= 0x01;
}
if f.dispose_to_background {
flags |= 0x02;
}
payload.push(flags);
for sub in &chosen {
write_chunk(&mut payload, &sub.fourcc, &sub.payload);
}
anmf_payloads.push(payload);
}
let mut body: Vec<u8> = Vec::new();
let mut flags: u8 = 0x02; if any_frame_has_alpha {
flags |= 0x10;
}
if options.metadata.icc.is_some() {
flags |= 0x20;
}
if options.metadata.exif.is_some() {
flags |= 0x08;
}
if options.metadata.xmp.is_some() {
flags |= 0x04;
}
let vp8x = vp8x_payload(flags, canvas_w, canvas_h);
write_chunk(&mut body, b"VP8X", &vp8x);
if let Some(icc) = options.metadata.icc {
write_chunk(&mut body, b"ICCP", icc);
}
let mut anim = [0u8; 6];
anim[0] = background_bgra[0];
anim[1] = background_bgra[1];
anim[2] = background_bgra[2];
anim[3] = background_bgra[3];
anim[4] = (loop_count & 0xff) as u8;
anim[5] = ((loop_count >> 8) & 0xff) as u8;
write_chunk(&mut body, b"ANIM", &anim);
for payload in &anmf_payloads {
write_chunk(&mut body, b"ANMF", payload);
}
if let Some(exif) = options.metadata.exif {
write_chunk(&mut body, b"EXIF", exif);
}
if let Some(xmp) = options.metadata.xmp {
write_chunk(&mut body, b"XMP ", xmp);
}
let riff_size = 4 + body.len();
let mut out = Vec::with_capacity(8 + riff_size);
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(riff_size as u32).to_le_bytes());
out.extend_from_slice(b"WEBP");
out.extend_from_slice(&body);
Ok(out)
}
struct AnmfSubChunk {
fourcc: [u8; 4],
payload: Vec<u8>,
}
fn encode_one_anmf_image(
f: &AnimFrame<'_>,
options: &AnimEncoderOptions<'_>,
) -> Result<Vec<AnmfSubChunk>> {
let lossless: Option<Vec<AnmfSubChunk>> = match options.mode {
AnimFrameMode::Lossy => None,
AnimFrameMode::Lossless | AnimFrameMode::Auto | AnimFrameMode::Delta(_) => {
Some(encode_lossless_anmf(f)?)
}
};
let lossy: Option<Vec<AnmfSubChunk>> = match options.mode {
AnimFrameMode::Lossless => None,
AnimFrameMode::Lossy | AnimFrameMode::Auto | AnimFrameMode::Delta(_) => {
encode_lossy_anmf(f, options.lossy_quality)?
}
};
match (lossless, lossy) {
(None, None) => unreachable!("at least one mode must produce a candidate"),
(Some(l), None) => Ok(l),
(None, Some(l)) => Ok(l),
(Some(ll), Some(ly)) => {
let cost = |subs: &[AnmfSubChunk]| -> usize {
subs.iter()
.map(|s| 8 + s.payload.len() + (s.payload.len() & 1))
.sum()
};
if cost(&ly) < cost(&ll) {
Ok(ly)
} else {
Ok(ll)
}
}
}
}
fn encode_lossless_anmf(f: &AnimFrame<'_>) -> Result<Vec<AnmfSubChunk>> {
let mut pixels = Vec::with_capacity((f.width as usize) * (f.height as usize));
let mut has_alpha = false;
for px in f.rgba.chunks_exact(4) {
let r = px[0] as u32;
let g = px[1] as u32;
let b = px[2] as u32;
let a = px[3] as u32;
if a != 0xff {
has_alpha = true;
}
pixels.push((a << 24) | (r << 16) | (g << 8) | b);
}
let vp8l_bytes = encode_vp8l_argb(f.width, f.height, &pixels, has_alpha)?;
Ok(vec![AnmfSubChunk {
fourcc: *b"VP8L",
payload: vp8l_bytes,
}])
}
fn encode_lossy_anmf(f: &AnimFrame<'_>, quality: f32) -> Result<Option<Vec<AnmfSubChunk>>> {
if f.width == 0 || f.height == 0 {
return Ok(None);
}
let qindex = crate::encoder_vp8::quality_to_qindex(quality);
let w = f.width as usize;
let h = f.height as usize;
let mut alpha_plane: Vec<u8> = Vec::with_capacity(w * h);
let (y_plane, u_plane, v_plane) =
crate::encoder_vp8::rgba_rows_to_yuv420(w, h, w * 4, f.rgba, &mut alpha_plane);
let has_alpha = alpha_plane.iter().any(|&a| a != 0xff);
let vp8_frame = oxideav_vp8::Vp8Frame {
width: f.width,
height: f.height,
pts: None,
y: y_plane,
u: u_plane,
v: v_plane,
y_stride: f.width,
uv_stride: (f.width + 1) / 2,
};
let vp8_bytes =
match oxideav_vp8::encoder::encode_vp8_keyframe(f.width, f.height, qindex, &vp8_frame) {
Ok(b) => b,
Err(_) => return Ok(None),
};
let mut subs: Vec<AnmfSubChunk> = Vec::with_capacity(2);
if has_alpha {
let alph = crate::encoder_vp8::encode_alph_chunk(f.width, f.height, &alpha_plane)
.map_err(|e| Error::invalid(format!("animated WebP: ALPH encode: {e}")))?;
let mut alph_payload = Vec::with_capacity(1 + alph.payload.len());
alph_payload.push(alph.header_byte);
alph_payload.extend_from_slice(&alph.payload);
subs.push(AnmfSubChunk {
fourcc: *b"ALPH",
payload: alph_payload,
});
}
subs.push(AnmfSubChunk {
fourcc: *b"VP8 ",
payload: vp8_bytes,
});
Ok(Some(subs))
}
fn build_animated_webp_delta(
canvas_w: u32,
canvas_h: u32,
background_bgra: [u8; 4],
loop_count: u16,
frames: &[AnimFrame<'_>],
options: &AnimEncoderOptions<'_>,
cfg: DeltaConfig,
) -> Result<Vec<u8>> {
if cfg.block_size == 0 {
return Err(Error::invalid(
"animated WebP delta: block_size must be ≥ 1",
));
}
if !(cfg.max_bbox_fraction >= 0.0 && cfg.max_bbox_fraction <= 1.0) {
return Err(Error::invalid(
"animated WebP delta: max_bbox_fraction must be in [0.0, 1.0]",
));
}
if let Some(n) = cfg.max_components_override {
if n == 0 {
return Err(Error::invalid(
"animated WebP delta: max_components_override must be ≥ 1",
));
}
}
if let Some(t) = cfg.auto_inner_threshold_bytes {
if t == 0 {
return Err(Error::invalid(
"animated WebP delta: auto_inner_threshold_bytes must be ≥ 1",
));
}
}
if cfg.auto_inner_threshold_bytes.is_some()
&& !(cfg.auto_inner_quality >= 0.0 && cfg.auto_inner_quality <= 100.0)
{
return Err(Error::invalid(
"animated WebP delta: auto_inner_quality must be in [0.0, 100.0]",
));
}
for (i, f) in frames.iter().enumerate() {
if f.width != canvas_w || f.height != canvas_h {
return Err(Error::invalid(format!(
"animated WebP delta: frame {i} must be canvas-sized ({canvas_w}x{canvas_h}), got {}x{}",
f.width, f.height
)));
}
if f.x_offset != 0 || f.y_offset != 0 {
return Err(Error::invalid(format!(
"animated WebP delta: frame {i} must be at origin (0,0), got ({},{})",
f.x_offset, f.y_offset
)));
}
if f.blend {
return Err(Error::invalid(format!(
"animated WebP delta: frame {i} must have blend=false (Delta mode forces overwrite)"
)));
}
if f.dispose_to_background {
return Err(Error::invalid(format!(
"animated WebP delta: frame {i} must have dispose_to_background=false (would invalidate the prior-canvas reference)"
)));
}
if f.duration_ms > 0x00FF_FFFF {
return Err(Error::invalid(
"animated WebP delta: duration_ms exceeds 24-bit field",
));
}
if f.rgba.len() != (f.width as usize) * (f.height as usize) * 4 {
return Err(Error::invalid(
"animated WebP delta: frame rgba length mismatch frame_w*frame_h*4",
));
}
}
let max_pixels = (canvas_w as u64).saturating_mul(canvas_h as u64);
let max_bbox_pixels = ((max_pixels as f64) * (cfg.max_bbox_fraction as f64)) as u64;
let mut tile_storage: Vec<Vec<u8>> = Vec::with_capacity(frames.len());
struct PlannedFrame {
x_offset: u32,
y_offset: u32,
width: u32,
height: u32,
duration_ms: u32,
blend: bool,
rgba_kind: RgbaKind,
rgba_idx: usize,
}
enum RgbaKind {
Full,
Tile,
}
let mut planned: Vec<PlannedFrame> = Vec::with_capacity(frames.len());
for (i, f) in frames.iter().enumerate() {
if i == 0 {
planned.push(PlannedFrame {
x_offset: 0,
y_offset: 0,
width: f.width,
height: f.height,
duration_ms: f.duration_ms,
blend: f.blend,
rgba_kind: RgbaKind::Full,
rgba_idx: i,
});
continue;
}
let prior = &frames[i - 1];
let budget = match cfg.max_components_override {
Some(n) => n,
None => {
let (_, raw_pixels) = changed_block_components_with_density(
prior.rgba,
f.rgba,
canvas_w,
canvas_h,
&cfg,
u32::MAX,
);
let density = if max_pixels == 0 {
0.0
} else {
(raw_pixels as f32) / (max_pixels as f32)
};
adaptive_max_components(density)
}
};
let components =
changed_block_components(prior.rgba, f.rgba, canvas_w, canvas_h, &cfg, budget);
if components.is_empty() {
let p0 = &prior.rgba[..4];
let mut tile = Vec::with_capacity(4);
tile.extend_from_slice(p0);
let tile_idx = tile_storage.len();
tile_storage.push(tile);
planned.push(PlannedFrame {
x_offset: 0,
y_offset: 0,
width: 1,
height: 1,
duration_ms: f.duration_ms,
blend: false, rgba_kind: RgbaKind::Tile,
rgba_idx: tile_idx,
});
continue;
}
let union_bbox = bbox_union(&components);
let union_pixels = (union_bbox.2 as u64) * (union_bbox.3 as u64);
let multi_pixels: u64 = components
.iter()
.map(|&(_, _, w, h)| (w as u64) * (h as u64))
.sum();
let use_multi = components.len() > 1
&& multi_pixels.saturating_mul(4) < union_pixels.saturating_mul(3)
&& multi_pixels <= max_bbox_pixels;
if !use_multi {
if union_pixels > max_bbox_pixels {
planned.push(PlannedFrame {
x_offset: 0,
y_offset: 0,
width: f.width,
height: f.height,
duration_ms: f.duration_ms,
blend: f.blend,
rgba_kind: RgbaKind::Full,
rgba_idx: i,
});
} else {
let (bx, by, bw, bh) = union_bbox;
let tile = extract_subrect(f.rgba, canvas_w, bx, by, bw, bh);
let tile_idx = tile_storage.len();
tile_storage.push(tile);
planned.push(PlannedFrame {
x_offset: bx,
y_offset: by,
width: bw,
height: bh,
duration_ms: f.duration_ms,
blend: false, rgba_kind: RgbaKind::Tile,
rgba_idx: tile_idx,
});
}
continue;
}
let n = components.len();
for (k, &(bx, by, bw, bh)) in components.iter().enumerate() {
let tile = extract_subrect(f.rgba, canvas_w, bx, by, bw, bh);
let tile_idx = tile_storage.len();
tile_storage.push(tile);
let dur = if k + 1 == n { f.duration_ms } else { 0 };
planned.push(PlannedFrame {
x_offset: bx,
y_offset: by,
width: bw,
height: bh,
duration_ms: dur,
blend: false, rgba_kind: RgbaKind::Tile,
rgba_idx: tile_idx,
});
}
}
let rewritten: Vec<AnimFrame<'_>> = planned
.iter()
.map(|p| {
let rgba: &[u8] = match p.rgba_kind {
RgbaKind::Full => frames[p.rgba_idx].rgba,
RgbaKind::Tile => tile_storage[p.rgba_idx].as_slice(),
};
AnimFrame {
width: p.width,
height: p.height,
x_offset: p.x_offset,
y_offset: p.y_offset,
duration_ms: p.duration_ms,
blend: p.blend,
dispose_to_background: false,
rgba,
}
})
.collect();
let is_subrect_tile: Vec<bool> = planned
.iter()
.map(|p| matches!(p.rgba_kind, RgbaKind::Tile))
.collect();
if cfg.auto_inner_threshold_bytes.is_none() {
let inner_options = AnimEncoderOptions {
mode: AnimFrameMode::Lossless,
lossy_quality: options.lossy_quality,
metadata: options.metadata.clone(),
};
return build_animated_webp_with_options(
canvas_w,
canvas_h,
background_bgra,
loop_count,
&rewritten,
inner_options,
);
}
let threshold = cfg.auto_inner_threshold_bytes.unwrap();
let lossy_inner_quality = cfg.auto_inner_quality;
build_animated_webp_inner_per_tile(
canvas_w,
canvas_h,
background_bgra,
loop_count,
&rewritten,
&is_subrect_tile,
&options.metadata,
threshold,
lossy_inner_quality,
)
}
#[allow(clippy::too_many_arguments)]
fn build_animated_webp_inner_per_tile(
canvas_w: u32,
canvas_h: u32,
background_bgra: [u8; 4],
loop_count: u16,
frames: &[AnimFrame<'_>],
is_subrect_tile: &[bool],
metadata: &WebpMetadata<'_>,
auto_inner_threshold_bytes: u32,
auto_inner_quality: f32,
) -> Result<Vec<u8>> {
debug_assert_eq!(
frames.len(),
is_subrect_tile.len(),
"frames and is_subrect_tile must be parallel"
);
let mut any_frame_has_alpha = false;
let mut anmf_payloads: Vec<Vec<u8>> = Vec::with_capacity(frames.len());
for (frame_idx, f) in frames.iter().enumerate() {
if f.width == 0 || f.height == 0 {
return Err(Error::invalid("animated WebP delta-inner: zero tile size"));
}
if f.x_offset
.checked_add(f.width)
.map(|r| r > canvas_w)
.unwrap_or(true)
|| f.y_offset
.checked_add(f.height)
.map(|r| r > canvas_h)
.unwrap_or(true)
{
return Err(Error::invalid(
"animated WebP delta-inner: tile bbox extends past canvas",
));
}
if f.rgba.len() != (f.width as usize) * (f.height as usize) * 4 {
return Err(Error::invalid(
"animated WebP delta-inner: tile rgba length mismatch",
));
}
if !any_frame_has_alpha && f.rgba.chunks_exact(4).any(|px| px[3] != 0xff) {
any_frame_has_alpha = true;
}
let lossless = encode_lossless_anmf(f)?;
let lossless_bytes: usize = lossless
.iter()
.map(|s| 8 + s.payload.len() + (s.payload.len() & 1))
.sum();
let chosen: Vec<AnmfSubChunk> =
if is_subrect_tile[frame_idx] && lossless_bytes > auto_inner_threshold_bytes as usize {
match encode_lossy_anmf(f, auto_inner_quality)? {
Some(lossy) => {
let lossy_bytes: usize = lossy
.iter()
.map(|s| 8 + s.payload.len() + (s.payload.len() & 1))
.sum();
if lossy_bytes < lossless_bytes {
lossy
} else {
lossless
}
}
None => lossless,
}
} else {
lossless
};
let nested_capacity = chosen.iter().map(|c| 8 + c.payload.len()).sum::<usize>();
let mut payload = Vec::with_capacity(16 + nested_capacity);
write_u24_le(&mut payload, (f.x_offset / 2) & 0x00FF_FFFF);
write_u24_le(&mut payload, (f.y_offset / 2) & 0x00FF_FFFF);
write_u24_le(&mut payload, (f.width - 1) & 0x00FF_FFFF);
write_u24_le(&mut payload, (f.height - 1) & 0x00FF_FFFF);
write_u24_le(&mut payload, f.duration_ms & 0x00FF_FFFF);
let mut flags: u8 = 0;
if !f.blend {
flags |= 0x01;
}
if f.dispose_to_background {
flags |= 0x02;
}
payload.push(flags);
for sub in &chosen {
write_chunk(&mut payload, &sub.fourcc, &sub.payload);
}
anmf_payloads.push(payload);
}
let mut body: Vec<u8> = Vec::new();
let mut flags: u8 = 0x02; if any_frame_has_alpha {
flags |= 0x10;
}
if metadata.icc.is_some() {
flags |= 0x20;
}
if metadata.exif.is_some() {
flags |= 0x08;
}
if metadata.xmp.is_some() {
flags |= 0x04;
}
let vp8x = vp8x_payload(flags, canvas_w, canvas_h);
write_chunk(&mut body, b"VP8X", &vp8x);
if let Some(icc) = metadata.icc {
write_chunk(&mut body, b"ICCP", icc);
}
let mut anim = [0u8; 6];
anim[0] = background_bgra[0];
anim[1] = background_bgra[1];
anim[2] = background_bgra[2];
anim[3] = background_bgra[3];
anim[4] = (loop_count & 0xff) as u8;
anim[5] = ((loop_count >> 8) & 0xff) as u8;
write_chunk(&mut body, b"ANIM", &anim);
for payload in &anmf_payloads {
write_chunk(&mut body, b"ANMF", payload);
}
if let Some(exif) = metadata.exif {
write_chunk(&mut body, b"EXIF", exif);
}
if let Some(xmp) = metadata.xmp {
write_chunk(&mut body, b"XMP ", xmp);
}
let riff_size = 4 + body.len();
let mut out = Vec::with_capacity(8 + riff_size);
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(riff_size as u32).to_le_bytes());
out.extend_from_slice(b"WEBP");
out.extend_from_slice(&body);
Ok(out)
}
fn extract_subrect(rgba: &[u8], canvas_w: u32, bx: u32, by: u32, bw: u32, bh: u32) -> Vec<u8> {
let canvas_w = canvas_w as usize;
let bx = bx as usize;
let by = by as usize;
let bw = bw as usize;
let bh = bh as usize;
let mut out = Vec::with_capacity(bw * bh * 4);
for row in 0..bh {
let src_off = ((by + row) * canvas_w + bx) * 4;
out.extend_from_slice(&rgba[src_off..src_off + bw * 4]);
}
out
}
fn compute_changed_grid(
prev: &[u8],
curr: &[u8],
canvas_w: u32,
canvas_h: u32,
cfg: &DeltaConfig,
) -> (Vec<bool>, u32, u32) {
let bs = cfg.block_size;
let cw = canvas_w as usize;
let ch = canvas_h as usize;
let n_bx = canvas_w.div_ceil(bs);
let n_by = canvas_h.div_ceil(bs);
let mut grid = vec![false; (n_bx as usize) * (n_by as usize)];
let threshold = if cfg.enable_msssim_cost {
cfg.msssim_threshold as u64
} else if cfg.enable_ssim_cost {
cfg.ssim_threshold as u64
} else {
cfg.threshold as u64
};
for by in 0..n_by {
let y0 = (by * bs) as usize;
let y1 = ((by + 1) * bs).min(canvas_h) as usize;
for bx in 0..n_bx {
let x0 = (bx * bs) as usize;
let x1 = ((bx + 1) * bs).min(canvas_w) as usize;
let cost = if cfg.enable_msssim_cost {
block_cost_msssim(
prev,
curr,
cw,
ch,
x0,
y0,
x1,
y1,
cfg.msssim_downsample_kernel,
)
} else if cfg.enable_ssim_cost {
block_cost_ssim(prev, curr, cw, x0, y0, x1, y1)
} else {
block_cost(prev, curr, cw, x0, y0, x1, y1)
};
if cost > threshold {
grid[(by as usize) * (n_bx as usize) + bx as usize] = true;
}
}
}
(grid, n_bx, n_by)
}
fn block_bbox_to_pixel_bbox(
min_bx: u32,
min_by: u32,
max_bx: u32,
max_by: u32,
canvas_w: u32,
canvas_h: u32,
bs: u32,
) -> (u32, u32, u32, u32) {
let mut px = min_bx * bs;
let mut py = min_by * bs;
let mut pw = ((max_bx + 1) * bs).min(canvas_w) - px;
let mut ph = ((max_by + 1) * bs).min(canvas_h) - py;
if px % 2 != 0 {
px -= 1;
pw += 1;
}
if py % 2 != 0 {
py -= 1;
ph += 1;
}
if px + pw > canvas_w {
pw = canvas_w - px;
}
if py + ph > canvas_h {
ph = canvas_h - py;
}
(px, py, pw, ph)
}
fn changed_block_components(
prev: &[u8],
curr: &[u8],
canvas_w: u32,
canvas_h: u32,
cfg: &DeltaConfig,
max_components: u32,
) -> Vec<(u32, u32, u32, u32)> {
changed_block_components_with_density(prev, curr, canvas_w, canvas_h, cfg, max_components).0
}
fn changed_block_components_with_density(
prev: &[u8],
curr: &[u8],
canvas_w: u32,
canvas_h: u32,
cfg: &DeltaConfig,
max_components: u32,
) -> (Vec<(u32, u32, u32, u32)>, u64) {
let bs = cfg.block_size;
let (grid, n_bx, n_by) = compute_changed_grid(prev, curr, canvas_w, canvas_h, cfg);
let nx = n_bx as usize;
let ny = n_by as usize;
if nx == 0 || ny == 0 {
return (Vec::new(), 0);
}
let mut label = vec![u32::MAX; nx * ny];
let mut bboxes: Vec<(u32, u32, u32, u32)> = Vec::new(); let mut stack: Vec<(u32, u32)> = Vec::new();
for sy in 0..n_by {
for sx in 0..n_bx {
let idx = (sy as usize) * nx + sx as usize;
if !grid[idx] || label[idx] != u32::MAX {
continue;
}
let comp_id = bboxes.len() as u32;
let mut min_bx = sx;
let mut min_by = sy;
let mut max_bx = sx;
let mut max_by = sy;
stack.clear();
stack.push((sx, sy));
label[idx] = comp_id;
while let Some((x, y)) = stack.pop() {
if x < min_bx {
min_bx = x;
}
if x > max_bx {
max_bx = x;
}
if y < min_by {
min_by = y;
}
if y > max_by {
max_by = y;
}
let ux = x as usize;
let uy = y as usize;
if ux + 1 < nx {
let n = uy * nx + ux + 1;
if grid[n] && label[n] == u32::MAX {
label[n] = comp_id;
stack.push((x + 1, y));
}
}
if ux > 0 {
let n = uy * nx + ux - 1;
if grid[n] && label[n] == u32::MAX {
label[n] = comp_id;
stack.push((x - 1, y));
}
}
if uy + 1 < ny {
let n = (uy + 1) * nx + ux;
if grid[n] && label[n] == u32::MAX {
label[n] = comp_id;
stack.push((x, y + 1));
}
}
if uy > 0 {
let n = (uy - 1) * nx + ux;
if grid[n] && label[n] == u32::MAX {
label[n] = comp_id;
stack.push((x, y - 1));
}
}
}
bboxes.push((min_bx, min_by, max_bx, max_by));
}
}
let raw_cluster_pixels: u64 = bboxes
.iter()
.map(|&(mnx, mny, mxx, mxy)| {
let (_, _, pw, ph) =
block_bbox_to_pixel_bbox(mnx, mny, mxx, mxy, canvas_w, canvas_h, bs);
(pw as u64) * (ph as u64)
})
.sum();
while bboxes.len() > max_components as usize && bboxes.len() > 1 {
let smallest_idx = bboxes
.iter()
.enumerate()
.min_by_key(|(_, b)| {
let w = (b.2 - b.0 + 1) as u64;
let h = (b.3 - b.1 + 1) as u64;
w * h
})
.map(|(i, _)| i)
.unwrap();
let small = bboxes[smallest_idx];
let mut best_other = usize::MAX;
let mut best_dist = u64::MAX;
for (j, b) in bboxes.iter().enumerate() {
if j == smallest_idx {
continue;
}
let gx = axis_gap(small.0, small.2, b.0, b.2);
let gy = axis_gap(small.1, small.3, b.1, b.3);
let d = (gx as u64) * (gx as u64) + (gy as u64) * (gy as u64);
if d < best_dist {
best_dist = d;
best_other = j;
}
}
if best_other == usize::MAX {
break; }
let other = bboxes[best_other];
let merged = (
small.0.min(other.0),
small.1.min(other.1),
small.2.max(other.2),
small.3.max(other.3),
);
let (hi, lo) = if smallest_idx > best_other {
(smallest_idx, best_other)
} else {
(best_other, smallest_idx)
};
bboxes.swap_remove(hi);
bboxes[lo] = merged;
}
let pixel_rects: Vec<(u32, u32, u32, u32)> = bboxes
.into_iter()
.map(|(mnx, mny, mxx, mxy)| {
block_bbox_to_pixel_bbox(mnx, mny, mxx, mxy, canvas_w, canvas_h, bs)
})
.collect();
(pixel_rects, raw_cluster_pixels)
}
fn axis_gap(a0: u32, a1: u32, b0: u32, b1: u32) -> u32 {
if a1 + 1 < b0 {
b0 - a1 - 1
} else if b1 + 1 < a0 {
a0 - b1 - 1
} else {
0
}
}
fn bbox_union(bboxes: &[(u32, u32, u32, u32)]) -> (u32, u32, u32, u32) {
let mut x0 = u32::MAX;
let mut y0 = u32::MAX;
let mut x1 = 0u32;
let mut y1 = 0u32;
for &(x, y, w, h) in bboxes {
if x < x0 {
x0 = x;
}
if y < y0 {
y0 = y;
}
if x + w > x1 {
x1 = x + w;
}
if y + h > y1 {
y1 = y + h;
}
}
let x0 = x0 & !1;
let y0 = y0 & !1;
(x0, y0, x1 - x0, y1 - y0)
}
#[allow(dead_code)]
fn changed_block_bbox(
prev: &[u8],
curr: &[u8],
canvas_w: u32,
canvas_h: u32,
cfg: &DeltaConfig,
) -> Option<(u32, u32, u32, u32)> {
let bs = cfg.block_size;
let cw = canvas_w as usize;
let n_bx = canvas_w.div_ceil(bs);
let n_by = canvas_h.div_ceil(bs);
let mut min_bx = u32::MAX;
let mut min_by = u32::MAX;
let mut max_bx: i64 = -1;
let mut max_by: i64 = -1;
let ch = canvas_h as usize;
let threshold = if cfg.enable_msssim_cost {
cfg.msssim_threshold as u64
} else if cfg.enable_ssim_cost {
cfg.ssim_threshold as u64
} else {
cfg.threshold as u64
};
for by in 0..n_by {
let y0 = (by * bs) as usize;
let y1 = ((by + 1) * bs).min(canvas_h) as usize;
for bx in 0..n_bx {
let x0 = (bx * bs) as usize;
let x1 = ((bx + 1) * bs).min(canvas_w) as usize;
let cost = if cfg.enable_msssim_cost {
block_cost_msssim(
prev,
curr,
cw,
ch,
x0,
y0,
x1,
y1,
cfg.msssim_downsample_kernel,
)
} else if cfg.enable_ssim_cost {
block_cost_ssim(prev, curr, cw, x0, y0, x1, y1)
} else {
block_cost(prev, curr, cw, x0, y0, x1, y1)
};
if cost > threshold {
if bx < min_bx {
min_bx = bx;
}
if by < min_by {
min_by = by;
}
if bx as i64 > max_bx {
max_bx = bx as i64;
}
if by as i64 > max_by {
max_by = by as i64;
}
}
}
}
if max_bx < 0 || max_by < 0 {
return None;
}
let mut px = min_bx * bs;
let mut py = min_by * bs;
let mut pw = ((max_bx as u32 + 1) * bs).min(canvas_w) - px;
let mut ph = ((max_by as u32 + 1) * bs).min(canvas_h) - py;
if px % 2 != 0 {
px -= 1;
pw += 1;
}
if py % 2 != 0 {
py -= 1;
ph += 1;
}
if px + pw > canvas_w {
pw = canvas_w - px;
}
if py + ph > canvas_h {
ph = canvas_h - py;
}
Some((px, py, pw, ph))
}
fn block_cost(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
) -> u64 {
let mut acc: u64 = 0;
for y in y0..y1 {
let row_off = y * canvas_w * 4;
for x in x0..x1 {
let off = row_off + x * 4;
let pr = prev[off] as i32;
let pg = prev[off + 1] as i32;
let pb = prev[off + 2] as i32;
let pa = prev[off + 3] as i32;
let cr = curr[off] as i32;
let cg = curr[off + 1] as i32;
let cb = curr[off + 2] as i32;
let ca = curr[off + 3] as i32;
let lp = (77 * pr + 150 * pg + 29 * pb + 128) >> 8;
let lc = (77 * cr + 150 * cg + 29 * cb + 128) >> 8;
let dl = (lp - lc).unsigned_abs() as u64;
let dr = (pr - cr).unsigned_abs() as u64;
let dg = (pg - cg).unsigned_abs() as u64;
let db = (pb - cb).unsigned_abs() as u64;
let da = (pa - ca).unsigned_abs() as u64;
acc += dl + ((dr + dg + db) >> 2) + da;
}
}
acc
}
fn block_cost_ssim(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
) -> u64 {
let n_pixels = (x1 - x0) * (y1 - y0);
if n_pixels == 0 {
return 0;
}
let mut sum_a: i64 = 0;
let mut sum_b: i64 = 0;
for y in y0..y1 {
let row_off = y * canvas_w * 4;
for x in x0..x1 {
let off = row_off + x * 4;
let pa_r = prev[off] as i32;
let pa_g = prev[off + 1] as i32;
let pa_b = prev[off + 2] as i32;
let pb_r = curr[off] as i32;
let pb_g = curr[off + 1] as i32;
let pb_b = curr[off + 2] as i32;
let la = (77 * pa_r + 150 * pa_g + 29 * pa_b + 128) >> 8;
let lb = (77 * pb_r + 150 * pb_g + 29 * pb_b + 128) >> 8;
sum_a += la as i64;
sum_b += lb as i64;
}
}
let n = n_pixels as f64;
let mean_a = sum_a as f64 / n;
let mean_b = sum_b as f64 / n;
let mut var_a_acc: f64 = 0.0;
let mut var_b_acc: f64 = 0.0;
let mut cov_acc: f64 = 0.0;
for y in y0..y1 {
let row_off = y * canvas_w * 4;
for x in x0..x1 {
let off = row_off + x * 4;
let pa_r = prev[off] as i32;
let pa_g = prev[off + 1] as i32;
let pa_b = prev[off + 2] as i32;
let pb_r = curr[off] as i32;
let pb_g = curr[off + 1] as i32;
let pb_b = curr[off + 2] as i32;
let la = ((77 * pa_r + 150 * pa_g + 29 * pa_b + 128) >> 8) as f64;
let lb = ((77 * pb_r + 150 * pb_g + 29 * pb_b + 128) >> 8) as f64;
let da = la - mean_a;
let db = lb - mean_b;
var_a_acc += da * da;
var_b_acc += db * db;
cov_acc += da * db;
}
}
let var_a = var_a_acc / n;
let var_b = var_b_acc / n;
let cov_ab = cov_acc / n;
const C1: f64 = 6.5025; const C2: f64 = 58.5225; let numer = (2.0 * mean_a * mean_b + C1) * (2.0 * cov_ab + C2);
let denom = (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
let ssim = numer / denom;
let cost_f = (1.0 - ssim) * 10_000.0;
if cost_f <= 0.0 {
0
} else {
cost_f.round() as u64
}
}
fn block_cost_msssim(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
canvas_h: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
kernel: DownsampleKernel,
) -> u64 {
let n_pixels = (x1 - x0) * (y1 - y0);
if n_pixels == 0 {
return 0;
}
let ssim0 = ssim_components_native(prev, curr, canvas_w, x0, y0, x1, y1);
let bw = x1 - x0;
let bh = y1 - y0;
let ext_x0 = x0.saturating_sub(bw / 2);
let ext_y0 = y0.saturating_sub(bh / 2);
let ext_x1 = (x1 + bw / 2).min(canvas_w);
let ext_y1 = (y1 + bh / 2).min(canvas_h);
let cs1 = downsampled_cs(
prev, curr, canvas_w, ext_x0, ext_y0, ext_x1, ext_y1, 2, kernel,
);
let ext2_x0 = x0.saturating_sub(3 * bw / 2);
let ext2_y0 = y0.saturating_sub(3 * bh / 2);
let ext2_x1 = (x1 + 3 * bw / 2).min(canvas_w);
let ext2_y1 = (y1 + 3 * bh / 2).min(canvas_h);
let cs2 = downsampled_cs(
prev, curr, canvas_w, ext2_x0, ext2_y0, ext2_x1, ext2_y1, 4, kernel,
);
const ALPHA: f64 = 0.2856; const BETA: f64 = 0.3001; const GAMMA: f64 = 0.4143;
let s0 = ssim0.clamp(1e-6, 1.0);
let s1 = cs1.clamp(1e-6, 1.0);
let s2 = cs2.clamp(1e-6, 1.0);
let msssim = s0.powf(ALPHA) * s1.powf(BETA) * s2.powf(GAMMA);
let cost_f = (1.0 - msssim) * 10_000.0;
if cost_f <= 0.0 {
0
} else {
cost_f.round() as u64
}
}
fn ssim_components_native(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
) -> f64 {
let n_pixels = (x1 - x0) * (y1 - y0);
if n_pixels == 0 {
return 1.0;
}
let mut sum_a: i64 = 0;
let mut sum_b: i64 = 0;
for y in y0..y1 {
let row_off = y * canvas_w * 4;
for x in x0..x1 {
let off = row_off + x * 4;
let pa_r = prev[off] as i32;
let pa_g = prev[off + 1] as i32;
let pa_b = prev[off + 2] as i32;
let pb_r = curr[off] as i32;
let pb_g = curr[off + 1] as i32;
let pb_b = curr[off + 2] as i32;
let la = (77 * pa_r + 150 * pa_g + 29 * pa_b + 128) >> 8;
let lb = (77 * pb_r + 150 * pb_g + 29 * pb_b + 128) >> 8;
sum_a += la as i64;
sum_b += lb as i64;
}
}
let n = n_pixels as f64;
let mean_a = sum_a as f64 / n;
let mean_b = sum_b as f64 / n;
let mut var_a_acc: f64 = 0.0;
let mut var_b_acc: f64 = 0.0;
let mut cov_acc: f64 = 0.0;
for y in y0..y1 {
let row_off = y * canvas_w * 4;
for x in x0..x1 {
let off = row_off + x * 4;
let pa_r = prev[off] as i32;
let pa_g = prev[off + 1] as i32;
let pa_b = prev[off + 2] as i32;
let pb_r = curr[off] as i32;
let pb_g = curr[off + 1] as i32;
let pb_b = curr[off + 2] as i32;
let la = ((77 * pa_r + 150 * pa_g + 29 * pa_b + 128) >> 8) as f64;
let lb = ((77 * pb_r + 150 * pb_g + 29 * pb_b + 128) >> 8) as f64;
let da = la - mean_a;
let db = lb - mean_b;
var_a_acc += da * da;
var_b_acc += db * db;
cov_acc += da * db;
}
}
let var_a = var_a_acc / n;
let var_b = var_b_acc / n;
let cov_ab = cov_acc / n;
const C1: f64 = 6.5025;
const C2: f64 = 58.5225;
let numer = (2.0 * mean_a * mean_b + C1) * (2.0 * cov_ab + C2);
let denom = (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
numer / denom
}
fn downsampled_cs(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
factor: usize,
kernel: DownsampleKernel,
) -> f64 {
if x1 <= x0 || y1 <= y0 || factor == 0 {
return 1.0;
}
let (prev_ds, curr_ds) = match kernel {
DownsampleKernel::Box => box_downsample_luma(prev, curr, canvas_w, x0, y0, x1, y1, factor),
DownsampleKernel::Gaussian => {
gaussian_downsample_luma(prev, curr, canvas_w, x0, y0, x1, y1, factor)
}
};
let n = prev_ds.len();
if n == 0 {
return 1.0;
}
let nf = n as f64;
let mean_a: f64 = prev_ds.iter().sum::<f64>() / nf;
let mean_b: f64 = curr_ds.iter().sum::<f64>() / nf;
let mut var_a_acc = 0.0;
let mut var_b_acc = 0.0;
let mut cov_acc = 0.0;
for i in 0..n {
let da = prev_ds[i] - mean_a;
let db = curr_ds[i] - mean_b;
var_a_acc += da * da;
var_b_acc += db * db;
cov_acc += da * db;
}
let var_a = var_a_acc / nf;
let var_b = var_b_acc / nf;
let cov_ab = cov_acc / nf;
const C2: f64 = 58.5225; let numer = 2.0 * cov_ab + C2;
let denom = var_a + var_b + C2;
numer / denom
}
fn box_downsample_luma(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
factor: usize,
) -> (Vec<f64>, Vec<f64>) {
let region_w = x1 - x0;
let region_h = y1 - y0;
let out_w = region_w.div_ceil(factor);
let out_h = region_h.div_ceil(factor);
if out_w == 0 || out_h == 0 {
return (Vec::new(), Vec::new());
}
let n = out_w * out_h;
let mut prev_ds = vec![0.0f64; n];
let mut curr_ds = vec![0.0f64; n];
for oy in 0..out_h {
for ox in 0..out_w {
let sy0 = y0 + oy * factor;
let sy1 = (sy0 + factor).min(y1);
let sx0 = x0 + ox * factor;
let sx1 = (sx0 + factor).min(x1);
let cell_pixels = (sy1 - sy0) * (sx1 - sx0);
if cell_pixels == 0 {
continue;
}
let mut sum_p: u64 = 0;
let mut sum_c: u64 = 0;
for y in sy0..sy1 {
let row_off = y * canvas_w * 4;
for x in sx0..sx1 {
let off = row_off + x * 4;
let pa_r = prev[off] as i32;
let pa_g = prev[off + 1] as i32;
let pa_b = prev[off + 2] as i32;
let pb_r = curr[off] as i32;
let pb_g = curr[off + 1] as i32;
let pb_b = curr[off + 2] as i32;
let la = (77 * pa_r + 150 * pa_g + 29 * pa_b + 128) >> 8;
let lb = (77 * pb_r + 150 * pb_g + 29 * pb_b + 128) >> 8;
sum_p += la as u64;
sum_c += lb as u64;
}
}
let denom_pix = cell_pixels as f64;
prev_ds[oy * out_w + ox] = sum_p as f64 / denom_pix;
curr_ds[oy * out_w + ox] = sum_c as f64 / denom_pix;
}
}
(prev_ds, curr_ds)
}
fn gaussian_downsample_luma(
prev: &[u8],
curr: &[u8],
canvas_w: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
factor: usize,
) -> (Vec<f64>, Vec<f64>) {
let region_w = x1 - x0;
let region_h = y1 - y0;
if region_w == 0 || region_h == 0 || factor == 0 {
return (Vec::new(), Vec::new());
}
if !factor.is_power_of_two() {
return box_downsample_luma(prev, curr, canvas_w, x0, y0, x1, y1, factor);
}
let mut prev_l = vec![0.0f64; region_w * region_h];
let mut curr_l = vec![0.0f64; region_w * region_h];
for y in 0..region_h {
let src_row = (y0 + y) * canvas_w * 4;
let dst_row = y * region_w;
for x in 0..region_w {
let off = src_row + (x0 + x) * 4;
let pa_r = prev[off] as i32;
let pa_g = prev[off + 1] as i32;
let pa_b = prev[off + 2] as i32;
let pb_r = curr[off] as i32;
let pb_g = curr[off + 1] as i32;
let pb_b = curr[off + 2] as i32;
let la = (77 * pa_r + 150 * pa_g + 29 * pa_b + 128) >> 8;
let lb = (77 * pb_r + 150 * pb_g + 29 * pb_b + 128) >> 8;
prev_l[dst_row + x] = la as f64;
curr_l[dst_row + x] = lb as f64;
}
}
let mut w = region_w;
let mut h = region_h;
let mut p = prev_l;
let mut c = curr_l;
let mut step = 2usize;
while step <= factor {
let (np, nw, nh) = gaussian_blur_decimate2(&p, w, h);
let (nc, _, _) = gaussian_blur_decimate2(&c, w, h);
p = np;
c = nc;
w = nw;
h = nh;
if nw == 0 || nh == 0 {
return (Vec::new(), Vec::new());
}
step *= 2;
}
(p, c)
}
fn gaussian_blur_decimate2(input: &[f64], w: usize, h: usize) -> (Vec<f64>, usize, usize) {
const K: [f64; 5] = [0.054, 0.244, 0.404, 0.244, 0.054];
if w == 0 || h == 0 {
return (Vec::new(), 0, 0);
}
let mut tmp = vec![0.0f64; w * h];
for y in 0..h {
let row = y * w;
for x in 0..w {
let mut acc = 0.0;
let mut wsum = 0.0;
for k in 0..5 {
let dx = k as isize - 2;
let sx = x as isize + dx;
if sx >= 0 && (sx as usize) < w {
acc += input[row + sx as usize] * K[k];
wsum += K[k];
}
}
tmp[row + x] = if wsum > 0.0 {
acc / wsum
} else {
input[row + x]
};
}
}
let mut blurred = vec![0.0f64; w * h];
for x in 0..w {
for y in 0..h {
let mut acc = 0.0;
let mut wsum = 0.0;
for k in 0..5 {
let dy = k as isize - 2;
let sy = y as isize + dy;
if sy >= 0 && (sy as usize) < h {
acc += tmp[(sy as usize) * w + x] * K[k];
wsum += K[k];
}
}
blurred[y * w + x] = if wsum > 0.0 {
acc / wsum
} else {
tmp[y * w + x]
};
}
}
let out_w = w.div_ceil(2);
let out_h = h.div_ceil(2);
let mut out = vec![0.0f64; out_w * out_h];
for oy in 0..out_h {
let sy = oy * 2;
for ox in 0..out_w {
let sx = ox * 2;
out[oy * out_w + ox] = blurred[sy * w + sx];
}
}
(out, out_w, out_h)
}
fn vp8x_payload(flags: u8, canvas_w: u32, canvas_h: u32) -> [u8; 10] {
let mut out = [0u8; 10];
out[0] = flags;
let w_minus_1 = canvas_w.saturating_sub(1) & 0x00FF_FFFF;
let h_minus_1 = canvas_h.saturating_sub(1) & 0x00FF_FFFF;
out[4] = (w_minus_1 & 0xff) as u8;
out[5] = ((w_minus_1 >> 8) & 0xff) as u8;
out[6] = ((w_minus_1 >> 16) & 0xff) as u8;
out[7] = (h_minus_1 & 0xff) as u8;
out[8] = ((h_minus_1 >> 8) & 0xff) as u8;
out[9] = ((h_minus_1 >> 16) & 0xff) as u8;
out
}
fn write_u24_le(out: &mut Vec<u8>, v: u32) {
out.push((v & 0xff) as u8);
out.push(((v >> 8) & 0xff) as u8);
out.push(((v >> 16) & 0xff) as u8);
}
fn write_chunk(out: &mut Vec<u8>, fourcc: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(fourcc);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if payload.len() & 1 == 1 {
out.push(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn solid_frame(w: u32, h: u32, rgba: [u8; 4]) -> Vec<u8> {
let mut v = Vec::with_capacity((w as usize) * (h as usize) * 4);
for _ in 0..(w * h) {
v.extend_from_slice(&rgba);
}
v
}
#[test]
fn build_animated_emits_vp8x_anim_anmf_in_order() {
let f0 = solid_frame(8, 8, [0xff, 0, 0, 0xff]);
let f1 = solid_frame(8, 8, [0, 0xff, 0, 0xff]);
let frames = [
AnimFrame {
width: 8,
height: 8,
x_offset: 0,
y_offset: 0,
duration_ms: 100,
blend: false,
dispose_to_background: false,
rgba: &f0,
},
AnimFrame {
width: 8,
height: 8,
x_offset: 0,
y_offset: 0,
duration_ms: 200,
blend: false,
dispose_to_background: false,
rgba: &f1,
},
];
let out = build_animated_webp(8, 8, [0; 4], 0, &frames).expect("build");
assert_eq!(&out[0..4], b"RIFF");
assert_eq!(&out[8..12], b"WEBP");
assert_eq!(&out[12..16], b"VP8X");
assert_ne!(out[20] & 0x02, 0, "ANIM flag must be set in VP8X");
let vp8x_chunk_len = u32::from_le_bytes([out[16], out[17], out[18], out[19]]) as usize;
let anim_off = 12 + 8 + vp8x_chunk_len + (vp8x_chunk_len & 1);
assert_eq!(&out[anim_off..anim_off + 4], b"ANIM");
let anim_chunk_len = u32::from_le_bytes([
out[anim_off + 4],
out[anim_off + 5],
out[anim_off + 6],
out[anim_off + 7],
]) as usize;
let anmf0_off = anim_off + 8 + anim_chunk_len + (anim_chunk_len & 1);
assert_eq!(&out[anmf0_off..anmf0_off + 4], b"ANMF");
}
#[test]
fn rejects_oversized_frame_bbox() {
let f = solid_frame(8, 8, [0; 4]);
let frames = [AnimFrame {
width: 8,
height: 8,
x_offset: 4,
y_offset: 4,
duration_ms: 0,
blend: false,
dispose_to_background: false,
rgba: &f,
}];
let r = build_animated_webp(8, 8, [0; 4], 0, &frames);
assert!(r.is_err(), "expected oversized-bbox to be rejected");
}
#[test]
fn auto_mode_picks_smaller_of_the_two_candidates() {
let w = 96u32;
let h = 96u32;
let mut rgba = vec![0u8; (w * h * 4) as usize];
for y in 0..h {
for x in 0..w {
let i = ((y * w + x) * 4) as usize;
let mut s = y.wrapping_mul(0x9E37_79B9) ^ x.wrapping_mul(0x85EB_CA77);
s ^= s.wrapping_shr(13);
s = s.wrapping_mul(0xC2B2_AE35);
s ^= s.wrapping_shr(16);
rgba[i] = ((s >> 0) & 0xff) as u8;
rgba[i + 1] = ((s >> 8) & 0xff) as u8;
rgba[i + 2] = ((s >> 16) & 0xff) as u8;
rgba[i + 3] = 0xff;
}
}
let frames = [AnimFrame {
width: w,
height: h,
x_offset: 0,
y_offset: 0,
duration_ms: 50,
blend: false,
dispose_to_background: false,
rgba: &rgba,
}];
let lossless = build_animated_webp_with_options(
w,
h,
[0; 4],
0,
&frames,
AnimEncoderOptions {
mode: AnimFrameMode::Lossless,
..Default::default()
},
)
.expect("encode lossless");
let lossy = build_animated_webp_with_options(
w,
h,
[0; 4],
0,
&frames,
AnimEncoderOptions {
mode: AnimFrameMode::Lossy,
..Default::default()
},
)
.expect("encode lossy");
let auto = build_animated_webp_with_options(
w,
h,
[0; 4],
0,
&frames,
AnimEncoderOptions::default(),
)
.expect("encode auto");
eprintln!(
"anim sizes (noise 96x96): lossless={} lossy={} auto={}",
lossless.len(),
lossy.len(),
auto.len()
);
let smaller = lossless.len().min(lossy.len());
assert!(
auto.len() <= smaller + 2,
"auto ({}) > min(lossless={}, lossy={}) + 2 — mode-selection broken",
auto.len(),
lossless.len(),
lossy.len(),
);
}
#[test]
fn auto_mode_picks_lossless_for_palette_frame() {
let w = 32u32;
let h = 32u32;
let rgba = solid_frame(w, h, [0x80, 0x40, 0x20, 0xff]);
let frames = [AnimFrame {
width: w,
height: h,
x_offset: 0,
y_offset: 0,
duration_ms: 50,
blend: false,
dispose_to_background: false,
rgba: &rgba,
}];
let auto = build_animated_webp_with_options(
w,
h,
[0; 4],
0,
&frames,
AnimEncoderOptions::default(),
)
.expect("encode auto");
let lossless = build_animated_webp_with_options(
w,
h,
[0; 4],
0,
&frames,
AnimEncoderOptions {
mode: AnimFrameMode::Lossless,
..Default::default()
},
)
.expect("encode lossless");
assert_eq!(
auto.len(),
lossless.len(),
"auto mode failed to pick lossless on a flat-colour fixture"
);
}
#[test]
fn ssim_cost_block_low_contrast_diff_picks_higher_cost_than_sad() {
let mut prev = vec![0u8; 8 * 8 * 4];
let mut curr = vec![0u8; 8 * 8 * 4];
for px in curr.chunks_exact_mut(4) {
px[0] = 128;
px[1] = 128;
px[2] = 128;
px[3] = 255;
}
for px in prev.chunks_exact_mut(4) {
px[0] = 128;
px[1] = 128;
px[2] = 128;
px[3] = 255;
}
for y in 0..8 {
let off = (y * 8 + 3) * 4;
prev[off] = 130;
prev[off + 1] = 130;
prev[off + 2] = 130;
prev[off + 3] = 255;
}
let sad = block_cost(&prev, &curr, 8, 0, 0, 8, 8);
let ssim = block_cost_ssim(&prev, &curr, 8, 0, 0, 8, 8);
let sad_threshold = DeltaConfig::default().threshold as u64;
assert!(
sad <= sad_threshold,
"SAD should be ≤ default threshold ({sad_threshold}) for low-contrast structural change, got {sad}"
);
let ssim_threshold = DeltaConfig::default().ssim_threshold as u64;
assert!(
ssim > ssim_threshold,
"SSIM should be > default ssim_threshold ({ssim_threshold}) for low-contrast structural change, got {ssim}"
);
assert!(
ssim > sad,
"SSIM ({ssim}) should rate this block as more 'different' than SAD ({sad})"
);
}
#[test]
fn ssim_cost_block_identical_inputs_returns_zero() {
let mut buf = vec![0u8; 8 * 8 * 4];
for (i, px) in buf.chunks_exact_mut(4).enumerate() {
px[0] = ((i * 13) & 0xff) as u8;
px[1] = ((i * 19) & 0xff) as u8;
px[2] = ((i * 23) & 0xff) as u8;
px[3] = 255;
}
let cost = block_cost_ssim(&buf, &buf, 8, 0, 0, 8, 8);
assert_eq!(
cost, 0,
"SSIM cost on identical blocks should be 0, got {cost}"
);
}
#[test]
fn ssim_cost_threshold_dispatches_correctly_via_config() {
let canvas_w = 16u32;
let canvas_h = 8u32;
let mut prev = vec![0u8; (canvas_w * canvas_h * 4) as usize];
let mut curr = vec![0u8; (canvas_w * canvas_h * 4) as usize];
for px in prev.chunks_exact_mut(4) {
px[0] = 128;
px[1] = 128;
px[2] = 128;
px[3] = 255;
}
curr.copy_from_slice(&prev);
for y in 0..8 {
let off = (y * canvas_w as usize + 11) * 4;
prev[off] = 130;
prev[off + 1] = 130;
prev[off + 2] = 130;
prev[off + 3] = 255;
}
let cfg_sad = DeltaConfig::default();
let cfg_ssim = DeltaConfig::default().enable_ssim_cost(true);
let (grid_sad, n_bx, _) = compute_changed_grid(&prev, &curr, canvas_w, canvas_h, &cfg_sad);
let (grid_ssim, _, _) = compute_changed_grid(&prev, &curr, canvas_w, canvas_h, &cfg_ssim);
assert_eq!(n_bx, 2, "16/8 = 2 block columns");
assert!(
!grid_sad[1],
"SAD-mode right block should NOT be flagged (low-contrast structural change)"
);
assert!(
grid_ssim[1],
"SSIM-mode right block SHOULD be flagged (structural correlation collapsed)"
);
assert!(!grid_sad[0]);
assert!(!grid_ssim[0]);
}
#[test]
fn msssim_cost_block_identical_inputs_returns_zero() {
let cw = 32usize;
let ch = 32usize;
let mut buf = vec![0u8; cw * ch * 4];
for (i, px) in buf.chunks_exact_mut(4).enumerate() {
px[0] = ((i * 13) & 0xff) as u8;
px[1] = ((i * 19) & 0xff) as u8;
px[2] = ((i * 23) & 0xff) as u8;
px[3] = 255;
}
let cost = block_cost_msssim(&buf, &buf, cw, ch, 12, 12, 20, 20, DownsampleKernel::Box);
assert_eq!(
cost, 0,
"MS-SSIM cost on identical blocks should be 0 at every scale, got {cost}"
);
let cost_g = block_cost_msssim(
&buf,
&buf,
cw,
ch,
12,
12,
20,
20,
DownsampleKernel::Gaussian,
);
assert_eq!(
cost_g, 0,
"MS-SSIM cost on identical blocks should be 0 under the Gaussian kernel, got {cost_g}"
);
}
#[test]
fn msssim_cost_catches_low_freq_drift_single_scale_misses() {
let cw = 32usize;
let ch = 32usize;
let mut prev = vec![0u8; cw * ch * 4];
let mut curr = vec![0u8; cw * ch * 4];
for px in prev.chunks_exact_mut(4) {
px[0] = 128;
px[1] = 128;
px[2] = 128;
px[3] = 255;
}
curr.copy_from_slice(&prev);
for y in 0..ch {
for x in 0..cw {
let centre = (12..20).contains(&y) && (12..20).contains(&x);
if !centre && (x % 2 == 0) {
let off = (y * cw + x) * 4;
curr[off] = 200;
curr[off + 1] = 200;
curr[off + 2] = 200;
}
}
}
let ssim_single = block_cost_ssim(&prev, &curr, cw, 12, 12, 20, 20);
let msssim = block_cost_msssim(&prev, &curr, cw, ch, 12, 12, 20, 20, DownsampleKernel::Box);
assert_eq!(
ssim_single, 0,
"single-scale SSIM at the identical centre block should be 0"
);
assert!(
msssim > 0,
"MS-SSIM SHOULD pick up the surrounding-context structural change (cost > 0), got {msssim}"
);
let msssim_threshold = DeltaConfig::default().msssim_threshold as u64;
assert!(
msssim > msssim_threshold,
"MS-SSIM cost ({msssim}) should exceed default msssim_threshold ({msssim_threshold})"
);
}
#[test]
fn msssim_cost_threshold_dispatches_correctly_via_config() {
let canvas_w = 32u32;
let canvas_h = 32u32;
let cw = canvas_w as usize;
let ch = canvas_h as usize;
let mut prev = vec![0u8; cw * ch * 4];
let mut curr = vec![0u8; cw * ch * 4];
for px in prev.chunks_exact_mut(4) {
px[0] = 128;
px[1] = 128;
px[2] = 128;
px[3] = 255;
}
curr.copy_from_slice(&prev);
for y in 0..ch {
for x in 0..cw {
let centre = (12..20).contains(&y) && (12..20).contains(&x);
if !centre && (x % 2 == 0) {
let off = (y * cw + x) * 4;
curr[off] = 200;
curr[off + 1] = 200;
curr[off + 2] = 200;
}
}
}
let mut prev = vec![0u8; cw * ch * 4];
let mut curr = vec![0u8; cw * ch * 4];
for px in prev.chunks_exact_mut(4) {
px[0] = 128;
px[1] = 128;
px[2] = 128;
px[3] = 255;
}
curr.copy_from_slice(&prev);
for y in 0..ch {
for x in 0..cw {
let centre = (8..16).contains(&y) && (8..16).contains(&x);
if !centre && (x % 2 == 0) {
let off = (y * cw + x) * 4;
curr[off] = 200;
curr[off + 1] = 200;
curr[off + 2] = 200;
}
}
}
let cfg_ssim = DeltaConfig::default().enable_ssim_cost(true);
let cfg_msssim = DeltaConfig::default().enable_msssim_cost(true);
let (grid_ssim, n_bx, _) =
compute_changed_grid(&prev, &curr, canvas_w, canvas_h, &cfg_ssim);
let (grid_msssim, _, _) =
compute_changed_grid(&prev, &curr, canvas_w, canvas_h, &cfg_msssim);
assert_eq!(n_bx, 4, "32/8 = 4 block columns");
let centre_idx = 1 * (n_bx as usize) + 1;
assert!(
!grid_ssim[centre_idx],
"single-scale SSIM should NOT flag identical centre block"
);
assert!(
grid_msssim[centre_idx],
"MS-SSIM SHOULD flag the centre block due to surrounding context drift"
);
}
#[test]
fn msssim_gaussian_kernel_disagrees_with_box_on_smooth_gradient() {
let cw = 32usize;
let ch = 32usize;
let mut prev = vec![0u8; cw * ch * 4];
let mut curr = vec![0u8; cw * ch * 4];
for y in 0..ch {
for x in 0..cw {
let v = if (x + y) & 1 == 0 { 64u8 } else { 192u8 };
let off = (y * cw + x) * 4;
prev[off] = v;
prev[off + 1] = v;
prev[off + 2] = v;
prev[off + 3] = 255;
let centre = (12..20).contains(&y) && (12..20).contains(&x);
let cv = if centre { v.saturating_add(20) } else { v };
curr[off] = cv;
curr[off + 1] = cv;
curr[off + 2] = cv;
curr[off + 3] = 255;
}
}
let cost_box =
block_cost_msssim(&prev, &curr, cw, ch, 12, 12, 20, 20, DownsampleKernel::Box);
let cost_gauss = block_cost_msssim(
&prev,
&curr,
cw,
ch,
12,
12,
20,
20,
DownsampleKernel::Gaussian,
);
assert!(
cost_box > 0,
"Box-kernel MS-SSIM cost should detect the centre-block luminance shift, got {cost_box}"
);
assert!(
cost_gauss > 0,
"Gaussian-kernel MS-SSIM cost should detect the centre-block luminance shift, got {cost_gauss}"
);
assert_ne!(
cost_box, cost_gauss,
"Box ({cost_box}) and Gaussian ({cost_gauss}) MS-SSIM costs must differ on a checkerboard with localised perturbation \
— if they match, the Gaussian path collapsed to box semantics"
);
}
#[test]
fn adaptive_max_components_pins_documented_density_band() {
assert_eq!(adaptive_max_components(0.0), 16);
assert_eq!(adaptive_max_components(0.01), 16);
assert_eq!(adaptive_max_components(0.05), 16);
assert_eq!(adaptive_max_components(0.30), 4);
assert_eq!(adaptive_max_components(0.50), 4);
assert_eq!(adaptive_max_components(1.00), 4);
assert_eq!(adaptive_max_components(0.175), 10);
let near_lo = adaptive_max_components(0.06);
let near_hi = adaptive_max_components(0.29);
assert!(
near_lo > near_hi,
"monotonic: lower density yields larger budget, got {near_lo} vs {near_hi}"
);
assert!(
(4..=16).contains(&near_lo) && (4..=16).contains(&near_hi),
"in-band budgets stay inside [4, 16]"
);
}
#[test]
fn loop_count_and_background_round_trip_on_disk() {
let f = solid_frame(4, 4, [0; 4]);
let frames = [AnimFrame {
width: 4,
height: 4,
x_offset: 0,
y_offset: 0,
duration_ms: 1,
blend: false,
dispose_to_background: false,
rgba: &f,
}];
let out = build_animated_webp(4, 4, [0x12, 0x34, 0x56, 0x78], 7, &frames).expect("build");
let vp8x_chunk_len = u32::from_le_bytes([out[16], out[17], out[18], out[19]]) as usize;
let anim_off = 12 + 8 + vp8x_chunk_len + (vp8x_chunk_len & 1);
let anim_payload = &out[anim_off + 8..anim_off + 8 + 6];
assert_eq!(&anim_payload[0..4], &[0x12, 0x34, 0x56, 0x78]);
let lc = u16::from_le_bytes([anim_payload[4], anim_payload[5]]);
assert_eq!(lc, 7);
}
}