Skip to main content

wmv_decoder/
color.rs

1//! WMV YUV colour conversion helpers.
2//!
3//! WMV3 Simple/Main profile does not carry an explicit transfer-matrix selector
4//! in the bitstream parsed by this crate. The original Siglus desktop path used
5//! the Windows media stack, so for unspecified matrix metadata we follow the
6//! Windows/DXVA convention: SD (source height <= 576) uses BT.601 and HD
7//! (source height > 576) uses BT.709.
8
9use crate::decoder::YuvFrame;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum VideoTransferMatrix {
13    Bt601,
14    Bt709,
15}
16
17impl VideoTransferMatrix {
18    /// Windows/DXVA fallback for unspecified transfer-matrix metadata.
19    ///
20    /// DXVA defines HD for this purpose as a source height greater than 576
21    /// lines; unknown SD content is treated as BT.601 and unknown HD content as
22    /// BT.709.
23    #[inline]
24    pub const fn for_unspecified_source(height: u32) -> Self {
25        if height > 576 {
26            Self::Bt709
27        } else {
28            Self::Bt601
29        }
30    }
31}
32
33#[derive(Clone, Copy)]
34struct LimitedRangeCoefficients {
35    rv: i32,
36    gu: i32,
37    gv: i32,
38    bu: i32,
39}
40
41impl VideoTransferMatrix {
42    #[inline]
43    const fn limited_range_coefficients(self) -> LimitedRangeCoefficients {
44        match self {
45            // 1.164(Y-16) + 1.596(V-128)
46            // 1.164(Y-16) - 0.391(U-128) - 0.813(V-128)
47            // 1.164(Y-16) + 2.016(U-128)
48            Self::Bt601 => LimitedRangeCoefficients {
49                rv: 409,
50                gu: 100,
51                gv: 208,
52                bu: 516,
53            },
54            // 1.164(Y-16) + 1.793(V-128)
55            // 1.164(Y-16) - 0.213(U-128) - 0.533(V-128)
56            // 1.164(Y-16) + 2.112(U-128)
57            Self::Bt709 => LimitedRangeCoefficients {
58                rv: 459,
59                gu: 55,
60                gv: 136,
61                bu: 541,
62            },
63        }
64    }
65}
66
67/// Convert one studio-range Y'CbCr sample to 8-bit RGB using the selected
68/// transfer matrix. The integer form intentionally preserves the decoder's
69/// existing BT.601 rounding convention while adding the corresponding BT.709
70/// coefficients.
71#[inline]
72pub fn yuv_limited_to_rgb(y: u8, cb: u8, cr: u8, matrix: VideoTransferMatrix) -> [u8; 3] {
73    let coeff = matrix.limited_range_coefficients();
74    let c = (y as i32 - 16).max(0);
75    let d = cb as i32 - 128;
76    let e = cr as i32 - 128;
77
78    let r = ((298 * c + coeff.rv * e + 128) >> 8).clamp(0, 255) as u8;
79    let g = ((298 * c - coeff.gu * d - coeff.gv * e + 128) >> 8).clamp(0, 255) as u8;
80    let b = ((298 * c + coeff.bu * d + 128) >> 8).clamp(0, 255) as u8;
81    [r, g, b]
82}
83
84/// Convert a WMV YUV420p frame to packed RGB24, selecting the same default
85/// transfer matrix Windows uses when the source does not specify one.
86pub fn yuv420p_to_rgb(frame: &YuvFrame) -> Vec<u8> {
87    let width = frame.width as usize;
88    let height = frame.height as usize;
89    let chroma_width = width / 2;
90    let matrix = VideoTransferMatrix::for_unspecified_source(frame.height);
91    let mut rgb = vec![0u8; width.saturating_mul(height).saturating_mul(3)];
92
93    for y in 0..height {
94        for x in 0..width {
95            let luma = frame.y.get(y * width + x).copied().unwrap_or(16);
96            let chroma_index = (y / 2).saturating_mul(chroma_width).saturating_add(x / 2);
97            let cb = frame.cb.get(chroma_index).copied().unwrap_or(128);
98            let cr = frame.cr.get(chroma_index).copied().unwrap_or(128);
99            let [r, g, b] = yuv_limited_to_rgb(luma, cb, cr, matrix);
100            let out = (y * width + x) * 3;
101            rgb[out] = r;
102            rgb[out + 1] = g;
103            rgb[out + 2] = b;
104        }
105    }
106
107    rgb
108}
109
110/// Convert a WMV YUV420p frame to packed RGBA8, selecting the Windows/DXVA
111/// default transfer matrix for unspecified metadata.
112pub fn yuv420p_to_rgba(frame: &YuvFrame) -> Vec<u8> {
113    yuv420p_to_rgba_scaled(frame, frame.width, frame.height)
114}
115
116/// Convert to a bounded output size directly from YUV, without a full-size
117/// RGBA allocation. Sampling uses the nearest source pixel.
118pub fn yuv420p_to_rgba_scaled(frame: &YuvFrame, output_width: u32, output_height: u32) -> Vec<u8> {
119    let width = frame.width as usize;
120    let height = frame.height as usize;
121    let output_width = output_width as usize;
122    let output_height = output_height as usize;
123    let chroma_width = width / 2;
124    let matrix = VideoTransferMatrix::for_unspecified_source(frame.height);
125    let mut rgba = vec![0u8; output_width.saturating_mul(output_height).saturating_mul(4)];
126
127    for out_y in 0..output_height {
128        let y = out_y * height / output_height;
129        for out_x in 0..output_width {
130            let x = out_x * width / output_width;
131            let luma = frame.y.get(y * width + x).copied().unwrap_or(16);
132            let chroma_index = (y / 2).saturating_mul(chroma_width).saturating_add(x / 2);
133            let cb = frame.cb.get(chroma_index).copied().unwrap_or(128);
134            let cr = frame.cr.get(chroma_index).copied().unwrap_or(128);
135            let [r, g, b] = yuv_limited_to_rgb(luma, cb, cr, matrix);
136            let out = (out_y * output_width + out_x) * 4;
137            rgba[out] = r;
138            rgba[out + 1] = g;
139            rgba[out + 2] = b;
140            rgba[out + 3] = 255;
141        }
142    }
143
144    rgba
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn windows_unspecified_matrix_switches_above_576_lines() {
153        assert_eq!(
154            VideoTransferMatrix::for_unspecified_source(480),
155            VideoTransferMatrix::Bt601
156        );
157        assert_eq!(
158            VideoTransferMatrix::for_unspecified_source(576),
159            VideoTransferMatrix::Bt601
160        );
161        assert_eq!(
162            VideoTransferMatrix::for_unspecified_source(577),
163            VideoTransferMatrix::Bt709
164        );
165        assert_eq!(
166            VideoTransferMatrix::for_unspecified_source(1080),
167            VideoTransferMatrix::Bt709
168        );
169    }
170
171    #[test]
172    fn studio_black_and_white_are_matrix_independent() {
173        for matrix in [VideoTransferMatrix::Bt601, VideoTransferMatrix::Bt709] {
174            assert_eq!(yuv_limited_to_rgb(16, 128, 128, matrix), [0, 0, 0]);
175            assert_eq!(yuv_limited_to_rgb(235, 128, 128, matrix), [255, 255, 255]);
176        }
177    }
178
179    #[test]
180    fn bt601_and_bt709_use_different_chroma_matrices() {
181        let sample = (100, 90, 200);
182        assert_ne!(
183            yuv_limited_to_rgb(sample.0, sample.1, sample.2, VideoTransferMatrix::Bt601,),
184            yuv_limited_to_rgb(sample.0, sample.1, sample.2, VideoTransferMatrix::Bt709,),
185        );
186    }
187}