mod common;
use common::{
libwebp_decode_rgba, libwebp_encode_lossless_rgba, libwebp_get_info, pattern_rgba,
photo_like_rgba,
};
use gamut_core::{DecodeImage, Dimensions, EncodeImage, ImageBuf, ImageRef, Rgb8, Rgba8};
use gamut_webp::{WebpDecoder, WebpEncoder};
const DIMENSIONS: &[(u32, u32)] = &[
(1, 1),
(2, 2),
(16, 16),
(17, 9),
(64, 48),
(255, 1),
(1, 255),
];
const LARGE_DIMENSIONS: &[(u32, u32)] =
&[(256, 256), (384, 288), (640, 480), (1024, 768), (300, 70)];
fn rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
rgba.chunks_exact(4)
.flat_map(|p| [p[0], p[1], p[2]])
.collect()
}
#[test]
fn libwebp_lossless_self_roundtrip() {
for (w, h) in [(1u32, 1u32), (16, 16), (17, 9), (64, 48)] {
let rgba = pattern_rgba(w, h);
let webp = libwebp_encode_lossless_rgba(&rgba, w, h);
assert!(!webp.is_empty(), "encode produced no bytes at {w}x{h}");
assert_eq!(
libwebp_get_info(&webp),
Some((w, h)),
"get_info mismatch at {w}x{h}"
);
let decoded = libwebp_decode_rgba(&webp);
assert_eq!((decoded.width, decoded.height), (w, h));
assert_eq!(
decoded.rgba, rgba,
"lossless must round-trip bit-exactly at {w}x{h}"
);
}
}
#[test]
fn gamut_decodes_libwebp_lossless_to_source() {
for &(w, h) in DIMENSIONS {
let rgba = pattern_rgba(w, h);
let webp = libwebp_encode_lossless_rgba(&rgba, w, h);
let got: ImageBuf<Rgb8> = WebpDecoder::new()
.decode_image(&webp)
.expect("gamut decode");
let dims = got.dimensions();
assert_eq!(
(dims.width, dims.height),
(w, h),
"dims mismatch at {w}x{h}"
);
assert_eq!(
got.as_samples(),
rgba_to_rgb(&rgba).as_slice(),
"pixel mismatch at {w}x{h}"
);
}
}
#[test]
fn libwebp_decodes_gamut_lossless_to_source() {
for &(w, h) in DIMENSIONS {
let rgb = rgba_to_rgb(&pattern_rgba(w, h));
let mut webp = Vec::new();
WebpEncoder::lossless()
.encode_image(
ImageRef::<Rgb8>::new(
&rgb,
Dimensions {
width: w,
height: h,
},
)
.unwrap(),
&mut webp,
)
.expect("gamut encode");
assert_eq!(libwebp_get_info(&webp), Some((w, h)), "get_info at {w}x{h}");
let decoded = libwebp_decode_rgba(&webp);
assert_eq!((decoded.width, decoded.height), (w, h));
assert_eq!(rgba_to_rgb(&decoded.rgba), rgb, "pixel mismatch at {w}x{h}");
}
}
fn assert_gamut_encode_libwebp_decode(rgb: &[u8], w: u32, h: u32, label: &str) {
let mut webp = Vec::new();
WebpEncoder::lossless()
.encode_image(
ImageRef::<Rgb8>::new(
rgb,
Dimensions {
width: w,
height: h,
},
)
.unwrap(),
&mut webp,
)
.expect("gamut encode");
let decoded = libwebp_decode_rgba(&webp);
assert_eq!((decoded.width, decoded.height), (w, h), "dims for {label}");
assert_eq!(rgba_to_rgb(&decoded.rgba), rgb, "pixels for {label}");
}
#[test]
fn libwebp_decodes_every_gamut_encoder_path() {
let (w, h) = (40u32, 40u32);
let n = (w * h) as usize;
let solid: Vec<u8> = [30u8, 60, 90].repeat(n);
assert_gamut_encode_libwebp_decode(&solid, w, h, "solid");
let palette = [[10u8, 20, 30], [40, 50, 60], [70, 80, 90]];
let few: Vec<u8> = (0..n).flat_map(|i| palette[i % 3]).collect();
assert_gamut_encode_libwebp_decode(&few, w, h, "few-color");
let regioned: Vec<u8> = (0..n)
.flat_map(|i| {
let (x, y) = (i as u32 % w, i as u32 / w);
let scatter = ((x * 7 + y * 11) % 16) as u8;
let base = if y < h / 2 { 0 } else { 16 };
let idx = base + scatter;
[idx, idx.wrapping_mul(7), idx.wrapping_mul(13)]
})
.collect();
assert_gamut_encode_libwebp_decode(®ioned, w, h, "multi-region");
let many: Vec<u8> = (0..n)
.flat_map(|i| {
let (x, y) = (i as u32 % w, i as u32 / w);
[
(x * 9 + y * 5) as u8,
(x * 13 + y * 7) as u8,
(x * 17 + y * 3) as u8,
]
})
.collect();
assert_gamut_encode_libwebp_decode(&many, w, h, "many-color");
}
#[test]
fn gamut_decodes_libwebp_lossless_realistic_and_large() {
for &(w, h) in DIMENSIONS.iter().chain(LARGE_DIMENSIONS) {
let rgba = photo_like_rgba(w, h, 0x51ed);
let webp = libwebp_encode_lossless_rgba(&rgba, w, h);
let got: ImageBuf<Rgb8> = WebpDecoder::new()
.decode_image(&webp)
.expect("gamut decode");
let dims = got.dimensions();
assert_eq!((dims.width, dims.height), (w, h), "dims at {w}x{h}");
assert_eq!(
got.as_samples(),
rgba_to_rgb(&rgba).as_slice(),
"pixel mismatch at {w}x{h}"
);
}
}
#[test]
fn libwebp_decodes_gamut_lossless_realistic_and_large() {
for &(w, h) in DIMENSIONS.iter().chain(LARGE_DIMENSIONS) {
let rgb = rgba_to_rgb(&photo_like_rgba(w, h, 0x9a1c));
assert_gamut_encode_libwebp_decode(&rgb, w, h, &format!("realistic {w}x{h}"));
}
}
fn synthetic_yuv(w: u32, h: u32) -> gamut_color::Yuv420 {
let (wu, hu) = (w as usize, h as usize);
let (cw, ch) = (
gamut_color::Yuv420::chroma_width(w) as usize,
gamut_color::Yuv420::chroma_height(h) as usize,
);
let y = (0..wu * hu)
.map(|i| ((i * 9 + (i / wu) * 5) & 0xff) as u8)
.collect();
let u = (0..cw * ch).map(|i| ((i * 3 + 80) & 0xff) as u8).collect();
let v = (0..cw * ch).map(|i| ((i * 7 + 150) & 0xff) as u8).collect();
gamut_color::Yuv420::new(w, h, y, u, v).unwrap()
}
fn detailed_yuv(w: u32, h: u32) -> gamut_color::Yuv420 {
let (wu, hu) = (w as usize, h as usize);
let (cw, ch) = (
gamut_color::Yuv420::chroma_width(w) as usize,
gamut_color::Yuv420::chroma_height(h) as usize,
);
let y = (0..wu * hu)
.map(|i| {
let (x, yy) = (i % wu, i / wu);
let v = match (x / 4 + yy / 4) % 4 {
0 => x * 18,
1 => yy * 18,
2 => (x + yy) * 18,
_ => x.wrapping_sub(yy).wrapping_mul(18),
};
(v & 0xff) as u8
})
.collect();
let u = (0..cw * ch).map(|i| ((i * 3) & 0xff) as u8).collect();
let v = (0..cw * ch).map(|i| ((i * 9 + 70) & 0xff) as u8).collect();
gamut_color::Yuv420::new(w, h, y, u, v).unwrap()
}
fn photo_like_yuv(w: u32, h: u32, seed: u32) -> gamut_color::Yuv420 {
let (wu, hu) = (w as usize, h as usize);
let (cw, ch) = (
gamut_color::Yuv420::chroma_width(w) as usize,
gamut_color::Yuv420::chroma_height(h) as usize,
);
let hash = |x: i64, y: i64, k: i64| -> i64 {
let mut v = x.wrapping_mul(374_761_393)
^ y.wrapping_mul(668_265_263)
^ k.wrapping_mul(2_654_435_761)
^ i64::from(seed).wrapping_mul(2_246_822_519);
v = (v ^ (v >> 13)).wrapping_mul(1_274_126_177);
(v ^ (v >> 16)) & 0xff
};
let clamp = |v: i64| v.clamp(0, 255) as u8;
let (wi, hi) = (wu.max(1) as i64, hu.max(1) as i64);
let y = (0..wu * hu)
.map(|i| {
let (x, yy) = ((i % wu) as i64, (i / wu) as i64);
let base = x * 170 / wi + yy * 60 / hi;
let detail = (hash(x, yy, 0) - 128) / 10;
let edge = if (x * 4 / wi + yy * 4 / hi) % 2 == 0 {
28
} else {
0
};
clamp(base + detail + edge)
})
.collect();
let (cwi, chi) = (cw.max(1) as i64, ch.max(1) as i64);
let u = (0..cw * ch)
.map(|i| {
let (x, yy) = ((i % cw) as i64, (i / cw) as i64);
clamp(100 + x * 70 / cwi + (hash(x, yy, 1) - 128) / 16)
})
.collect();
let v = (0..cw * ch)
.map(|i| {
let (x, yy) = ((i % cw) as i64, (i / cw) as i64);
clamp(140 + yy * 60 / chi + (hash(x, yy, 2) - 128) / 16)
})
.collect();
gamut_color::Yuv420::new(w, h, y, u, v).unwrap()
}
#[test]
fn gamut_lossy_bpred_matches_libwebp_bit_exact() {
use common::libwebp_decode_yuv;
use gamut_riff::write_simple_lossy;
use gamut_webp::vp8::frame::{decode_frame, encode_frame};
for &(w, h) in &[(16u32, 16u32), (32, 32), (48, 48), (49, 33), (64, 16)] {
for &quant_index in &[0u8, 8, 40] {
let (payload, _) = encode_frame(&detailed_yuv(w, h), quant_index);
let webp = write_simple_lossy(&payload);
let lib = libwebp_decode_yuv(&webp);
let gamut = decode_frame(&payload).expect("gamut decode").to_yuv420();
assert_eq!((lib.width, lib.height), (w, h), "dims at {w}x{h}");
assert_eq!(
gamut.y(),
lib.y.as_slice(),
"B_PRED Y mismatch at {w}x{h} q{quant_index}"
);
assert_eq!(
gamut.u(),
lib.u.as_slice(),
"B_PRED U mismatch at {w}x{h} q{quant_index}"
);
assert_eq!(
gamut.v(),
lib.v.as_slice(),
"B_PRED V mismatch at {w}x{h} q{quant_index}"
);
}
}
}
#[test]
fn gamut_lossy_options_match_libwebp_bit_exact() {
use common::libwebp_decode_yuv;
use gamut_riff::write_simple_lossy;
use gamut_webp::vp8::frame::{EncodeOptions, decode_frame, encode_frame_filtered};
let base = EncodeOptions::default();
let cases = [
(
"simple-filter",
EncodeOptions {
simple_filter: true,
..base
},
),
(
"segmented",
EncodeOptions {
segmented: true,
..base
},
),
(
"segmented+simple",
EncodeOptions {
simple_filter: true,
segmented: true,
..base
},
),
(
"partitions-2",
EncodeOptions {
partitions: 2,
..base
},
),
(
"partitions-4",
EncodeOptions {
partitions: 4,
..base
},
),
(
"partitions-8",
EncodeOptions {
partitions: 8,
..base
},
),
(
"everything",
EncodeOptions {
simple_filter: true,
segmented: true,
partitions: 4,
},
),
];
for (label, opts) in cases {
for &(w, h) in &[(32u32, 32u32), (48, 48), (49, 33), (33, 145)] {
for &q in &[12u8, 48] {
let (payload, _) = encode_frame_filtered(&detailed_yuv(w, h), q, opts);
let webp = write_simple_lossy(&payload);
let lib = libwebp_decode_yuv(&webp);
let gamut = decode_frame(&payload).expect("gamut decode").to_yuv420();
assert_eq!(gamut.y(), lib.y.as_slice(), "{label} Y at {w}x{h} q{q}");
assert_eq!(gamut.u(), lib.u.as_slice(), "{label} U at {w}x{h} q{q}");
assert_eq!(gamut.v(), lib.v.as_slice(), "{label} V at {w}x{h} q{q}");
}
}
}
}
#[test]
fn gamut_lossy_yuv_matches_libwebp_bit_exact() {
use common::libwebp_decode_yuv;
use gamut_riff::write_simple_lossy;
use gamut_webp::vp8::frame::{decode_frame, encode_frame};
for &(w, h) in &[
(16u32, 16u32),
(32, 32),
(17, 9),
(64, 48),
(80, 16),
(33, 49),
] {
for &quant_index in &[0u8, 20, 60, 110] {
let (payload, _) = encode_frame(&synthetic_yuv(w, h), quant_index);
let webp = write_simple_lossy(&payload);
let lib = libwebp_decode_yuv(&webp);
let gamut = decode_frame(&payload).expect("gamut decode").to_yuv420();
assert_eq!((lib.width, lib.height), (w, h), "dims at {w}x{h}");
assert_eq!(
gamut.y(),
lib.y.as_slice(),
"Y mismatch at {w}x{h} q{quant_index}"
);
assert_eq!(
gamut.u(),
lib.u.as_slice(),
"U mismatch at {w}x{h} q{quant_index}"
);
assert_eq!(
gamut.v(),
lib.v.as_slice(),
"V mismatch at {w}x{h} q{quant_index}"
);
}
}
}
#[test]
fn gamut_lossy_yuv_realistic_and_large_matches_libwebp() {
use common::libwebp_decode_yuv;
use gamut_riff::write_simple_lossy;
use gamut_webp::vp8::frame::{decode_frame, encode_frame};
let dims = [
(32u32, 32u32),
(64, 48),
(256, 256),
(384, 288),
(640, 480),
(1024, 768),
(513, 97),
];
for &(w, h) in &dims {
for &quant_index in &[12u8, 56] {
let (payload, _) = encode_frame(&photo_like_yuv(w, h, 0x7e57), quant_index);
let webp = write_simple_lossy(&payload);
let lib = libwebp_decode_yuv(&webp);
let gamut = decode_frame(&payload).expect("gamut decode").to_yuv420();
assert_eq!((lib.width, lib.height), (w, h), "dims at {w}x{h}");
assert_eq!(gamut.y(), lib.y.as_slice(), "Y at {w}x{h} q{quant_index}");
assert_eq!(gamut.u(), lib.u.as_slice(), "U at {w}x{h} q{quant_index}");
assert_eq!(gamut.v(), lib.v.as_slice(), "V at {w}x{h} q{quant_index}");
}
}
}
fn vp8_payload(webp: &[u8]) -> Vec<u8> {
use gamut_riff::{RiffReader, WebpChunkId};
RiffReader::new(webp)
.expect("riff")
.filter_map(Result::ok)
.find(|c| matches!(WebpChunkId::from(c.fourcc), WebpChunkId::Vp8))
.expect("VP8 chunk")
.payload
.to_vec()
}
#[test]
fn gamut_decodes_libwebp_lossy_bit_exact() {
use common::{libwebp_decode_yuv, libwebp_encode_lossy_rgba};
for &(w, h) in &[
(16u32, 16u32),
(32, 32),
(64, 48),
(49, 33),
(80, 17),
(255, 3),
] {
for q in [6.0f32, 35.0, 70.0, 100.0] {
let rgba = pattern_rgba(w, h);
let webp = libwebp_encode_lossy_rgba(&rgba, w, h, q);
let gamut = gamut_webp::vp8::frame::decode_frame(&vp8_payload(&webp))
.expect("gamut decode")
.to_yuv420();
let lib = libwebp_decode_yuv(&webp);
assert_eq!((lib.width, lib.height), (w, h), "dims at {w}x{h}");
assert_eq!(gamut.y(), lib.y.as_slice(), "Y at {w}x{h} q{q}");
assert_eq!(gamut.u(), lib.u.as_slice(), "U at {w}x{h} q{q}");
assert_eq!(gamut.v(), lib.v.as_slice(), "V at {w}x{h} q{q}");
}
}
}
#[test]
fn gamut_decodes_libwebp_lossy_realistic_and_large() {
use common::{libwebp_decode_yuv, libwebp_encode_lossy_rgba};
let small = [
(16u32, 16u32),
(32, 32),
(64, 48),
(49, 33),
(80, 17),
(255, 3),
];
for &(w, h) in small.iter().chain(LARGE_DIMENSIONS) {
for q in [20.0f32, 80.0] {
let rgba = photo_like_rgba(w, h, 0x1d0f);
let webp = libwebp_encode_lossy_rgba(&rgba, w, h, q);
let gamut = gamut_webp::vp8::frame::decode_frame(&vp8_payload(&webp))
.expect("gamut decode")
.to_yuv420();
let lib = libwebp_decode_yuv(&webp);
assert_eq!((lib.width, lib.height), (w, h), "dims at {w}x{h}");
assert_eq!(gamut.y(), lib.y.as_slice(), "Y at {w}x{h} q{q}");
assert_eq!(gamut.u(), lib.u.as_slice(), "U at {w}x{h} q{q}");
assert_eq!(gamut.v(), lib.v.as_slice(), "V at {w}x{h} q{q}");
}
}
}
#[test]
fn gamut_decodes_libwebp_lossy_forced_features_bit_exact() {
use common::{LibwebpLossyConfig, libwebp_decode_yuv, libwebp_encode_lossy_rgba_config};
let base = LibwebpLossyConfig {
quality: 75.0,
filter_type: 1,
segments: 1,
method: 4,
filter_strength: 60,
};
{
let (w, h) = (64u32, 48);
let rgba = photo_like_rgba(w, h, 0x00c0_ffee);
let one = libwebp_encode_lossy_rgba_config(&rgba, w, h, &base);
let simple = libwebp_encode_lossy_rgba_config(
&rgba,
w,
h,
&LibwebpLossyConfig {
filter_type: 0,
..base
},
);
assert_ne!(
one, simple,
"switching to the simple loop filter must change the stream"
);
let multi_seg = libwebp_encode_lossy_rgba_config(
&rgba,
w,
h,
&LibwebpLossyConfig {
segments: 4,
..base
},
);
assert_ne!(
one, multi_seg,
"forcing four segments must change the stream"
);
}
let mut cases: Vec<(String, LibwebpLossyConfig)> = Vec::new();
for filter_type in [0, 1] {
cases.push((
format!("filter_type={filter_type}"),
LibwebpLossyConfig {
filter_type,
..base
},
));
}
for segments in [1, 2, 3, 4] {
cases.push((
format!("segments={segments}"),
LibwebpLossyConfig { segments, ..base },
));
}
for method in [0, 3, 6] {
cases.push((
format!("method={method}"),
LibwebpLossyConfig { method, ..base },
));
}
for filter_strength in [0, 30] {
cases.push((
format!("filter_strength={filter_strength}"),
LibwebpLossyConfig {
filter_strength,
..base
},
));
}
cases.push((
"combined".into(),
LibwebpLossyConfig {
filter_type: 0,
segments: 4,
method: 6,
filter_strength: 20,
..base
},
));
for &(w, h) in &[(32u32, 32u32), (64, 48), (49, 33), (128, 96)] {
let rgba = photo_like_rgba(w, h, 0x00c0_ffee);
for (label, cfg) in &cases {
let webp = libwebp_encode_lossy_rgba_config(&rgba, w, h, cfg);
let gamut = gamut_webp::vp8::frame::decode_frame(&vp8_payload(&webp))
.expect("gamut decode")
.to_yuv420();
let lib = libwebp_decode_yuv(&webp);
assert_eq!((lib.width, lib.height), (w, h), "dims {label} at {w}x{h}");
assert_eq!(gamut.y(), lib.y.as_slice(), "Y {label} at {w}x{h}");
assert_eq!(gamut.u(), lib.u.as_slice(), "U {label} at {w}x{h}");
assert_eq!(gamut.v(), lib.v.as_slice(), "V {label} at {w}x{h}");
}
}
}
#[test]
fn gamut_rgb_to_yuv_matches_libwebp_limited_range() {
use common::libwebp_rgba_to_yuv;
use gamut_color::{Bt601Range, Yuv420};
let chroma_max = |a: &[u8], b: &[u8]| {
a.iter()
.zip(b)
.map(|(x, y)| (i32::from(*x) - i32::from(*y)).abs())
.max()
.unwrap_or(0)
};
for &(w, h) in DIMENSIONS.iter().chain(&[(128u32, 96u32), (300, 70)]) {
let rgba = photo_like_rgba(w, h, 0x5eed);
let gamut =
Yuv420::from_rgb8(&rgba_to_rgb(&rgba), w, h, Bt601Range::Limited).expect("from_rgb8");
let lib = libwebp_rgba_to_yuv(&rgba, w, h);
assert_eq!(
gamut.y(),
lib.y.as_slice(),
"luma must be bit-exact at {w}x{h}"
);
assert!(chroma_max(gamut.u(), &lib.u) <= 2, "U within 2 at {w}x{h}");
assert!(chroma_max(gamut.v(), &lib.v) <= 2, "V within 2 at {w}x{h}");
}
}
#[test]
fn gamut_lossy_webp_decodes_correctly_in_libwebp() {
for &(w, h) in &[(64u32, 48u32), (128, 96), (49, 33)] {
let rgb = rgba_to_rgb(&photo_like_rgba(w, h, 0x1cef));
let mut webp = Vec::new();
WebpEncoder::lossy(90)
.encode_image(
ImageRef::<Rgb8>::new(
&rgb,
Dimensions {
width: w,
height: h,
},
)
.unwrap(),
&mut webp,
)
.expect("gamut encode");
let lib = rgba_to_rgb(&libwebp_decode_rgba(&webp).rgba);
let mae: f64 = lib
.iter()
.zip(&rgb)
.map(|(a, b)| f64::from((i32::from(*a) - i32::from(*b)).unsigned_abs()))
.sum::<f64>()
/ rgb.len() as f64;
assert!(
mae <= 8.0,
"gamut→libwebp interop MAE {mae:.2} too high at {w}x{h} (colour shift regression?)"
);
}
}
#[test]
fn gamut_decodes_libwebp_lossy_close_to_libwebp() {
use common::libwebp_encode_lossy_rgba;
let max_abs = |a: &[u8], b: &[u8]| {
a.iter()
.zip(b)
.map(|(x, y)| (i32::from(*x) - i32::from(*y)).abs())
.max()
.unwrap_or(0)
};
for &(w, h) in &[(64u32, 48u32), (128, 96), (49, 33)] {
let rgba = photo_like_rgba(w, h, 0x2ab0);
let webp = libwebp_encode_lossy_rgba(&rgba, w, h, 90.0);
let gamut: ImageBuf<Rgb8> = WebpDecoder::new()
.decode_image(&webp)
.expect("gamut decode");
let lib = rgba_to_rgb(&libwebp_decode_rgba(&webp).rgba);
assert!(
max_abs(gamut.as_samples(), &lib) <= 24,
"gamut vs libwebp decode differs by >24 at {w}x{h} (more than chroma upsampling)"
);
}
}
#[test]
fn libwebp_decodes_gamut_lossy_alpha_exactly() {
use common::libwebp_decode_rgba;
for &(w, h) in &[(16u32, 16u32), (32, 24), (17, 9), (49, 33)] {
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 * 11 + y * 5) & 0xff) as u8,
]
})
.collect();
let mut file = Vec::new();
WebpEncoder::lossy(70)
.encode_image(
ImageRef::<Rgba8>::new(
&rgba,
Dimensions {
width: w,
height: h,
},
)
.unwrap(),
&mut file,
)
.expect("gamut rgba encode");
let decoded = libwebp_decode_rgba(&file);
assert_eq!((decoded.width, decoded.height), (w, h), "dims at {w}x{h}");
let lib_alpha: Vec<u8> = decoded.rgba.chunks_exact(4).map(|p| p[3]).collect();
let src_alpha: Vec<u8> = rgba.chunks_exact(4).map(|p| p[3]).collect();
assert_eq!(
lib_alpha, src_alpha,
"libwebp must recover gamut's exact alpha at {w}x{h}"
);
}
}
#[test]
fn gamut_decodes_libwebp_lossy_alpha_exactly() {
use common::libwebp_encode_lossy_rgba;
for &(w, h) in &[(16u32, 16u32), (32, 24), (49, 33), (80, 17)] {
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 * 11 + y * 5) & 0xff) as u8,
]
})
.collect();
let webp = libwebp_encode_lossy_rgba(&rgba, w, h, 75.0);
let got: ImageBuf<Rgba8> = WebpDecoder::new()
.decode_image(&webp)
.expect("gamut decode libwebp lossy+alpha");
assert_eq!(
got.dimensions(),
Dimensions {
width: w,
height: h
}
);
let dec_alpha: Vec<u8> = got.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,
"gamut must recover libwebp's exact alpha at {w}x{h}"
);
}
}