use enough::Unstoppable;
use zenavif::{DecoderConfig, ManagedAvifDecoder};
#[cfg(feature = "zencodec")]
use zencodec::gainmap::GainMapSource;
const SEINE_SDR_GAINMAP: &str = "tests/vectors/libavif/seine_sdr_gainmap_srgb.avif";
const SEINE_HDR_GAINMAP: &str = "tests/vectors/libavif/seine_hdr_gainmap_srgb.avif";
const WHITE_1X1: &str = "tests/vectors/libavif/white_1x1.avif";
const UNSUPPORTED_VERSION: &str = "tests/vectors/libavif/unsupported_gainmap_version.avif";
const UNSUPPORTED_MIN_VERSION: &str =
"tests/vectors/libavif/unsupported_gainmap_minimum_version.avif";
const SUPPORTED_WRITER_EXTRA: &str =
"tests/vectors/libavif/supported_gainmap_writer_version_with_extra_bytes.avif";
const SEINE_HDR_GAINMAP_SMALL: &str = "tests/vectors/libavif/seine_hdr_gainmap_small_srgb.avif";
const NOGRID_ALPHA_NOGRID_GAINMAP_GRID: &str =
"tests/vectors/libavif/color_nogrid_alpha_nogrid_gainmap_grid.avif";
fn load_vector(path: &str) -> Option<Vec<u8>> {
match std::fs::read(path) {
Ok(data) => Some(data),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("skipping: {path} not found (download with: just download-vectors)");
None
}
Err(e) => panic!("Failed to read {path}: {e}"),
}
}
macro_rules! require_vector {
($expr:expr) => {
match $expr {
Some(data) => data,
None => return,
}
};
}
#[test]
fn probe_gain_map_present() {
let data = require_vector!(load_vector(SEINE_SDR_GAINMAP));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info.gain_map.as_ref().expect("gain map should be present");
assert!(
gm.metadata.is_multichannel,
"seine test file uses multichannel gain map"
);
assert!(gm.metadata.use_base_colour_space);
assert_eq!(gm.metadata.base_hdr_headroom_n, 0);
assert_eq!(gm.metadata.base_hdr_headroom_d, 1);
assert_eq!(gm.metadata.alternate_hdr_headroom_n, 13);
assert_eq!(gm.metadata.alternate_hdr_headroom_d, 10);
assert!(!gm.gain_map_data.is_empty(), "gain map data should exist");
let first_byte = gm.gain_map_data[0];
let obu_type = (first_byte >> 3) & 0x0F;
assert!(
(1..=8).contains(&obu_type),
"first OBU type should be valid: got {obu_type}"
);
assert!(
gm.alt_color_info.is_some(),
"tmap colr property should be present"
);
}
#[test]
fn probe_gain_map_absent() {
let data = require_vector!(load_vector(WHITE_1X1));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
assert!(info.gain_map.is_none(), "normal image has no gain map");
}
#[test]
fn probe_hdr_gain_map_present() {
let data = require_vector!(load_vector(SEINE_HDR_GAINMAP));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info.gain_map.as_ref().expect("gain map should be present");
assert!(
!gm.gain_map_data.is_empty(),
"HDR gain map data should exist"
);
}
#[test]
fn decode_full_has_gain_map() {
let data = require_vector!(load_vector(SEINE_SDR_GAINMAP));
let mut decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let (_pixels, info) = decoder
.decode_full(&Unstoppable)
.expect("decode should succeed");
let gm = info
.gain_map
.as_ref()
.expect("decode_full should include gain map");
assert!(gm.metadata.is_multichannel);
assert!(!gm.gain_map_data.is_empty());
assert_ne!(
gm.metadata.channels[0].gain_map_min_n, gm.metadata.channels[1].gain_map_min_n,
"multichannel should have different per-channel values"
);
}
#[test]
fn decode_full_no_gain_map() {
let data = require_vector!(load_vector(WHITE_1X1));
let mut decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let (_pixels, info) = decoder
.decode_full(&Unstoppable)
.expect("decode should succeed");
assert!(
info.gain_map.is_none(),
"normal image should not have gain map after decode"
);
}
#[test]
fn gain_map_channel_params_valid() {
let data = require_vector!(load_vector(SEINE_SDR_GAINMAP));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info.gain_map.unwrap();
for (i, ch) in gm.metadata.channels.iter().enumerate() {
assert!(
ch.gain_map_min_d > 0,
"channel {i} gain_map_min_d should be non-zero"
);
assert!(
ch.gain_map_max_d > 0,
"channel {i} gain_map_max_d should be non-zero"
);
assert!(ch.gamma_d > 0, "channel {i} gamma_d should be non-zero");
assert!(
ch.base_offset_d > 0,
"channel {i} base_offset_d should be non-zero"
);
assert!(
ch.alternate_offset_d > 0,
"channel {i} alternate_offset_d should be non-zero"
);
assert!(ch.gamma_n > 0, "channel {i} gamma should be positive");
}
}
#[test]
fn unsupported_gainmap_version_still_decodes_base() {
let data = require_vector!(load_vector(UNSUPPORTED_VERSION));
let result = ManagedAvifDecoder::new(&data, &DecoderConfig::default());
assert!(
result.is_err(),
"unsupported gain map version should cause parse error"
);
}
#[test]
fn unsupported_gainmap_minimum_version_rejected() {
let data = require_vector!(load_vector(UNSUPPORTED_MIN_VERSION));
let result = ManagedAvifDecoder::new(&data, &DecoderConfig::default());
assert!(
result.is_err(),
"unsupported gain map minimum version should cause parse error"
);
}
#[test]
fn supported_writer_version_with_extra_bytes() {
let data = require_vector!(load_vector(SUPPORTED_WRITER_EXTRA));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info
.gain_map
.as_ref()
.expect("gain map should be present despite extra bytes");
assert!(!gm.gain_map_data.is_empty());
}
#[test]
fn gain_map_small_dimensions() {
let data = require_vector!(load_vector(SEINE_HDR_GAINMAP_SMALL));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info.gain_map.as_ref().expect("gain map should be present");
assert!(
!gm.gain_map_data.is_empty(),
"small gain map data should be non-empty"
);
}
#[test]
fn nogrid_color_with_gainmap_grid() {
let data = require_vector!(load_vector(NOGRID_ALPHA_NOGRID_GAINMAP_GRID));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info
.gain_map
.as_ref()
.expect("gain map should be present for grid gain map file");
assert!(!gm.gain_map_data.is_empty());
}
#[test]
fn gain_map_data_has_valid_obu_structure() {
let data = require_vector!(load_vector(SEINE_SDR_GAINMAP));
let decoder =
ManagedAvifDecoder::new(&data, &DecoderConfig::default()).expect("decoder should open");
let info = decoder.probe_info().expect("probe should succeed");
let gm = info.gain_map.unwrap();
let mut pos = 0;
let data = &gm.gain_map_data;
let mut obu_count = 0;
while pos < data.len() {
let header = data[pos];
let forbidden = header >> 7;
assert_eq!(forbidden, 0, "OBU forbidden bit must be 0 at pos {pos}");
let obu_type = (header >> 3) & 0x0F;
assert!(
obu_type <= 8 || obu_type == 15,
"invalid OBU type {obu_type} at pos {pos}"
);
let has_extension = (header >> 2) & 1;
let has_size = (header >> 1) & 1;
pos += 1;
if has_extension != 0 {
pos += 1; }
if has_size != 0 {
let mut size: u64 = 0;
let mut shift = 0;
loop {
if pos >= data.len() {
break;
}
let byte = data[pos] as u64;
pos += 1;
size |= (byte & 0x7F) << shift;
if byte & 0x80 == 0 {
break;
}
shift += 7;
if shift > 56 {
break;
}
}
pos += size as usize;
} else {
pos = data.len();
}
obu_count += 1;
}
assert!(
obu_count >= 2,
"gain map AV1 should have at least 2 OBUs (sequence header + frame), got {obu_count}"
);
}
#[cfg(feature = "zencodec")]
#[test]
fn decode_gain_map_via_zencodec_extras() {
use zencodec::decode::{Decode as _, DecodeJob as _, DecoderConfig as _};
let data = require_vector!(load_vector(SEINE_SDR_GAINMAP));
let dec = zenavif::AvifDecoderConfig::new();
let output = dec
.job()
.decoder(std::borrow::Cow::Borrowed(&data), &[])
.expect("decoder")
.decode()
.expect("decode");
let gm = output
.extras::<GainMapSource>()
.expect("gain map should be present as extras");
assert!(!gm.data.is_empty());
assert_eq!(gm.format, zencodec::ImageFormat::Avif);
assert_eq!(gm.depth, 0);
assert_eq!(gm.metadata.channels, 3);
assert!(gm.metadata.alternate_cicp.is_some());
}
#[cfg(feature = "zencodec")]
#[test]
fn decode_no_gain_map_extras_on_normal_image() {
use zencodec::decode::{Decode as _, DecodeJob as _, DecoderConfig as _};
let data = require_vector!(load_vector(WHITE_1X1));
let dec = zenavif::AvifDecoderConfig::new();
let output = dec
.job()
.decoder(std::borrow::Cow::Borrowed(&data), &[])
.expect("decoder")
.decode()
.expect("decode");
assert!(
output.extras::<GainMapSource>().is_none(),
"normal image should not have gain map extras"
);
}