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#[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 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, }
159 }
160}
161
162#[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#[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 if data.starts_with(b"MOTION ") {
635 tracing::debug!("detected MOTION format header");
636 return parse_motion_header(data, path);
637 }
638
639 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; 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}