open-standards/wai/wai-rs/tests/media_decode_bounds.rs
Transaction Science 59feeeff98
Some checks are pending
WAI conformance / does this change touch it (push) Has been skipped
WAI conformance / cargo-class corpora · linux (push) Has been skipped
WAI conformance / cargo-class corpora · linux-arm64 (push) Has been skipped
WAI conformance / cargo-class corpora · macos (push) Has been skipped
WAI conformance / cargo-class corpora · windows (push) Has been skipped
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (push) Has been skipped
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (push) Has been skipped
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (push) Has been skipped
WAI conformance / v2 vocabulary · manifests validate as stated (push) Has been skipped
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (push) Has been skipped
WAI conformance / independent verifiers · every verifier step, each corpus's Rust side (push) Has been skipped
WAI conformance / each feature of the independent verifiers' steps compiles alone (push) Has been skipped
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (push) Has been skipped
WAI conformance / cross-arch byte-equality · linux (push) Has been skipped
WAI conformance / cross-arch byte-equality · linux-arm64 (push) Has been skipped
WAI conformance / cross-arch byte-equality · macos (push) Has been skipped
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (push) Has been skipped
WAI conformance / C2PA manifest emission · signed, read back, validated (push) Has been skipped
WAI conformance / independent verifiers · each needs= is what a run without packages shows (nightly) (push) Has been skipped
wai-audit collector and player integration / does this change touch it (push) Successful in 1m23s
wai-pack / wai-pack · does this change touch it (push) Successful in 1m26s
wai-fleet service / does this change touch it (push) Successful in 1m28s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m29s
WAI operation catalogue / does this change touch it (push) Successful in 1m29s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m28s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 1m34s
wai-ledger service / does this change touch it (push) Successful in 1m30s
wai-quantum studio and desktop / does this change touch it (push) Successful in 45s
wai-sink-swift native sink package / does this change touch it (push) Successful in 43s
wai-sink-android native sink library / does this change touch it (push) Successful in 45s
WAI Studio / does this change touch it (push) Successful in 43s
wai-app and Create / wai-app · does this change touch it (push) Successful in 5m4s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m10s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 5m35s
joule-moq-agent AI participants / joule-moq-agent · does this change touch it (push) Successful in 6m6s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 5m54s
wai-fleet service / service · macos (push) Successful in 42s
wai-pack / wai-pack · the floor through a media prober (push) Successful in 1m12s
wai-ledger service / service · macos (push) Successful in 1m24s
wai-pack / wai-pack · linux-arm64 (push) Successful in 4m46s
wai-pack / wai-pack · dependency licences (push) Successful in 31s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m25s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m11s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m2s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m35s
joule-moq-agent AI participants / joule-moq-agent, every target (push) Successful in 3m57s
wai-pack / wai-pack · macos (push) Successful in 10m52s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 3m34s
WAI examples / WAI examples · does this change touch them (push) Successful in 21s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m18s
WAI conformance / a push to main · corpus provenance and sidecars, signer ids, the registry export (push) Successful in 1m11s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m51s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 14m44s
WAI Studio / Studio in WebKit · linux (push) Has started running
wai-fleet service / service · linux (push) Successful in 4m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 52s
wai-pack / wai-pack · linux (push) Successful in 5m17s
wai-audit collector and player integration / collector · linux (push) Successful in 6m1s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 20m38s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 17m33s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 2m15s
wai-ledger service / service · linux (push) Successful in 3m58s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 15m28s
wai-pack / wai-pack · independent verifier (Python) (push) Successful in 25m9s
wai-quantum for JavaScript / javascript · linux (push) Successful in 9m42s
wai-quantum studio and desktop / studio · browser (push) Successful in 9m44s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 13m26s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 12m14s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 13m17s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 18m24s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 4m23s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 37m2s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 12m43s
WAI examples / WAI examples · portable · linux (push) Successful in 18m37s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 12m41s
wai-rs: jxl_encode and the C2PA identity signer on jpegxl-rs 0.16 and c2pa 0.92, and the SDK identity corpus signed again at 0.92.1
The 2026-11-14 wai-conformance nightly (run 35860) passed every job but
"codec wrappers" and "C2PA manifest emission", both compile errors after
526554191 moved jpegxl-rs 0.14->0.16 and c2pa 0.91->0.92 in paths no
per-push job builds:
- codecs::image::jxl_encode and tests/media_decode_bounds.rs: jpegxl-rs 0.16's
  encode takes the pixel type alone and returns the bytes.
