Skip to main content

oxihuman_export/
r3d_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! RED RAW R3D stub export.
6
7/// R3D codec variant.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum R3dCodec {
10    Redcode28,
11    Redcode36,
12    Redcode42,
13}
14
15impl R3dCodec {
16    pub fn compression_ratio(&self) -> u32 {
17        match self {
18            R3dCodec::Redcode28 => 28,
19            R3dCodec::Redcode36 => 36,
20            R3dCodec::Redcode42 => 42,
21        }
22    }
23}
24
25/// A RED RAW R3D stub frame.
26#[derive(Debug, Clone)]
27pub struct R3dFrame {
28    pub frame_index: u32,
29    pub width: u32,
30    pub height: u32,
31    pub iso: u32,
32    pub kelvin: u32,
33}
34
35/// R3D export stub.
36#[derive(Debug, Clone)]
37pub struct R3dExport {
38    pub codec: R3dCodec,
39    pub fps: f32,
40    pub frames: Vec<R3dFrame>,
41    pub camera_serial: String,
42}
43
44/// Create a new R3D export.
45pub fn new_r3d_export(codec: R3dCodec, fps: f32) -> R3dExport {
46    R3dExport {
47        codec,
48        fps,
49        frames: Vec::new(),
50        camera_serial: "RED-000001".to_string(),
51    }
52}
53
54/// Add a frame to the export.
55pub fn r3d_add_frame(
56    export: &mut R3dExport,
57    frame_index: u32,
58    width: u32,
59    height: u32,
60    iso: u32,
61    kelvin: u32,
62) {
63    export.frames.push(R3dFrame {
64        frame_index,
65        width,
66        height,
67        iso,
68        kelvin,
69    });
70}
71
72/// Frame count.
73pub fn r3d_frame_count(export: &R3dExport) -> usize {
74    export.frames.len()
75}
76
77/// Duration in seconds.
78pub fn r3d_duration_seconds(export: &R3dExport) -> f32 {
79    if export.fps <= 0.0 {
80        return 0.0;
81    }
82    export.frames.len() as f32 / export.fps
83}
84
85/// Estimate the R3D file size in bytes (stub).
86pub fn r3d_size_estimate(export: &R3dExport) -> usize {
87    export
88        .frames
89        .iter()
90        .map(|f| {
91            let pixels = f.width as usize * f.height as usize;
92            /* 16-bit RAW / compression ratio */
93            pixels * 2 / export.codec.compression_ratio() as usize
94        })
95        .sum()
96}
97
98/// Validate the export.
99pub fn validate_r3d(export: &R3dExport) -> bool {
100    export.fps > 0.0 && !export.frames.is_empty()
101}
102
103/// Generate a stub R3D metadata string.
104pub fn r3d_metadata_string(export: &R3dExport) -> String {
105    format!(
106        "R3D|Codec:REDCODE{}|FPS:{}|Frames:{}|Serial:{}",
107        export.codec.compression_ratio(),
108        export.fps,
109        export.frames.len(),
110        export.camera_serial
111    )
112}
113
114/// Return the resolution of the first frame.
115pub fn r3d_resolution(export: &R3dExport) -> Option<(u32, u32)> {
116    export.frames.first().map(|f| (f.width, f.height))
117}
118
119/// Average ISO across frames.
120pub fn r3d_average_iso(export: &R3dExport) -> f32 {
121    if export.frames.is_empty() {
122        return 0.0;
123    }
124    export.frames.iter().map(|f| f.iso as f32).sum::<f32>() / export.frames.len() as f32
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn sample() -> R3dExport {
132        let mut exp = new_r3d_export(R3dCodec::Redcode28, 24.0);
133        r3d_add_frame(&mut exp, 0, 8192, 4320, 800, 5600);
134        r3d_add_frame(&mut exp, 1, 8192, 4320, 1600, 5600);
135        exp
136    }
137
138    #[test]
139    fn test_frame_count() {
140        assert_eq!(r3d_frame_count(&sample()), 2);
141    }
142
143    #[test]
144    fn test_duration() {
145        let exp = sample();
146        assert!((r3d_duration_seconds(&exp) - 2.0 / 24.0).abs() < 1e-4);
147    }
148
149    #[test]
150    fn test_size_estimate() {
151        assert!(r3d_size_estimate(&sample()) > 0);
152    }
153
154    #[test]
155    fn test_validate_valid() {
156        assert!(validate_r3d(&sample()));
157    }
158
159    #[test]
160    fn test_validate_no_frames() {
161        let exp = new_r3d_export(R3dCodec::Redcode36, 24.0);
162        assert!(!validate_r3d(&exp));
163    }
164
165    #[test]
166    fn test_metadata_string() {
167        let s = r3d_metadata_string(&sample());
168        assert!(s.contains("28"));
169    }
170
171    #[test]
172    fn test_resolution() {
173        let exp = sample();
174        assert_eq!(r3d_resolution(&exp), Some((8192, 4320)));
175    }
176
177    #[test]
178    fn test_average_iso() {
179        let exp = sample();
180        assert!((r3d_average_iso(&exp) - 1200.0).abs() < 1e-3);
181    }
182
183    #[test]
184    fn test_codec_ratios() {
185        assert_eq!(R3dCodec::Redcode28.compression_ratio(), 28);
186        assert_eq!(R3dCodec::Redcode42.compression_ratio(), 42);
187    }
188}