use crate::lossless::prelude::*;
use crate::{container, image};
pub(crate) mod animation;
pub(crate) mod bit_io;
pub(crate) mod color_cache;
pub(crate) mod constants;
pub(crate) mod decoder;
pub(crate) mod encoder;
pub(crate) mod histogram;
pub(crate) mod huffman;
pub(crate) mod lz77;
pub(crate) mod prelude;
pub(crate) mod transform;
pub(crate) mod vp8l;
pub(crate) mod work;
#[cfg(feature = "bench")]
pub mod bench {
#[inline]
#[must_use]
pub fn sweep_blue(
green: &[i8],
red: &[u8],
blue_base: &[i32],
sweep_red: bool,
channel: &mut alloc::vec::Vec<i8>,
) -> i8 {
crate::lossless::transform::cross_color::bench_sweep_blue(
green, red, blue_base, sweep_red, channel,
)
}
#[inline]
#[must_use]
pub fn sweep_blue_reference(
green: &[i8],
red: &[u8],
blue_base: &[i32],
sweep_red: bool,
) -> i8 {
crate::lossless::transform::cross_color::bench_sweep_blue_reference(
green, red, blue_base, sweep_red,
)
}
#[inline]
pub fn cross_color_inverse_row(row: &mut [u32], y: usize, bits: u32, tile_data: &[u32]) {
crate::lossless::transform::cross_color::inverse_row(row, y, bits, tile_data);
}
#[inline]
pub fn cross_color_inverse_row_reference(
row: &mut [u32],
y: usize,
bits: u32,
tile_data: &[u32],
) {
crate::lossless::transform::cross_color::inverse_row_reference(row, y, bits, tile_data);
}
}
pub use crate::{Codec, Dimensions, Effort, Error, Image, ImageRef, Metadata, PixelLayout, Result};
pub use animation::{
AnimInfo, BlendMode, CompositedFrame, CompositedFrames, DisposalMode, Frame, FrameMeta, Frames,
Vp8lFrameDecoder, decode_frames, decode_frames_with,
};
#[cfg(feature = "std")]
pub use decoder::Decoder;
pub use decoder::{
DecodeOptions, DecodedFrame, FrameDecoder, FramePayload, ImageInfo, IncrementalDecoder,
Progress, RowDrain, decode_vp8l,
};
pub use encoder::{AnimationEncoder, Empty, EncoderConfig, HasFrames, MetadataPolicy};
pub fn decode(input: &[u8]) -> Result<Image> {
decoder::decode_image(input, &DecodeOptions::default())
}
pub fn decode_with(input: &[u8], options: &DecodeOptions) -> Result<Image> {
decoder::decode_image(input, options)
}
pub fn decode_rgba(input: &[u8]) -> Result<(Dimensions, Vec<u8>)> {
let image = decode(input)?;
Ok((image.dimensions(), image.into_pixels()))
}
pub fn decode_alpha(payload: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
decode_alpha_with(payload, width, height, &DecodeOptions::default())
}
pub fn decode_alpha_with(
payload: &[u8],
width: u32,
height: u32,
options: &DecodeOptions,
) -> Result<Vec<u8>> {
let pixels = u64::from(width) * u64::from(height);
if let Some(limit) = options.max_pixels.filter(|&limit| pixels > limit) {
return Err(Error::LimitExceeded { pixels, limit });
}
vp8l::decode::decode_alpha_stream(payload, width, height)
}
#[must_use]
pub fn encode_alpha(alpha: &[u8], width: u32, height: u32) -> Vec<u8> {
vp8l::encode::encode_alpha_stream(alpha, width, height)
}
#[cfg(feature = "std")]
pub fn decode_reader<R: std::io::Read>(reader: R) -> Result<Image> {
Decoder::new(reader).decode()
}
#[allow(
clippy::unnecessary_wraps,
reason = "the Result is a deliberate, stable part of the public API so future \
encoder options can fail without a breaking signature change"
)]
pub fn encode(image: ImageRef<'_>, config: &EncoderConfig) -> Result<Vec<u8>> {
let argb = image::unpack_pixels(image.layout(), image.as_bytes());
let has_alpha = image::argb_has_alpha(&argb);
let dims = image.dimensions();
let payload = encoder::encode_payload(config.effort, dims.width(), dims.height(), &argb);
Ok(container::writer::wrap(
&payload,
dims,
&config.metadata,
has_alpha,
))
}
pub fn encode_image(image: &Image, config: &EncoderConfig) -> Result<Vec<u8>> {
let effective = EncoderConfig {
effort: config.effort,
metadata: config.resolve_metadata(image.metadata()),
policy: config.policy,
};
encode(image.as_image_ref(), &effective)
}
#[cfg(feature = "std")]
pub fn encode_to<W: std::io::Write>(
image: ImageRef<'_>,
config: &EncoderConfig,
mut writer: W,
) -> Result<()> {
let bytes = encode(image, config)?;
writer.write_all(&bytes)?;
Ok(())
}
#[must_use]
pub const fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[cfg(feature = "oracle")]
#[doc(hidden)]
#[must_use]
pub fn __vp8l_stream_equals_one_shot(webp: &[u8]) -> bool {
let Ok(parsed) = container::reader::parse_container(webp, false) else {
return false;
};
let payload = parsed.vp8l;
let one_shot = vp8l::decode::decode(payload).map(|d| (d.width, d.height, d.argb));
vp8l::decode_incr::split_patterns(payload.len())
.into_iter()
.all(|splits| {
let streamed = vp8l::decode_incr::stream_over_splits(payload, &splits)
.map(|(w, h, _a, px)| (w, h, px));
match (&one_shot, &streamed) {
(Ok(a), Ok(b)) => a == b,
(Err(_), Err(_)) => true,
_ => false,
}
})
}
#[cfg(test)]
mod tests {
use super::{
DecodeOptions, Dimensions, Effort, EncoderConfig, Error, Image, ImageRef, Metadata,
MetadataPolicy, PixelLayout, decode, decode_alpha, decode_alpha_with, decode_rgba,
decode_with, encode, encode_alpha, encode_image, version,
};
fn round_trip(width: u32, height: u32, rgba: &[u8]) {
let dims = Dimensions::new(width, height).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, rgba).unwrap();
let file = encode(img, &EncoderConfig::default()).unwrap();
assert_eq!(decode_rgba(&file).unwrap(), (dims, rgba.to_vec()));
}
#[test]
fn version_is_reported() {
assert!(!version().is_empty());
}
#[test]
fn decode_rejects_non_webp_input() {
assert_eq!(decode(&[]).unwrap_err(), Error::Truncated);
assert_eq!(
decode(b"not a webp file at all").unwrap_err(),
Error::NotWebp
);
}
#[test]
fn round_trips_a_1x1_pixel() {
round_trip(1, 1, &[10, 20, 30, 40]);
}
#[test]
fn round_trips_a_solid_block() {
let rgba: Vec<u8> = [12u8, 34, 56, 78]
.iter()
.copied()
.cycle()
.take(6 * 4)
.collect();
round_trip(3, 2, &rgba);
}
#[test]
fn round_trips_a_small_gradient() {
let (width, height) = (4u32, 3u32);
let mut rgba = Vec::new();
for y in 0..height {
for x in 0..width {
let red = u8::try_from(x * 40).unwrap();
let green = u8::try_from(y * 60).unwrap();
let blue = u8::try_from((x + y) * 20).unwrap();
rgba.extend_from_slice(&[red, green, blue, 255]);
}
}
round_trip(width, height, &rgba);
}
#[test]
fn image_ref_rejects_length_mismatch() {
let dims = Dimensions::new(2, 2).unwrap();
assert_eq!(
ImageRef::new(dims, PixelLayout::Rgba8, &[0u8; 15]).unwrap_err(),
Error::PixelBufferMismatch
);
}
#[test]
fn round_trips_a_non_rgba_layout() {
let bgra = vec![30u8, 20, 10, 255, 60, 50, 40, 200];
let dims = Dimensions::new(2, 1).unwrap();
let img = ImageRef::new(dims, PixelLayout::Bgra8, &bgra).unwrap();
let file = encode(img, &EncoderConfig::default()).unwrap();
let opts = DecodeOptions::default().layout(PixelLayout::Bgra8);
assert_eq!(decode_with(&file, &opts).unwrap().as_bytes(), &bgra[..]);
}
#[test]
fn round_trips_metadata_via_vp8x() {
let rgba = vec![1u8, 2, 3, 255, 4, 5, 6, 255];
let dims = Dimensions::new(2, 1).unwrap();
let metadata = Metadata {
icc_profile: Some(vec![9, 8, 7]),
xmp: Some(vec![b'<', b'x', b'>']),
..Metadata::none()
};
let config = EncoderConfig::new().with_metadata(metadata.clone());
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = encode(img, &config).unwrap();
let decoded = decode(&file).unwrap();
assert_eq!(decoded.metadata(), &metadata);
assert_eq!(decoded.as_bytes(), &rgba[..]);
}
#[test]
fn encode_image_preserves_metadata_by_default() {
let rgba = vec![1u8, 2, 3, 255, 4, 5, 6, 255];
let dims = Dimensions::new(2, 1).unwrap();
let metadata = Metadata {
icc_profile: Some(vec![9, 8, 7]),
exif: Some(vec![1, 1, 2, 3]),
xmp: Some(vec![b'<', b'x', b'>']),
};
let img = Image::from_parts(
dims,
PixelLayout::Rgba8,
rgba.clone(),
false,
metadata.clone(),
);
let file = encode_image(&img, &EncoderConfig::default()).unwrap();
let decoded = decode(&file).unwrap();
assert_eq!(decoded.metadata(), &metadata);
assert_eq!(decoded.as_bytes(), &rgba[..]);
}
#[test]
fn encode_image_strip_private_keeps_icc_only() {
let rgba = vec![1u8, 2, 3, 255, 4, 5, 6, 255];
let dims = Dimensions::new(2, 1).unwrap();
let metadata = Metadata {
icc_profile: Some(vec![9, 8, 7]),
exif: Some(vec![4, 5, 6]),
xmp: Some(vec![b'<', b'x', b'>']),
};
let img = Image::from_parts(dims, PixelLayout::Rgba8, rgba, false, metadata);
let config = EncoderConfig::default().with_metadata_policy(MetadataPolicy::StripPrivate);
let decoded = decode(&encode_image(&img, &config).unwrap()).unwrap();
assert_eq!(
decoded.metadata().icc_profile.as_deref(),
Some(&[9, 8, 7][..])
);
assert_eq!(decoded.metadata().exif, None);
assert_eq!(decoded.metadata().xmp, None);
}
#[test]
fn encode_image_config_override_wins() {
let rgba = vec![1u8, 2, 3, 255, 4, 5, 6, 255];
let dims = Dimensions::new(2, 1).unwrap();
let image_meta = Metadata {
exif: Some(vec![1]),
..Metadata::none()
};
let img = Image::from_parts(dims, PixelLayout::Rgba8, rgba, false, image_meta);
let config = EncoderConfig::default().with_metadata(Metadata {
exif: Some(vec![2]),
..Metadata::none()
});
let decoded = decode(&encode_image(&img, &config).unwrap()).unwrap();
assert_eq!(decoded.metadata().exif.as_deref(), Some(&[2][..]));
}
#[test]
fn encode_image_no_metadata_is_byte_identical_to_encode() {
let rgba = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
let dims = Dimensions::new(2, 1).unwrap();
let img = Image::from_parts(
dims,
PixelLayout::Rgba8,
rgba.clone(),
false,
Metadata::none(),
);
let via_image = encode_image(&img, &EncoderConfig::default()).unwrap();
let via_ref = encode(
ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap(),
&EncoderConfig::default(),
)
.unwrap();
assert_eq!(via_image, via_ref);
assert_eq!(&via_image[8..12], b"WEBP");
assert_eq!(&via_image[12..16], b"VP8L");
assert!(!via_image.windows(4).any(|w| w == b"VP8X"));
}
#[test]
fn decode_alpha_with_rejects_before_alloc() {
let opts = DecodeOptions::default().max_pixels(1024);
assert_eq!(
decode_alpha_with(&[0u8; 4], 16384, 16384, &opts).unwrap_err(),
Error::LimitExceeded {
pixels: 16384 * 16384,
limit: 1024,
}
);
}
#[test]
fn fast_effort_still_round_trips() {
let rgba: Vec<u8> = (0..16u8)
.flat_map(|v| [v, v.wrapping_mul(3), v.wrapping_mul(7), 255])
.collect();
let dims = Dimensions::new(4, 4).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = encode(img, &EncoderConfig::new().with_effort(Effort::Fast)).unwrap();
assert_eq!(decode_rgba(&file).unwrap(), (dims, rgba));
}
#[test]
fn version_matches_cargo_pkg_version() {
assert_eq!(version(), env!("CARGO_PKG_VERSION"));
}
#[test]
fn encode_alpha_round_trips_through_decode_alpha() {
let plane = vec![10u8, 90, 200, 5];
let payload = encode_alpha(&plane, 2, 2);
assert_eq!(decode_alpha(&payload, 2, 2).unwrap(), plane);
}
#[test]
fn decode_alpha_with_allows_exactly_the_limit() {
let plane = vec![10u8, 90, 200, 5];
let payload = encode_alpha(&plane, 2, 2);
let opts = DecodeOptions::default().max_pixels(4);
assert_eq!(decode_alpha_with(&payload, 2, 2, &opts).unwrap(), plane);
}
#[cfg(feature = "std")]
#[test]
fn encode_to_writes_the_encode_bytes() {
let rgba = vec![10u8, 20, 30, 255, 40, 50, 60, 255];
let dims = Dimensions::new(2, 1).unwrap();
let cfg = EncoderConfig::default();
let mut buf = Vec::new();
super::encode_to(
ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap(),
&cfg,
&mut buf,
)
.unwrap();
let direct = encode(
ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap(),
&cfg,
)
.unwrap();
assert!(!buf.is_empty());
assert_eq!(buf, direct);
}
#[cfg(feature = "bench")]
#[test]
fn bench_sweep_blue_shims_return_the_kernel_result() {
let green = [32i8];
let red = [0u8];
let blue_base = [20i32];
let mut channel = alloc::vec::Vec::new();
assert_eq!(
super::bench::sweep_blue(&green, &red, &blue_base, false, &mut channel),
20
);
assert_eq!(
super::bench::sweep_blue_reference(&green, &red, &blue_base, false),
20
);
}
#[cfg(feature = "bench")]
#[test]
fn bench_cross_color_inverse_row_shims_apply_the_transform() {
let expected = [0xFF30_4018u32];
let mut row = [0xFFF0_4000u32];
super::bench::cross_color_inverse_row(&mut row, 0, 0, &[0x0010_0020]);
assert_eq!(row, expected);
let mut row_ref = [0xFFF0_4000u32];
super::bench::cross_color_inverse_row_reference(&mut row_ref, 0, 0, &[0x0010_0020]);
assert_eq!(row_ref, expected);
}
#[cfg(feature = "oracle")]
#[test]
fn stream_equals_one_shot_true_on_valid_webp() {
let rgba = vec![
10u8, 20, 30, 255, 40, 50, 60, 255, 1, 2, 3, 255, 4, 5, 6, 255,
];
let dims = Dimensions::new(2, 2).unwrap();
let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap();
let file = encode(img, &EncoderConfig::default()).unwrap();
assert!(super::__vp8l_stream_equals_one_shot(&file));
}
#[cfg(feature = "oracle")]
#[test]
fn stream_equals_one_shot_false_on_non_webp() {
assert!(!super::__vp8l_stream_equals_one_shot(
b"definitely not a webp file"
));
}
#[cfg(feature = "oracle")]
#[test]
fn stream_equals_one_shot_true_when_both_paths_error() {
let rgba = vec![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 mut file = encode(img, &EncoderConfig::default()).unwrap();
let vp8l = file.windows(4).position(|w| w == b"VP8L").unwrap();
let sig = vp8l + 8; assert_ne!(file[sig], 0x00); file[sig] = 0x00; assert!(super::__vp8l_stream_equals_one_shot(&file));
}
}