fix(export): tag SDR exports with a colr box (#2142)

ColorSignal::apply_to wrote the sample entry's colr box only for HDR, so
SDR ProRes, DNxHR and APV MOVs carried no colour that Apple's media stack
reads (it takes their colour only from colr, not from the codecs' frame
headers): QuickTime Player and Final Cut saw no primaries, transfer or
matrix. SDR exports now get colr with the export's colour too (nclc in
MOV, nclx in MP4); HDR metadata stays PQ-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sandweiss
2026-10-15 18:53:32 -07:00
co-authored by Claude Opus 5.5
parent 8309f21a6c
commit 678fe73221
3 changed files with 42 additions and 4 deletions
+14
View File
@@ -199,3 +199,17 @@ fn the_software_h264_sei_is_the_static_metadata_laid_out_as_messages() {
want.push(0x80);
assert_eq!(sei, want);
}
/// #2142: SDR gets its `colr` box as well (nclc in MOV, nclx in MP4), and no HDR metadata.
#[test]
fn sdr_gets_a_colr_box_and_no_hdr_metadata() {
let mut e = filmcraft_isobmff::SampleEntry::prores(filmcraft_isobmff::FourCc(*b"apch"), 64, 64);
ColorSignal::default().apply_to(&mut e, true);
let v = e.video.as_ref().unwrap();
assert_eq!(v.color, Some(filmcraft_isobmff::ColorInfo::Nclc { primaries: 1, transfer: 1, matrix: 1 }));
assert!(v.mastering_display.is_none() && v.content_light.is_none());
let mut e = filmcraft_isobmff::SampleEntry::hevc(Default::default(), 64, 64);
ColorSignal::default().apply_to(&mut e, false);
let v = e.video.as_ref().unwrap();
assert_eq!(v.color, Some(filmcraft_isobmff::ColorInfo::Nclx { primaries: 1, transfer: 1, matrix: 1, full_range: false }));
}
+3 -4
View File
@@ -520,11 +520,10 @@ impl ColorSignal {
if self.matrix == 9 { (0.2627, 0.0593) } else { (0.2126, 0.0722) }
}
/// Sample-entry boxes: `colr` (nclx for MP4, nclc for MOV) plus `mdcv`/`clli` for PQ
/// (mastering display BT.2020/D65, 1000/0.0001 cd/m²; MaxCLL/MaxFALL 0 = unknown).
/// (mastering display BT.2020/D65, 1000/0.0001 cd/m²; MaxCLL/MaxFALL 0 = unknown). SDR gets
/// its `colr` too: Apple's media stack (QuickTime Player, Final Cut Pro) reads the colour of
/// ProRes, DNxHR and APV only from this box, not from their frame headers (#2142).
pub fn apply_to(&self, e: &mut SampleEntry, mov: bool) {
if !self.is_hdr() {
return;
}
if let Some(v) = e.video.as_mut() {
let (p, t, m) = (self.primaries as u16, self.transfer as u16, self.matrix as u16);
v.color = Some(if mov {
+25
View File
@@ -126,6 +126,31 @@ fn prores_export_roundtrip() {
assert!(f[0] > 240 && f[1] < 15 && f[2] < 15, "{:?}", &f[..4]);
}
/// #2142: SDR ProRes, DNxHR and APV (MOV) and H.264 (MP4) exports carry a `colr` box with the
/// export's BT.709 colour: Apple's media stack reads the colour of the MOV codecs only from there.
#[test]
fn sdr_exports_carry_a_colr_box() {
use filmcraft_isobmff::ColorInfo;
let (p, seq, m) = project();
let nclc = ColorInfo::Nclc { primaries: 1, transfer: 1, matrix: 1 };
let nclx = ColorInfo::Nclx { primaries: 1, transfer: 1, matrix: 1, full_range: false };
for (format, name, want) in [
(Format::ProRes, "colr-prores.mov", nclc.clone()),
(Format::DnxHr, "colr-dnxhr.mov", nclc.clone()),
(Format::Apv, "colr-apv.mov", nclc.clone()),
(Format::H264, "colr-h264.mp4", nclx),
] {
let path = tmp(name);
export(&p, seq, &ExportSettings { format, path: path.clone(), include_audio: false, ..Default::default() }, &m, &Progress::default()).unwrap();
let bytes = std::fs::read(&path).unwrap();
let file = filmcraft_isobmff::open(bytes).unwrap();
let t = file.track_of_kind(filmcraft_isobmff::TrackKind::Video).unwrap();
let v = file.tracks[t].entries[0].video.as_ref().unwrap();
assert_eq!(v.color.as_ref(), Some(&want), "{name}");
assert!(v.mastering_display.is_none() && v.content_light.is_none(), "{name}: no HDR metadata on SDR");
}
}
/// ProRes 4444 / 4444 XQ export (#342): 4:4:4 frames under the `ap4h` / `ap4x` codes, and with
/// Include Alpha Channel the picture's alpha survives instead of being flattened over black.
#[test]