#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(
clippy::float_arithmetic,
reason = "the codecs must be bit-deterministic across platforms; floating-point \
rounding is not portable. Use fixed-point integer math."
)]
#![allow(
clippy::redundant_pub_crate,
reason = "pub(crate) is our honest internal visibility; this nursery lint conflicts \
with the rustc unreachable_pub lint that we also enable"
)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(not(feature = "alloc"))]
compile_error!("webp requires an allocator: enable the `alloc` feature (implied by `std`)");
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[doc(hidden)]
pub mod alpha;
#[doc(hidden)]
pub mod anim;
#[doc(hidden)]
pub mod container;
#[doc(hidden)]
pub mod effort;
mod encoder;
#[doc(hidden)]
pub mod error;
#[doc(hidden)]
pub mod image;
#[cfg(feature = "image")]
mod interop;
#[doc(hidden)]
pub mod lossless;
#[doc(hidden)]
pub mod lossy;
mod prelude;
#[doc(hidden)]
pub mod stream;
#[cfg(feature = "work-count")]
#[doc(hidden)]
pub mod work_count;
pub use crate::anim::{
AnimInfo, BlendMode, CompositedFrame, DisposalMode, Frame, FrameMeta,
decode_frames_with_decoder,
};
pub use crate::effort::Effort;
pub use crate::error::{Codec, Error, Result};
pub use crate::image::{
Dimensions, Image, ImageRef, MAX_DIMENSION, Metadata, MetadataPolicy, PixelLayout,
};
pub use crate::stream::{
DEFAULT_MAX_PIXELS, DecodeOptions, DecodedFrame, FrameDecoder, FramePayload, ImageInfo,
Progress, RowDrain,
};
pub use encoder::{Encoder, Lossless, Lossy};
pub use crate::lossless::AnimationEncoder;
use crate::alpha::{AlphaCompression, parse_header, unfilter};
use crate::container::fourcc::FourCc;
use crate::container::reader::{ImageChunk, read_container};
use crate::container::scan::{declared_len, is_complete, scan_chunks};
use crate::container::vp8x::{VP8X_PAYLOAD_LEN, Vp8xInfo};
#[cfg(feature = "alloc")]
pub type Frames<'a> = crate::anim::Frames<'a, WebpFrameDecoder>;
#[cfg(feature = "alloc")]
pub type CompositedFrames<'a> = crate::anim::CompositedFrames<'a, WebpFrameDecoder>;
pub fn decode(input: &[u8]) -> Result<Image> {
decode_with(input, &DecodeOptions::default())
}
#[cfg(feature = "alloc")]
pub fn decode_with(input: &[u8], options: &DecodeOptions) -> Result<Image> {
let c = read_container(input, options.read_metadata)?;
if c.animated {
return first_composited_frame(input, options);
}
match c.image.ok_or(Error::MissingImage)? {
ImageChunk::Lossless(payload) => {
let image = crate::lossless::decode_vp8l(payload, options)?;
if c.vp8x.is_some_and(|vp8x| vp8x.canvas != image.dimensions()) {
return Err(Error::InvalidContainer);
}
Ok(image.with_metadata(c.metadata))
},
ImageChunk::Lossy(payload) => {
let mut image = crate::lossy::decode_with(payload, options)?;
if let Some(alph) = c.alpha {
let plane = alpha_plane_with(alph, image.width(), image.height(), options)?;
image.apply_alpha_plane(&plane)?;
}
Ok(image.with_metadata(c.metadata))
},
}
}
#[cfg(feature = "alloc")]
pub fn decode_rgba(input: &[u8]) -> Result<(Dimensions, Vec<u8>)> {
let image = decode(input)?;
Ok((image.dimensions(), image.into_pixels()))
}
#[cfg(feature = "std")]
pub fn decode_reader<R: std::io::Read>(mut reader: R) -> Result<Image> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
decode(&buf)
}
#[cfg(feature = "alloc")]
pub fn encode_lossless_rgba(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>> {
let image = ImageRef::new(Dimensions::new(width, height)?, PixelLayout::Rgba8, rgba)?;
Encoder::lossless().encode_ref(image)
}
#[cfg(feature = "alloc")]
pub fn encode_lossy_rgba(width: u32, height: u32, rgba: &[u8], quality: u8) -> Result<Vec<u8>> {
let image = ImageRef::new(Dimensions::new(width, height)?, PixelLayout::Rgba8, rgba)?;
Encoder::lossy().quality(quality).encode_ref(image)
}
#[cfg(feature = "alloc")]
fn alpha_plane(alph: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
alpha_plane_with(alph, width, height, &DecodeOptions::default())
}
#[cfg(feature = "alloc")]
fn alpha_plane_with(
alph: &[u8],
width: u32,
height: u32,
options: &DecodeOptions,
) -> Result<Vec<u8>> {
let (w, h) = (width as usize, height as usize);
let count = w.checked_mul(h).ok_or(Error::Truncated)?;
let (header, data) = parse_header(alph)?;
let mut plane = match header.compression {
AlphaCompression::None => data.get(..count).ok_or(Error::Truncated)?.to_vec(),
AlphaCompression::Lossless => {
crate::lossless::decode_alpha_with(data, width, height, options)?
},
};
unfilter(header.filter, &mut plane, w, h);
Ok(plane)
}
#[cfg(feature = "alloc")]
pub fn decode_frames(input: &[u8]) -> Result<Frames<'_>> {
decode_frames_with(input, &DecodeOptions::default())
}
#[cfg(feature = "alloc")]
pub fn decode_frames_with<'a>(input: &'a [u8], options: &DecodeOptions) -> Result<Frames<'a>> {
crate::decode_frames_with_decoder(input, options, WebpFrameDecoder)
}
#[cfg(feature = "alloc")]
#[must_use]
pub fn incremental_decoder() -> IncrementalDecoder {
IncrementalDecoder::new()
}
#[cfg(feature = "alloc")]
enum Backend {
Undecided,
Lossless(Box<crate::lossless::IncrementalDecoder<WebpFrameDecoder>>),
Lossy(Box<crate::lossy::IncrementalDecoder>),
Deferred,
}
#[cfg(feature = "alloc")]
enum Decision {
Lossless,
Lossy,
Deferred(ImageInfo),
NeedMore,
}
#[cfg(feature = "alloc")]
pub struct IncrementalDecoder {
buf: Vec<u8>,
options: DecodeOptions,
reported_header: bool,
image: Option<Image>,
backend: Backend,
}
#[cfg(feature = "alloc")]
impl IncrementalDecoder {
#[must_use]
pub fn new() -> Self {
Self::with_options(DecodeOptions::default())
}
#[must_use]
pub const fn with_options(options: DecodeOptions) -> Self {
Self {
buf: Vec::new(),
options,
reported_header: false,
image: None,
backend: Backend::Undecided,
}
}
pub fn push(&mut self, chunk: &[u8]) -> Result<Progress> {
match &mut self.backend {
Backend::Lossless(be) => return be.push(chunk),
Backend::Lossy(be) => return be.push(chunk),
Backend::Deferred => {
self.buf.extend_from_slice(chunk);
return self.drive_deferred();
},
Backend::Undecided => {},
}
if self.image.is_some() {
return Ok(Progress::Finished);
}
self.buf.extend_from_slice(chunk);
match classify(&self.buf)? {
Decision::Lossless => {
let mut be = crate::lossless::IncrementalDecoder::with_options_and_decoder(
self.options.clone(),
WebpFrameDecoder,
);
let progress = be.push(&self.buf)?;
self.backend = Backend::Lossless(Box::new(be));
Ok(progress)
},
Decision::Lossy => {
let mut be = crate::lossy::IncrementalDecoder::with_options(self.options.clone());
let progress = be.push(&self.buf)?;
self.backend = Backend::Lossy(Box::new(be));
Ok(progress)
},
Decision::Deferred(info) => {
self.backend = Backend::Deferred;
if !self.reported_header {
self.reported_header = true;
return Ok(Progress::HeaderReady(info));
}
self.drive_deferred()
},
Decision::NeedMore => {
if is_complete(&self.buf) {
self.image = Some(decode(&self.buf)?);
Ok(Progress::Finished)
} else {
Ok(Progress::NeedMoreInput)
}
},
}
}
fn drive_deferred(&mut self) -> Result<Progress> {
if is_complete(&self.buf) {
self.image = Some(decode(&self.buf)?);
Ok(Progress::Finished)
} else {
Ok(Progress::NeedMoreInput)
}
}
#[must_use]
pub fn frame_image(&self) -> Option<&Image> {
match &self.backend {
Backend::Lossless(be) => be.frame_image(),
_ => None,
}
}
pub fn drain_rows(&mut self) -> Option<RowDrain<'_>> {
match &mut self.backend {
Backend::Lossless(be) => be.drain_rows(),
Backend::Lossy(be) => be.drain_rows(),
Backend::Undecided | Backend::Deferred => None,
}
}
pub fn into_image(self) -> Result<Image> {
if let Some(image) = self.image {
return Ok(image);
}
match self.backend {
Backend::Lossless(be) => be.into_image(),
Backend::Lossy(be) => be.into_image(),
Backend::Undecided | Backend::Deferred => decode(&self.buf),
}
}
}
#[cfg(feature = "alloc")]
impl Default for IncrementalDecoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "alloc")]
fn classify(buf: &[u8]) -> Result<Decision> {
if buf.len() < 12 {
return Ok(Decision::NeedMore);
}
if buf[0..4] != FourCc::RIFF.0 || buf[8..12] != FourCc::WEBP.0 {
return Err(Error::NotWebp);
}
let mut vp8x_info: Option<ImageInfo> = None;
for chunk in scan_chunks(buf) {
match chunk.id {
FourCc::VP8X => {
let Some(data) =
buf.get(chunk.payload_start..chunk.payload_start + VP8X_PAYLOAD_LEN)
else {
return Ok(Decision::NeedMore);
};
let info = Vp8xInfo::parse(data)?;
if info.flags.is_animated() {
return Ok(Decision::Lossless);
}
vp8x_info = Some(ImageInfo::new(
info.canvas,
info.flags.has_alpha(),
info.flags.has_icc() || info.flags.has_exif() || info.flags.has_xmp(),
false,
));
},
FourCc::VP8L | FourCc::ANIM | FourCc::ANMF => return Ok(Decision::Lossless),
FourCc::VP8 => {
if let Some(info) = vp8x_info {
return Ok(Decision::Deferred(info)); }
return match declared_len(buf) {
Some(riff_end) if chunk.next >= riff_end => Ok(Decision::Lossy),
Some(_) => Ok(bare_deferred(buf.get(chunk.payload_start..))),
None => Ok(Decision::NeedMore),
};
},
_ => {},
}
}
Ok(Decision::NeedMore)
}
#[cfg(feature = "alloc")]
fn bare_deferred(vp8_payload: Option<&[u8]>) -> Decision {
vp8_payload
.and_then(|p| crate::lossy::peek_dimensions(p).ok())
.map_or(Decision::NeedMore, |dimensions| {
Decision::Deferred(ImageInfo::new(dimensions, false, false, false))
})
}
#[cfg(feature = "alloc")]
fn first_composited_frame(input: &[u8], options: &DecodeOptions) -> Result<Image> {
decode_frames_with(input, options)?
.composited()
.next()
.ok_or(Error::MissingImage)?
.map(CompositedFrame::into_image)
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, Copy, Default)]
pub struct WebpFrameDecoder;
#[cfg(feature = "alloc")]
impl crate::FrameDecoder for WebpFrameDecoder {
fn decode_frame(
&self,
frame: crate::FramePayload<'_>,
options: &DecodeOptions,
) -> Result<crate::DecodedFrame> {
if frame.vp8l.is_some() {
return crate::lossless::Vp8lFrameDecoder.decode_frame(frame, options);
}
let Some(payload) = frame.vp8 else {
return Err(Error::MissingImage);
};
let pixels = frame.dims.pixel_count();
if let Some(limit) = options.max_pixels.filter(|&l| pixels > l) {
return Err(Error::LimitExceeded { pixels, limit });
}
let (dims, mut argb) = crate::lossy::decode_argb(payload)?;
if dims != frame.dims {
return Err(Error::InvalidContainer);
}
if let Some(alph) = frame.alph {
let plane = alpha_plane(alph, dims.width(), dims.height())?;
for (pixel, &a) in argb.iter_mut().zip(&plane) {
*pixel = (*pixel & 0x00FF_FFFF) | (u32::from(a) << 24);
}
}
Ok(crate::DecodedFrame {
argb,
alpha_used: frame.alph.is_some(),
})
}
}
pub fn is_animated(input: &[u8]) -> Result<bool> {
crate::container::reader::is_animated(input)
}
#[must_use]
pub const fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[cfg(test)]
mod tests {
use crate::container::fourcc::FourCc;
use crate::container::writer::{push_chunk, riff_envelope};
use super::{
BlendMode, DecodeOptions, Dimensions, DisposalMode, Effort, Encoder, Error, FrameMeta,
Image, ImageRef, IncrementalDecoder, Metadata, MetadataPolicy, PixelLayout, Progress,
decode, decode_with,
};
#[test]
fn round_trips_a_lossless_image_through_the_umbrella() {
let rgba = [10u8, 20, 30, 255, 40, 50, 60, 255];
let dims = Dimensions::new(2, 1).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = Encoder::lossless().encode_ref(img).unwrap();
let decoded = decode(&file).unwrap();
assert_eq!(decoded.dimensions(), dims);
assert_eq!(decoded.as_bytes(), &rgba[..]);
}
#[test]
fn round_trips_a_lossy_image_through_the_umbrella() {
let mut rgba = Vec::new();
for y in 0..16u8 {
for x in 0..16u8 {
rgba.extend_from_slice(&[x * 16, y * 16, 128, 255]);
}
}
let dims = Dimensions::new(16, 16).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = Encoder::lossy().quality(90).encode_ref(img).unwrap();
assert_eq!(&file[12..16], b"VP8 ", "lossy chunk fourcc");
let decoded = decode(&file).unwrap();
assert_eq!(decoded.dimensions(), dims);
assert!(decoded.as_bytes().chunks_exact(4).all(|p| p[3] == 0xff));
}
#[test]
fn lossy_alpha_round_trips_byte_exact_through_the_umbrella() {
let (w, h) = (24u32, 20u32);
let mut rgba = Vec::new();
let mut source_alpha = Vec::new();
for y in 0..h {
for x in 0..w {
let a = if x < 4 && y < 4 {
0
} else if x >= w - 4 && y >= h - 4 {
255
} else {
u8::try_from(((x + y) * 255) / (w + h - 2)).unwrap_or(255)
};
source_alpha.push(a);
let px = [
u8::try_from((x * 9) & 0xff).unwrap_or(0),
u8::try_from((y * 11) & 0xff).unwrap_or(0),
100,
a,
];
rgba.extend_from_slice(&px);
}
}
let dims = Dimensions::new(w, h).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = Encoder::lossy().quality(90).encode_ref(img).unwrap();
assert_eq!(
&file[12..16],
b"VP8X",
"alpha image must use the extended form"
);
assert!(
file.windows(4).any(|c| c == b"ALPH"),
"must carry an ALPH chunk"
);
let decoded = decode(&file).unwrap();
assert_eq!(decoded.dimensions(), dims);
assert!(decoded.has_alpha());
let decoded_alpha: Vec<u8> = decoded.as_bytes().chunks_exact(4).map(|p| p[3]).collect();
assert_eq!(decoded_alpha, source_alpha, "alpha must be lossless");
}
#[test]
fn round_trips_each_lossy_effort_through_the_umbrella() {
let mut rgba = Vec::new();
for y in 0..16u8 {
for x in 0..16u8 {
rgba.extend_from_slice(&[x * 16, y * 16, 128, 255]);
}
}
let dims = Dimensions::new(16, 16).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
for effort in [Effort::Fast, Effort::Balanced, Effort::Best] {
let file = Encoder::lossy()
.quality(90)
.effort(effort)
.encode_ref(img)
.unwrap();
assert_eq!(&file[12..16], b"VP8 ", "{effort:?}: lossy chunk fourcc");
let decoded = decode(&file).unwrap();
assert_eq!(decoded.dimensions(), dims, "{effort:?}: dims");
assert!(
decoded.as_bytes().chunks_exact(4).all(|p| p[3] == 0xff),
"{effort:?}: not fully opaque"
);
}
}
#[test]
fn encode_image_lossy_preserves_metadata_through_the_umbrella() {
let mut rgba = Vec::new();
for y in 0..16u8 {
for x in 0..16u8 {
rgba.extend_from_slice(&[x * 16, y * 16, 128, 255]);
}
}
let dims = Dimensions::new(16, 16).unwrap();
let metadata = Metadata {
icc_profile: Some(b"icc".to_vec()),
exif: Some(b"exif".to_vec()),
xmp: Some(b"<x/>".to_vec()),
};
let img = Image::from_parts(dims, PixelLayout::Rgba8, rgba, false, metadata.clone());
let file = Encoder::lossy().quality(90).encode(&img).unwrap();
assert_eq!(
&file[12..16],
b"VP8X",
"metadata must force the extended form"
);
let find = |id: &[u8; 4]| -> Option<Vec<u8>> {
crate::container::reader::chunks(&file)
.unwrap()
.filter_map(Result::ok)
.find(|c| &c.id.0 == id)
.map(|c| c.data.to_vec())
};
assert_eq!(find(b"ICCP").as_deref(), metadata.icc_profile.as_deref());
assert_eq!(find(b"EXIF").as_deref(), metadata.exif.as_deref());
assert_eq!(find(b"XMP ").as_deref(), metadata.xmp.as_deref());
assert_eq!(decode(&file).unwrap().dimensions(), dims);
}
#[test]
fn lossy_decode_surfaces_vp8x_metadata_and_round_trips() {
let mut rgba = Vec::new();
for y in 0..16u8 {
for x in 0..16u8 {
rgba.extend_from_slice(&[x * 16, y * 16, 200, 255]);
}
}
let dims = Dimensions::new(16, 16).unwrap();
let metadata = Metadata {
icc_profile: Some(b"icc-profile-bytes".to_vec()),
exif: Some(b"exif-bytes".to_vec()),
xmp: Some(b"<x:xmpmeta/>".to_vec()),
};
let img = Image::from_parts(dims, PixelLayout::Rgba8, rgba, false, metadata.clone());
let file = Encoder::lossy().quality(90).encode(&img).unwrap();
let decoded = decode(&file).unwrap();
assert_eq!(
decoded.metadata().icc_profile.as_deref(),
metadata.icc_profile.as_deref()
);
assert_eq!(decoded.metadata().exif.as_deref(), metadata.exif.as_deref());
assert_eq!(decoded.metadata().xmp.as_deref(), metadata.xmp.as_deref());
let refile = Encoder::lossy().quality(90).encode(&decoded).unwrap();
let round = decode(&refile).unwrap();
assert_eq!(
round.metadata(),
&metadata,
"decode → Encoder::lossy().encode must preserve metadata"
);
let bare = Encoder::lossy()
.quality(90)
.encode_ref(ImageRef::new(dims, PixelLayout::Rgba8, img.as_bytes()).unwrap())
.unwrap();
assert_eq!(decode(&bare).unwrap().metadata(), &Metadata::none());
}
#[test]
fn dispatches_a_lossy_file_to_the_vp8_decoder() {
let vp8_key_frame = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0];
let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8, &vp8_key_frame);
let file = riff_envelope(&body);
let image = decode(&file).unwrap();
assert_eq!(image.dimensions(), Dimensions::new(16, 16).unwrap());
}
#[test]
fn composites_a_raw_alpha_chunk_onto_a_lossy_image() {
let vp8_key_frame = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0];
let mut alph = Vec::new();
alph.push(0x00u8); alph.resize(1 + 16 * 16, 0x80); let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8, &vp8_key_frame);
push_chunk(&mut body, FourCc::ALPH, &alph);
let file = riff_envelope(&body);
let image = decode(&file).unwrap();
assert_eq!(image.dimensions(), Dimensions::new(16, 16).unwrap());
assert!(image.has_alpha());
assert!(image.as_bytes().chunks_exact(4).all(|px| px[3] == 0x80));
}
#[test]
fn rejects_a_non_webp_input() {
assert_eq!(
decode(b"definitely not a webp file").unwrap_err(),
Error::NotWebp
);
}
#[test]
fn unified_streams_a_bare_lossy_still_and_drains_rows() {
let vp8 = include_bytes!("../tests/fixtures/noise_32x24_q30.vp8");
let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8, vp8);
let file = riff_envelope(&body);
let expected = decode(&file).unwrap();
let mut dec = IncrementalDecoder::new();
let mut drained = 0u32;
for byte in &file {
dec.push(core::slice::from_ref(byte)).unwrap();
if let Some(rows) = dec.drain_rows() {
drained += rows.rows;
}
}
assert!(drained > 0, "a lossy still must stream rows");
assert_eq!(dec.into_image().unwrap().as_bytes(), expected.as_bytes());
}
#[test]
fn unified_streams_a_lossless_still() {
let rgba: Vec<u8> = (0..16u8).flat_map(|i| [i * 3, i * 5, i * 7, 255]).collect();
let dims = Dimensions::new(4, 4).unwrap();
let file = Encoder::lossless()
.encode_ref(ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap())
.unwrap();
let expected = decode(&file).unwrap();
let mut dec = IncrementalDecoder::new();
for chunk in file.chunks(3) {
dec.push(chunk).unwrap();
}
assert_eq!(dec.into_image().unwrap().as_bytes(), expected.as_bytes());
}
#[test]
fn unified_defers_a_lossy_still_with_alpha() {
let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0];
let mut alph = vec![0x00u8]; alph.resize(1 + 16 * 16, 0x80);
let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8, &vp8);
push_chunk(&mut body, FourCc::ALPH, &alph);
let file = riff_envelope(&body);
let expected = decode(&file).unwrap();
let mut dec = IncrementalDecoder::new();
let mut any_drain = false;
for byte in &file {
dec.push(core::slice::from_ref(byte)).unwrap();
any_drain |= dec.drain_rows().is_some();
}
assert!(!any_drain, "the deferred alpha path streams no rows");
let image = dec.into_image().unwrap();
assert!(image.has_alpha());
assert_eq!(image.as_bytes(), expected.as_bytes());
}
#[test]
fn decode_with_enforces_max_pixels_on_a_lossless_still() {
let rgba: Vec<u8> = (0u8..64).flat_map(|i| [i, 0, 0, 255]).collect();
let dims = Dimensions::new(8, 8).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = Encoder::lossless().encode_ref(img).unwrap();
assert_eq!(
decode_with(&file, &DecodeOptions::default().max_pixels(10)).unwrap_err(),
Error::LimitExceeded {
pixels: 64,
limit: 10,
}
);
assert_eq!(
decode_with(&file, &DecodeOptions::default())
.unwrap()
.dimensions(),
dims
);
}
#[test]
fn plain_decode_is_bounded_by_default_on_a_hostile_header() {
let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0xFF, 0x3F, 0xFF, 0x3F];
let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8, &vp8);
let file = riff_envelope(&body);
assert!(
matches!(decode(&file), Err(Error::LimitExceeded { limit, .. })
if limit == crate::DEFAULT_MAX_PIXELS),
"plain decode must enforce DEFAULT_MAX_PIXELS on an oversized header"
);
assert!(
!matches!(
decode_with(&file, &DecodeOptions::default().unbounded()),
Err(Error::LimitExceeded { .. })
),
"unbounded() must lift the pixel cap"
);
}
#[test]
fn decode_with_enforces_max_pixels_on_lossy_still_and_frames() {
let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0];
let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8, &vp8);
let file = riff_envelope(&body);
assert_eq!(
decode_with(&file, &DecodeOptions::default().max_pixels(4)).unwrap_err(),
Error::LimitExceeded {
pixels: 256,
limit: 4,
}
);
let canvas = Dimensions::new(2, 2).unwrap();
let red = [255u8, 0, 0, 255].repeat(4);
let meta = FrameMeta {
x: 0,
y: 0,
dimensions: canvas,
duration_ms: 100,
blend: BlendMode::Blend,
dispose: DisposalMode::Keep,
};
let anim = crate::lossless::AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &red).unwrap(),
meta,
)
.unwrap()
.finish();
assert!(matches!(
decode_with(&anim, &DecodeOptions::default().max_pixels(1)),
Err(Error::LimitExceeded { .. })
));
}
#[test]
fn metadata_policy_is_reexported_from_the_umbrella() {
let dims = Dimensions::new(1, 1).unwrap();
let metadata = Metadata {
icc_profile: Some(vec![1]),
exif: Some(vec![2]),
xmp: Some(vec![3]),
};
let img = Image::from_parts(
dims,
PixelLayout::Rgba8,
vec![0, 0, 0, 255],
false,
metadata,
);
let file = Encoder::lossless()
.metadata_policy(MetadataPolicy::StripPrivate)
.encode(&img)
.unwrap();
let decoded = decode(&file).unwrap();
assert_eq!(decoded.metadata().icc_profile.as_deref(), Some(&[1][..]));
assert_eq!(decoded.metadata().exif, None);
assert_eq!(decoded.metadata().xmp, None);
}
#[test]
fn unified_streams_animation_frames() {
let canvas = Dimensions::new(2, 2).unwrap();
let red = [255u8, 0, 0, 255].repeat(4);
let blue = [0u8, 0, 255, 255].repeat(4);
let meta = |ms| FrameMeta {
x: 0,
y: 0,
dimensions: canvas,
duration_ms: ms,
blend: BlendMode::Blend,
dispose: DisposalMode::Keep,
};
let file = crate::lossless::AnimationEncoder::new(canvas)
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &red).unwrap(),
meta(100),
)
.unwrap()
.add_frame(
ImageRef::new(canvas, PixelLayout::Rgba8, &blue).unwrap(),
meta(100),
)
.unwrap()
.finish();
let expected = decode(&file).unwrap();
let mut dec = IncrementalDecoder::new();
let mut chunks = file.chunks(5);
let mut frames = 0;
loop {
let progress = match chunks.next() {
Some(chunk) => dec.push(chunk),
None => dec.push(&[]),
}
.unwrap();
match progress {
Progress::FrameComplete(_) => {
frames += 1;
assert!(dec.frame_image().is_some());
},
Progress::Finished => break,
_ => {},
}
}
assert_eq!(frames, 2, "both frames composited");
assert_eq!(dec.into_image().unwrap().as_bytes(), expected.as_bytes());
}
}