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 build_model = extra_data
399        .as_ref()
400        .and_then(|e| e.metadata.as_ref())
401        .and_then(|m| m.build_model.clone());
402
403    let camera_model: Option<String> = device_profile.as_ref()
404        .and_then(|p| p.device_model.clone())
405        .filter(|s| !s.is_empty())
406        .or_else(|| json.unique_camera_model.filter(|s| !s.is_empty()))
407        .or_else(|| build_model.filter(|s| !s.is_empty()));
408
409    let sensor_make = extra_data
410        .as_ref()
411        .and_then(|e| e.metadata.as_ref())
412        .and_then(|m| m.build_manufacturer.clone())
413        .unwrap_or_default();
414
415    let aperture = json.apertures.and_then(|mut a| a.pop());
416    let focal_length = json.focal_lengths.and_then(|mut a| a.pop());
417    let audio_sample_rate = extra_data.as_ref()
418        .and_then(|e| e.audio_sample_rate)
419        .unwrap_or(0) as u32;
420    let audio_channels = extra_data.as_ref()
421        .and_then(|e| e.audio_channels)
422        .unwrap_or(0) as u16;
423    let has_audio = audio_channels > 0;
424
425    let color_matrix = json.color_matrix1.clone().or(json.forward_matrix1.clone())
426        .and_then(|m| {
427            if m.len() == 9 {
428                Some(m.try_into().ok()?)
429            } else {
430                None
431            }
432        });
433
434    let color_matrix2 = json.color_matrix2.clone().and_then(|m| {
435        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
436    });
437    let forward_matrix1 = json.forward_matrix1.clone().and_then(|m| {
438        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
439    });
440    let forward_matrix2 = json.forward_matrix2.clone().and_then(|m| {
441        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
442    });
443    let calibration_matrix1 = json.calibration_matrix1.clone().and_then(|m| {
444        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
445    });
446    let calibration_matrix2 = json.calibration_matrix2.clone().and_then(|m| {
447        if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
448    });
449
450    let bit_depth = json.white_level
451        .map(detect_bit_depth_from_white_level)
452        .unwrap_or(12);
453
454    let mut frame_count: u32 = 0;
455    let mut width: u16 = 0;
456    let mut height: u16 = 0;
457    let mut fps: f64 = 0.0;
458
459    let black_level = json.black_level
460        .as_ref()
461        .map(|levels| {
462            if levels.is_empty() { 0.0 }
463            else { levels.iter().sum::<f64>() / levels.len() as f64 }
464        })
465        .unwrap_or(0.0);
466
467    let white_level = json.white_level.unwrap_or(16383.0);
468
469    if let Some((num_offsets, offsets, timestamps)) = parse_buffer_index(data) {
470        frame_count = num_offsets;
471        if num_offsets >= 2 {
472            let mut sorted_ts = timestamps.clone();
473            sorted_ts.sort();
474            let duration_ns = sorted_ts[num_offsets as usize - 1] - sorted_ts[0];
475            if duration_ns > 0 {
476                fps = (num_offsets as f64 - 1.0) / (duration_ns as f64 / 1_000_000_000.0);
477            }
478        }
479    }
480
481    Ok(McrawFileInfo {
482        path: path.to_string_lossy().into_owned(),
483        size: data.len() as u64,
484        format_version,
485        frame_count,
486        width,
487        height,
488        fps,
489        has_audio,
490        audio_sample_rate,
491        audio_channels,
492        bit_depth,
493        bayer_pattern,
494        camera_metadata: CameraMetadata {
495            sensor_make: if sensor_make.is_empty() { None } else { Some(sensor_make) },
496            sensor_model: None,
497            camera_model,
498            lens_model: None,
499            focal_length,
500            aperture,
501            iso: None,
502            exposure_time: None,
503            white_balance: None,
504            capture_date: None,
505            color_matrix,
506            color_matrix2,
507            forward_matrix1,
508            forward_matrix2,
509            calibration_matrix1,
510            calibration_matrix2,
511            calibration_illuminant1: None,
512            calibration_illuminant2: None,
513            calibration_illuminant: None,
514            wb_multipliers: None,
515        },
516        frame_offsets: Vec::new(),
517        audio_offset: None,
518        audio_length: None,
519        sensor_width: 0,
520        sensor_height: 0,
521        active_offset_x: 0,
522        active_offset_y: 0,
523        active_width: 0,
524        active_height: 0,
525        white_level,
526        black_level,
527    })
528}
529
530fn parse_buffer_index(data: &[u8]) -> Option<(u32, Vec<i64>, Vec<i64>)> {
531    let file_len = data.len();
532    if file_len < 8 {
533        return None;
534    }
535
536    let item_size = u32::from_le_bytes([
537        data[file_len - 8],
538        data[file_len - 7],
539        data[file_len - 6],
540        data[file_len - 5],
541    ]) as usize;
542
543    let idx_data_start = file_len - 8 - item_size;
544    if idx_data_start < 16 {
545        return None;
546    }
547
548    let buf_idx_type = u32::from_le_bytes([
549        data[idx_data_start],
550        data[idx_data_start + 1],
551        data[idx_data_start + 2],
552        data[idx_data_start + 3],
553    ]);
554    if buf_idx_type != ITEM_TYPE_BUFFER_INDEX {
555        return None;
556    }
557
558    let buf_idx_size = u32::from_le_bytes([
559        data[idx_data_start + 4],
560        data[idx_data_start + 5],
561        data[idx_data_start + 6],
562        data[idx_data_start + 7],
563    ]) as usize;
564
565    let magic = u32::from_le_bytes([
566        data[idx_data_start + 8],
567        data[idx_data_start + 9],
568        data[idx_data_start + 10],
569        data[idx_data_start + 11],
570    ]);
571    if magic != INDEX_MAGIC {
572        return None;
573    }
574
575    let num_offsets = u32::from_le_bytes([
576        data[idx_data_start + 12],
577        data[idx_data_start + 13],
578        data[idx_data_start + 14],
579        data[idx_data_start + 15],
580    ]);
581
582    let data_offset = i64::from_le_bytes([
583        data[idx_data_start + 16],
584        data[idx_data_start + 17],
585        data[idx_data_start + 18],
586        data[idx_data_start + 19],
587        data[idx_data_start + 20],
588        data[idx_data_start + 21],
589        data[idx_data_start + 22],
590        data[idx_data_start + 23],
591    ]);
592
593    let mut offsets = Vec::new();
594    let mut timestamps = Vec::new();
595
596    for i in 0..num_offsets {
597        let pos = data_offset as usize + (i as usize) * 16;
598        if pos + 16 > data.len() {
599            break;
600        }
601        let offset = i64::from_le_bytes([
602            data[pos],
603            data[pos + 1],
604            data[pos + 2],
605            data[pos + 3],
606            data[pos + 4],
607            data[pos + 5],
608            data[pos + 6],
609            data[pos + 7],
610        ]);
611        let timestamp = i64::from_le_bytes([
612            data[pos + 8],
613            data[pos + 9],
614            data[pos + 10],
615            data[pos + 11],
616            data[pos + 12],
617            data[pos + 13],
618            data[pos + 14],
619            data[pos + 15],
620        ]);
621        offsets.push(offset);
622        timestamps.push(timestamp);
623    }
624
625    Some((num_offsets, offsets, timestamps))
626}
627
628fn parse_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
629    if data.len() < 17 {
630        anyhow::bail!("File {:?} is too small to be a valid file (need at least 17 bytes, got {})", path, data.len());
631    }
632
633    // Check for "MOTION " magic header (new format with JSON metadata)
634    if data.starts_with(b"MOTION ") {
635        tracing::debug!("detected MOTION format header");
636        return parse_motion_header(data, path);
637    }
638
639    // Check for "MCRAW" magic header (legacy binary format)
640    if data.len() < 36 {
641        anyhow::bail!("File {:?} is too small to be a valid MCRAW file (need at least 36 bytes, got {})", path, data.len());
642    }
643
644    let magic = &data[0..5];
645    if magic != b"MCRAW" {
646        anyhow::bail!(
647            "Invalid MCRAW magic header in {:?}: expected 'MCRAW', found {:?}",
648            path,
649            magic
650        );
651    }
652    tracing::debug!("detected MCRAW legacy format header");
653
654    let format_version = u32::from_be_bytes([data[5], data[6], data[7], data[8]]);
655    let frame_count = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
656    let width = u16::from_be_bytes([data[13], data[14]]);
657    let height = u16::from_be_bytes([data[15], data[16]]);
658    let fps = f64::from_be_bytes([
659        data[17], data[18], data[19], data[20], data[21], data[22], data[23], data[24],
660    ]);
661    let has_audio = data[25] != 0;
662
663    let audio_sample_rate = if has_audio && data.len() >= 30 {
664        u32::from_be_bytes([data[26], data[27], data[28], data[29]])
665    } else {
666        0
667    };
668
669    let audio_channels = if data.len() >= 32 {
670        u16::from_be_bytes([data[30], data[31]])
671    } else {
672        0
673    };
674
675    let bit_depth = if data.len() >= 34 {
676        u16::from_be_bytes([data[32], data[33]])
677    } else {
678        0
679    };
680
681    let bayer_pattern_id = if data.len() >= 35 {
682        data[34]
683    } else {
684        0
685    };
686    let bayer_pattern = BayerPattern::from_u8(bayer_pattern_id);
687
688    let mut offset = 36;
689    let mut camera_metadata = CameraMetadata::default();
690    let mut frame_offsets = Vec::new();
691    let mut audio_offset: Option<u64> = None;
692    let mut audio_length: Option<u64> = None;
693    let mut sensor_width: u16 = 0;
694    let mut sensor_height: u16 = 0;
695    let mut active_offset_x: u16 = 0;
696    let mut active_offset_y: u16 = 0;
697    let mut active_width: u16 = 0;
698    let mut active_height: u16 = 0;
699    let mut _color_matrix: Option<[f64; 9]> = None;
700    let mut _calibration_illuminant: Option<String> = None;
701
702    if offset < data.len() {
703        let block_length = read_u32_be(&data, offset) as usize;
704        offset += 4;
705        let block_end = offset + block_length;
706
707        while offset < block_end && offset < data.len() {
708            let tag = data[offset];
709            offset += 1;
710
711            match tag {
712                0x01 => {
713                    if let Ok(s) = parse_string(&data, &mut offset) {
714                        camera_metadata.sensor_make = Some(s);
715                    }
716                }
717                0x02 => {
718                    if let Ok(s) = parse_string(&data, &mut offset) {
719                        camera_metadata.sensor_model = Some(s);
720                    }
721                }
722                0x03 => {
723                    if let Ok(s) = parse_string(&data, &mut offset) {
724                        camera_metadata.camera_model = Some(s);
725                    }
726                }
727                0x04 => {
728                    if let Ok(s) = parse_string(&data, &mut offset) {
729                        camera_metadata.lens_model = Some(s);
730                    }
731                }
732                0x05 => {
733                    if let Ok(v) = parse_f64(&data, &mut offset) {
734                        camera_metadata.focal_length = Some(v);
735                    }
736                }
737                0x06 => {
738                    if let Ok(v) = parse_f64(&data, &mut offset) {
739                        camera_metadata.aperture = Some(v);
740                    }
741                }
742                0x07 => {
743                    if let Ok(v) = parse_u32_be(&data, &mut offset) {
744                        camera_metadata.iso = Some(v);
745                    }
746                }
747                0x08 => {
748                    if let Ok(v) = parse_f64(&data, &mut offset) {
749                        camera_metadata.exposure_time = Some(v);
750                    }
751                }
752                0x09 => {
753                    if let Ok(v) = parse_f64(&data, &mut offset) {
754                        camera_metadata.white_balance = Some(v);
755                    }
756                }
757                0x0A => {
758                    if let Ok(s) = parse_string(&data, &mut offset) {
759                        camera_metadata.capture_date = Some(s);
760                    }
761                }
762                0x0B => {
763                    let matrix = parse_f64_array(&data, &mut offset, 9);
764                    if matrix.len() == 9 {
765                        let arr: [f64; 9] = matrix.try_into().ok().unwrap_or([0.0; 9]);
766                          _color_matrix = Some(arr);
767                    }
768                }
769                0x0C => {
770                    if let Ok(s) = parse_string(&data, &mut offset) {
771                        _calibration_illuminant = Some(s);
772                    }
773                }
774                0x10 => {
775                    let count = parse_u32_be(&data, &mut offset);
776                    if let Ok(n) = count {
777                        let mut offsets = Vec::with_capacity(n as usize);
778                        for _ in 0..n {
779                            if offset + 8 <= data.len() {
780                                let val = u64::from_be_bytes([
781                                    data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
782                                    data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
783                                ]);
784                                offsets.push(val);
785                                offset += 8;
786                            } else {
787                                break;
788                            }
789                        }
790                        frame_offsets = offsets;
791                    }
792                }
793                0x11 => {
794                    if has_audio && offset + 8 <= data.len() {
795                        audio_offset = Some(u64::from_be_bytes([
796                            data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
797                            data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
798                        ]));
799                        offset += 8;
800                    }
801                }
802                0x12 => {
803                    if has_audio && offset + 8 <= data.len() {
804                        audio_length = Some(u64::from_be_bytes([
805                            data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
806                            data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
807                        ]));
808                        offset += 8;
809                    }
810                }
811                0x13 => {
812                    if offset + 2 <= data.len() {
813                        sensor_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
814                        offset += 2;
815                    }
816                }
817                0x14 => {
818                    if offset + 2 <= data.len() {
819                        sensor_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
820                        offset += 2;
821                    }
822                }
823                0x15 => {
824                    if offset + 2 <= data.len() {
825                        active_offset_x = u16::from_be_bytes([data[offset], data[offset + 1]]);
826                        offset += 2;
827                    }
828                }
829                0x16 => {
830                    if offset + 2 <= data.len() {
831                        active_offset_y = u16::from_be_bytes([data[offset], data[offset + 1]]);
832                        offset += 2;
833                    }
834                }
835                0x17 => {
836                    if offset + 2 <= data.len() {
837                        active_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
838                        offset += 2;
839                    }
840                }
841                0x18 => {
842                    if offset + 2 <= data.len() {
843                        active_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
844                        offset += 2;
845                    }
846                }
847                _ => {
848                    offset += 1;
849                }
850            }
851        }
852    }
853
854    Ok(McrawFileInfo {
855        path: path.to_string_lossy().into_owned(),
856        size: data.len() as u64,
857        format_version,
858        frame_count,
859        width,
860        height,
861        fps,
862        has_audio,
863        audio_sample_rate,
864        audio_channels,
865        bit_depth,
866        bayer_pattern,
867        camera_metadata,
868        frame_offsets,
869        audio_offset,
870        audio_length,
871        sensor_width,
872        sensor_height,
873        active_offset_x,
874        active_offset_y,
875        active_width,
876        active_height,
877        white_level: 16383.0,
878        black_level: 0.0,
879    })
880}
881
882fn read_u32_be(data: &[u8], offset: usize) -> u32 {
883    u32::from_be_bytes([
884        data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
885    ])
886}
887
888fn parse_u32_be(data: &[u8], offset: &mut usize) -> Result<u32> {
889    if *offset + 4 > data.len() {
890        return Err(anyhow::anyhow!("Unexpected end of data"));
891    }
892    let val = u32::from_be_bytes([
893        data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
894    ]);
895    *offset += 4;
896    Ok(val)
897}
898
899fn parse_f64(data: &[u8], offset: &mut usize) -> Result<f64> {
900    if *offset + 8 > data.len() {
901        return Err(anyhow::anyhow!("Unexpected end of data"));
902    }
903    let val = f64::from_be_bytes([
904        data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
905        data[*offset + 4], data[*offset + 5], data[*offset + 6], data[*offset + 7],
906    ]);
907    *offset += 8;
908    Ok(val)
909}
910
911fn parse_f64_array(data: &[u8], offset: &mut usize, len: usize) -> Vec<f64> {
912    let mut result = Vec::with_capacity(len);
913    for _ in 0..len {
914        if let Ok(v) = parse_f64(data, offset) {
915            result.push(v);
916        } else {
917            break;
918        }
919    }
920    result
921}
922
923fn parse_string(data: &[u8], offset: &mut usize) -> Result<String> {
924    if *offset + 4 > data.len() {
925        return Err(anyhow::anyhow!("Unexpected end of data"));
926    }
927    let str_len = u32::from_be_bytes([
928        data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
929    ]) as usize;
930    *offset += 4;
931    if *offset + str_len > data.len() {
932        return Err(anyhow::anyhow!("String extends beyond data"));
933    }
934    let s = std::str::from_utf8(&data[*offset..*offset + str_len])
935        .map_err(|e| anyhow::anyhow!("Invalid UTF-8 string: {}", e))?;
936    *offset += str_len;
937    Ok(s.to_string())
938}
939
940pub fn detect_bit_depth_from_white_level(white_level: f64) -> u16 {
941    if white_level <= 1024.0 {
942        10
943    } else if white_level <= 4096.0 {
944        12
945    } else if white_level <= 16384.0 {
946        14
947    } else if white_level <= 65536.0 {
948        16
949    } else {
950        12
951    }
952}
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957
958    #[test]
959    fn test_bayer_pattern_from_u8() {
960        assert_eq!(BayerPattern::from_u8(0), BayerPattern::RGGB);
961        assert_eq!(BayerPattern::from_u8(1), BayerPattern::GRBG);
962        assert_eq!(BayerPattern::from_u8(2), BayerPattern::GBRG);
963        assert_eq!(BayerPattern::from_u8(3), BayerPattern::BGGR);
964        assert_eq!(BayerPattern::from_u8(4), BayerPattern::QuadBayerRGGB);
965        assert_eq!(BayerPattern::from_u8(5), BayerPattern::QuadBayerGRBG);
966        assert_eq!(BayerPattern::from_u8(6), BayerPattern::QuadBayerGBRG);
967        assert_eq!(BayerPattern::from_u8(7), BayerPattern::QuadBayerBGGR);
968        assert_eq!(BayerPattern::from_u8(99), BayerPattern::RGGB);
969    }
970
971    #[test]
972    fn test_bayer_pattern_to_u8() {
973        assert_eq!(BayerPattern::RGGB.to_u8(), 0);
974        assert_eq!(BayerPattern::GRBG.to_u8(), 1);
975        assert_eq!(BayerPattern::GBRG.to_u8(), 2);
976        assert_eq!(BayerPattern::BGGR.to_u8(), 3);
977        assert_eq!(BayerPattern::QuadBayerRGGB.to_u8(), 4);
978        assert_eq!(BayerPattern::QuadBayerGRBG.to_u8(), 5);
979        assert_eq!(BayerPattern::QuadBayerGBRG.to_u8(), 6);
980        assert_eq!(BayerPattern::QuadBayerBGGR.to_u8(), 7);
981    }
982
983    #[test]
984    fn test_detect_bit_depth_from_white_level() {
985        assert_eq!(detect_bit_depth_from_white_level(1023.0), 10);
986        assert_eq!(detect_bit_depth_from_white_level(1024.0), 10);
987        assert_eq!(detect_bit_depth_from_white_level(1025.0), 12);
988        assert_eq!(detect_bit_depth_from_white_level(4095.0), 12);
989        assert_eq!(detect_bit_depth_from_white_level(4096.0), 12);
990        assert_eq!(detect_bit_depth_from_white_level(4097.0), 14);
991        assert_eq!(detect_bit_depth_from_white_level(16383.0), 14);
992        assert_eq!(detect_bit_depth_from_white_level(16384.0), 14);
993        assert_eq!(detect_bit_depth_from_white_level(16385.0), 16);
994        assert_eq!(detect_bit_depth_from_white_level(65535.0), 16);
995        assert_eq!(detect_bit_depth_from_white_level(65536.0), 16);
996        assert_eq!(detect_bit_depth_from_white_level(65537.0), 12);
997        assert_eq!(detect_bit_depth_from_white_level(0.0), 10);
998    }
999
1000    #[test]
1001    fn test_parse_header_minimal() {
1002        let mut data = vec![0u8; 36];
1003        data[0..5].copy_from_slice(b"MCRAW");
1004        data[5..9].copy_from_slice(&2u32.to_be_bytes());
1005        data[9..13].copy_from_slice(&10u32.to_be_bytes());
1006        data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
1007        data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
1008        data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1009        data[25] = 0;
1010
1011        let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1012        assert_eq!(info.format_version, 2);
1013        assert_eq!(info.frame_count, 10);
1014        assert_eq!(info.width, 1920);
1015        assert_eq!(info.height, 1080);
1016        assert!((info.fps - 30.0).abs() < 0.001);
1017        assert!(!info.has_audio);
1018    }
1019
1020    #[test]
1021    fn test_duration_seconds() {
1022        let mut data = vec![0u8; 36];
1023        data[0..5].copy_from_slice(b"MCRAW");
1024        data[9..13].copy_from_slice(&600u32.to_be_bytes());
1025        data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1026        let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1027        assert!((info.duration_seconds() - 20.0).abs() < 0.001);
1028    }
1029
1030    #[test]
1031    fn test_resolution_label() {
1032        let make_info = |w: u16, h: u16| McrawFileInfo {
1033            path: String::new(),
1034            size: 0,
1035            format_version: 2,
1036            frame_count: 0,
1037            width: w,
1038            height: h,
1039            fps: 30.0,
1040            has_audio: false,
1041            audio_sample_rate: 0,
1042            audio_channels: 0,
1043            bit_depth: 0,
1044            bayer_pattern: BayerPattern::RGGB,
1045            camera_metadata: CameraMetadata::default(),
1046            frame_offsets: Vec::new(),
1047            audio_offset: None,
1048            audio_length: None,
1049            sensor_width: 0,
1050            sensor_height: 0,
1051            active_offset_x: 0,
1052            active_offset_y: 0,
1053            active_width: 0,
1054            active_height: 0,
1055white_level: 16383.0,
1056        black_level: 0.0,
1057        };
1058
1059        assert_eq!(make_info(1920, 1080).resolution_label(), "1080p");
1060        assert_eq!(make_info(2560, 1440).resolution_label(), "1440p");
1061        assert_eq!(make_info(3840, 2160).resolution_label(), "4K");
1062        assert_eq!(make_info(4096, 2160).resolution_label(), "4K DCI");
1063        assert_eq!(make_info(1280, 720).resolution_label(), "Custom");
1064    }
1065
1066    #[test]
1067    fn test_parse_header_with_string_metadata() {
1068        let mut data = vec![0u8; 64];
1069        data[0..5].copy_from_slice(b"MCRAW");
1070        data[5] = 2;
1071        data[9..13].copy_from_slice(&1u32.to_be_bytes());
1072        data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
1073        data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
1074        data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
1075        data[25] = 0;
1076
1077        let camera_model = "TestCamera";
1078        let block_offset = 36;
1079        let str_len = camera_model.len() as u32;
1080        let block_len = 1 + 4 + str_len as u32; // 1 tag + 4 str_len + str data
1081        data[block_offset..block_offset + 4].copy_from_slice(&(block_len as u32).to_be_bytes());
1082        data[block_offset + 4] = 0x03;
1083        data[block_offset + 5..block_offset + 9].copy_from_slice(&str_len.to_be_bytes());
1084        data[block_offset + 9..block_offset + 9 + camera_model.len()]
1085            .copy_from_slice(camera_model.as_bytes());
1086
1087        let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
1088        assert_eq!(info.camera_metadata.camera_model, Some("TestCamera".to_string()));
1089    }
1090
1091    #[test]
1092    fn test_parse_header_invalid_magic() {
1093        let data = vec![b'X'; 36];
1094        let result = parse_header(&data, std::path::Path::new("test.mcraw"));
1095        assert!(result.is_err());
1096    }
1097
1098    #[test]
1099    fn test_parse_header_too_small() {
1100        let data = vec![0u8; 10];
1101        let result = parse_header(&data, std::path::Path::new("test.mcraw"));
1102        assert!(result.is_err());
1103    }
1104}