Skip to main content

mcraw_tui/
metadata.rs

1use ratatui::style::Color;
2use ratatui::text::{Line, Span};
3
4use crate::file::McrawFileInfo;
5
6pub fn format_metadata_for_display(info: &McrawFileInfo) -> Vec<Line<'static>> {
7    let mut lines = Vec::new();
8    lines.extend(format_general_section(info));
9    lines.extend(format_video_section(info));
10    lines.extend(format_camera_section(info));
11    lines.extend(format_audio_section(info));
12    lines
13}
14
15pub fn format_general_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
16    let mut lines = Vec::new();
17    lines.push(Line::from(Span::styled(
18        "General",
19        Color::Yellow,
20    )));
21
22    let filename = info
23        .path
24        .split('/')
25        .last()
26        .unwrap_or(&info.path);
27    lines.push(Line::from(format!(
28        "  Filename:     {}",
29        filename
30    )));
31    lines.push(Line::from(format!("  Path:         {}", info.path)));
32    lines.push(Line::from(format!("  Size:         {}", format_size(info.size))));
33    lines.push(Line::from(format!(
34        "  Format:       {}",
35        info.format_name()
36    )));
37
38    if let Some(ref date) = info.camera_metadata.capture_date {
39        lines.push(Line::from(format!(
40            "  Capture Date: {}",
41            format_capture_date(date)
42        )));
43    }
44
45    lines.push(Line::from(""));
46    lines
47}
48
49pub fn format_camera_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
50    let mut lines = Vec::new();
51    lines.push(Line::from(Span::styled(
52        "Camera",
53        Color::Yellow,
54    )));
55
56    if let Some(ref model) = info.camera_metadata.camera_model {
57        if !model.is_empty() {
58            lines.push(Line::from(format!("  Camera:       {}", model)));
59        }
60    }
61    if let Some(ref lens) = info.camera_metadata.lens_model {
62        lines.push(Line::from(format!("  Lens:         {}", lens)));
63    }
64    if let Some(fl) = info.camera_metadata.focal_length {
65        lines.push(Line::from(format!("  Focal Length: {:.1}mm", fl)));
66    }
67    if let Some(ap) = info.camera_metadata.aperture {
68        lines.push(Line::from(format!("  Aperture:     f/{:.1}", ap)));
69    }
70    if let Some(iso) = info.camera_metadata.iso {
71        lines.push(Line::from(format!("  ISO:          {}", iso)));
72    }
73    if let Some(et) = info.camera_metadata.exposure_time {
74        lines.push(Line::from(format!(
75            "  Exposure:     {}",
76            format_exposure_time(et)
77        )));
78    }
79    if let Some(wb) = info.camera_metadata.white_balance {
80        lines.push(Line::from(format!("  White Balance:{:.0}K", wb)));
81    }
82    if let Some(ref cm) = info.camera_metadata.color_matrix {
83        let vals: Vec<String> = cm.iter().map(|v| format!("{:.2}", v)).collect();
84        lines.push(Line::from(format!("  Color Matrix1: [{}]", vals.join(", "))));
85    }
86    if let Some(ref cm) = info.camera_metadata.color_matrix2 {
87        let vals: Vec<String> = cm.iter().map(|v| format!("{:.2}", v)).collect();
88        lines.push(Line::from(format!("  Color Matrix2: [{}]", vals.join(", "))));
89    }
90    if let Some(i1) = info.camera_metadata.calibration_illuminant1 {
91        if let Some(i2) = info.camera_metadata.calibration_illuminant2 {
92            lines.push(Line::from(format!("  Cal Illuminants: {} / {}", i1, i2)));
93        }
94    }
95
96    lines.push(Line::from(""));
97    lines
98}
99
100pub fn format_video_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
101    let mut lines = Vec::new();
102    lines.push(Line::from(Span::styled(
103        "Video",
104        Color::Yellow,
105    )));
106
107    lines.push(Line::from(format!(
108        "  Resolution:   {}x{} ({})",
109        info.width, info.height, info.resolution_label()
110    )));
111    lines.push(Line::from(format!("  FPS:          {:.2}", info.fps)));
112    lines.push(Line::from(format!(
113        "  Duration:     {}",
114        format_duration(info.duration_seconds())
115    )));
116    lines.push(Line::from(format!("  Frames:       {}", info.frame_count)));
117    lines.push(Line::from(format!(
118        "  Bit Depth:    {}-bit",
119        info.bit_depth
120    )));
121    lines.push(Line::from(format!(
122        "  Bayer:        {}",
123        info.bayer_pattern.name()
124    )));
125
126    if info.active_width > 0 && info.active_height > 0 {
127        lines.push(Line::from(format!(
128            "  Active Area:  {}x{} @({},{})",
129            info.active_width, info.active_height, info.active_offset_x, info.active_offset_y
130        )));
131    }
132
133    lines.push(Line::from(""));
134    lines
135}
136
137pub fn format_audio_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
138    let mut lines = Vec::new();
139    lines.push(Line::from(Span::styled(
140        "Audio",
141        Color::Yellow,
142    )));
143
144    if info.has_audio {
145        lines.push(Line::from("  Has Audio:    Yes".to_string()));
146        if info.audio_sample_rate > 0 {
147            lines.push(Line::from(format!(
148                "  Sample Rate:  {} Hz",
149                info.audio_sample_rate
150            )));
151        }
152        if info.audio_channels > 0 {
153            let ch_name = if info.audio_channels == 1 {
154                "mono"
155            } else if info.audio_channels == 2 {
156                "stereo"
157            } else {
158                "multi"
159            };
160            lines.push(Line::from(format!(
161                "  Channels:    {} ({})",
162                info.audio_channels, ch_name
163            )));
164        }
165        if let Some(length) = info.audio_length {
166            lines.push(Line::from(format!("  Audio Length: {} bytes", length)));
167        }
168        if let Some(offset) = info.audio_offset {
169            lines.push(Line::from(format!("  Audio Offset: {} bytes", offset)));
170        }
171    } else {
172        lines.push(Line::from("  Has Audio:    No".to_string()));
173    }
174
175    lines.push(Line::from(""));
176    lines
177}
178
179pub fn format_duration(seconds: f64) -> String {
180    if seconds <= 0.0 {
181        return "0:00".to_string();
182    }
183
184    let total_secs = seconds as u64;
185    let hours = total_secs / 3600;
186    let minutes = (total_secs % 3600) / 60;
187    let secs = total_secs % 60;
188
189    if hours > 0 {
190        format!("{}:{:02}:{:02}", hours, minutes, secs)
191    } else {
192        format!("{}:{:02}", minutes, secs)
193    }
194}
195
196pub fn format_size(bytes: u64) -> String {
197    const KB: u64 = 1024;
198    const MB: u64 = 1024 * 1024;
199    const GB: u64 = 1024 * 1024 * 1024;
200
201    if bytes >= GB {
202        format!("{:.2} GB", bytes as f64 / GB as f64)
203    } else if bytes >= MB {
204        format!("{:.2} MB", bytes as f64 / MB as f64)
205    } else if bytes >= KB {
206        format!("{:.2} KB", bytes as f64 / KB as f64)
207    } else {
208        format!("{} B", bytes)
209    }
210}
211
212pub fn format_exposure_time(value: f64) -> String {
213    if value <= 0.0 {
214        return "Unknown".to_string();
215    }
216
217    let denominator = (1.0 / value).round() as u64;
218    if denominator > 0 && denominator <= 10000 {
219        format!("1/{}s", denominator)
220    } else {
221        format!("{:.2}s", value)
222    }
223}
224
225pub fn format_capture_date(raw: &str) -> String {
226    let raw = raw.trim();
227
228    if raw.len() >= 19 {
229        let date_part = &raw[..10];
230        let time_part = &raw[11..19];
231        let tz_part = raw[19..].trim();
232
233        let mut result = format!("{} {}", date_part, time_part);
234        if !tz_part.is_empty() {
235            result.push_str(tz_part);
236        }
237        return result;
238    }
239
240    raw.to_string()
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_format_duration_minutes() {
249        assert_eq!(format_duration(0.0), "0:00");
250        assert_eq!(format_duration(60.0), "1:00");
251        assert_eq!(format_duration(120.0), "2:00");
252        assert_eq!(format_duration(90.0), "1:30");
253    }
254
255    #[test]
256    fn test_format_duration_hours() {
257        assert_eq!(format_duration(3600.0), "1:00:00");
258        assert_eq!(format_duration(3725.0), "1:02:05");
259        assert_eq!(format_duration(7200.0), "2:00:00");
260    }
261
262    #[test]
263    fn test_format_size_bytes() {
264        assert_eq!(format_size(500), "500 B");
265        assert_eq!(format_size(1024), "1.00 KB");
266        assert_eq!(format_size(1024 * 512), "512.00 KB");
267    }
268
269    #[test]
270    fn test_format_size_kb() {
271        assert_eq!(format_size(1024 * 10), "10.00 KB");
272        assert_eq!(format_size(1024 * 1024 - 1), "1024.00 KB");
273    }
274
275    #[test]
276    fn test_format_size_mb() {
277        assert_eq!(format_size(1024 * 1024), "1.00 MB");
278        assert_eq!(format_size(1024 * 1024 * 10), "10.00 MB");
279        assert_eq!(format_size(1024 * 1024 * 256), "256.00 MB");
280    }
281
282    #[test]
283    fn test_format_size_gb() {
284        assert_eq!(format_size(1024 * 1024 * 1024), "1.00 GB");
285        assert_eq!(format_size(1024 * 1024 * 1024 * 2), "2.00 GB");
286        assert_eq!(format_size(1024 * 1024 * 1024 * 4), "4.00 GB");
287    }
288
289    #[test]
290    fn test_format_exposure_time() {
291        assert_eq!(format_exposure_time(0.0), "Unknown");
292        assert_eq!(format_exposure_time(1.0), "1/1s");
293        assert_eq!(format_exposure_time(0.5), "1/2s");
294        assert_eq!(format_exposure_time(1.0 / 60.0), "1/60s");
295        assert_eq!(format_exposure_time(1.0 / 120.0), "1/120s");
296        assert_eq!(format_exposure_time(1.0 / 1000.0), "1/1000s");
297    }
298
299    #[test]
300    fn test_format_capture_date() {
301        assert_eq!(
302            format_capture_date("2024-01-15T10:30:45+00:00"),
303            "2024-01-15 10:30:45+00:00"
304        );
305        assert_eq!(
306            format_capture_date("2024-06-20T14:00:00-05:00"),
307            "2024-06-20 14:00:00-05:00"
308        );
309        assert_eq!(format_capture_date("raw-date"), "raw-date");
310    }
311}