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 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 if data.starts_with(b"MOTION ") {
624 tracing::debug!("detected MOTION format header");
625 return parse_motion_header(data, path);
626 }
627
628 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; 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}