use crate::container::writer::{wrap_vp8, wrap_vp8_extended};
use crate::image::{self, Image, Metadata, PixelLayout};
use crate::lossy::alpha::compress_alpha;
use crate::lossy::frame;
use crate::lossy::prelude::*;
use crate::lossy::quant::quality_to_base_q;
use crate::{Dimensions, Effort, Error, ImageRef, Result};
const MAX_VP8_DIMENSION: u32 = 16383;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Quality(u8);
impl Quality {
pub const DEFAULT: Self = Self(75);
#[must_use]
pub const fn new(q: u8) -> Self {
Self(if q > 100 { 100 } else { q })
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
impl Default for Quality {
fn default() -> Self {
Self::DEFAULT
}
}
const fn effort_tier(effort: Effort) -> frame::Effort {
match effort {
Effort::Fast => frame::Effort::Fast,
Effort::Balanced => frame::Effort::Full,
Effort::Best => frame::Effort::Best,
}
}
pub use crate::image::MetadataPolicy;
#[derive(Clone, Debug, Default)]
pub struct LossyConfig {
quality: Quality,
effort: Effort,
metadata: Metadata,
policy: MetadataPolicy,
}
impl LossyConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_quality(mut self, quality: u8) -> Self {
self.quality = Quality::new(quality);
self
}
#[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
}
#[must_use]
pub const fn quality(&self) -> Quality {
self.quality
}
#[must_use]
pub const fn effort(&self) -> Effort {
self.effort
}
#[must_use]
pub const fn metadata(&self) -> &Metadata {
&self.metadata
}
#[must_use]
pub const fn policy(&self) -> MetadataPolicy {
self.policy
}
pub(crate) fn resolve_metadata(&self, inherited: &Metadata) -> Metadata {
self.metadata.resolve(inherited, self.policy)
}
}
pub fn encode(image: ImageRef<'_>, config: &LossyConfig) -> Result<Vec<u8>> {
let dims = image.dimensions();
check_dimensions(dims)?;
let argb = image::unpack_pixels(image.layout(), image.as_bytes());
let metadata = config.resolve_metadata(&Metadata::none());
Ok(assemble(&argb, dims, config, &metadata))
}
pub fn encode_image(image: &Image, config: &LossyConfig) -> Result<Vec<u8>> {
let dims = image.dimensions();
check_dimensions(dims)?;
let argb = image::unpack_pixels(image.layout(), image.as_bytes());
let metadata = config.resolve_metadata(image.metadata());
Ok(assemble(&argb, dims, config, &metadata))
}
fn assemble(argb: &[u32], dims: Dimensions, config: &LossyConfig, metadata: &Metadata) -> Vec<u8> {
let vp8 = encode_vp8_argb(argb, dims, config);
let has_alpha = image::argb_has_alpha(argb);
if !has_alpha && metadata.is_empty() {
return wrap_vp8(&vp8);
}
let alph = has_alpha.then(|| {
let alpha: Vec<u8> = argb.iter().map(|&p| (p >> 24) as u8).collect();
compress_alpha(&alpha, dims.width(), dims.height())
});
wrap_vp8_extended(&vp8, alph.as_deref(), dims, metadata)
}
pub fn encode_vp8(image: ImageRef<'_>, config: &LossyConfig) -> Result<(Dimensions, Vec<u8>)> {
let dims = image.dimensions();
check_dimensions(dims)?;
let argb = image::unpack_pixels(image.layout(), image.as_bytes());
Ok((dims, encode_vp8_argb(&argb, dims, config)))
}
fn encode_vp8_argb(argb: &[u32], dims: Dimensions, config: &LossyConfig) -> Vec<u8> {
let rgba = image::pack_pixels(PixelLayout::Rgba8, argb);
let base_q = quality_to_base_q(config.quality.get());
frame::encode_frame(
&rgba,
dims.width() as usize,
dims.height() as usize,
base_q,
effort_tier(config.effort),
)
}
const fn check_dimensions(dims: Dimensions) -> Result<()> {
if dims.width() > MAX_VP8_DIMENSION || dims.height() > MAX_VP8_DIMENSION {
Err(Error::InvalidDimensions)
} else {
Ok(())
}
}
#[cfg(feature = "std")]
pub fn encode_to<W: std::io::Write>(
image: ImageRef<'_>,
config: &LossyConfig,
mut writer: W,
) -> Result<()> {
let bytes = encode(image, config)?;
writer.write_all(&bytes)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
Effort, LossyConfig, MetadataPolicy, Quality, effort_tier, encode, encode_image, encode_vp8,
};
use crate::alpha::{AlphaCompression, parse_header, unfilter};
use crate::container::fourcc::FourCc;
use crate::container::reader::{ImageChunk, chunks, locate_image_with_alpha};
use crate::container::writer::wrap_vp8;
use crate::image::{Dimensions, Image, ImageRef, Metadata, PixelLayout};
fn chunk_bytes(file: &[u8], id: FourCc) -> Option<Vec<u8>> {
chunks(file)
.unwrap()
.filter_map(Result::ok)
.find(|c| c.id == id)
.map(|c| c.data.to_vec())
}
fn meta_fixture() -> (Metadata, Vec<u8>, Dimensions) {
let metadata = Metadata {
icc_profile: Some(b"icc-bytes".to_vec()), exif: Some(b"exif-bytes".to_vec()),
xmp: Some(b"<x:xmpmeta/>".to_vec()),
};
(metadata, solid(16, 16), Dimensions::new(16, 16).unwrap())
}
fn byte(v: u32) -> u8 {
u8::try_from(v & 0xff).unwrap_or(0)
}
fn solid(width: u32, height: u32) -> Vec<u8> {
let mut buf = Vec::new();
for _ in 0..width * height {
buf.extend_from_slice(&[100, 150, 200, 255]);
}
buf
}
fn recover_alpha(file: &[u8], width: u32, height: u32) -> Vec<u8> {
let located = locate_image_with_alpha(file).unwrap();
let alph = located
.alpha
.expect("an alpha image must carry an ALPH chunk");
let (w, h) = (width as usize, height as usize);
let (header, data) = parse_header(alph).unwrap();
let mut plane = match header.compression {
AlphaCompression::None => data[..w * h].to_vec(),
AlphaCompression::Lossless => {
crate::lossless::decode_alpha(data, width, height).unwrap()
},
};
unfilter(header.filter, &mut plane, w, h);
plane
}
#[test]
fn opaque_image_stays_a_bare_vp8_container() {
let dims = Dimensions::new(24, 16).unwrap();
let pixels = solid(24, 16);
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let cfg = LossyConfig::new();
let file = encode(img, &cfg).unwrap();
let (_dims, payload) = encode_vp8(img, &cfg).unwrap();
assert_eq!(
file,
wrap_vp8(&payload),
"opaque encode must be byte-identical"
);
assert_eq!(&file[12..16], b"VP8 ");
assert!(!file.windows(4).any(|w| w == b"VP8X"));
assert!(!file.windows(4).any(|w| w == b"ALPH"));
}
#[test]
fn non_opaque_image_upgrades_to_extended_and_alpha_round_trips_byte_exact() {
let (w, h) = (20u32, 16u32);
let mut pixels = Vec::new();
let mut source_alpha = Vec::new();
for y in 0..h {
for x in 0..w {
let a = byte(((x + y) * 255) / (w + h - 2));
source_alpha.push(a);
pixels.extend_from_slice(&[byte(x * 12), byte(y * 15), 128, a]);
}
}
let dims = Dimensions::new(w, h).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let file = encode(img, &LossyConfig::new().with_quality(90)).unwrap();
assert_eq!(&file[12..16], b"VP8X");
assert!(file.windows(4).any(|c| c == b"ALPH"));
match locate_image_with_alpha(&file).unwrap().image {
ImageChunk::Lossy(_) => {},
ImageChunk::Lossless(_) => panic!("expected a lossy VP8 image"),
}
assert_eq!(recover_alpha(&file, w, h), source_alpha);
}
#[test]
fn encode_is_deterministic_with_alpha() {
let (w, h) = (12u32, 12u32);
let mut pixels = Vec::new();
for y in 0..h {
for x in 0..w {
let a = byte(x * y);
pixels.extend_from_slice(&[byte(x), byte(y), 64, a]);
}
}
let dims = Dimensions::new(w, h).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let cfg = LossyConfig::new();
assert_eq!(encode(img, &cfg).unwrap(), encode(img, &cfg).unwrap());
}
#[test]
fn quality_clamps_and_defaults() {
assert_eq!(Quality::default().get(), 75);
assert_eq!(Quality::new(200).get(), 100);
assert_eq!(Quality::new(0).get(), 0);
assert_eq!(LossyConfig::new().quality().get(), 75);
assert_eq!(LossyConfig::new().with_quality(150).quality().get(), 100);
}
#[test]
fn effort_defaults_to_balanced_and_builds() {
assert_eq!(Effort::default(), Effort::Balanced);
assert_eq!(LossyConfig::new().effort(), Effort::Balanced);
assert_eq!(LossyConfig::default().effort(), Effort::Balanced);
assert_eq!(
LossyConfig::new().with_effort(Effort::Fast).effort(),
Effort::Fast
);
let cfg = LossyConfig::new()
.with_quality(40)
.with_effort(Effort::Best);
assert_eq!(cfg.effort(), Effort::Best);
assert_eq!(cfg.quality().get(), 40);
}
#[test]
fn efforts_map_to_their_frame_tiers() {
use crate::lossy::frame::Effort as FrameEffort;
assert_eq!(effort_tier(Effort::Fast), FrameEffort::Fast);
assert_eq!(effort_tier(Effort::Balanced), FrameEffort::Full);
assert_eq!(effort_tier(Effort::Best), FrameEffort::Best);
}
#[test]
fn encode_produces_a_decodable_lossy_webp() {
let dims = Dimensions::new(24, 16).unwrap();
let pixels = solid(24, 16);
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let file = encode(img, &LossyConfig::new()).unwrap();
assert_eq!(&file[0..4], b"RIFF");
assert_eq!(&file[8..12], b"WEBP");
assert_eq!(&file[12..16], b"VP8 ");
let (_dims, payload) = encode_vp8(img, &LossyConfig::new()).unwrap();
let decoded = crate::lossy::decode(&payload).unwrap();
assert_eq!((decoded.width(), decoded.height()), (24, 16));
}
#[test]
fn rejects_oversized_dimensions() {
let dims = Dimensions::new(16384, 1).unwrap();
let pixels = vec![0u8; 16384 * 4];
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
assert!(encode_vp8(img, &LossyConfig::new()).is_err());
}
#[test]
fn honors_non_rgba_layouts() {
let dims = Dimensions::new(16, 16).unwrap();
let mut pixels = Vec::new();
for _ in 0..16 * 16 {
pixels.extend_from_slice(&[200, 150, 100, 255]); }
let img = ImageRef::new(dims, PixelLayout::Bgra8, &pixels).unwrap();
let (_dims, payload) = encode_vp8(img, &LossyConfig::new()).unwrap();
let decoded = crate::lossy::decode(&payload).unwrap();
assert_eq!((decoded.width(), decoded.height()), (16, 16));
}
#[test]
fn metadata_free_opaque_encode_is_byte_identical_to_bare_vp8() {
let dims = Dimensions::new(24, 16).unwrap();
let pixels = solid(24, 16);
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let cfg = LossyConfig::new();
let file = encode(img, &cfg).unwrap();
let (_dims, payload) = encode_vp8(img, &cfg).unwrap();
assert_eq!(
file,
wrap_vp8(&payload),
"opaque metadata-free encode must be bare VP8"
);
assert!(!file.windows(4).any(|w| w == b"VP8X"));
}
#[test]
fn config_metadata_upgrades_opaque_to_vp8x_and_survives_byte_exact() {
let (metadata, pixels, dims) = meta_fixture();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let cfg = LossyConfig::new().with_metadata(metadata.clone());
let file = encode(img, &cfg).unwrap();
assert_eq!(
&file[12..16],
b"VP8X",
"metadata must force the extended form"
);
assert!(
!file.windows(4).any(|w| w == b"ALPH"),
"opaque: no ALPH chunk"
);
assert_eq!(chunk_bytes(&file, FourCc::ICCP), metadata.icc_profile);
assert_eq!(chunk_bytes(&file, FourCc::EXIF), metadata.exif);
assert_eq!(chunk_bytes(&file, FourCc::XMP), metadata.xmp);
let vp8x = locate_image_with_alpha(&file).unwrap().vp8x.unwrap();
assert!(vp8x.flags.has_icc() && vp8x.flags.has_exif() && vp8x.flags.has_xmp());
assert!(!vp8x.flags.has_alpha());
let decoded = crate::lossy::decode(vp8_of(&file)).unwrap();
assert_eq!((decoded.width(), decoded.height()), (16, 16));
}
#[test]
fn encode_image_preserves_metadata_by_default_with_alpha() {
let (metadata, _opaque, dims) = meta_fixture();
let mut pixels = Vec::new();
for y in 0..16u32 {
for x in 0..16u32 {
pixels.extend_from_slice(&[byte(x * 9), byte(y * 11), 100, byte((x + y) * 8)]);
}
}
let img = Image::from_parts(dims, PixelLayout::Rgba8, pixels, true, metadata.clone());
let file = encode_image(&img, &LossyConfig::new().with_quality(90)).unwrap();
assert_eq!(&file[12..16], b"VP8X");
assert!(file.windows(4).any(|w| w == b"ALPH"), "must carry ALPH");
assert_eq!(chunk_bytes(&file, FourCc::ICCP), metadata.icc_profile);
assert_eq!(chunk_bytes(&file, FourCc::EXIF), metadata.exif);
assert_eq!(chunk_bytes(&file, FourCc::XMP), metadata.xmp);
}
#[test]
fn encode_image_strip_private_keeps_icc_only() {
let (metadata, pixels, dims) = meta_fixture();
let img = Image::from_parts(dims, PixelLayout::Rgba8, pixels, false, metadata.clone());
let cfg = LossyConfig::new().with_metadata_policy(MetadataPolicy::StripPrivate);
let file = encode_image(&img, &cfg).unwrap();
assert_eq!(chunk_bytes(&file, FourCc::ICCP), metadata.icc_profile);
assert_eq!(chunk_bytes(&file, FourCc::EXIF), None);
assert_eq!(chunk_bytes(&file, FourCc::XMP), None);
}
#[test]
fn encode_image_no_metadata_is_byte_identical_to_bare_vp8() {
let dims = Dimensions::new(16, 16).unwrap();
let pixels = solid(16, 16);
let img = Image::from_parts(
dims,
PixelLayout::Rgba8,
pixels.clone(),
false,
Metadata::none(),
);
let via_image = encode_image(&img, &LossyConfig::new()).unwrap();
let via_ref = encode(
ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap(),
&LossyConfig::new(),
)
.unwrap();
assert_eq!(via_image, via_ref);
assert_eq!(&via_image[12..16], b"VP8 ");
assert!(!via_image.windows(4).any(|w| w == b"VP8X"));
}
#[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 = LossyConfig::new()
.with_metadata(override_meta.clone())
.with_metadata_policy(MetadataPolicy::StripPrivate);
assert_eq!(
config.resolve_metadata(&inherited),
override_meta.resolve(&inherited, MetadataPolicy::StripPrivate),
);
}
#[test]
fn metadata_encode_is_deterministic() {
let (metadata, pixels, dims) = meta_fixture();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let cfg = LossyConfig::new().with_metadata(metadata);
assert_eq!(encode(img, &cfg).unwrap(), encode(img, &cfg).unwrap());
}
#[test]
fn check_dimensions_accepts_the_exact_maximum_and_rejects_one_over() {
assert!(
super::check_dimensions(Dimensions::new(16383, 1).unwrap()).is_ok(),
"max width is valid"
);
assert!(
super::check_dimensions(Dimensions::new(1, 16383).unwrap()).is_ok(),
"max height is valid"
);
assert!(
super::check_dimensions(Dimensions::new(16383, 16383).unwrap()).is_ok(),
"max square is valid"
);
assert!(
super::check_dimensions(Dimensions::new(16384, 1).unwrap()).is_err(),
"one past max width is rejected"
);
assert!(
super::check_dimensions(Dimensions::new(1, 16384).unwrap()).is_err(),
"one past max height is rejected"
);
}
#[cfg(feature = "std")]
#[test]
fn encode_to_writes_the_same_bytes_as_encode() {
let dims = Dimensions::new(16, 16).unwrap();
let pixels = solid(16, 16);
let img = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap();
let cfg = LossyConfig::new();
let mut buf = Vec::new();
super::encode_to(img, &cfg, &mut buf).unwrap();
assert_eq!(
buf,
encode(img, &cfg).unwrap(),
"encode_to must write exactly the encode() bytes"
);
assert!(!buf.is_empty(), "encode_to must not write an empty buffer");
}
fn vp8_of(file: &[u8]) -> &[u8] {
match locate_image_with_alpha(file).unwrap().image {
ImageChunk::Lossy(payload) => payload,
ImageChunk::Lossless(_) => panic!("expected a lossy VP8 image"),
}
}
}