use std::{borrow::Cow, io::Write};
use image::{ExtendedColorType, ImageEncoder, ImageFormat, RgbaImage, codecs::jpeg::JpegEncoder};
use png::{ColorType, Compression, Filter};
use serde::Deserialize;
use image_webp::WebPEncoder;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use crate::{Error::IoError, Result};
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ImageOutputFormat {
WebP,
Png,
Jpeg,
}
impl ImageOutputFormat {
pub fn content_type(&self) -> &'static str {
match self {
ImageOutputFormat::WebP => "image/webp",
ImageOutputFormat::Png => "image/png",
ImageOutputFormat::Jpeg => "image/jpeg",
}
}
}
impl From<ImageOutputFormat> for ImageFormat {
fn from(format: ImageOutputFormat) -> Self {
match format {
ImageOutputFormat::WebP => Self::WebP,
ImageOutputFormat::Png => Self::Png,
ImageOutputFormat::Jpeg => Self::Jpeg,
}
}
}
#[derive(Debug, Clone)]
pub struct AnimationFrame {
pub image: RgbaImage,
pub duration_ms: u32,
}
impl AnimationFrame {
pub fn new(image: RgbaImage, duration_ms: u32) -> Self {
Self { image, duration_ms }
}
}
const U24_MAX: u32 = 0xffffff;
fn strip_alpha_channel(image: &RgbaImage) -> Vec<u8> {
let pixels = bytemuck::cast_slice::<u8, [u8; 4]>(image.as_raw());
let mut rgb = Vec::with_capacity(pixels.len() * 3);
for [r, g, b, _] in pixels {
rgb.extend_from_slice(&[*r, *g, *b]);
}
rgb
}
fn has_any_alpha_pixel(image: &RgbaImage) -> bool {
bytemuck::cast_slice::<u8, [u8; 4]>(image.as_raw())
.iter()
.any(|[_, _, _, a]| *a != u8::MAX)
}
pub fn write_image<T: Write>(
image: &RgbaImage,
destination: &mut T,
format: ImageOutputFormat,
quality: Option<u8>,
) -> Result<()> {
match format {
ImageOutputFormat::Jpeg => {
let rgb = strip_alpha_channel(image);
let encoder = JpegEncoder::new_with_quality(destination, quality.unwrap_or(75));
encoder.write_image(&rgb, image.width(), image.height(), ExtendedColorType::Rgb8)?;
}
ImageOutputFormat::Png => {
let mut encoder = png::Encoder::new(destination, image.width(), image.height());
let has_alpha = has_any_alpha_pixel(image);
let image_data = if has_alpha {
Cow::Borrowed(image.as_raw())
} else {
Cow::Owned(strip_alpha_channel(image))
};
encoder.set_color(if has_alpha {
ColorType::Rgba
} else {
ColorType::Rgb
});
let quality = quality.unwrap_or(75);
if quality >= 90 {
encoder.set_compression(Compression::Balanced);
} else {
encoder.set_compression(Compression::Fast);
}
encoder.set_filter(Filter::Sub);
let mut writer = encoder.write_header()?;
writer.write_image_data(&image_data)?;
writer.finish()?;
}
ImageOutputFormat::WebP => {
let encoder = WebPEncoder::new(destination);
let has_alpha = has_any_alpha_pixel(image);
let image_data = if has_alpha {
Cow::Borrowed(image.as_raw())
} else {
Cow::Owned(strip_alpha_channel(image))
};
encoder.encode(
&image_data,
image.width(),
image.height(),
if has_alpha {
image_webp::ColorType::Rgba8
} else {
image_webp::ColorType::Rgb8
},
)?;
}
}
Ok(())
}
fn vp8_payload_coords(buf: &[u8]) -> Option<(usize, usize)> {
if buf.len() < 12 {
return None;
}
let mut i = 12;
let buf_len = buf.len();
while i + 8 <= buf_len {
let tag = &buf[i..i + 4];
let len = u32::from_le_bytes(buf[i + 4..i + 8].try_into().ok()?) as usize;
if tag == b"VP8 " || tag == b"VP8L" {
let start = i + 8;
let end = start.checked_add(len)?;
if end > buf_len {
return None;
}
return Some((start, len));
}
let padding = len & 1;
let chunk_size = len.checked_add(padding)?;
i = (i + 8).checked_add(chunk_size)?;
}
None
}
const BASE_HEADER_SIZE: u32 = 8;
const ANMF_HEADER_SIZE: u32 = 16;
const VP8X_HEADER_SIZE: u32 = 10;
const ANIM_HEADER_SIZE: u32 = 6;
fn estimate_vp8_payload_size(buf: &[u8]) -> Result<u32> {
let (_, len) = vp8_payload_coords(buf)
.ok_or_else(|| IoError(std::io::Error::other("VP8/VP8L chunk not found")))?;
let padding = len & 1;
Ok(BASE_HEADER_SIZE + ANMF_HEADER_SIZE + BASE_HEADER_SIZE + len as u32 + padding as u32)
}
fn estimate_riff_size<'a, I: Iterator<Item = &'a [u8]>>(frames: I) -> Result<u32> {
let mut size = 4 + BASE_HEADER_SIZE + VP8X_HEADER_SIZE + BASE_HEADER_SIZE + ANIM_HEADER_SIZE;
for frame in frames {
size += estimate_vp8_payload_size(frame)?;
}
Ok(size)
}
pub fn encode_animated_webp<W: Write>(
frames: &[AnimationFrame],
destination: &mut W,
blend: bool,
dispose: bool,
loop_count: Option<u16>,
) -> Result<()> {
assert_ne!(frames.len(), 0);
#[cfg(feature = "rayon")]
let frames_payloads: Vec<(&AnimationFrame, Vec<u8>)> = frames
.par_iter()
.map(|frame| {
let mut buf = Vec::new();
WebPEncoder::new(&mut buf).encode(
&frame.image,
frame.image.width(),
frame.image.height(),
image_webp::ColorType::Rgba8,
)?;
Ok((frame, buf))
})
.collect::<Result<Vec<(&AnimationFrame, Vec<u8>)>>>()?;
#[cfg(not(feature = "rayon"))]
let frames_payloads: Vec<(&AnimationFrame, Vec<u8>)> = frames
.iter()
.map(|frame| {
let mut buf = Vec::new();
WebPEncoder::new(&mut buf)
.encode(
&frame.image,
frame.image.width(),
frame.image.height(),
image_webp::ColorType::Rgba8,
)
.map_err(|_| IoError(std::io::Error::other("WebP encode error")))?;
Ok((frame, buf))
})
.collect::<Result<Vec<(&AnimationFrame, Vec<u8>)>>>()?;
let riff_size = estimate_riff_size(frames_payloads.iter().map(|(_, buf)| buf.as_slice()))?;
destination.write_all(b"RIFF")?;
destination.write_all(&(riff_size as u32).to_le_bytes())?;
destination.write_all(b"WEBP")?;
let vp8x_flags: u8 = (1 << 1) | (1 << 4); let cw = (frames[0].image.width() - 1).to_le_bytes();
let ch = (frames[0].image.height() - 1).to_le_bytes();
destination.write_all(b"VP8X")?;
destination.write_all(&VP8X_HEADER_SIZE.to_le_bytes())?;
destination.write_all(&[vp8x_flags])?;
destination.write_all(&[0u8; 3])?;
destination.write_all(&cw[..3])?;
destination.write_all(&ch[..3])?;
destination.write_all(b"ANIM")?;
destination.write_all(&ANIM_HEADER_SIZE.to_le_bytes())?;
destination.write_all(&[0u8; 4])?; destination.write_all(&loop_count.unwrap_or(0).to_le_bytes())?;
let frame_flags = ((blend as u8) << 1) | (dispose as u8);
for (frame, vp8_data) in frames_payloads.into_iter() {
let w_bytes = (frame.image.width() - 1).to_le_bytes();
let h_bytes = (frame.image.height() - 1).to_le_bytes();
let (start, len) = vp8_payload_coords(&vp8_data)
.ok_or_else(|| IoError(std::io::Error::other("VP8/VP8L chunk not found")))?;
let vp8_payload = &vp8_data[start..start + len];
let padding = vp8_payload.len() & 1;
let anmf_size = ANMF_HEADER_SIZE + BASE_HEADER_SIZE + vp8_payload.len() as u32 + padding as u32;
destination.write_all(b"ANMF")?;
destination.write_all(&anmf_size.to_le_bytes())?;
destination.write_all(&[0u8; 6])?; destination.write_all(&w_bytes[..3])?; destination.write_all(&h_bytes[..3])?; destination.write_all(&frame.duration_ms.clamp(0, U24_MAX).to_le_bytes()[..3])?; destination.write_all(&[frame_flags])?;
destination.write_all(b"VP8L")?;
destination.write_all(&(vp8_payload.len() as u32).to_le_bytes())?;
destination.write_all(vp8_payload)?;
if padding == 1 {
destination.write_all(&[0u8])?;
}
}
destination.flush()?;
Ok(())
}
pub fn encode_animated_png<W: Write>(
frames: &[AnimationFrame],
destination: &mut W,
loop_count: Option<u16>,
) -> Result<()> {
assert_ne!(frames.len(), 0);
let mut encoder = png::Encoder::new(
destination,
frames[0].image.width(),
frames[0].image.height(),
);
encoder.set_color(ColorType::Rgba);
encoder.set_compression(png::Compression::Fastest);
encoder.set_animated(frames.len() as u32, loop_count.unwrap_or(0) as u32)?;
let min_duration_ms = frames
.iter()
.map(|frame| frame.duration_ms)
.min()
.unwrap_or(0);
encoder.set_frame_delay(min_duration_ms.clamp(0, u16::MAX as u32) as u16, 1000)?;
let mut writer = encoder.write_header()?;
for frame in frames {
writer.write_image_data(frame.image.as_raw())?;
}
writer.finish()?;
Ok(())
}