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    if info.black_level_count > 0 {
134        lines.push(Line::from(format!(
135            "  Black Level:  {}",
136            info.black_level_per_channel[..info.black_level_count.min(4) as usize]
137                .iter()
138                .map(|v| format!("{}", v))
139                .collect::<Vec<_>>()
140                .join(", ")
141        )));
142    }
143    lines.push(Line::from(format!("  White Level:  {}", info.white_level)));
144
145    if let Some(ref lsm) = info.lens_shading_map {
146        lines.push(Line::from(format!(
147            "  Lens Shading: {}x{} grid, 4 ch",
148            lsm.width, lsm.height
149        )));
150    } else {
151        lines.push(Line::from("  Lens Shading: none".to_string()));
152    }
153
154    lines.push(Line::from(""));
155    lines
156}
157
158pub fn format_audio_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
159    let mut lines = Vec::new();
160    lines.push(Line::from(Span::styled(
161        "Audio",
162        Color::Yellow,
163    )));
164
165    if info.has_audio {
166        lines.push(Line::from("  Has Audio:    Yes".to_string()));
167        if info.audio_sample_rate > 0 {
168            lines.push(Line::from(format!(
169                "  Sample Rate:  {} Hz",
170                info.audio_sample_rate
171            )));
172        }
173        if info.audio_channels > 0 {
174            let ch_name = if info.audio_channels == 1 {
175                "mono"
176            } else if info.audio_channels == 2 {
177                "stereo"
178            } else {
179                "multi"
180            };
181            lines.push(Line::from(format!(
182                "  Channels:    {} ({})",
183                info.audio_channels, ch_name
184            )));
185        }
186        if let Some(length) = info.audio_length {
187            lines.push(Line::from(format!("  Audio Length: {} bytes", length)));
188        }
189        if let Some(offset) = info.audio_offset {
190            lines.push(Line::from(format!("  Audio Offset: {} bytes", offset)));
191        }
192    } else {
193        lines.push(Line::from("  Has Audio:    No".to_string()));
194    }
195
196    lines.push(Line::from(""));
197    lines
198}
199
200pub fn format_duration(seconds: f64) -> String {
201    if seconds <= 0.0 {
202        return "0:00".to_string();
203    }
204
205    let total_secs = seconds as u64;
206    let hours = total_secs / 3600;
207    let minutes = (total_secs % 3600) / 60;
208    let secs = total_secs % 60;
209
210    if hours > 0 {
211        format!("{}:{:02}:{:02}", hours, minutes, secs)
212    } else {
213        format!("{}:{:02}", minutes, secs)
214    }
215}
216
217pub fn format_size(bytes: u64) -> String {
218    const KB: u64 = 1024;
219    const MB: u64 = 1024 * 1024;
220    const GB: u64 = 1024 * 1024 * 1024;
221
222    if bytes >= GB {
223        format!("{:.2} GB", bytes as f64 / GB as f64)
224    } else if bytes >= MB {
225        format!("{:.2} MB", bytes as f64 / MB as f64)
226    } else if bytes >= KB {
227        format!("{:.2} KB", bytes as f64 / KB as f64)
228    } else {
229        format!("{} B", bytes)
230    }
231}
232
233pub fn format_exposure_time(value: f64) -> String {
234    if value <= 0.0 {
235        return "Unknown".to_string();
236    }
237
238    let denominator = (1.0 / value).round() as u64;
239    if denominator > 0 && denominator <= 10000 {
240        format!("1/{}s", denominator)
241    } else {
242        format!("{:.2}s", value)
243    }
244}
245
246pub fn format_capture_date(raw: &str) -> String {
247    let raw = raw.trim();
248
249    if raw.len() >= 19 {
250        let date_part = &raw[..10];
251        let time_part = &raw[11..19];
252        let tz_part = raw[19..].trim();
253
254        let mut result = format!("{} {}", date_part, time_part);
255        if !tz_part.is_empty() {
256            result.push_str(tz_part);
257        }
258        return result;
259    }
260
261    raw.to_string()
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_format_duration_minutes() {
270        assert_eq!(format_duration(0.0), "0:00");
271        assert_eq!(format_duration(60.0), "1:00");
272        assert_eq!(format_duration(120.0), "2:00");
273        assert_eq!(format_duration(90.0), "1:30");
274    }
275
276    #[test]
277    fn test_format_duration_hours() {
278        assert_eq!(format_duration(3600.0), "1:00:00");
279        assert_eq!(format_duration(3725.0), "1:02:05");
280        assert_eq!(format_duration(7200.0), "2:00:00");
281    }
282
283    #[test]
284    fn test_format_size_bytes() {
285        assert_eq!(format_size(500), "500 B");
286        assert_eq!(format_size(1024), "1.00 KB");
287        assert_eq!(format_size(1024 * 512), "512.00 KB");
288    }
289
290    #[test]
291    fn test_format_size_kb() {
292        assert_eq!(format_size(1024 * 10), "10.00 KB");
293        assert_eq!(format_size(1024 * 1024 - 1), "1024.00 KB");
294    }
295
296    #[test]
297    fn test_format_size_mb() {
298        assert_eq!(format_size(1024 * 1024), "1.00 MB");
299        assert_eq!(format_size(1024 * 1024 * 10), "10.00 MB");
300        assert_eq!(format_size(1024 * 1024 * 256), "256.00 MB");
301    }
302
303    #[test]
304    fn test_format_size_gb() {
305        assert_eq!(format_size(1024 * 1024 * 1024), "1.00 GB");
306        assert_eq!(format_size(1024 * 1024 * 1024 * 2), "2.00 GB");
307        assert_eq!(format_size(1024 * 1024 * 1024 * 4), "4.00 GB");
308    }
309
310    #[test]
311    fn test_format_exposure_time() {
312        assert_eq!(format_exposure_time(0.0), "Unknown");
313        assert_eq!(format_exposure_time(1.0), "1/1s");
314        assert_eq!(format_exposure_time(0.5), "1/2s");
315        assert_eq!(format_exposure_time(1.0 / 60.0), "1/60s");
316        assert_eq!(format_exposure_time(1.0 / 120.0), "1/120s");
317        assert_eq!(format_exposure_time(1.0 / 1000.0), "1/1000s");
318    }
319
320    #[test]
321    fn test_format_capture_date() {
322        assert_eq!(
323            format_capture_date("2024-01-15T10:30:45+00:00"),
324            "2024-01-15 10:30:45+00:00"
325        );
326        assert_eq!(
327            format_capture_date("2024-06-20T14:00:00-05:00"),
328            "2024-06-20 14:00:00-05:00"
329        );
330        assert_eq!(format_capture_date("raw-date"), "raw-date");
331    }
332}