From 0c1437029ec7903ae82214c567c23d3e29a351fe Mon Sep 17 00:00:00 2001 From: lutyjj <10267813+lutyjj@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:02:18 +0000 Subject: [PATCH] feat: add ColorDescription::with_full_range and is_hdr From the hgaiser/moonshine#146 review. The presets pin primaries, transfer and matrix as a group, and full range is the only field that changes on its own, so every consumer ends up writing `ColorDescription { full_range, ..ColorDescription::bt2020_pq() }`. `with_full_range` gives that the same shape as the EncodeConfig builders, and ColorDescription is `#[must_use]` now like the other builder types. `is_hdr` moves the "PQ transfer function is what makes the stream HDR" check onto the type instead of every consumer redefining it; the luma range and primaries don't decide it. The H.273 code points the presets use are named constants now instead of bare numbers. --- src/encoder/mod.rs | 69 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 8f324a4..c4a4497 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -232,6 +232,7 @@ pub struct Dimensions { /// Describes how color is encoded in the video stream, allowing decoders /// to correctly interpret the color space. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[must_use] pub struct ColorDescription { /// Color primaries (1=BT.709, 9=BT.2020). pub color_primaries: u8, @@ -244,12 +245,20 @@ pub struct ColorDescription { } impl ColorDescription { + // H.273 code points for the fields above. + const PRIMARIES_BT709: u8 = 1; + const PRIMARIES_BT2020: u8 = 9; + const TRANSFER_BT709: u8 = 1; + const TRANSFER_ST2084_PQ: u8 = 16; + const MATRIX_BT709: u8 = 1; + const MATRIX_BT2020_NCL: u8 = 9; + /// BT.709 color description (standard SDR, limited range). pub fn bt709() -> Self { Self { - color_primaries: 1, - transfer_characteristics: 1, - matrix_coefficients: 1, + color_primaries: Self::PRIMARIES_BT709, + transfer_characteristics: Self::TRANSFER_BT709, + matrix_coefficients: Self::MATRIX_BT709, full_range: false, } } @@ -257,12 +266,26 @@ impl ColorDescription { /// BT.2020 with PQ transfer function (HDR10). pub fn bt2020_pq() -> Self { Self { - color_primaries: 9, - transfer_characteristics: 16, - matrix_coefficients: 9, + color_primaries: Self::PRIMARIES_BT2020, + transfer_characteristics: Self::TRANSFER_ST2084_PQ, + matrix_coefficients: Self::MATRIX_BT2020_NCL, full_range: false, } } + + /// Set full range (0-255) rather than limited/TV range (16-235). + pub fn with_full_range(mut self, full_range: bool) -> Self { + self.full_range = full_range; + self + } + + /// Whether this description makes the stream HDR. + /// + /// The PQ (ST 2084) transfer function is what decides it; primaries and + /// luma range do not. + pub fn is_hdr(&self) -> bool { + self.transfer_characteristics == Self::TRANSFER_ST2084_PQ + } } /// Encode configuration. @@ -1004,5 +1027,39 @@ mod tests { assert_eq!(cd.matrix_coefficients, 9); assert!(!cd.full_range); } + + #[test] + fn test_with_full_range() { + let cd = ColorDescription::bt709().with_full_range(true); + assert!(cd.full_range); + // Only the range changes; the preset stays intact. + assert_eq!(cd.with_full_range(false), ColorDescription::bt709()); + } + + #[test] + fn test_is_hdr() { + // The luma range doesn't decide it. + for full_range in [false, true] { + assert!( + ColorDescription::bt2020_pq() + .with_full_range(full_range) + .is_hdr() + ); + assert!( + !ColorDescription::bt709() + .with_full_range(full_range) + .is_hdr() + ); + } + + // Neither do the primaries: PQ on BT.709 primaries is still HDR. + // 16 is the H.273 code point for ST 2084, written literally so the + // test pins the constant rather than echoing it. + let pq_709 = ColorDescription { + transfer_characteristics: 16, + ..ColorDescription::bt709() + }; + assert!(pq_709.is_hdr()); + } } }