use gamut_color::{Bt601Range, Yuv420};
use gamut_core::{Dimensions, EncodeImage, ImageRef, Result, Rgb8, Rgba8};
use gamut_riff::{FourCc, Vp8xHeader, write_extended, write_simple_lossless, write_simple_lossy};
use crate::alpha;
use crate::config::{WebpConfig, WebpMode};
use crate::vp8::frame::encode_frame;
use crate::vp8l::encoder::encode as encode_vp8l;
use crate::vp8l::transform::make_argb;
fn quality_to_quant(quality: u8) -> u8 {
let q = u32::from(quality.min(100));
((100 - q) * 127 / 100) as u8
}
#[derive(Debug, Clone, Default)]
pub struct WebpEncoder {
config: WebpConfig,
}
impl WebpEncoder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn lossless() -> Self {
Self {
config: WebpConfig {
mode: WebpMode::Lossless,
..WebpConfig::default()
},
}
}
#[must_use]
pub fn lossy(quality: u8) -> Self {
Self {
config: WebpConfig {
mode: WebpMode::Lossy,
quality,
},
}
}
#[must_use]
pub fn config(&self) -> WebpConfig {
self.config
}
fn encode_rgb8_inner(
&self,
pixels: &[u8],
dims: Dimensions,
out: &mut Vec<u8>,
) -> Result<usize> {
match self.config.mode {
WebpMode::Lossless => {
let argb: Vec<u32> = pixels
.chunks_exact(3)
.map(|p| make_argb(0xff, p[0], p[1], p[2]))
.collect();
let bitstream = encode_vp8l(&argb, dims)?;
let file = write_simple_lossless(&bitstream);
let written = file.len();
out.extend_from_slice(&file);
Ok(written)
}
WebpMode::Lossy => {
let yuv = Yuv420::from_rgb8(pixels, dims.width, dims.height, Bt601Range::Limited)?;
let (payload, _recon) = encode_frame(&yuv, quality_to_quant(self.config.quality));
let file = write_simple_lossy(&payload);
let written = file.len();
out.extend_from_slice(&file);
Ok(written)
}
}
}
fn encode_rgba8_inner(
&self,
pixels: &[u8],
dims: Dimensions,
out: &mut Vec<u8>,
) -> Result<usize> {
let file = match self.config.mode {
WebpMode::Lossless => {
let argb: Vec<u32> = pixels
.chunks_exact(4)
.map(|p| make_argb(p[3], p[0], p[1], p[2]))
.collect();
write_simple_lossless(&encode_vp8l(&argb, dims)?)
}
WebpMode::Lossy => {
let rgb: Vec<u8> = pixels
.chunks_exact(4)
.flat_map(|p| [p[0], p[1], p[2]])
.collect();
let yuv = Yuv420::from_rgb8(&rgb, dims.width, dims.height, Bt601Range::Limited)?;
let (vp8, _) = encode_frame(&yuv, quality_to_quant(self.config.quality));
if pixels.chunks_exact(4).all(|p| p[3] == 0xff) {
write_simple_lossy(&vp8)
} else {
let alpha: Vec<u8> = pixels.chunks_exact(4).map(|p| p[3]).collect();
let alph =
alpha::write_alph(&alpha, dims.width as usize, dims.height as usize)?;
let header = Vp8xHeader {
alpha: true,
canvas_width: dims.width,
canvas_height: dims.height,
..Default::default()
};
write_extended(&header, &[(FourCc::ALPH, &alph), (FourCc::VP8, &vp8)])
}
}
};
let written = file.len();
out.extend_from_slice(&file);
Ok(written)
}
}
impl EncodeImage<Rgb8> for WebpEncoder {
fn encode_image(&self, image: ImageRef<'_, Rgb8>, out: &mut Vec<u8>) -> Result<usize> {
self.encode_rgb8_inner(image.as_samples(), image.dimensions(), out)
}
}
impl EncodeImage<Rgba8> for WebpEncoder {
fn encode_image(&self, image: ImageRef<'_, Rgba8>, out: &mut Vec<u8>) -> Result<usize> {
self.encode_rgba8_inner(image.as_samples(), image.dimensions(), out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use gamut_core::{DecodeImage, ImageBuf};
fn dims(w: u32, h: u32) -> Dimensions {
Dimensions {
width: w,
height: h,
}
}
#[test]
fn constructors_select_mode() {
assert_eq!(WebpEncoder::new().config().mode, WebpMode::Lossless);
assert_eq!(WebpEncoder::lossless().config().mode, WebpMode::Lossless);
let lossy = WebpEncoder::lossy(40);
assert_eq!(lossy.config().mode, WebpMode::Lossy);
assert_eq!(lossy.config().quality, 40);
}
#[test]
fn rejects_mismatched_buffer_length() {
assert!(ImageRef::<Rgb8>::new(&[0u8; 10], dims(2, 2)).is_err());
}
#[test]
fn lossless_encodes_a_valid_webp_file() {
let mut out = Vec::new();
let rgb = [0x10, 0x20, 0x30].repeat(4);
let written = WebpEncoder::lossless()
.encode_image(ImageRef::<Rgb8>::new(&rgb, dims(2, 2)).unwrap(), &mut out)
.expect("encode");
assert_eq!(written, out.len());
assert_eq!(&out[0..4], b"RIFF");
let decoded: ImageBuf<Rgb8> = crate::WebpDecoder::new()
.decode_image(&out)
.expect("decode");
assert_eq!(decoded.dimensions(), dims(2, 2));
assert_eq!(decoded.as_samples(), rgb.as_slice());
}
#[test]
fn lossy_encodes_a_decodable_webp_file() {
let mut out = Vec::new();
let rgb = [40u8, 80, 120].repeat(16 * 16);
let written = WebpEncoder::lossy(60)
.encode_image(ImageRef::<Rgb8>::new(&rgb, dims(16, 16)).unwrap(), &mut out)
.expect("lossy encode");
assert_eq!(written, out.len());
assert_eq!(&out[0..4], b"RIFF");
let decoded: ImageBuf<Rgb8> = crate::WebpDecoder::new()
.decode_image(&out)
.expect("decode");
assert_eq!(decoded.dimensions(), dims(16, 16));
assert_eq!(decoded.as_samples().len(), 16 * 16 * 3);
}
#[test]
fn lossy_rgba_round_trips_alpha_exactly() {
let (w, h) = (32u32, 24u32);
let rgba: Vec<u8> = (0..(w * h) as usize)
.flat_map(|i| {
let (x, y) = (i as u32 % w, i as u32 / w);
[
(x * 7) as u8,
(y * 9) as u8,
(x ^ y) as u8,
((x * 5 + y * 3) & 0xff) as u8,
]
})
.collect();
let mut file = Vec::new();
WebpEncoder::lossy(75)
.encode_image(
ImageRef::<Rgba8>::new(&rgba, dims(w, h)).unwrap(),
&mut file,
)
.expect("rgba encode");
assert_eq!(&file[0..4], b"RIFF");
let decoded: ImageBuf<Rgba8> = crate::WebpDecoder::new()
.decode_image(&file)
.expect("rgba decode");
assert_eq!(decoded.dimensions(), dims(w, h));
let dec_alpha: Vec<u8> = decoded.as_samples().chunks_exact(4).map(|p| p[3]).collect();
let src_alpha: Vec<u8> = rgba.chunks_exact(4).map(|p| p[3]).collect();
assert_eq!(dec_alpha, src_alpha, "alpha must round-trip losslessly");
}
#[test]
fn opaque_rgba_uses_the_simple_lossy_format() {
use gamut_riff::{RiffReader, WebpChunkId};
let rgba = [120u8, 60, 200, 0xff].repeat(16 * 16);
let mut file = Vec::new();
WebpEncoder::lossy(60)
.encode_image(
ImageRef::<Rgba8>::new(&rgba, dims(16, 16)).unwrap(),
&mut file,
)
.expect("rgba encode");
let ids: Vec<_> = RiffReader::new(&file)
.unwrap()
.map(|c| WebpChunkId::from(c.unwrap().fourcc))
.collect();
assert_eq!(ids, vec![WebpChunkId::Vp8]);
}
#[test]
fn encode_image_is_object_safe() {
let mut out = Vec::new();
let rgb = [7u8, 8, 9];
let enc: &dyn EncodeImage<Rgb8> = &WebpEncoder::new();
let written = enc
.encode_image(ImageRef::<Rgb8>::new(&rgb, dims(1, 1)).unwrap(), &mut out)
.expect("encode via trait");
assert_eq!(written, out.len());
assert_eq!(&out[0..4], b"RIFF");
}
}