use heifer::heifer_isobmff::HeifFile;
use heifer::heifer_isobmff::boxes::{ColorInfo, Property};
use heifer::{EncodeOptions, Image, Metadata};
fn load(name: &str) -> Option<Vec<u8>> {
let path = format!("{}/../../tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"));
let data = std::fs::read(&path).ok();
if data.is_none() {
eprintln!("skipping: {path} not found (run scripts/fetch-fixtures.sh)");
}
data
}
fn srgb_profile() -> Option<Vec<u8>> {
std::fs::read("/System/Library/ColorSync/Profiles/sRGB Profile.icc").ok()
}
fn colors(file: &HeifFile<'_>, id: heifer::heifer_isobmff::ItemId) -> Vec<ColorInfo> {
file.item_properties(id)
.unwrap()
.filter_map(|p| match p {
Property::Color(c) => Some(c.clone()),
_ => None,
})
.collect()
}
fn gradient(width: u32, height: u32) -> Image {
let mut image = Image::filled(width, height, 8, [0, 0, 0, 255]);
for y in 0..height {
for x in 0..width {
let i = (y * width + x) as usize;
image.data[i * 4] = u16::from((x * 255 / width.max(1)) as u8);
image.data[i * 4 + 1] = u16::from((y * 255 / height.max(1)) as u8);
image.data[i * 4 + 2] = u16::from(((x + y) * 255 / (width + height).max(1)) as u8);
}
}
image
}
#[test]
fn files_without_colour_information_are_left_as_coded() {
for name in ["alpha.heic", "grid.heic", "single_image.heic"] {
let Some(bytes) = load(name) else { continue };
let raw = heifer::decode(&bytes).unwrap();
let srgb = heifer::decode_srgb(&bytes).unwrap();
assert_eq!((srgb.width, srgb.height), (raw.width, raw.height), "{name}");
assert!(srgb.data == raw.data, "{name}: samples changed");
}
}
#[test]
fn nclx_srgb_files_are_left_as_coded() {
let rgba: Vec<u8> = (0..64 * 48)
.flat_map(|i| {
let v = (i % 256) as u8;
[v, v.wrapping_mul(3), v.wrapping_mul(7), 255]
})
.collect();
let bytes = heifer::encode_rgba8(64, 48, &rgba, &EncodeOptions::default()).unwrap();
let raw = heifer::decode(&bytes).unwrap();
let srgb = heifer::decode_srgb(&bytes).unwrap();
assert!(srgb.data == raw.data);
assert!(
heifer::decode_srgb_rgba8(&bytes, &heifer::Options::default())
.unwrap()
.data
== heifer::decode_rgba8(&bytes, &heifer::Options::default())
.unwrap()
.data
);
}
#[test]
fn icc_profile_is_applied_to_the_decoded_image() {
let Some(bytes) = load("real/heif_other__pug.heic") else {
return;
};
let raw = heifer::decode(&bytes).unwrap();
let srgb = heifer::decode_srgb(&bytes).unwrap();
assert_eq!((srgb.width, srgb.height), (raw.width, raw.height));
assert_eq!(srgb.has_alpha, raw.has_alpha);
let changed = raw
.data
.iter()
.zip(&srgb.data)
.filter(|(a, b)| a != b)
.count();
assert!(
changed > raw.data.len() / 4,
"the ICC profile was ignored ({changed} samples changed)"
);
if raw.bit_depth <= 8 {
let narrow = heifer::decode_srgb_rgba8(&bytes, &heifer::Options::default()).unwrap();
assert!(narrow.data == srgb.to_rgba8());
}
}
#[test]
fn icc_profile_replaces_nclx_on_every_item() {
let Some(icc) = srgb_profile() else {
eprintln!("skipping: no system sRGB profile");
return;
};
let image = gradient(600, 600); let metadata = Metadata {
icc_profile: Some(icc.clone()),
..Default::default()
};
let bytes = heifer::encode_with_metadata(&image, &metadata, &EncodeOptions::default()).unwrap();
let file = HeifFile::parse(&bytes).unwrap();
let grid = file.primary_id;
assert!(file.item(grid).unwrap().item_type.0 == *b"grid");
let mut items = vec![grid];
items.extend_from_slice(file.referenced_items(grid, b"dimg")); items.extend(file.referencing_items(grid, b"thmb")); for id in items {
let colors = colors(&file, id);
assert_eq!(colors.len(), 1, "item {id}: {colors:?}");
match &colors[0] {
ColorInfo::Icc(embedded) => assert_eq!(embedded, &icc),
other => panic!("item {id}: expected the ICC profile, got {other:?}"),
}
}
let read = heifer::read_metadata(&bytes).unwrap();
assert_eq!(read.icc_profile.as_deref(), Some(icc.as_slice()));
heifer::decode_srgb(&bytes).unwrap();
}
#[test]
fn srgb_icc_conversion_is_nearly_the_identity() {
let Some(icc) = srgb_profile() else {
eprintln!("skipping: no system sRGB profile");
return;
};
let image = gradient(64, 48);
let metadata = Metadata {
icc_profile: Some(icc),
..Default::default()
};
let options = EncodeOptions {
quality: 100,
..Default::default()
};
let bytes = heifer::encode_with_metadata(&image, &metadata, &options).unwrap();
let raw = heifer::decode(&bytes).unwrap();
let decoded = heifer::decode_srgb(&bytes).unwrap();
let max = raw
.data
.iter()
.zip(&decoded.data)
.map(|(a, b)| i32::from(*a).abs_diff(i32::from(*b)))
.max()
.unwrap();
assert!(max <= 1, "the sRGB profile changed the samples by {max}");
}