Skip to main content

oximedia_codec/reconstruct/
output.rs

1//! Output formatting for decoded frames.
2//!
3//! This module handles final output formatting including pixel format
4//! conversion, bit depth handling, and frame metadata attachment.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::unreadable_literal)]
8#![allow(clippy::items_after_statements)]
9#![allow(clippy::unnecessary_wraps)]
10#![allow(clippy::struct_excessive_bools)]
11#![allow(clippy::identity_op)]
12#![allow(clippy::range_plus_one)]
13#![allow(clippy::needless_range_loop)]
14#![allow(clippy::useless_conversion)]
15#![allow(clippy::redundant_closure_for_method_calls)]
16#![allow(clippy::single_match_else)]
17#![allow(dead_code)]
18#![allow(clippy::doc_markdown)]
19#![allow(clippy::unused_self)]
20#![allow(clippy::trivially_copy_pass_by_ref)]
21#![allow(clippy::cast_possible_truncation)]
22#![allow(clippy::cast_sign_loss)]
23#![allow(clippy::cast_possible_wrap)]
24#![allow(clippy::missing_errors_doc)]
25#![allow(clippy::similar_names)]
26#![allow(clippy::match_same_arms)]
27#![allow(clippy::cast_precision_loss)]
28#![allow(clippy::cast_lossless)]
29#![allow(clippy::too_many_arguments)]
30#![allow(clippy::many_single_char_names)]
31#![allow(clippy::comparison_chain)]
32#![allow(clippy::if_then_some_else_none)]
33
34use super::{FrameBuffer, PlaneBuffer, ReconstructResult};
35
36// =============================================================================
37// Output Format
38// =============================================================================
39
40/// Output pixel format.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum OutputFormat {
43    /// YUV planar (same as internal).
44    #[default]
45    YuvPlanar,
46    /// YUV semi-planar (NV12/NV21).
47    YuvSemiPlanar,
48    /// RGB interleaved.
49    Rgb,
50    /// RGBA interleaved.
51    Rgba,
52    /// BGR interleaved.
53    Bgr,
54    /// BGRA interleaved.
55    Bgra,
56}
57
58impl OutputFormat {
59    /// Get number of output planes.
60    #[must_use]
61    pub const fn num_planes(self) -> usize {
62        match self {
63            Self::YuvPlanar => 3,
64            Self::YuvSemiPlanar => 2,
65            Self::Rgb | Self::Bgr => 1,
66            Self::Rgba | Self::Bgra => 1,
67        }
68    }
69
70    /// Get bytes per pixel for packed formats.
71    #[must_use]
72    pub const fn bytes_per_pixel(self) -> usize {
73        match self {
74            Self::YuvPlanar | Self::YuvSemiPlanar => 1,
75            Self::Rgb | Self::Bgr => 3,
76            Self::Rgba | Self::Bgra => 4,
77        }
78    }
79
80    /// Check if this is a planar format.
81    #[must_use]
82    pub const fn is_planar(self) -> bool {
83        matches!(self, Self::YuvPlanar | Self::YuvSemiPlanar)
84    }
85
86    /// Check if this is an RGB format.
87    #[must_use]
88    pub const fn is_rgb(self) -> bool {
89        matches!(self, Self::Rgb | Self::Rgba | Self::Bgr | Self::Bgra)
90    }
91}
92
93// =============================================================================
94// Output Configuration
95// =============================================================================
96
97/// Configuration for output formatting.
98#[derive(Clone, Debug)]
99pub struct OutputConfig {
100    /// Output format.
101    pub format: OutputFormat,
102    /// Output bit depth.
103    pub bit_depth: u8,
104    /// Dither when reducing bit depth.
105    pub dither: bool,
106    /// Full range output (0-255 vs 16-235).
107    pub full_range: bool,
108    /// Output width (for scaling).
109    pub width: Option<u32>,
110    /// Output height (for scaling).
111    pub height: Option<u32>,
112}
113
114impl Default for OutputConfig {
115    fn default() -> Self {
116        Self {
117            format: OutputFormat::YuvPlanar,
118            bit_depth: 8,
119            dither: false,
120            full_range: false,
121            width: None,
122            height: None,
123        }
124    }
125}
126
127impl OutputConfig {
128    /// Create a new output configuration.
129    #[must_use]
130    pub fn new(format: OutputFormat) -> Self {
131        Self {
132            format,
133            ..Default::default()
134        }
135    }
136
137    /// Set output bit depth.
138    #[must_use]
139    pub const fn with_bit_depth(mut self, bit_depth: u8) -> Self {
140        self.bit_depth = bit_depth;
141        self
142    }
143
144    /// Enable dithering.
145    #[must_use]
146    pub const fn with_dither(mut self) -> Self {
147        self.dither = true;
148        self
149    }
150
151    /// Set full range output.
152    #[must_use]
153    pub const fn with_full_range(mut self) -> Self {
154        self.full_range = true;
155        self
156    }
157
158    /// Set output dimensions.
159    #[must_use]
160    pub const fn with_dimensions(mut self, width: u32, height: u32) -> Self {
161        self.width = Some(width);
162        self.height = Some(height);
163        self
164    }
165}
166
167// =============================================================================
168// Output Buffer
169// =============================================================================
170
171/// Output buffer for formatted frames.
172#[derive(Clone, Debug)]
173pub struct OutputBuffer {
174    /// Output data planes.
175    planes: Vec<Vec<u8>>,
176    /// Width.
177    width: u32,
178    /// Height.
179    height: u32,
180    /// Output format.
181    format: OutputFormat,
182    /// Bit depth.
183    bit_depth: u8,
184    /// Timestamp.
185    timestamp: i64,
186}
187
188impl OutputBuffer {
189    /// Create a new output buffer.
190    #[must_use]
191    pub fn new(width: u32, height: u32, format: OutputFormat, bit_depth: u8) -> Self {
192        let bytes_per_sample = if bit_depth > 8 { 2 } else { 1 };
193
194        let planes = match format {
195            OutputFormat::YuvPlanar => {
196                vec![
197                    vec![0u8; width as usize * height as usize * bytes_per_sample],
198                    vec![0u8; (width as usize / 2) * (height as usize / 2) * bytes_per_sample],
199                    vec![0u8; (width as usize / 2) * (height as usize / 2) * bytes_per_sample],
200                ]
201            }
202            OutputFormat::YuvSemiPlanar => {
203                vec![
204                    vec![0u8; width as usize * height as usize * bytes_per_sample],
205                    vec![0u8; (width as usize / 2) * (height as usize / 2) * 2 * bytes_per_sample],
206                ]
207            }
208            OutputFormat::Rgb | OutputFormat::Bgr => {
209                vec![vec![
210                    0u8;
211                    width as usize * height as usize * 3 * bytes_per_sample
212                ]]
213            }
214            OutputFormat::Rgba | OutputFormat::Bgra => {
215                vec![vec![
216                    0u8;
217                    width as usize * height as usize * 4 * bytes_per_sample
218                ]]
219            }
220        };
221
222        Self {
223            planes,
224            width,
225            height,
226            format,
227            bit_depth,
228            timestamp: 0,
229        }
230    }
231
232    /// Get width.
233    #[must_use]
234    pub const fn width(&self) -> u32 {
235        self.width
236    }
237
238    /// Get height.
239    #[must_use]
240    pub const fn height(&self) -> u32 {
241        self.height
242    }
243
244    /// Get format.
245    #[must_use]
246    pub const fn format(&self) -> OutputFormat {
247        self.format
248    }
249
250    /// Get bit depth.
251    #[must_use]
252    pub const fn bit_depth(&self) -> u8 {
253        self.bit_depth
254    }
255
256    /// Get timestamp.
257    #[must_use]
258    pub const fn timestamp(&self) -> i64 {
259        self.timestamp
260    }
261
262    /// Set timestamp.
263    pub fn set_timestamp(&mut self, timestamp: i64) {
264        self.timestamp = timestamp;
265    }
266
267    /// Get plane data.
268    #[must_use]
269    pub fn plane(&self, index: usize) -> &[u8] {
270        self.planes.get(index).map_or(&[], Vec::as_slice)
271    }
272
273    /// Get mutable plane data.
274    pub fn plane_mut(&mut self, index: usize) -> &mut [u8] {
275        self.planes
276            .get_mut(index)
277            .map_or(&mut [], Vec::as_mut_slice)
278    }
279
280    /// Get all planes.
281    #[must_use]
282    pub fn planes(&self) -> &[Vec<u8>] {
283        &self.planes
284    }
285
286    /// Get total size in bytes.
287    #[must_use]
288    pub fn size_bytes(&self) -> usize {
289        self.planes.iter().map(Vec::len).sum()
290    }
291}
292
293// =============================================================================
294// Output Formatter
295// =============================================================================
296
297/// Output formatter for converting decoded frames.
298#[derive(Debug)]
299pub struct OutputFormatter {
300    /// Current configuration.
301    config: OutputConfig,
302    /// Dither state.
303    dither_state: u32,
304}
305
306impl Default for OutputFormatter {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312impl OutputFormatter {
313    /// Create a new output formatter.
314    #[must_use]
315    pub fn new() -> Self {
316        Self {
317            config: OutputConfig::default(),
318            dither_state: 0,
319        }
320    }
321
322    /// Create with configuration.
323    #[must_use]
324    pub fn with_config(config: OutputConfig) -> Self {
325        Self {
326            config,
327            dither_state: 0,
328        }
329    }
330
331    /// Set configuration.
332    pub fn set_config(&mut self, config: OutputConfig) {
333        self.config = config;
334    }
335
336    /// Get current configuration.
337    #[must_use]
338    pub fn config(&self) -> &OutputConfig {
339        &self.config
340    }
341
342    /// Format a frame to the configured output format.
343    ///
344    /// # Errors
345    ///
346    /// Returns error if formatting fails.
347    pub fn format(&mut self, frame: &FrameBuffer) -> ReconstructResult<OutputBuffer> {
348        let width = self.config.width.unwrap_or(frame.width());
349        let height = self.config.height.unwrap_or(frame.height());
350
351        let mut output =
352            OutputBuffer::new(width, height, self.config.format, self.config.bit_depth);
353        output.set_timestamp(frame.timestamp());
354
355        match self.config.format {
356            OutputFormat::YuvPlanar => self.format_yuv_planar(frame, &mut output)?,
357            OutputFormat::YuvSemiPlanar => self.format_yuv_semi_planar(frame, &mut output)?,
358            OutputFormat::Rgb => self.format_rgb(frame, &mut output, false, false)?,
359            OutputFormat::Rgba => self.format_rgb(frame, &mut output, true, false)?,
360            OutputFormat::Bgr => self.format_rgb(frame, &mut output, false, true)?,
361            OutputFormat::Bgra => self.format_rgb(frame, &mut output, true, true)?,
362        }
363
364        Ok(output)
365    }
366
367    /// Format to YUV planar.
368    fn format_yuv_planar(
369        &mut self,
370        frame: &FrameBuffer,
371        output: &mut OutputBuffer,
372    ) -> ReconstructResult<()> {
373        // Copy Y plane
374        self.copy_plane(frame.y_plane(), output.plane_mut(0));
375
376        // Copy U plane
377        if let Some(u) = frame.u_plane() {
378            self.copy_plane(u, output.plane_mut(1));
379        }
380
381        // Copy V plane
382        if let Some(v) = frame.v_plane() {
383            self.copy_plane(v, output.plane_mut(2));
384        }
385
386        Ok(())
387    }
388
389    /// Format to YUV semi-planar (NV12).
390    fn format_yuv_semi_planar(
391        &mut self,
392        frame: &FrameBuffer,
393        output: &mut OutputBuffer,
394    ) -> ReconstructResult<()> {
395        // Copy Y plane
396        self.copy_plane(frame.y_plane(), output.plane_mut(0));
397
398        // Interleave UV
399        if let (Some(u), Some(v)) = (frame.u_plane(), frame.v_plane()) {
400            let uv_plane = output.plane_mut(1);
401            let u_data = u.data();
402            let v_data = v.data();
403
404            let mut dst_idx = 0;
405            for (u_val, v_val) in u_data.iter().zip(v_data.iter()) {
406                let u_byte = self.convert_sample(*u_val, frame.bit_depth(), self.config.bit_depth);
407                let v_byte = self.convert_sample(*v_val, frame.bit_depth(), self.config.bit_depth);
408
409                if dst_idx + 1 < uv_plane.len() {
410                    uv_plane[dst_idx] = u_byte;
411                    uv_plane[dst_idx + 1] = v_byte;
412                    dst_idx += 2;
413                }
414            }
415        }
416
417        Ok(())
418    }
419
420    /// Format to RGB.
421    fn format_rgb(
422        &mut self,
423        frame: &FrameBuffer,
424        output: &mut OutputBuffer,
425        with_alpha: bool,
426        bgr_order: bool,
427    ) -> ReconstructResult<()> {
428        let width = output.width() as usize;
429        let height = output.height() as usize;
430        let bpp = if with_alpha { 4 } else { 3 };
431        let rgb_data = output.plane_mut(0);
432
433        let y_plane = frame.y_plane();
434        let u_plane = frame.u_plane();
435        let v_plane = frame.v_plane();
436
437        for y in 0..height {
438            for x in 0..width {
439                // Get YUV values
440                let y_val = y_plane.get(x as u32, y as u32);
441                let (u_val, v_val) = if let (Some(u), Some(v)) = (u_plane, v_plane) {
442                    let cx = (x / 2) as u32;
443                    let cy = (y / 2) as u32;
444                    (u.get(cx, cy), v.get(cx, cy))
445                } else {
446                    (128, 128)
447                };
448
449                // Convert to RGB using BT.709 coefficients
450                let (r, g, b) = yuv_to_rgb(y_val, u_val, v_val, frame.bit_depth());
451
452                // Write to output
453                let idx = (y * width + x) * bpp;
454                if idx + bpp <= rgb_data.len() {
455                    if bgr_order {
456                        rgb_data[idx] = b;
457                        rgb_data[idx + 1] = g;
458                        rgb_data[idx + 2] = r;
459                    } else {
460                        rgb_data[idx] = r;
461                        rgb_data[idx + 1] = g;
462                        rgb_data[idx + 2] = b;
463                    }
464                    if with_alpha {
465                        rgb_data[idx + 3] = 255;
466                    }
467                }
468            }
469        }
470
471        Ok(())
472    }
473
474    /// Copy a plane with bit depth conversion.
475    fn copy_plane(&mut self, src: &PlaneBuffer, dst: &mut [u8]) {
476        let src_bd = src.bit_depth();
477        let dst_bd = self.config.bit_depth;
478        let src_data = src.data();
479
480        if self.config.dither && dst_bd < src_bd {
481            // Dithered conversion
482            for (i, &sample) in src_data.iter().enumerate() {
483                if i < dst.len() {
484                    dst[i] = self.convert_sample_dithered(sample, src_bd, dst_bd);
485                }
486            }
487        } else {
488            // Direct conversion
489            for (i, &sample) in src_data.iter().enumerate() {
490                if i < dst.len() {
491                    dst[i] = self.convert_sample(sample, src_bd, dst_bd);
492                }
493            }
494        }
495    }
496
497    /// Convert a sample between bit depths.
498    fn convert_sample(&self, sample: i16, src_bd: u8, dst_bd: u8) -> u8 {
499        let src_max = (1i32 << src_bd) - 1;
500        let dst_max = (1i32 << dst_bd) - 1;
501
502        let clamped = (i32::from(sample)).clamp(0, src_max);
503
504        if src_bd == dst_bd {
505            clamped as u8
506        } else if dst_bd < src_bd {
507            // Scale down
508            let shift = src_bd - dst_bd;
509            (clamped >> shift) as u8
510        } else {
511            // Scale up
512            let shift = dst_bd - src_bd;
513            ((clamped << shift) | (clamped >> (src_bd - shift))).min(dst_max) as u8
514        }
515    }
516
517    /// Convert a sample with dithering.
518    fn convert_sample_dithered(&mut self, sample: i16, src_bd: u8, dst_bd: u8) -> u8 {
519        let src_max = (1i32 << src_bd) - 1;
520
521        let clamped = (i32::from(sample)).clamp(0, src_max);
522        let shift = src_bd.saturating_sub(dst_bd);
523
524        if shift == 0 {
525            return clamped as u8;
526        }
527
528        // Simple ordered dither
529        let dither = (self.dither_state & ((1 << shift) - 1)) as i32;
530        self.dither_state = self
531            .dither_state
532            .wrapping_mul(1664525)
533            .wrapping_add(1013904223);
534
535        let result = (clamped + dither) >> shift;
536        result.min(255) as u8
537    }
538}
539
540/// Convert YUV to RGB using BT.709 coefficients.
541fn yuv_to_rgb(y: i16, u: i16, v: i16, bd: u8) -> (u8, u8, u8) {
542    let half = 1i32 << (bd - 1);
543
544    let y_val = i32::from(y);
545    let u_val = i32::from(u) - half;
546    let v_val = i32::from(v) - half;
547
548    // BT.709 coefficients (scaled by 256)
549    let r = y_val + ((359 * v_val) >> 8);
550    let g = y_val - ((88 * u_val + 183 * v_val) >> 8);
551    let b = y_val + ((454 * u_val) >> 8);
552
553    // Scale to 8-bit if needed
554    let shift = bd.saturating_sub(8);
555
556    (
557        ((r >> shift).clamp(0, 255)) as u8,
558        ((g >> shift).clamp(0, 255)) as u8,
559        ((b >> shift).clamp(0, 255)) as u8,
560    )
561}
562
563// =============================================================================
564// Tests
565// =============================================================================
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::reconstruct::ChromaSubsampling;
571
572    #[test]
573    fn test_output_format() {
574        assert_eq!(OutputFormat::YuvPlanar.num_planes(), 3);
575        assert_eq!(OutputFormat::YuvSemiPlanar.num_planes(), 2);
576        assert_eq!(OutputFormat::Rgb.num_planes(), 1);
577        assert_eq!(OutputFormat::Rgba.num_planes(), 1);
578
579        assert_eq!(OutputFormat::Rgb.bytes_per_pixel(), 3);
580        assert_eq!(OutputFormat::Rgba.bytes_per_pixel(), 4);
581
582        assert!(OutputFormat::YuvPlanar.is_planar());
583        assert!(!OutputFormat::Rgb.is_planar());
584        assert!(OutputFormat::Rgb.is_rgb());
585    }
586
587    #[test]
588    fn test_output_config() {
589        let config = OutputConfig::new(OutputFormat::Rgb)
590            .with_bit_depth(8)
591            .with_full_range()
592            .with_dither()
593            .with_dimensions(1920, 1080);
594
595        assert_eq!(config.format, OutputFormat::Rgb);
596        assert_eq!(config.bit_depth, 8);
597        assert!(config.full_range);
598        assert!(config.dither);
599        assert_eq!(config.width, Some(1920));
600        assert_eq!(config.height, Some(1080));
601    }
602
603    #[test]
604    fn test_output_buffer() {
605        let buffer = OutputBuffer::new(1920, 1080, OutputFormat::YuvPlanar, 8);
606
607        assert_eq!(buffer.width(), 1920);
608        assert_eq!(buffer.height(), 1080);
609        assert_eq!(buffer.format(), OutputFormat::YuvPlanar);
610        assert_eq!(buffer.bit_depth(), 8);
611
612        // Check plane sizes
613        assert_eq!(buffer.plane(0).len(), 1920 * 1080);
614        assert_eq!(buffer.plane(1).len(), 960 * 540);
615        assert_eq!(buffer.plane(2).len(), 960 * 540);
616    }
617
618    #[test]
619    fn test_output_buffer_rgb() {
620        let buffer = OutputBuffer::new(64, 64, OutputFormat::Rgb, 8);
621        assert_eq!(buffer.plane(0).len(), 64 * 64 * 3);
622    }
623
624    #[test]
625    fn test_output_buffer_rgba() {
626        let buffer = OutputBuffer::new(64, 64, OutputFormat::Rgba, 8);
627        assert_eq!(buffer.plane(0).len(), 64 * 64 * 4);
628    }
629
630    #[test]
631    fn test_output_formatter_creation() {
632        let formatter = OutputFormatter::new();
633        assert_eq!(formatter.config().format, OutputFormat::YuvPlanar);
634    }
635
636    #[test]
637    fn test_output_formatter_with_config() {
638        let config = OutputConfig::new(OutputFormat::Rgb);
639        let formatter = OutputFormatter::with_config(config);
640        assert_eq!(formatter.config().format, OutputFormat::Rgb);
641    }
642
643    #[test]
644    fn test_output_formatter_format_yuv() {
645        let frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
646        let mut formatter = OutputFormatter::new();
647
648        let output = formatter.format(&frame).expect("should succeed");
649        assert_eq!(output.width(), 64);
650        assert_eq!(output.height(), 64);
651        assert_eq!(output.format(), OutputFormat::YuvPlanar);
652    }
653
654    #[test]
655    fn test_output_formatter_format_rgb() {
656        let frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
657        let config = OutputConfig::new(OutputFormat::Rgb);
658        let mut formatter = OutputFormatter::with_config(config);
659
660        let output = formatter.format(&frame).expect("should succeed");
661        assert_eq!(output.format(), OutputFormat::Rgb);
662        assert_eq!(output.plane(0).len(), 64 * 64 * 3);
663    }
664
665    #[test]
666    fn test_yuv_to_rgb() {
667        // Black
668        let (r, g, b) = yuv_to_rgb(0, 128, 128, 8);
669        assert!(r < 10 && g < 10 && b < 10);
670
671        // White
672        let (r, g, b) = yuv_to_rgb(255, 128, 128, 8);
673        assert!(r > 245 && g > 245 && b > 245);
674    }
675
676    #[test]
677    fn test_convert_sample() {
678        let formatter = OutputFormatter::new();
679
680        // Same bit depth
681        assert_eq!(formatter.convert_sample(128, 8, 8), 128);
682
683        // Scale down
684        assert_eq!(formatter.convert_sample(512, 10, 8), 128);
685
686        // Scale down from 12-bit to 8-bit
687        assert_eq!(formatter.convert_sample(2048, 12, 8), 128);
688    }
689
690    #[test]
691    fn test_convert_sample_clamping() {
692        let formatter = OutputFormatter::new();
693
694        // Negative value should clamp to 0
695        assert_eq!(formatter.convert_sample(-10, 8, 8), 0);
696
697        // Over max should clamp
698        assert_eq!(formatter.convert_sample(300, 8, 8), 255);
699    }
700}