oxihuman_export/
r3d_export.rs1#![allow(dead_code)]
4
5#[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#[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#[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
44pub 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
54pub 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
72pub fn r3d_frame_count(export: &R3dExport) -> usize {
74 export.frames.len()
75}
76
77pub 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
85pub 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 pixels * 2 / export.codec.compression_ratio() as usize
94 })
95 .sum()
96}
97
98pub fn validate_r3d(export: &R3dExport) -> bool {
100 export.fps > 0.0 && !export.frames.is_empty()
101}
102
103pub 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
114pub fn r3d_resolution(export: &R3dExport) -> Option<(u32, u32)> {
116 export.frames.first().map(|f| (f.width, f.height))
117}
118
119pub 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}