- c2pa_emit::identity's signer wrapper: c2pa 0.92's Signer passes a
  &Context to send_timestamp_request; the wrapper passes it on.
- live-conformance/c2pa-sdk-identity signed again with 0.92.1, as its README
  says it is when the lockfile moves the SDK: all 20 vectors keep 0.91.11's
  validator reports, instant and anchors; SDK_VERSION, the README and D6 say
  0.92.1. The 21 corpus records and SDK_VERSION are acknowledged breaks.

Checked on build hosts: the codec wrappers' five test binaries, the image
codec unit tests, the semantic path, wai_prov and clippy (macOS arm64);
the emitter, session and identity tests, the SDK identity vectors and
clippy (Linux aarch64); wai breaking: no unacknowledged break.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-14 07:02:48 -05:00

357 lines
16 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The image, audio and video decoders hold a payload to the reader limits of SPEC §2:
//! a picture past the limits, an audio payload that declares or decodes to more samples
//! than the limit, and a video payload past them are refused by name, from what the
//! payload declares wherever it declares it, and one at a limit is not refused for it.
//!
//! - Pictures: `check_dimensions` at and one past each limit; real PNG, JPEG XL and
//! AVIF payloads one pixel past the side limit; PNG, JPEG and AVIF headers that
//! declare more pixels than the limit with sides within it. Each decoder decodes only
//! the format its capability names, and JPEG XL decodes to three channels whatever
//! the source holds. The writers refuse what a reader may refuse.
//! - Audio: a FLAC stream whose STREAMINFO declares 2^36 - 1 samples is refused from the
//! declaration (it used to ask the allocator for 256 GiB up front, which aborts the
//! process wherever the allocator refuses that); the declared count is
//! taken over every channel; a 24-bit stream decodes to [-1, 1]; both decoders refuse
//! output past a limit the caller gives, and not at it.
//! - Video: the framing header past each limit; more temporal units than the header
//! declares; a frame larger than it declares; the whole-output budget, at and one
//! past it; monochrome and 10-bit 4:4:4 streams, which the decoder used to panic on
//! or misread.
//!
//! Exercises the codec wrappers, so it requires the `codecs` feature.
#![cfg(feature = "codecs")]
use wai::codecs::audio::{
flac_decode, flac_decode_within, flac_encode, opus_decode_within, opus_encode,
AUDIO_DECODED_MAX_SAMPLES,
};
use wai::codecs::image::{
avif_decode, avif_encode, check_dimensions, jpeg_decode, jpeg_encode, jxl_decode,
jxl_encode, png_decode, png_encode, IMAGE_MAX_PIXELS, IMAGE_MAX_SIDE,
};
use wai::codecs::video::{
av1_decode, av1_decode_each, av1_decode_within, av1_encode, av1_header,
VIDEO_MAX_TEMPORAL_UNITS,
};
fn why<T: std::fmt::Debug, E: std::fmt::Debug>(r: Result<T, E>) -> String {
format!("{:?}", r.expect_err("the payload must be refused"))
}
fn grey(h: u32, w: u32) -> Vec<u8> {
(0..h * w * 3).map(|i| (i % 251) as u8).collect()
}
// ---- pictures ------------------------------------------------------------------
#[test]
fn picture_limits_hold_at_and_one_past_each_bound() {
let side = IMAGE_MAX_SIDE as u64;
assert_eq!(IMAGE_MAX_PIXELS, 1 << 27);
for (w, h) in [(side, 1), (1, side), (16_384, 8_192), (side, IMAGE_MAX_PIXELS / side)] {
check_dimensions(w, h).unwrap_or_else(|e| panic!("{w} × {h} is within the limits: {e:?}"));
}
for (w, h) in [(side + 1, 1), (1, side + 1), (16_384, 8_193), (side, IMAGE_MAX_PIXELS / side + 1)] {
let e = why(check_dimensions(w, h));
assert!(e.contains("image_too_large:"), "{w} × {h}: {e}");
}
}
#[test]
fn png_and_jxl_one_pixel_past_the_side_limit_are_refused() {
// Made with the libraries themselves: the reference writers refuse such a picture.
let w = IMAGE_MAX_SIDE + 1;
let rgb = grey(1, w);
let mut png = Vec::new();
image::ImageEncoder::write_image(
image::codecs::png::PngEncoder::new(&mut png), &rgb, w, 1, image::ExtendedColorType::Rgb8,
)
.unwrap();
assert!(why(png_decode(&png)).contains("image_too_large:"));
let mut enc = jpegxl_rs::encoder_builder().lossless(true).uses_original_profile(true).build().unwrap();
let jxl = enc.encode::<u8>(&rgb, w, 1).unwrap();
assert!(why(jxl_decode(&jxl)).contains("image_too_large:"));
// At the side limit both decode.
let rgb = grey(1, IMAGE_MAX_SIDE);
let (out, h, w) = png_decode(&png_encode(&rgb, 1, IMAGE_MAX_SIDE).unwrap()).unwrap();
assert_eq!((out.len(), h, w), (rgb.len(), 1, IMAGE_MAX_SIDE));
let (out, h, w) = jxl_decode(&jxl_encode(&rgb, 1, IMAGE_MAX_SIDE, None).unwrap()).unwrap();
assert_eq!((out, h, w), (rgb, 1, IMAGE_MAX_SIDE));
}
/// CRC-32 (ISO 3309), for PNG chunks.
fn crc32(bytes: &[u8]) -> u32 {
let mut c = 0xFFFF_FFFFu32;
for &b in bytes {
c ^= b as u32;
for _ in 0..8 {
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
}
}
!c
}
#[test]
fn png_and_jpeg_headers_past_the_pixel_limit_are_refused_before_decoding() {
// 16 384 × 8 193: each side within the limit, one row of pixels past it. Only the
// header says so; the picture data is an 8 × 8's, so a decoder that read it first
// would fail for another reason.
let rgb = grey(8, 8);
let mut png = png_encode(&rgb, 8, 8).unwrap();
assert_eq!(&png[12..16], b"IHDR");
png[16..20].copy_from_slice(&16_384u32.to_be_bytes());
png[20..24].copy_from_slice(&8_193u32.to_be_bytes());
let crc = crc32(&png[12..29]);
png[29..33].copy_from_slice(&crc.to_be_bytes());
assert!(why(png_decode(&png)).contains("image_too_large:"));
let mut jpg = jpeg_encode(&rgb, 8, 8, 90).unwrap();
let sof = jpg.windows(2).position(|m| m == [0xFF, 0xC0]).expect("a baseline SOF0");
jpg[sof + 5..sof + 7].copy_from_slice(&8_193u16.to_be_bytes());
jpg[sof + 7..sof + 9].copy_from_slice(&16_384u16.to_be_bytes());
assert!(why(jpeg_decode(&jpg)).contains("image_too_large:"));
}
#[test]
fn avif_declarations_past_the_limits_are_refused_and_a_small_one_decodes() {
let rgb = grey(16, 16);
let avif = avif_encode(&rgb, 16, 16, 80).unwrap();
let (out, h, w) = avif_decode(&avif).unwrap();
assert_eq!((out.len(), h, w), (rgb.len(), 16, 16));
let ispe = avif.windows(4).position(|b| b == b"ispe").expect("an ispe property");
for (w, h) in [(IMAGE_MAX_SIDE + 1, 1u32), (16_384, 8_193)] {
let mut a = avif.clone();
a[ispe + 8..ispe + 12].copy_from_slice(&w.to_be_bytes());
a[ispe + 12..ispe + 16].copy_from_slice(&h.to_be_bytes());
let e = why(avif_decode(&a));
assert!(e.contains("image_too_large:"), "{w} × {h}: {e}");
}
}
#[test]
fn each_image_decoder_decodes_only_its_own_format() {
let rgb = grey(8, 8);
let png = png_encode(&rgb, 8, 8).unwrap();
let jpg = jpeg_encode(&rgb, 8, 8, 90).unwrap();
assert!(png_decode(&jpg).is_err(), "png_decode read a JPEG");
assert!(jpeg_decode(&png).is_err(), "jpeg_decode read a PNG");
assert!(png_decode(&png).is_ok() && jpeg_decode(&jpg).is_ok());
}
#[test]
fn the_writers_refuse_what_a_reader_may_refuse() {
let w = IMAGE_MAX_SIDE + 1;
let rgb = grey(1, w);
assert!(why(png_encode(&rgb, 1, w)).contains("image_too_large:"));
assert!(why(jpeg_encode(&rgb, 1, w, 90)).contains("image_too_large:"));
assert!(why(avif_encode(&rgb, 1, w, 80)).contains("image_too_large:"));
assert!(why(jxl_encode(&rgb, 1, w, None)).contains("image_too_large:"));
let long = vec![0f32; AUDIO_DECODED_MAX_SAMPLES + 1];
assert!(why(flac_encode(&long, 48_000)).contains("audio_decoded_too_long:"));
assert!(why(opus_encode(&long, 48_000, 64_000)).contains("audio_decoded_too_long:"));
drop(long);
let frame = grey(1, w);
assert!(why(av1_encode(&[frame], 1, w, 30, 1, false, 60)).contains("video_too_large:"));
}
// ---- audio ---------------------------------------------------------------------
fn tone(n: usize, amp: f32) -> Vec<f32> {
(0..n).map(|i| amp * (i as f32 * 0.05).sin()).collect()
}
/// Rewrites a FLAC stream's STREAMINFO: its channel count and its 36-bit total.
fn declare(flac: &[u8], channels: u8, total: u64) -> Vec<u8> {
let mut f = flac.to_vec();
assert_eq!(&f[..4], b"fLaC");
let si = 8; // after the marker and the STREAMINFO block header
// Byte 12 of STREAMINFO: the low bit of the sample rate, three bits of channels - 1,
// the high bit of bits per sample - 1. Byte 13: four bits of bps - 1, then the
// total's top four bits; bytes 14..18 the rest of it.
f[si + 12] = (f[si + 12] & 0xF1) | ((channels - 1) << 1);
f[si + 13] = (f[si + 13] & 0xF0) | ((total >> 32) as u8 & 0x0F);
f[si + 14..si + 18].copy_from_slice(&(total as u32).to_be_bytes());
f
}
#[test]
fn a_flac_stream_that_declares_too_many_samples_is_refused_from_the_declaration() {
let flac = flac_encode(&tone(4_096, 0.5), 48_000).unwrap();
let max = AUDIO_DECODED_MAX_SAMPLES as u64;
// 2^36 - 1, the largest total STREAMINFO can carry: the reader used to reserve that
// many samples, 256 GiB, before decoding one. Now it refuses, having allocated
// nothing for it.
for (ch, total) in [(1, (1u64 << 36) - 1), (1, max + 1), (2, max / 2 + 1)] {
let e = why(flac_decode(&declare(&flac, ch, total)));
assert!(e.contains("audio_decoded_too_long:") && e.contains("declares"), "{ch} × {total}: {e}");
}
// At the limit the declaration is not refused (the stream then decodes what it holds).
let at = declare(&flac, 1, max);
let (out, _) = flac_decode(&at).unwrap();
assert_eq!(out.len(), 4_096);
}
#[test]
fn flac_decodes_a_24_bit_stream_to_unit_scale() {
use flac_bound::{FlacEncoder, WriteWrapper};
let x = tone(4_800, 0.5);
let ints: Vec<i32> = x.iter().map(|&s| (s * 8_388_607.0).round() as i32).collect();
let mut bytes = Vec::new();
{
let mut ww = WriteWrapper(&mut bytes);
let mut enc = FlacEncoder::new().unwrap().channels(1).bits_per_sample(24)
.sample_rate(48_000).init_write(&mut ww).unwrap();
enc.process_interleaved(&ints, ints.len() as u32).unwrap();
enc.finish().unwrap();
}
let (out, _) = flac_decode(&bytes).unwrap();
let peak = out.iter().fold(0f32, |m, &v| m.max(v.abs()));
assert!((0.49..=0.51).contains(&peak), "a 0.5 tone decoded to a peak of {peak}");
}
#[test]
fn the_audio_decoders_refuse_output_past_the_callers_limit_and_not_at_it() {
let x = tone(9_600, 0.5);
let flac = flac_encode(&x, 48_000).unwrap();
// A stream that declares its total is refused from the declaration; one whose total
// is 0 (unknown) is refused from the count as samples decode.
assert!(why(flac_decode_within(&declare(&flac, 1, 9_600), 9_599)).contains("declares"));
let unknown = declare(&flac, 1, 0);
assert!(why(flac_decode_within(&unknown, 9_599)).contains("decodes to"));
assert_eq!(flac_decode_within(&unknown, 9_600).unwrap().0.len(), 9_600);
let opus = opus_encode(&x, 48_000, 64_000).unwrap();
let n = opus_decode_within(&opus, usize::MAX).unwrap().0.len();
assert!(n > 9_000, "decoded {n} samples of 9 600");
assert_eq!(opus_decode_within(&opus, n).unwrap().0.len(), n);
assert!(why(opus_decode_within(&opus, n - 1)).contains("audio_decoded_too_long:"));
}
// ---- video ---------------------------------------------------------------------
fn framing(n: u32, h: u32, w: u32, units: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
for v in [n, h, w, 30, 1] {
out.extend_from_slice(&v.to_le_bytes());
}
for u in units {
out.extend_from_slice(&(u.len() as u32).to_le_bytes());
out.extend_from_slice(u);
}
out
}
/// The temporal units of an AV1 payload, after its framing header.
fn units_of(payload: &[u8]) -> Vec<Vec<u8>> {
let mut at = 20;
let mut out = Vec::new();
while at < payload.len() {
let n = u32::from_le_bytes(payload[at..at + 4].try_into().unwrap()) as usize;
out.push(payload[at + 4..at + 4 + n].to_vec());
at += 4 + n;
}
out
}
#[test]
fn the_av1_framing_header_is_held_to_the_limits() {
let n = VIDEO_MAX_TEMPORAL_UNITS;
let side = IMAGE_MAX_SIDE;
assert!(av1_header(&framing(n, 2_048, side, &[])).is_ok(), "at every limit");
for (n, h, w) in [(n + 1, 64, 64), (1, 1, side + 1), (1, 8_193, 16_384)] {
let e = why(av1_header(&framing(n, h, w, &[])));
assert!(e.contains("video_too_large:"), "{n} × {w}×{h}: {e}");
}
}
#[test]
fn av1_refuses_more_units_than_declared_and_frames_larger_than_declared() {
let clip: Vec<Vec<u8>> = (0..3).map(|_| grey(64, 64)).collect();
let payload = av1_encode(&clip, 64, 64, 30, 1, false, 60).unwrap();
let units = units_of(&payload);
assert_eq!(units.len(), 3, "one temporal unit a frame");
assert_eq!(av1_decode(&payload).unwrap().0.len(), 3);
let e = why(av1_decode(&framing(2, 64, 64, &units)));
assert!(e.contains("more than the 2 temporal units"), "{e}");
// A frame with more pixels than the header declares never reaches a picture: dav1d
// is held to h × w pixels (the test below). One with as many pixels, but wider than
// the header says, is decoded and then refused by its shape.
assert!(av1_decode(&framing(3, 32, 32, &units)).is_err());
let wide = av1_unit(128, 16, rav1e::color::ChromaSampling::Cs420, 8, [100, 128, 128]);
let e = why(av1_decode(&framing(1, 32, 64, &[wide])));
assert!(e.contains("128×16 frame in a payload whose frames are at most 64×32"), "{e}");
}
#[test]
fn the_whole_output_budget_refuses_from_the_header_and_streaming_does_not_need_one() {
let clip: Vec<Vec<u8>> = (0..4).map(|_| grey(64, 64)).collect();
let payload = av1_encode(&clip, 64, 64, 30, 1, false, 60).unwrap();
let need = 4 * 64 * 64 * 3;
assert_eq!(av1_decode_within(&payload, need).unwrap().0.len(), 4);
assert!(why(av1_decode_within(&payload, need - 1)).contains("video_decoded_too_large:"));
let mut seen = 0;
let hd = av1_decode_each(&payload, |rgb, w, h| {
assert_eq!((rgb.len(), w, h), (64 * 64 * 3, 64, 64));
seen += 1;
Ok(())
})
.unwrap();
assert_eq!((seen, hd.n), (4, 4));
}
#[test]
fn a_sequence_header_larger_than_the_framing_header_is_refused_before_it_allocates() {
// A 256×256 stream in a framing header that says 16×16: dav1d is held to 16×16
// pixels, so it refuses the stream's sequence header instead of allocating its
// frames and leaving the size check to the decoded picture.
let unit = av1_unit(256, 256, rav1e::color::ChromaSampling::Cs420, 8, [100, 128, 128]);
let e = why(av1_decode(&framing(1, 16, 16, std::slice::from_ref(&unit))));
assert!(!e.contains("256×256 frame"), "the frame was decoded before it was refused: {e}");
assert!(av1_decode(&framing(1, 256, 256, &[unit])).is_ok());
}
/// One AV1 temporal unit of a `w`×`h` picture in the given layout and depth, every
/// sample of plane p equal to `fill[p]`, made with rav1e.
fn av1_unit(w: usize, h: usize, cs: rav1e::color::ChromaSampling, depth: usize, fill: [u16; 3]) -> Vec<u8> {
use rav1e::{Config, EncoderConfig, EncoderStatus};
let mut cfg = EncoderConfig::with_speed_preset(10);
cfg.width = w;
cfg.height = h;
cfg.chroma_sampling = cs;
cfg.bit_depth = depth;
cfg.still_picture = true;
cfg.quantizer = 0;
cfg.min_quantizer = 0;
let mut ctx: rav1e::Context<u16> = Config::new().with_encoder_config(cfg).new_context().unwrap();
let mut f = ctx.new_frame();
let planes = if cs == rav1e::color::ChromaSampling::Cs400 { 1 } else { 3 };
for (p, plane) in f.planes.iter_mut().enumerate().take(planes) {
for v in plane.data_origin_mut().iter_mut() {
*v = fill[p];
}
}
ctx.send_frame(f).unwrap();
ctx.flush();
let mut out = Vec::new();
loop {
match ctx.receive_packet() {
Ok(p) => out.extend_from_slice(&p.data),
Err(EncoderStatus::Encoded) => continue,
Err(_) => break,
}
}
out
}
#[test]
fn av1_decodes_monochrome_and_10_bit_4_4_4() {
use rav1e::color::ChromaSampling::{Cs400, Cs444};
// Monochrome, 8 bits: Y = 100 everywhere, so every RGB sample is 100.
let mono = framing(1, 16, 16, &[av1_unit(16, 16, Cs400, 8, [100, 0, 0])]);
let (frames, ..) = av1_decode(&mono).unwrap();
assert!(frames[0].iter().all(|&v| v == 100), "monochrome: {:?}", &frames[0][..6]);
// 10-bit 4:4:4: Y = 400, Cb = Cr = 512 (neutral), so every sample is 400 / 4 = 100.
let deep = framing(1, 16, 16, &[av1_unit(16, 16, Cs444, 10, [400, 512, 512])]);
let (frames, ..) = av1_decode(&deep).unwrap();
assert!(frames[0].iter().all(|&v| v == 100), "10-bit 4:4:4: {:?}", &frames[0][..6]);
}