LR-HDR-EXPORT: HDR AVIF (10-bit Rec. 2020 PQ)

`encode_avif_pq`: linear Rec. 2020 with SDR white at 1.0 → PQ (SDR white at
203 cd/m², BT.2408), BT.2020 NCL YCbCr 4:4:4, 10-bit, with CICP nclx
(9/16/9, full range) and a clli box. ravif fixes the sRGB curve, so this
drives the rav1e + avif-serialize it already uses (no new crates).

Export: AVIF with `hdr` renders the HDR edit in Rec. 2020 and writes PQ.
Photos without an HDR edit export as before, in every format. CoreImage
decodes the file as HDR (same 3.1 % of the frame above SDR white as the gain
map JPEG of the same edit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
akvaithi
2026-10-09 22:40:37 -05:00
co-authored by Claude Opus 5.5
parent af06f2dbc1
commit 7828f93c32
12 changed files with 203 additions and 23 deletions
+18
View File
@@ -422,3 +422,21 @@ fn hdr_float_tiff_keeps_highlights_above_white() {
assert!(sdr <= 1.0 + 1e-3, "SDR float TIFF stays in 0..1 ({sdr})");
assert!(hdr > 1.2 && hdr <= 4.0 + 1e-3, "HDR float TIFF max {hdr}");
}
#[test]
fn hdr_avif_export_is_pq_rec2020() {
let mut s = Session::with_demo();
let id = sunset(&mut s);
let o = ExportOptions::from_json(&json!({"longEdge": 160, "format": "avif", "hdr": true, "quality": 70}));
assert!(o.hdr_output());
let sdr = export_photo(&mut s, id, &o, 1).unwrap();
s.execute("develop.hdr", &json!({"enabled": true, "maxEv": 2})).unwrap();
let hdr = export_photo(&mut s, id, &o, 1).unwrap();
// the `colr` nclx box: BT.2020 primaries (9), PQ (16), BT.2020 NCL (9), full range
let nclx = |b: &[u8]| b.windows(4).position(|w| w == b"nclx").map(|i| b[i + 4..i + 11].to_vec());
let c = nclx(&hdr.bytes).expect("colr nclx box");
assert_eq!(c, vec![0, 9, 0, 16, 0, 9, 0x80]);
assert!(hdr.bytes.windows(4).any(|w| w == b"clli"), "content light level box");
// a photo without an HDR edit exports as an ordinary (sRGB) AVIF
assert!(nclx(&sdr.bytes).is_none_or(|c| c[3] != 16));
}