Skip to main content

mcraw_tui/
file.rs

1use anyhow::{Context, Result};
2use serde::Deserialize;
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Deserialize)]
7struct MotionJsonMetadata {
8    #[serde(rename = "sensorArrangment", default)]
9    sensor_arrangement: Option<String>,
10    #[serde(rename = "sensorOrientation", default)]
11    sensor_orientation: Option<i64>,
12    #[serde(rename = "forwardMatrix1", default)]
13    forward_matrix1: Option<Vec<f64>>,
14    #[serde(rename = "forwardMatrix2", default)]
15    forward_matrix2: Option<Vec<f64>>,
16    #[serde(rename = "colorMatrix1", default)]
17    color_matrix1: Option<Vec<f64>>,
18    #[serde(rename = "colorMatrix2", default)]
19    color_matrix2: Option<Vec<f64>>,
20    #[serde(rename = "calibrationMatrix1", default)]
21    calibration_matrix1: Option<Vec<f64>>,
22    #[serde(rename = "calibrationMatrix2", default)]
23    calibration_matrix2: Option<Vec<f64>>,
24    #[serde(rename = "whiteLevel", default)]
25    white_level: Option<f64>,
26    #[serde(rename = "blackLevel", default)]
27    black_level: Option<Vec<f64>>,
28    #[serde(rename = "baselineExposure", default)]
29    baseline_exposure: Option<f64>,
30    #[serde(rename = "apertures", default)]
31    apertures: Option<Vec<f64>>,
32    #[serde(rename = "focalLengths", default)]
33    focal_lengths: Option<Vec<f64>>,
34    #[serde(rename = "uniqueCameraModel", default)]
35    unique_camera_model: Option<String>,
36    #[serde(rename = "numSegments", default)]
37    num_segments: Option<i64>,
38    #[serde(rename = "extraData", default)]
39    extra_data: Option<ExtraData>,
40    #[serde(rename = "deviceSpecificProfile", default)]
41    device_specific_profile: Option<DeviceProfile>,
42}
43
44#[derive(Debug, Deserialize)]
45struct ExtraData {
46    #[serde(rename = "recordingType", default)]
47    recording_type: Option<String>,
48    #[serde(rename = "audioSampleRate", default)]
49    audio_sample_rate: Option<i64>,
50    #[serde(rename = "audioChannels", default)]
51    audio_channels: Option<i64>,
52    #[serde(rename = "useAccurateTimestamp", default)]
53    use_accurate_timestamp: Option<bool>,
54    #[serde(rename = "metadata", default)]
55    metadata: Option<BuildMetadata>,
56}
57
58#[derive(Debug, Deserialize)]
59struct BuildMetadata {
60    #[serde(rename = "build.model", default)]
61    build_model: Option<String>,
62    #[serde(rename = "build.manufacturer", default)]
63    build_manufacturer: Option<String>,
64    #[serde(rename = "version.major", default)]
65    version_major: Option<String>,
66    #[serde(rename = "version.build", default)]
67    version_build: Option<String>,
68}
69
70#[derive(Debug, Deserialize)]
71struct DeviceProfile {
72    #[serde(rename = "cameraId", default)]
73    camera_id: Option<String>,
74    #[serde(rename = "deviceModel", default)]
75    device_model: Option<String>,
76}
77
78const INDEX_MAGIC: u32 = 0x8A905612;
79const ITEM_TYPE_BUFFER_INDEX: u32 = 0;
80const ITEM_TYPE_METADATA: u32 = 3;
81
82#[derive(Debug)]
83struct BufferIndex {
84    magic: u32,
85    num_offsets: u32,
86    data_offset: i64,
87}
88
89#[derive(Debug)]
90struct BufferOffset {
91    offset: i64,
92    timestamp: i64,
93}
94
95/// Bayer filter pattern IDs as defined in the MCRAW spec (Appendix A)
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum BayerPattern {
98    RGGB,
99    GRBG,
100    GBRG,
101    BGGR,
102    QuadBayerRGGB,
103    QuadBayerGRBG,
104    QuadBayerGBRG,
105    QuadBayerBGGR,
106}
107
108impl BayerPattern {
109    pub fn from_u8(value: u8) -> Self {
110        match value {
111            0 => BayerPattern::RGGB,
112            1 => BayerPattern::GRBG,
113            2 => BayerPattern::GBRG,
114            3 => BayerPattern::BGGR,
115            4 => BayerPattern::QuadBayerRGGB,
116            5 => BayerPattern::QuadBayerGRBG,
117            6 => BayerPattern::QuadBayerGBRG,
118            7 => BayerPattern::QuadBayerBGGR,
119            _ => BayerPattern::RGGB,
120        }
121    }
122
123    pub fn to_u8(&self) -> u8 {
124        match self {
125            BayerPattern::RGGB => 0,
126            BayerPattern::GRBG => 1,
127            BayerPattern::GBRG => 2,
128            BayerPattern::BGGR => 3,
129            BayerPattern::QuadBayerRGGB => 4,
130            BayerPattern::QuadBayerGRBG => 5,
131            BayerPattern::QuadBayerGBRG => 6,
132            BayerPattern::QuadBayerBGGR => 7,
133        }
134    }
135
136    pub fn name(&self) -> &'static str {
137        match self {
138            BayerPattern::RGGB => "RGGB",
139            BayerPattern::GRBG => "GRBG",
140            BayerPattern::GBRG => "GBRG",
141            BayerPattern::BGGR => "BGGR",
142            BayerPattern::QuadBayerRGGB => "QuadBayer RGGB",
143            BayerPattern::QuadBayerGRBG => "QuadBayer GRBG",
144            BayerPattern::QuadBayerGBRG => "QuadBayer GBRG",
145            BayerPattern::QuadBayerBGGR => "QuadBayer BGGR",
146        }
147    }
148
149    /// Dcraw-style `filters` encoding for WGSL shaders.
150    /// Maps Bayer pattern to a u32 bitfield R=0, G1=1, G2=3(!), B=2.
151    pub fn to_dcraw_filters(&self) -> u32 {
152        match self {
153            BayerPattern::RGGB => 0x94949494,
154            BayerPattern::BGGR => 0x16161616,
155            BayerPattern::GRBG => 0x61616161,
156            BayerPattern::GBRG => 0x49494949,
157            _ => 0x94949494, // QuadBayer patterns fall back to RGGB
158        }
159    }
160}
161
162/// Camera metadata extracted from the MCRAW header block
163#[derive(Debug, Clone)]
164pub struct CameraMetadata {
165    pub sensor_make: Option<String>,
166    pub sensor_model: Option<String>,
167    pub camera_model: Option<String>,
168    pub lens_model: Option<String>,
169    pub focal_length: Option<f64>,
170    pub aperture: Option<f64>,
171    pub iso: Option<u32>,
172    pub exposure_time: Option<f64>,
173    pub white_balance: Option<f64>,
174    pub capture_date: Option<String>,
175    pub color_matrix: Option<[f64; 9]>,
176    pub color_matrix2: Option<[f64; 9]>,
177    pub forward_matrix1: Option<[f64; 9]>,
178    pub forward_matrix2: Option<[f64; 9]>,
179    pub calibration_matrix1: Option<[f64; 9]>,
180    pub calibration_matrix2: Option<[f64; 9]>,
181    pub calibration_illuminant1: Option<i32>,
182    pub calibration_illuminant2: Option<i32>,
183    pub calibration_illuminant: Option<String>,
184    pub wb_multipliers: Option<[f32; 3]>,
185}
186
187impl Default for CameraMetadata {
188    fn default() -> Self {
189        CameraMetadata {
190            sensor_make: None,
191            sensor_model: None,
192            camera_model: None,
193            lens_model: None,
194            focal_length: None,
195            aperture: None,
196            iso: None,
197            exposure_time: None,
198            white_balance: None,
199            capture_date: None,
200            color_matrix: None,
201            color_matrix2: None,
202            forward_matrix1: None,
203            forward_matrix2: None,
204            calibration_matrix1: None,
205            calibration_matrix2: None,
206            calibration_illuminant1: None,
207            calibration_illuminant2: None,
208            calibration_illuminant: None,
209            wb_multipliers: None,
210        }
211    }
212}
213
214/// Complete parsed information from an MCRAW file header
215#[derive(Debug, Clone)]
216pub struct McrawFileInfo {
217    pub path: String,
218    pub size: u64,
219    pub format_version: u32,
220    pub frame_count: u32,
221    pub width: u16,
222    pub height: u16,
223    pub fps: f64,
224    pub has_audio: bool,
225    pub audio_sample_rate: u32,
226    pub audio_channels: u16,
227    pub bit_depth: u16,
228    pub bayer_pattern: BayerPattern,
229    pub camera_metadata: CameraMetadata,
230    pub frame_offsets: Vec<u64>,
231    pub audio_offset: Option<u64>,
232    pub audio_length: Option<u64>,
233    pub sensor_width: u16,
234    pub sensor_height: u16,
235    pub active_offset_x: u16,
236    pub active_offset_y: u16,
237    pub active_width: u16,
238    pub active_height: u16,
239    pub white_level: f64,
240    pub black_level: f64,
241}
242
243impl McrawFileInfo {
244    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
245        let path = path.as_ref();
246        tracing::debug!("McrawFileInfo::from_path: {:?}", path);
247        let metadata = fs::metadata(&path)
248            .with_context(|| format!("Failed to read metadata for {:?}", path))?;
249
250        let data = fs::read(&path)
251            .with_context(|| format!("Failed to read file {:?}", path))?;
252
253        let info = parse_header(&data, path)?;
254        Ok(McrawFileInfo {
255            path: path.to_string_lossy().into_owned(),
256            size: metadata.len(),
257            ..info
258        })
259    }
260
261    pub fn enhance_with_decoder(&mut self) {
262        let path = self.path.clone();
263        tracing::debug!("enhance_with_decoder: {}", path);
264        let decoder_result = crate::decoder::Decoder::new(&path);
265        let decoder = match decoder_result {
266            Ok(d) => d,
267            Err(e) => {
268                tracing::warn!("failed to open decoder for {}: {}", path, e);
269                return;
270            }
271        };
272        if let Ok(container_meta) = decoder.container_metadata() {
273            if container_meta.white_level > 0.0 {
274                self.white_level = container_meta.white_level;
275                tracing::debug!("white_level from container: {}", self.white_level);
276            }
277            if container_meta.black_level_count > 0 {
278                self.black_level = container_meta.black_level[0];
279                tracing::debug!("black_level from container: {}", self.black_level);
280            }
281
282            let as_f64 = |v: &[f32; 9]| -> [f64; 9] {
283                let mut r = [0.0; 9];
284                for (i, &x) in v.iter().enumerate() { r[i] = x as f64; }
285                r
286            };
287
288            self.camera_metadata.color_matrix = Some(as_f64(&container_meta.color_matrix1));
289            let non_zero = |m: &[f32; 9]| m.iter().any(|&x| x != 0.0);
290
291            if non_zero(&container_meta.color_matrix2) {
292                self.camera_metadata.color_matrix2 = Some(as_f64(&container_meta.color_matrix2));
293            }
294            if non_zero(&container_meta.forward_matrix1) {
295                self.camera_metadata.forward_matrix1 = Some(as_f64(&container_meta.forward_matrix1));
296            }
297            if non_zero(&container_meta.forward_matrix2) {
298                self.camera_metadata.forward_matrix2 = Some(as_f64(&container_meta.forward_matrix2));
299            }
300            if non_zero(&container_meta.calibration_matrix1) {
301                self.camera_metadata.calibration_matrix1 = Some(as_f64(&container_meta.calibration_matrix1));
302            }
303            if non_zero(&container_meta.calibration_matrix2) {
304                self.camera_metadata.calibration_matrix2 = Some(as_f64(&container_meta.calibration_matrix2));
305            }
306            if container_meta.has_calibration_illuminants {
307                self.camera_metadata.calibration_illuminant1 = Some(container_meta.calibration_illuminant1);
308                self.camera_metadata.calibration_illuminant2 = Some(container_meta.calibration_illuminant2);
309                tracing::debug!("calibration_illuminants: illum1={}, illum2={}",
310                    container_meta.calibration_illuminant1, container_meta.calibration_illuminant2);
311            }
312        }
313        if let Ok(timestamps) = decoder.timestamps() {
314            if self.frame_count == 0 && !timestamps.is_empty() {
315                self.frame_count = timestamps.len() as u32;
316                if timestamps.len() >= 2 {
317                    let duration_ns = timestamps[timestamps.len() - 1] - timestamps[0];
318                    if duration_ns > 0 && self.fps == 0.0 {
319                        let duration_in_seconds = duration_ns as f64 / 1_000_000_000.0;
320                        self.fps = (self.frame_count.saturating_sub(1)) as f64 / duration_in_seconds;
321                    }
322                }
323                tracing::debug!("enhanced from timestamps: {} frames, {:.2} fps", self.frame_count, self.fps);
324            }
325            if let Ok(first_frame_meta) = decoder.load_frame_metadata(timestamps[0]) {
326                if self.width == 0 || self.height == 0 {
327                    self.width = first_frame_meta.width as u16;
328                    self.height = first_frame_meta.height as u16;
329                    tracing::debug!("enhanced dimensions: {}x{}", first_frame_meta.width, first_frame_meta.height);
330                }
331                let n = first_frame_meta.as_shot_neutral;
332                if n[0] > 1e-6 && n[1] > 1e-6 && n[2] > 1e-6 {
333                    let r_gain = n[1] / n[0];
334                    let b_gain = n[1] / n[2];
335                    self.camera_metadata.wb_multipliers = Some([r_gain, 1.0, b_gain]);
336                    tracing::debug!("wb_multipliers: R={:.3} G={:.3} B={:.3}", r_gain, 1.0, b_gain);
337                }
338            }
339        }
340    }
341
342    pub fn format_name(&self) -> &'static str {
343        match self.format_version {
344            1 => "MotionCam v1 (Legacy)",
345            2 => "MotionCam v2",
346            3 => "MotionCam v3",
347            _ => "Unknown format",
348        }
349    }
350
351    pub fn duration_seconds(&self) -> f64 {
352        self.frame_count as f64 / self.fps
353    }
354
355    pub fn resolution_label(&self) -> &'static str {
356        match (self.width, self.height) {
357            (1920, 1080) => "1080p",
358            (2560, 1440) => "1440p",
359            (3840, 2160) => "4K",
360            (4096, 2160) => "4K DCI",
361            _ => "Custom",
362        }
363    }
364}
365
366fn parse_motion_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
367    if data.len() < 17 {
368        anyhow::bail!("File {:?} is too small for MOTION header", path);
369    }
370
371    let format_version = data[7] as u32;
372    tracing::debug!("parse_motion_header: version={} json_len={}", format_version, u32::from_le_bytes([data[12], data[13], data[14], data[15]]));
373    let json_len = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
374    let json_start = 16;
375    let json_end = json_start + json_len;
376
377    if json_end > data.len() {
378        anyhow::bail!("JSON metadata extends beyond file data");
379    }
380
381    let json_str = std::str::from_utf8(&data[json_start..json_end])
382        .with_context(|| "Invalid UTF-8 in MOTION JSON metadata")?;
383
384    let json: MotionJsonMetadata = serde_json::from_str(json_str)
385        .with_context(|| "Failed to parse MOTION JSON metadata")?;
386
387    let bayer_pattern = match json.sensor_arrangement.as_deref() {
388        Some("rggb") | Some("standard") => BayerPattern::RGGB,
389        Some("grbg") => BayerPattern::GRBG,
390        Some("gbrg") => BayerPattern::GBRG,
391        Some("bggr") => BayerPattern::BGGR,
392        _ => BayerPattern::RGGB,
393    };
394
395    let extra_data = json.extra_data;
396    let device_profile = json.device_specific_profile;
397
398    let camera_model_opt: Option<String> = json.unique_camera_model
399        .or_else(|| device_profile.as_ref().and_then(|p| p.device_model.clone()));
400    let camera_model = camera_model_opt.unwrap_or_default();
401
402    let sensor_make = extra_data
403        .as_ref()
404        .and_then(|e| e.metadata.as_ref())
405        .and_then(|m| m.build_manufacturer.clone())
406        .unwrap_or_default();
407
408    let build_model = extra_data
409        .as_ref()
410        .and_then(|e| e.metadata.as_ref())
411        .and_then(|m| m.build_model.clone())
412        .unwrap_or(camera_model.clone());
413
414    let aperture = json.apertures.and_then(|mut a| a.pop());
415    let focal_length = json.focal_lengths.and_then(|mut a| a.pop());
416    let audio_sample_rate = extra_data.as_ref()
417        .and_then(|e| e.audio_sample_rate)
418        .unwrap_or(0) as u32;
419    let audio_channels = extra_data.as_ref()
420        .and_then(|e| e.audio_channels)
421        .unwrap_or(0) as u16;
422    let has_audio = audio_channels > 0;
423
424    let color_matrix = json.color_matrix1.clone().or(json.forward_matrix1.clone())
425        .and_then(|m| {
426            if m.len() == 9 {
427                Some(m.try_into().ok()?)
428            } else {
429                None
430            }
431        });
432
433    let color_matrix2 = json.color_matrix2.clone().and_then(|m| {
434        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
435    });
436    let forward_matrix1 = json.forward_matrix1.clone().and_then(|m| {
437        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
438    });
439    let forward_matrix2 = json.forward_matrix2.clone().and_then(|m| {
440        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
441    });
442    let calibration_matrix1 = json.calibration_matrix1.clone().and_then(|m| {
443        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
444    });
445    let calibration_matrix2 = json.calibration_matrix2.clone().and_then(|m| {
446        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
447    });
448
449    let bit_depth = json.white_level
450        .map(detect_bit_depth_from_white_level)
451        .unwrap_or(12);
452
453    let mut frame_count: u32 = 0;
454    let mut width: u16 = 0;
455    let mut height: u16 = 0;
456    let mut fps: f64 = 0.0;
457
458    if let Some((num_offsets, offsets, timestamps)) = parse_buffer_index(data) {
459        frame_count = num_offsets;
460        if num_offsets >= 2 {
461            let mut sorted_ts = timestamps.clone();
462            sorted_ts.sort();
463            let duration_ns = sorted_ts[num_offsets as usize - 1] - sorted_ts[0];
464            if duration_ns > 0 {
465                fps = (num_offsets as f64 - 1.0) / (duration_ns as f64 / 1_000_000_000.0);
466            }
467        }
468    }
469
470    Ok(McrawFileInfo {
471        path: path.to_string_lossy().into_owned(),
472        size: data.len() as u64,
473        format_version,
474        frame_count,
475        width,
476        height,
477        fps,
478        has_audio,
479        audio_sample_rate,
480        audio_channels,
481        bit_depth,
482        bayer_pattern,
483        camera_metadata: CameraMetadata {
484            sensor_make: if sensor_make.is_empty() { None } else { Some(sensor_make) },
485            sensor_model: None,
486            camera_model: if build_model.is_empty() { None } else { Some(build_model) },
487            lens_model: None,
488            focal_length,
489            aperture,
490            iso: None,
491            exposure_time: None,
492            white_balance: None,
493            capture_date: None,
494            color_matrix,
495            color_matrix2,
496            forward_matrix1,
497            forward_matrix2,
498            calibration_matrix1,
499            calibration_matrix2,
500            calibration_illuminant1: None,
501            calibration_illuminant2: None,
502            calibration_illuminant: None,
503            wb_multipliers: None,
504        },
505        frame_offsets: Vec::new(),
506        audio_offset: None,
507        audio_length: None,
508        sensor_width: 0,
509        sensor_height: 0,
510        active_offset_x: 0,
511        active_offset_y: 0,
512        active_width: 0,
513        active_height: 0,
514        white_level: 16383.0,
515        black_level: 0.0,
516    })
517}
518
519fn parse_buffer_index(data: &[u8]) -> Option<(u32, Vec<i64>, Vec<i64>)> {
520    let file_len = data.len();
521    if file_len < 8 {
522        return None;
523    }
524
525    let item_size = u32::from_le_bytes([
526        data[file_len - 8],
527        data[file_len - 7],
528        data[file_len - 6],
529        data[file_len - 5],
530    ]) as usize;
531
532    let idx_data_start = file_len - 8 - item_size;
533    if idx_data_start < 16 {
534        return None;
535    }
536
537    let buf_idx_type = u32::from_le_bytes([
538        data[idx_data_start],
539        data[idx_data_start + 1],
540        data[idx_data_start + 2],
541        data[idx_data_start + 3],
542    ]);
543    if buf_idx_type != ITEM_TYPE_BUFFER_INDEX {
544        return None;
545    }
546
547    let buf_idx_size = u32::from_le_bytes([
548        data[idx_data_start + 4],
549        data[idx_data_start + 5],
550        data[idx_data_start + 6],
551        data[idx_data_start + 7],
552    ]) as usize;
553
554    let magic = u32::from_le_bytes([
555        data[idx_data_start + 8],
556        data[idx_data_start + 9],
557        data[idx_data_start + 10],
558        data[idx_data_start + 11],
559    ]);
560    if magic != INDEX_MAGIC {
561        return None;
562    }
563
564    let num_offsets = u32::from_le_bytes([
565        data[idx_data_start + 12],
566        data[idx_data_start + 13],
567        data[idx_data_start + 14],
568        data[idx_data_start + 15],
569    ]);
570
571    let data_offset = i64::from_le_bytes([
572        data[idx_data_start + 16],
573        data[idx_data_start + 17],
574        data[idx_data_start + 18],
575        data[idx_data_start + 19],
576        data[idx_data_start + 20],
577        data[idx_data_start + 21],
578        data[idx_data_start + 22],
579        data[idx_data_start + 23],
580    ]);
581
582    let mut offsets = Vec::new();
583    let mut timestamps = Vec::new();
584
585    for i in 0..num_offsets {
586        let pos = data_offset as usize + (i as usize) * 16;
587        if pos + 16 > data.len() {
588            break;
589        }
590        let offset = i64::from_le_bytes([
591            data[pos],
592            data[pos + 1],
593            data[pos + 2],
594            data[pos + 3],
595            data[pos + 4],
596            data[pos + 5],
597            data[pos + 6],
598            data[pos + 7],
599        ]);
600        let timestamp = i64::from_le_bytes([
601            data[pos + 8],
602            data[pos + 9],
603            data[pos + 10],
604            data[pos + 11],
605            data[pos + 12],
606            data[pos + 13],
607            data[pos + 14],
608            data[pos + 15],
609        ]);
610        offsets.push(offset);
611        timestamps.push(timestamp);
612    }
613
614    Some((num_offsets, offsets, timestamps))
615}
616
617fn parse_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
618    if data.len() < 17 {
619        anyhow::bail!("File {:?} is too small to be a valid file (need at least 17 bytes, got {})", path, data.len());
620    }
621
622    // Check for "MOTION " magic header (new format with JSON metadata)
623    if data.starts_with(b"MOTION ") {
624        tracing::debug!("detected MOTION format header");
625        return parse_motion_header(data, path);
626    }
627
628    // Check for "MCRAW" magic header (legacy binary format)
629    if data.len() < 36 {
630        anyhow::bail!("File {:?} is too small to be a valid MCRAW file (need at least 36 bytes, got {})", path, data.len());
631    }
632
633    let magic = &data[0..5];
634    if magic != b"MCRAW" {
635        anyhow::bail!(
636            "Invalid MCRAW magic header in {:?}: expected 'MCRAW', found {:?}",
637            path,
638            magic
639        );
640    }
641    tracing::debug!("detected MCRAW legacy format header");
642
643    let format_version = u32::from_be_bytes([data[5], data[6], data[7], data[8]]);
644    let frame_count = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
645    let width = u16::from_be_bytes([data[13], data[14]]);
646    let height = u16::from_be_bytes([data[15], data[16]]);
647    let fps = f64::from_be_bytes([
648        data[17], data[18], data[19], data[20], data[21], data[22], data[23], data[24],
649    ]);
650    let has_audio = data[25] != 0;
651
652    let audio_sample_rate = if has_audio && data.len() >= 30 {
653        u32::from_be_bytes([data[26], data[27], data[28], data[29]])
654    } else {
655        0
656    };
657
658    let audio_channels = if data.len() >= 32 {
659        u16::from_be_bytes([data[30], data[31]])
660    } else {
661        0
662    };
663
664    let bit_depth = if data.len() >= 34 {
665        u16::from_be_bytes([data[32], data[33]])
666    } else {
667        0
668    };
669
670    let bayer_pattern_id = if data.len() >= 35 {
671        data[34]
672    } else {
673        0
674    };
675    let bayer_pattern = BayerPattern::from_u8(bayer_pattern_id);
676
677    let mut offset = 36;
678    let mut camera_metadata = CameraMetadata::default();
679    let mut frame_offsets = Vec::new();
680    let mut audio_offset: Option<u64> = None;
681    let mut audio_length: Option<u64> = None;
682    let mut sensor_width: u16 = 0;
683    let mut sensor_height: u16 = 0;
684    let mut active_offset_x: u16 = 0;
685    let mut active_offset_y: u16 = 0;
686    let mut active_width: u16 = 0;
687    let mut active_height: u16 = 0;
688    let mut _color_matrix: Option<[f64; 9]> = None;
689    let mut _calibration_illuminant: Option<String> = None;
690
691    if offset < data.len() {
692        let block_length = read_u32_be(&data, offset) as usize;
693        offset += 4;
694        let block_end = offset + block_length;
695
696        while offset < block_end && offset < data.len() {
697            let tag = data[offset];
698            offset += 1;
699
700            match tag {
701                0x01 => {
702                    if let Ok(s) = parse_string(&data, &mut offset) {
703                        camera_metadata.sensor_make = Some(s);
704                    }
705                }
706                0x02 => {
707                    if let Ok(s) = parse_string(&data, &mut offset) {
708                        camera_metadata.sensor_model = Some(s);
709                    }
710                }
711                0x03 => {
712                    if let Ok(s) = parse_string(&data, &mut offset) {
713                        camera_metadata.camera_model = Some(s);
714                    }
715                }
716                0x04 => {
717                    if let Ok(s) = parse_string(&data, &mut offset) {
718                        camera_metadata.lens_model = Some(s);
719                    }
720                }
721                0x05 => {
722                    if let Ok(v) = parse_f64(&data, &mut offset) {
723                        camera_metadata.focal_length = Some(v);
724                    }
725                }
726                0x06 => {
727                    if let Ok(v) = parse_f64(&data, &mut offset) {
728                        camera_metadata.aperture = Some(v);
729                    }
730                }
731                0x07 => {
732                    if let Ok(v) = parse_u32_be(&data, &mut offset) {
733                        camera_metadata.iso = Some(v);
734                    }
735                }
736                0x08 => {
737                    if let Ok(v) = parse_f64(&data, &mut offset) {
738                        camera_metadata.exposure_time = Some(v);
739                    }
740                }
741                0x09 => {
742                    if let Ok(v) = parse_f64(&data, &mut offset) {
743                        camera_metadata.white_balance = Some(v);
744                    }
745                }
746                0x0A => {
747                    if let Ok(s) = parse_string(&data, &mut offset) {
748                        camera_metadata.capture_date = Some(s);
749                    }
750                }
751                0x0B => {
752                    let matrix = parse_f64_array(&data, &mut offset, 9);
753                    if matrix.len() == 9 {
754                        let arr: [f64; 9] = matrix.try_into().ok().unwrap_or([0.0; 9]);
755                          _color_matrix = Some(arr);
756                    }
757                }
758                0x0C => {
759                    if let Ok(s) = parse_string(&data, &mut offset) {
760                        _calibration_illuminant = Some(s);
761                    }
762                }
763                0x10 => {
764                    let count = parse_u32_be(&data, &mut offset);
765                    if let Ok(n) = count {
766                        let mut offsets = Vec::with_capacity(n as usize);
767                        for _ in 0..n {
768                            if offset + 8 <= data.len() {
769                                let val = u64::from_be_bytes([
770                                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
771                                    data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
772                                ]);
773                                offsets.push(val);
774                                offset += 8;
775                            } else {
776                                break;
777                            }
778                        }
779                        frame_offsets = offsets;
780                    }
781                }
782                0x11 => {
783                    if has_audio && offset + 8 <= data.len() {
784                        audio_offset = Some(u64::from_be_bytes([
785                            data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
786                            data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
787                        ]));
788                        offset += 8;
789                    }
790                }
791                0x12 => {
792                    if has_audio && offset + 8 <= data.len() {
793                        audio_length = Some(u64::from_be_bytes([
794                            data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
795                            data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
796                        ]));
797                        offset += 8;
798                    }
799                }
800                0x13 => {
801                    if offset + 2 <= data.len() {
802                        sensor_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
803                        offset += 2;
804                    }
805                }
806                0x14 => {
807                    if offset + 2 <= data.len() {
808                        sensor_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
809                        offset += 2;
810                    }
811                }
812                0x15 => {
813                    if offset + 2 <= data.len() {
814                        active_offset_x = u16::from_be_bytes([data[offset], data[offset + 1]]);
815                        offset += 2;
816                    }
817                }
818                0x16 => {
819                    if offset + 2 <= data.len() {
820                        active_offset_y = u16::from_be_bytes([data[offset], data[offset + 1]]);
821                        offset += 2;
822                    }
823                }
824                0x17 => {
825                    if offset + 2 <= data.len() {
826                        active_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
827                        offset += 2;
828                    }
829                }
830                0x18 => {
831                    if offset + 2 <= data.len() {
832                        active_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
833                        offset += 2;
834                    }
835                }
836                _ => {
837                    offset += 1;
838                }
839            }
840        }
841    }
842
843    Ok(McrawFileInfo {
844        path: path.to_string_lossy().into_owned(),
845        size: data.len() as u64,
846        format_version,
847        frame_count,
848        width,
849        height,
850        fps,
851        has_audio,
852        audio_sample_rate,
853        audio_channels,
854        bit_depth,
855        bayer_pattern,
856        camera_metadata,
857        frame_offsets,
858        audio_offset,
859        audio_length,
860        sensor_width,
861        sensor_height,
862        active_offset_x,
863        active_offset_y,
864        active_width,
865        active_height,
866        white_level: 16383.0,
867        black_level: 0.0,
868    })
869}
870
871fn read_u32_be(data: &[u8], offset: usize) -> u32 {
872    u32::from_be_bytes([
873        data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
874    ])
875}
876
877fn parse_u32_be(data: &[u8], offset: &mut usize) -> Result<u32> {
878    if *offset + 4 > data.len() {
879        return Err(anyhow::anyhow!("Unexpected end of data"));
880    }
881    let val = u32::from_be_bytes([
882        data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
883    ]);
884    *offset += 4;
885    Ok(val)
886}
887
888fn parse_f64(data: &[u8], offset: &mut usize) -> Result<f64> {
889    if *offset + 8 > data.len() {
890        return Err(anyhow::anyhow!("Unexpected end of data"));
891    }
892    let val = f64::from_be_bytes([
893        data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
894        data[*offset + 4], data[*offset + 5], data[*offset + 6], data[*offset + 7],
895    ]);
896    *offset += 8;
897    Ok(val)
898}
899
900fn parse_f64_array(data: &[u8], offset: &mut usize, len: usize) -> Vec<f64> {
901    let mut result = Vec::with_capacity(len);
902    for _ in 0..len {
903        if let Ok(v) = parse_f64(data, offset) {
904            result.push(v);
905        } else {
906            break;
907        }
908    }
909    result
910}
911
912fn parse_string(data: &[u8], offset: &mut usize) -> Result<String> {
913    if *offset + 4 > data.len() {
914        return Err(anyhow::anyhow!("Unexpected end of data"));
915    }
916    let str_len = u32::from_be_bytes([
917        data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
918    ]) as usize;
919    *offset += 4;
920    if *offset + str_len > data.len() {
921        return Err(anyhow::anyhow!("String extends beyond data"));
922    }
923    let s = std::str::from_utf8(&data[*offset..*offset + str_len])
924        .map_err(|e| anyhow::anyhow!("Invalid UTF-8 string: {}", e))?;
925    *offset += str_len;
926    Ok(s.to_string())
927}
928
929pub fn detect_bit_depth_from_white_level(white_level: f64) -> u16 {
930    if white_level <= 1024.0 {
931        10
932    } else if white_level <= 4096.0 {
933        12
934    } else if white_level <= 16384.0 {
935        14
936    } else if white_level <= 65536.0 {
937        16
938    } else {
939        12
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    #[test]
948    fn test_bayer_pattern_from_u8() {
949        assert_eq!(BayerPattern::from_u8(0), BayerPattern::RGGB);
950        assert_eq!(BayerPattern::from_u8(1), BayerPattern::GRBG);
951        assert_eq!(BayerPattern::from_u8(2), BayerPattern::GBRG);
952        assert_eq!(BayerPattern::from_u8(3), BayerPattern::BGGR);
953        assert_eq!(BayerPattern::from_u8(4), BayerPattern::QuadBayerRGGB);
954        assert_eq!(BayerPattern::from_u8(5), BayerPattern::QuadBayerGRBG);
955        assert_eq!(BayerPattern::from_u8(6), BayerPattern::QuadBayerGBRG);
956        assert_eq!(BayerPattern::from_u8(7), BayerPattern::QuadBayerBGGR);
957        assert_eq!(BayerPattern::from_u8(99), BayerPattern::RGGB);
958    }
959
960    #[test]
961    fn test_bayer_pattern_to_u8() {
962        assert_eq!(BayerPattern::RGGB.to_u8(), 0);
963        assert_eq!(BayerPattern::GRBG.to_u8(), 1);
964        assert_eq!(BayerPattern::GBRG.to_u8(), 2);
965        assert_eq!(BayerPattern::BGGR.to_u8(), 3);
966        assert_eq!(BayerPattern::QuadBayerRGGB.to_u8(), 4);
967        assert_eq!(BayerPattern::QuadBayerGRBG.to_u8(), 5);
968        assert_eq!(BayerPattern::QuadBayerGBRG.to_u8(), 6);
969        assert_eq!(BayerPattern::QuadBayerBGGR.to_u8(), 7);
970    }
971
972    #[test]
973    fn test_detect_bit_depth_from_white_level() {
974        assert_eq!(detect_bit_depth_from_white_level(1023.0), 10);
975        assert_eq!(detect_bit_depth_from_white_level(1024.0), 10);
976        assert_eq!(detect_bit_depth_from_white_level(1025.0), 12);
977        assert_eq!(detect_bit_depth_from_white_level(4095.0), 12);
978        assert_eq!(detect_bit_depth_from_white_level(4096.0), 12);
979        assert_eq!(detect_bit_depth_from_white_level(4097.0), 14);
980        assert_eq!(detect_bit_depth_from_white_level(16383.0), 14);
981        assert_eq!(detect_bit_depth_from_white_level(16384.0), 14);
982        assert_eq!(detect_bit_depth_from_white_level(16385.0), 16);
983        assert_eq!(detect_bit_depth_from_white_level(65535.0), 16);
984        assert_eq!(detect_bit_depth_from_white_level(65536.0), 16);
985        assert_eq!(detect_bit_depth_from_white_level(65537.0), 12);
986        assert_eq!(detect_bit_depth_from_white_level(0.0), 10);
987    }
988
989    #[test]
990    fn test_parse_header_minimal() {
991        let mut data = vec![0u8; 36];
992        data[0..5].copy_from_slice(b"MCRAW");
993        data[5..9].copy_from_slice(&2u32.to_be_bytes());
994        data[9..13].copy_from_slice(&10u32.to_be_bytes());
995        data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
996        data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
997        data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
998        data[25] = 0;
999
1000        let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1001        assert_eq!(info.format_version, 2);
1002        assert_eq!(info.frame_count, 10);
1003        assert_eq!(info.width, 1920);
1004        assert_eq!(info.height, 1080);
1005        assert!((info.fps - 30.0).abs() < 0.001);
1006        assert!(!info.has_audio);
1007    }
1008
1009    #[test]
1010    fn test_duration_seconds() {
1011        let mut data = vec![0u8; 36];
1012        data[0..5].copy_from_slice(b"MCRAW");
1013        data[9..13].copy_from_slice(&600u32.to_be_bytes());
1014        data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1015        let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1016        assert!((info.duration_seconds() - 20.0).abs() < 0.001);
1017    }
1018
1019    #[test]
1020    fn test_resolution_label() {
1021        let make_info = |w: u16, h: u16| McrawFileInfo {
1022            path: String::new(),
1023            size: 0,
1024            format_version: 2,
1025            frame_count: 0,
1026            width: w,
1027            height: h,
1028            fps: 30.0,
1029            has_audio: false,
1030            audio_sample_rate: 0,
1031            audio_channels: 0,
1032            bit_depth: 0,
1033            bayer_pattern: BayerPattern::RGGB,
1034            camera_metadata: CameraMetadata::default(),
1035            frame_offsets: Vec::new(),
1036            audio_offset: None,
1037            audio_length: None,
1038            sensor_width: 0,
1039            sensor_height: 0,
1040            active_offset_x: 0,
1041            active_offset_y: 0,
1042            active_width: 0,
1043            active_height: 0,
1044            white_level: 16383.0,
1045            black_level: 0.0,
1046        };
1047
1048        assert_eq!(make_info(1920, 1080).resolution_label(), "1080p");
1049        assert_eq!(make_info(2560, 1440).resolution_label(), "1440p");
1050        assert_eq!(make_info(3840, 2160).resolution_label(), "4K");
1051        assert_eq!(make_info(4096, 2160).resolution_label(), "4K DCI");
1052        assert_eq!(make_info(1280, 720).resolution_label(), "Custom");
1053    }
1054
1055    #[test]
1056    fn test_parse_header_with_string_metadata() {
1057        let mut data = vec![0u8; 64];
1058        data[0..5].copy_from_slice(b"MCRAW");
1059        data[5] = 2;
1060        data[9..13].copy_from_slice(&1u32.to_be_bytes());
1061        data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
1062        data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
1063        data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1064        data[25] = 0;
1065
1066        let camera_model = "TestCamera";
1067        let block_offset = 36;
1068        let str_len = camera_model.len() as u32;
1069        let block_len = 1 + 4 + str_len as u32; // 1 tag + 4 str_len + str data
1070        data[block_offset..block_offset + 4].copy_from_slice(&(block_len as u32).to_be_bytes());
1071        data[block_offset + 4] = 0x03;
1072        data[block_offset + 5..block_offset + 9].copy_from_slice(&str_len.to_be_bytes());
1073        data[block_offset + 9..block_offset + 9 + camera_model.len()]
1074            .copy_from_slice(camera_model.as_bytes());
1075
1076        let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1077        assert_eq!(info.camera_metadata.camera_model, Some("TestCamera".to_string()));
1078    }
1079
1080    #[test]
1081    fn test_parse_header_invalid_magic() {
1082        let data = vec![b'X'; 36];
1083        let result = parse_header(&data, std::path::Path::new("test.mcraw"));
1084        assert!(result.is_err());
1085    }
1086
1087    #[test]
1088    fn test_parse_header_too_small() {
1089        let data = vec![0u8; 10];
1090        let result = parse_header(&data, std::path::Path::new("test.mcraw"));
1091        assert!(result.is_err());
1092    }
1093}