use anyhow::{Context, Result};
use serde::Deserialize;
use std::fs;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
#[derive(Debug, Deserialize)]
struct MotionJsonMetadata {
#[serde(rename = "sensorArrangment", default)]
sensor_arrangement: Option<String>,
#[serde(rename = "sensorOrientation", default)]
sensor_orientation: Option<i64>,
#[serde(rename = "forwardMatrix1", default)]
forward_matrix1: Option<Vec<f64>>,
#[serde(rename = "forwardMatrix2", default)]
forward_matrix2: Option<Vec<f64>>,
#[serde(rename = "colorMatrix1", default)]
color_matrix1: Option<Vec<f64>>,
#[serde(rename = "colorMatrix2", default)]
color_matrix2: Option<Vec<f64>>,
#[serde(rename = "calibrationMatrix1", default)]
calibration_matrix1: Option<Vec<f64>>,
#[serde(rename = "calibrationMatrix2", default)]
calibration_matrix2: Option<Vec<f64>>,
#[serde(rename = "whiteLevel", default)]
white_level: Option<f64>,
#[serde(rename = "blackLevel", default)]
black_level: Option<Vec<f64>>,
#[serde(rename = "baselineExposure", default)]
baseline_exposure: Option<f64>,
#[serde(rename = "apertures", default)]
apertures: Option<Vec<f64>>,
#[serde(rename = "focalLengths", default)]
focal_lengths: Option<Vec<f64>>,
#[serde(rename = "uniqueCameraModel", default)]
unique_camera_model: Option<String>,
#[serde(rename = "numSegments", default)]
num_segments: Option<i64>,
#[serde(rename = "extraData", default)]
extra_data: Option<ExtraData>,
#[serde(rename = "deviceSpecificProfile", default)]
device_specific_profile: Option<DeviceProfile>,
#[serde(rename = "colorIlluminant1", default)]
color_illuminant1: Option<String>,
#[serde(rename = "colorIlluminant2", default)]
color_illuminant2: Option<String>,
#[serde(rename = "lensShadingMap", default)]
lens_shading_map: Option<Vec<Vec<f64>>>,
#[serde(rename = "lensShadingMapWidth", default)]
lens_shading_map_width: Option<i64>,
#[serde(rename = "lensShadingMapHeight", default)]
lens_shading_map_height: Option<i64>,
}
#[derive(Debug, Deserialize)]
struct ExtraData {
#[serde(rename = "recordingType", default)]
recording_type: Option<String>,
#[serde(rename = "audioSampleRate", default)]
audio_sample_rate: Option<i64>,
#[serde(rename = "audioChannels", default)]
audio_channels: Option<i64>,
#[serde(rename = "useAccurateTimestamp", default)]
use_accurate_timestamp: Option<bool>,
#[serde(rename = "metadata", default)]
metadata: Option<BuildMetadata>,
}
#[derive(Debug, Deserialize)]
struct BuildMetadata {
#[serde(rename = "build.model", default)]
build_model: Option<String>,
#[serde(rename = "build.manufacturer", default)]
build_manufacturer: Option<String>,
#[serde(rename = "version.major", default)]
version_major: Option<String>,
#[serde(rename = "version.build", default)]
version_build: Option<String>,
}
#[derive(Debug, Deserialize)]
struct DeviceProfile {
#[serde(rename = "cameraId", default)]
camera_id: Option<String>,
#[serde(rename = "deviceModel", default)]
device_model: Option<String>,
}
const INDEX_MAGIC: u32 = 0x8A905612;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BayerPattern {
RGGB,
GRBG,
GBRG,
BGGR,
QuadBayerRGGB,
QuadBayerGRBG,
QuadBayerGBRG,
QuadBayerBGGR,
}
impl BayerPattern {
pub fn from_u8(value: u8) -> Self {
match value {
0 => BayerPattern::RGGB,
1 => BayerPattern::GRBG,
2 => BayerPattern::GBRG,
3 => BayerPattern::BGGR,
4 => BayerPattern::QuadBayerRGGB,
5 => BayerPattern::QuadBayerGRBG,
6 => BayerPattern::QuadBayerGBRG,
7 => BayerPattern::QuadBayerBGGR,
_ => BayerPattern::RGGB,
}
}
pub fn to_u8(&self) -> u8 {
match self {
BayerPattern::RGGB => 0,
BayerPattern::GRBG => 1,
BayerPattern::GBRG => 2,
BayerPattern::BGGR => 3,
BayerPattern::QuadBayerRGGB => 4,
BayerPattern::QuadBayerGRBG => 5,
BayerPattern::QuadBayerGBRG => 6,
BayerPattern::QuadBayerBGGR => 7,
}
}
pub fn name(&self) -> &'static str {
match self {
BayerPattern::RGGB => "RGGB",
BayerPattern::GRBG => "GRBG",
BayerPattern::GBRG => "GBRG",
BayerPattern::BGGR => "BGGR",
BayerPattern::QuadBayerRGGB => "QuadBayer RGGB",
BayerPattern::QuadBayerGRBG => "QuadBayer GRBG",
BayerPattern::QuadBayerGBRG => "QuadBayer GBRG",
BayerPattern::QuadBayerBGGR => "QuadBayer BGGR",
}
}
pub fn to_dcraw_filters(&self) -> u32 {
match self {
BayerPattern::RGGB => 0x94949494,
BayerPattern::BGGR => 0x16161616,
BayerPattern::GRBG => 0x61616161,
BayerPattern::GBRG => 0x49494949,
_ => 0x94949494, }
}
}
#[derive(Debug, Clone)]
pub struct CameraMetadata {
pub sensor_make: Option<String>,
pub sensor_model: Option<String>,
pub camera_model: Option<String>,
pub lens_model: Option<String>,
pub focal_length: Option<f64>,
pub aperture: Option<f64>,
pub iso: Option<u32>,
pub exposure_time: Option<f64>,
pub white_balance: Option<f64>,
pub capture_date: Option<String>,
pub color_matrix: Option<[f64; 9]>,
pub color_matrix2: Option<[f64; 9]>,
pub forward_matrix1: Option<[f64; 9]>,
pub forward_matrix2: Option<[f64; 9]>,
pub calibration_matrix1: Option<[f64; 9]>,
pub calibration_matrix2: Option<[f64; 9]>,
pub calibration_illuminant1: Option<i32>,
pub calibration_illuminant2: Option<i32>,
pub calibration_illuminant: Option<String>,
pub wb_multipliers: Option<[f32; 3]>,
}
impl Default for CameraMetadata {
fn default() -> Self {
CameraMetadata {
sensor_make: None,
sensor_model: None,
camera_model: None,
lens_model: None,
focal_length: None,
aperture: None,
iso: None,
exposure_time: None,
white_balance: None,
capture_date: None,
color_matrix: None,
color_matrix2: None,
forward_matrix1: None,
forward_matrix2: None,
calibration_matrix1: None,
calibration_matrix2: None,
calibration_illuminant1: None,
calibration_illuminant2: None,
calibration_illuminant: None,
wb_multipliers: None,
}
}
}
#[derive(Debug, Clone)]
pub struct McrawFileInfo {
pub path: String,
pub size: u64,
pub format_version: u32,
pub frame_count: u32,
pub width: u16,
pub height: u16,
pub fps: f64,
pub has_audio: bool,
pub audio_sample_rate: u32,
pub audio_channels: u16,
pub bit_depth: u16,
pub bayer_pattern: BayerPattern,
pub camera_metadata: CameraMetadata,
pub frame_offsets: Vec<u64>,
pub audio_offset: Option<u64>,
pub audio_length: Option<u64>,
pub sensor_width: u16,
pub sensor_height: u16,
pub active_offset_x: u16,
pub active_offset_y: u16,
pub active_width: u16,
pub active_height: u16,
pub white_level: f64,
pub black_level: f64,
pub black_level_per_channel: [f64; 4],
pub black_level_count: i32,
pub lens_shading_map: Option<crate::decoder::LensShadingMap>,
pub dynamic_black_level: Option<[f32; 4]>,
pub dynamic_white_level: Option<f32>,
pub first_timestamp: Option<i64>,
}
struct FirstFrameMeta {
width: u16,
height: u16,
wb_gains: Option<[f32; 3]>,
}
fn read_first_frame_meta(file: &mut fs::File, frame0_offset: u64) -> Option<FirstFrameMeta> {
file.seek(SeekFrom::Start(frame0_offset)).ok()?;
let mut hdr = [0u8; 8];
file.read_exact(&mut hdr).ok()?;
let buf_type = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
let buf_size = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
if buf_type != 2 {
return None;
}
file.seek(SeekFrom::Current(buf_size as i64)).ok()?;
file.read_exact(&mut hdr).ok()?;
let meta_type = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
let meta_size = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
if meta_type != 3 {
return None;
}
let mut json_buf = vec![0u8; meta_size as usize];
file.read_exact(&mut json_buf).ok()?;
let json: serde_json::Value = serde_json::from_slice(&json_buf).ok()?;
let w = json.get("width")?.as_u64()? as u16;
let h = json.get("height")?.as_u64()? as u16;
if w == 0 || h == 0 {
return None;
}
let wb_gains = json.get("asShotNeutral").and_then(|v| v.as_array()).and_then(|arr| {
if arr.len() >= 3 {
let r = arr[0].as_f64()?;
let g = arr[1].as_f64()?;
let b = arr[2].as_f64()?;
if r > 1e-6 && g > 1e-6 && b > 1e-6 {
Some([(g / r) as f32, 1.0, (g / b) as f32])
} else {
None
}
} else {
None
}
});
tracing::debug!("read_first_frame_meta: w={} h={} wb_gains={:?}", w, h, wb_gains);
Some(FirstFrameMeta { width: w, height: h, wb_gains })
}
impl McrawFileInfo {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
tracing::debug!("McrawFileInfo::from_path: {:?}", path);
let file_meta = fs::metadata(&path)
.with_context(|| format!("Failed to read metadata for {:?}", path))?;
let file_size = file_meta.len();
let mut file = std::fs::File::open(path)
.with_context(|| format!("Failed to open {:?}", path))?;
let mut magic_buf = [0u8; 16];
file.read_exact(&mut magic_buf)
.with_context(|| format!("Failed to read header from {:?}", path))?;
let info = if magic_buf.starts_with(b"MOTION ") {
let json_len = u32::from_le_bytes([
magic_buf[12], magic_buf[13], magic_buf[14], magic_buf[15],
]) as usize;
let mut json_buf = vec![0u8; json_len];
file.read_exact(&mut json_buf)
.with_context(|| format!("Failed to read MOTION JSON block from {:?}", path))?;
let mut data = Vec::with_capacity(16 + json_len);
data.extend_from_slice(&magic_buf);
data.extend_from_slice(&json_buf);
let mut info = parse_motion_header(&data, path)?;
if file_size >= 24 {
let mut end_buf = [0u8; 24];
file.seek(SeekFrom::End(-24))
.with_context(|| format!("Failed to seek to BufferIndex in {:?}", path))?;
file.read_exact(&mut end_buf)
.with_context(|| format!("Failed to read BufferIndex from {:?}", path))?;
let idx_magic = u32::from_le_bytes([end_buf[8], end_buf[9], end_buf[10], end_buf[11]]);
if idx_magic == INDEX_MAGIC {
let num_offsets = u32::from_le_bytes([end_buf[12], end_buf[13], end_buf[14], end_buf[15]]);
let idx_data_offset = i64::from_le_bytes([
end_buf[16], end_buf[17], end_buf[18], end_buf[19],
end_buf[20], end_buf[21], end_buf[22], end_buf[23],
]) as u64;
info.frame_count = num_offsets;
if num_offsets > 0 && idx_data_offset + (num_offsets as u64 * 16) <= file_size {
let mut offset_buf = vec![0u8; num_offsets as usize * 16];
file.seek(SeekFrom::Start(idx_data_offset))
.with_context(|| format!("Failed to seek to offset data in {:?}", path))?;
file.read_exact(&mut offset_buf)
.with_context(|| format!("Failed to read offset data from {:?}", path))?;
let mut first_frame_offset: u64 = 0;
let mut timestamps = Vec::with_capacity(num_offsets as usize);
for i in 0..num_offsets as usize {
let off = i64::from_le_bytes([
offset_buf[i*16], offset_buf[i*16+1], offset_buf[i*16+2], offset_buf[i*16+3],
offset_buf[i*16+4], offset_buf[i*16+5], offset_buf[i*16+6], offset_buf[i*16+7],
]);
let ts = i64::from_le_bytes([
offset_buf[i*16+8], offset_buf[i*16+9], offset_buf[i*16+10], offset_buf[i*16+11],
offset_buf[i*16+12], offset_buf[i*16+13], offset_buf[i*16+14], offset_buf[i*16+15],
]);
if i == 0 { first_frame_offset = off as u64; }
timestamps.push(ts);
}
timestamps.sort();
info.first_timestamp = Some(timestamps[0]);
if num_offsets >= 2 {
let duration_ns = timestamps[num_offsets as usize - 1] - timestamps[0];
if duration_ns > 0 {
info.fps = (num_offsets as f64 - 1.0) / (duration_ns as f64 / 1_000_000_000.0);
}
}
if first_frame_offset > 0 {
if let Some(meta) = read_first_frame_meta(&mut file, first_frame_offset) {
info.width = meta.width;
info.height = meta.height;
if let Some(wb) = meta.wb_gains {
info.camera_metadata.wb_multipliers = Some(wb);
}
} else {
tracing::debug!("from_path: read_first_frame_meta returned None for offset={}", first_frame_offset);
}
}
}
}
}
info
} else if &magic_buf[..5] == b"MCRAW" {
let mut rest_header = [0u8; 20];
file.read_exact(&mut rest_header)
.with_context(|| format!("Failed to read legacy header from {:?}", path))?;
let mut data = Vec::with_capacity(36);
data.extend_from_slice(&magic_buf);
data.extend_from_slice(&rest_header);
if file_size > 36 {
let mut block_len_buf = [0u8; 4];
file.read_exact(&mut block_len_buf)
.with_context(|| format!("Failed to read TLV block length from {:?}", path))?;
let block_length = u32::from_be_bytes(block_len_buf) as usize;
let mut tlv_buf = vec![0u8; block_length];
file.read_exact(&mut tlv_buf)
.with_context(|| format!("Failed to read TLV block from {:?}", path))?;
data.extend_from_slice(&block_len_buf);
data.extend_from_slice(&tlv_buf);
}
parse_header(&data, path)?
} else {
anyhow::bail!(
"Invalid MCRAW magic header in {:?}: expected 'MCRAW' or 'MOTION ', got {:?}",
path,
&magic_buf[..7]
);
};
Ok(McrawFileInfo {
path: path.to_string_lossy().into_owned(),
size: file_size,
..info
})
}
pub fn is_metadata_complete(&self) -> bool {
self.width > 0 && self.height > 0 && self.frame_count > 0
&& self.first_timestamp.is_some()
&& self.camera_metadata.wb_multipliers.is_some()
}
pub fn enhance_from_decoder(&mut self, decoder: &crate::decoder::Decoder) {
if self.camera_metadata.color_matrix.is_none() {
if let Ok(container_meta) = decoder.container_metadata() {
if container_meta.white_level > 0.0 {
self.white_level = container_meta.white_level;
tracing::debug!("white_level from container: {}", self.white_level);
}
if container_meta.black_level_count > 0 {
self.black_level = container_meta.black_level[0];
self.black_level_per_channel = container_meta.black_level;
self.black_level_count = container_meta.black_level_count;
tracing::debug!("black_level from container: {} ({} ch)", self.black_level, self.black_level_count);
}
self.lens_shading_map = container_meta.lens_shading_map.clone();
let as_f64 = |v: &[f32; 9]| -> [f64; 9] {
let mut r = [0.0; 9];
for (i, &x) in v.iter().enumerate() { r[i] = x as f64; }
r
};
self.camera_metadata.color_matrix = Some(as_f64(&container_meta.color_matrix1));
let non_zero = |m: &[f32; 9]| m.iter().any(|&x| x != 0.0);
if non_zero(&container_meta.color_matrix2) {
self.camera_metadata.color_matrix2 = Some(as_f64(&container_meta.color_matrix2));
}
if non_zero(&container_meta.forward_matrix1) {
self.camera_metadata.forward_matrix1 = Some(as_f64(&container_meta.forward_matrix1));
}
if non_zero(&container_meta.forward_matrix2) {
self.camera_metadata.forward_matrix2 = Some(as_f64(&container_meta.forward_matrix2));
}
if non_zero(&container_meta.calibration_matrix1) {
self.camera_metadata.calibration_matrix1 = Some(as_f64(&container_meta.calibration_matrix1));
}
if non_zero(&container_meta.calibration_matrix2) {
self.camera_metadata.calibration_matrix2 = Some(as_f64(&container_meta.calibration_matrix2));
}
if container_meta.has_calibration_illuminants {
self.camera_metadata.calibration_illuminant1 = Some(container_meta.calibration_illuminant1);
self.camera_metadata.calibration_illuminant2 = Some(container_meta.calibration_illuminant2);
tracing::debug!("calibration_illuminants: illum1={}, illum2={}",
container_meta.calibration_illuminant1, container_meta.calibration_illuminant2);
}
}
}
if let Ok(timestamps) = decoder.timestamps() {
if !timestamps.is_empty() {
self.frame_count = timestamps.len() as u32;
if timestamps.len() >= 2 {
let duration_ns = timestamps[timestamps.len() - 1] - timestamps[0];
if duration_ns > 0 {
let duration_in_seconds = duration_ns as f64 / 1_000_000_000.0;
self.fps = (self.frame_count.saturating_sub(1)) as f64 / duration_in_seconds;
}
}
tracing::debug!("enhanced from timestamps: {} frames, {:.2} fps", self.frame_count, self.fps);
}
if let Ok(first_frame_meta) = decoder.load_frame_metadata(timestamps[0]) {
if self.width == 0 || self.height == 0 {
self.width = first_frame_meta.width as u16;
self.height = first_frame_meta.height as u16;
tracing::debug!("enhanced dimensions: {}x{}", first_frame_meta.width, first_frame_meta.height);
}
let n = first_frame_meta.as_shot_neutral;
if self.camera_metadata.wb_multipliers.is_none()
&& n[0] > 1e-6 && n[1] > 1e-6 && n[2] > 1e-6
{
let r_gain = n[1] / n[0];
let b_gain = n[1] / n[2];
self.camera_metadata.wb_multipliers = Some([r_gain, 1.0, b_gain]);
tracing::debug!("wb_multipliers: R={:.3} G={:.3} B={:.3}", r_gain, 1.0, b_gain);
}
if first_frame_meta.dynamic_black_level.is_some() {
self.dynamic_black_level = first_frame_meta.dynamic_black_level;
tracing::debug!("dynamic_black_level from first frame: {:?}", self.dynamic_black_level);
}
if first_frame_meta.dynamic_white_level.is_some() {
self.dynamic_white_level = first_frame_meta.dynamic_white_level;
tracing::debug!("dynamic_white_level from first frame: {:?}", self.dynamic_white_level);
}
}
}
}
pub fn enhance_with_decoder(&mut self) {
if self.camera_metadata.color_matrix.is_some() {
tracing::debug!("enhance_with_decoder: metadata already populated, skipping decoder");
return;
}
let path = self.path.clone();
tracing::debug!("enhance_with_decoder: {}", path);
let decoder_result = crate::decoder::Decoder::new(&path);
let decoder = match decoder_result {
Ok(d) => d,
Err(e) => {
tracing::warn!("failed to open decoder for {}: {}", path, e);
return;
}
};
self.enhance_from_decoder(&decoder);
}
pub fn format_name(&self) -> &'static str {
match self.format_version {
1 => "MotionCam v1 (Legacy)",
2 => "MotionCam v2",
3 => "MotionCam v3",
_ => "Unknown format",
}
}
pub fn duration_seconds(&self) -> f64 {
self.frame_count as f64 / self.fps
}
pub fn resolution_label(&self) -> &'static str {
match (self.width, self.height) {
(1920, 1080) => "1080p",
(2560, 1440) => "1440p",
(3840, 2160) => "4K",
(4096, 2160) => "4K DCI",
_ => "Custom",
}
}
}
fn parse_motion_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
if data.len() < 17 {
anyhow::bail!("File {:?} is too small for MOTION header", path);
}
let format_version = data[7] as u32;
tracing::debug!("parse_motion_header: version={} json_len={}", format_version, u32::from_le_bytes([data[12], data[13], data[14], data[15]]));
let json_len = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
let json_start = 16;
let json_end = json_start + json_len;
if json_end > data.len() {
anyhow::bail!("JSON metadata extends beyond file data");
}
let json_str = std::str::from_utf8(&data[json_start..json_end])
.with_context(|| "Invalid UTF-8 in MOTION JSON metadata")?;
let json: MotionJsonMetadata = serde_json::from_str(json_str)
.with_context(|| "Failed to parse MOTION JSON metadata")?;
let bayer_pattern = match json.sensor_arrangement.as_deref() {
Some("rggb") | Some("standard") => BayerPattern::RGGB,
Some("grbg") => BayerPattern::GRBG,
Some("gbrg") => BayerPattern::GBRG,
Some("bggr") => BayerPattern::BGGR,
_ => BayerPattern::RGGB,
};
let extra_data = json.extra_data;
let device_profile = json.device_specific_profile;
let build_model = extra_data
.as_ref()
.and_then(|e| e.metadata.as_ref())
.and_then(|m| m.build_model.clone());
let camera_model: Option<String> = device_profile.as_ref()
.and_then(|p| p.device_model.clone())
.filter(|s| !s.is_empty())
.or_else(|| json.unique_camera_model.filter(|s| !s.is_empty()))
.or_else(|| build_model.filter(|s| !s.is_empty()));
let sensor_make = extra_data
.as_ref()
.and_then(|e| e.metadata.as_ref())
.and_then(|m| m.build_manufacturer.clone())
.unwrap_or_default();
let aperture = json.apertures.and_then(|mut a| a.pop());
let focal_length = json.focal_lengths.and_then(|mut a| a.pop());
let audio_sample_rate = extra_data.as_ref()
.and_then(|e| e.audio_sample_rate)
.unwrap_or(0) as u32;
let audio_channels = extra_data.as_ref()
.and_then(|e| e.audio_channels)
.unwrap_or(0) as u16;
let has_audio = audio_channels > 0;
let color_matrix = json.color_matrix1.clone().or(json.forward_matrix1.clone())
.and_then(|m| {
if m.len() == 9 {
Some(m.try_into().ok()?)
} else {
None
}
});
let color_matrix2 = json.color_matrix2.clone().and_then(|m| {
if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
});
let forward_matrix1 = json.forward_matrix1.clone().and_then(|m| {
if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
});
let forward_matrix2 = json.forward_matrix2.clone().and_then(|m| {
if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
});
let calibration_matrix1 = json.calibration_matrix1.clone().and_then(|m| {
if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
});
let calibration_matrix2 = json.calibration_matrix2.clone().and_then(|m| {
if m.len() == 9 { Some(m.try_into().ok()?) } else { None }
});
let bit_depth = json.white_level
.map(detect_bit_depth_from_white_level)
.unwrap_or(12);
let frame_count: u32 = 0;
let width: u16 = 0;
let height: u16 = 0;
let fps: f64 = 0.0;
let (black_level, black_level_per_channel, black_level_count) = json.black_level
.as_ref()
.map(|levels| {
let count = levels.len() as i32;
let avg = if levels.is_empty() { 0.0 } else { levels.iter().sum::<f64>() / levels.len() as f64 };
let mut per_ch = [avg; 4];
for (i, &v) in levels.iter().enumerate().take(4) {
per_ch[i] = v;
}
(avg, per_ch, count)
})
.unwrap_or((0.0, [0.0; 4], 0));
let white_level = json.white_level.unwrap_or(16383.0);
let wb_multipliers: Option<[f32; 3]> = None;
let json_illuminant_to_const = |s: &str| -> Option<i32> {
match s.trim().to_lowercase().as_str() {
"d50" | "horizon" | "cool_white" => Some(23),
"d55" => Some(22),
"d65" | "daylight" | "fine_weather" | "cloudy" => Some(21),
"d75" | "shade" => Some(24),
"standardlighta" | "standard_a" | "tungsten" | "incandescent" | "warm_white" | "iso_studio_tungsten" => Some(17),
"fluorescent" | "tl84" => Some(12),
"flash" | "standardlightb" => Some(4),
_ => None,
}
};
let calibration_illuminant1 = json.color_illuminant1.as_deref().and_then(json_illuminant_to_const);
let calibration_illuminant2 = json.color_illuminant2.as_deref().and_then(json_illuminant_to_const);
let lens_shading_map = json.lens_shading_map.as_ref().and_then(|channels| {
let width = json.lens_shading_map_width? as u32;
let height = json.lens_shading_map_height? as u32;
if channels.len() < 4 { return None; }
let f32_channels: Vec<Vec<f32>> = channels.iter().take(4).map(|ch| ch.iter().map(|&v| v as f32).collect()).collect();
if f32_channels.len() < 4 { return None; }
Some(crate::decoder::LensShadingMap { channels: f32_channels, width, height })
});
Ok(McrawFileInfo {
path: path.to_string_lossy().into_owned(),
size: data.len() as u64,
format_version,
frame_count,
width,
height,
fps,
has_audio,
audio_sample_rate,
audio_channels,
bit_depth,
bayer_pattern,
camera_metadata: CameraMetadata {
sensor_make: if sensor_make.is_empty() { None } else { Some(sensor_make) },
sensor_model: None,
camera_model,
lens_model: None,
focal_length,
aperture,
iso: None,
exposure_time: None,
white_balance: None,
capture_date: None,
color_matrix,
color_matrix2,
forward_matrix1,
forward_matrix2,
calibration_matrix1,
calibration_matrix2,
calibration_illuminant1,
calibration_illuminant2,
calibration_illuminant: None,
wb_multipliers,
},
frame_offsets: Vec::new(),
audio_offset: None,
audio_length: None,
sensor_width: 0,
sensor_height: 0,
active_offset_x: 0,
active_offset_y: 0,
active_width: 0,
active_height: 0,
white_level,
black_level,
black_level_per_channel,
black_level_count,
lens_shading_map,
dynamic_black_level: None,
dynamic_white_level: None,
first_timestamp: None,
})
}
fn parse_header(data: &[u8], path: &Path) -> Result<McrawFileInfo> {
if data.len() < 17 {
anyhow::bail!("File {:?} is too small to be a valid file (need at least 17 bytes, got {})", path, data.len());
}
if data.starts_with(b"MOTION ") {
tracing::debug!("detected MOTION format header");
return parse_motion_header(data, path);
}
if data.len() < 36 {
anyhow::bail!("File {:?} is too small to be a valid MCRAW file (need at least 36 bytes, got {})", path, data.len());
}
let magic = &data[0..5];
if magic != b"MCRAW" {
anyhow::bail!(
"Invalid MCRAW magic header in {:?}: expected 'MCRAW', found {:?}",
path,
magic
);
}
tracing::debug!("detected MCRAW legacy format header");
let format_version = u32::from_be_bytes([data[5], data[6], data[7], data[8]]);
let frame_count = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
let width = u16::from_be_bytes([data[13], data[14]]);
let height = u16::from_be_bytes([data[15], data[16]]);
let fps = f64::from_be_bytes([
data[17], data[18], data[19], data[20], data[21], data[22], data[23], data[24],
]);
let has_audio = data[25] != 0;
let audio_sample_rate = if has_audio && data.len() >= 30 {
u32::from_be_bytes([data[26], data[27], data[28], data[29]])
} else {
0
};
let audio_channels = if data.len() >= 32 {
u16::from_be_bytes([data[30], data[31]])
} else {
0
};
let bit_depth = if data.len() >= 34 {
u16::from_be_bytes([data[32], data[33]])
} else {
0
};
let bayer_pattern_id = if data.len() >= 35 {
data[34]
} else {
0
};
let bayer_pattern = BayerPattern::from_u8(bayer_pattern_id);
let mut offset = 36;
let mut camera_metadata = CameraMetadata::default();
let mut frame_offsets = Vec::new();
let mut audio_offset: Option<u64> = None;
let mut audio_length: Option<u64> = None;
let mut sensor_width: u16 = 0;
let mut sensor_height: u16 = 0;
let mut active_offset_x: u16 = 0;
let mut active_offset_y: u16 = 0;
let mut active_width: u16 = 0;
let mut active_height: u16 = 0;
let mut _color_matrix: Option<[f64; 9]> = None;
let mut _calibration_illuminant: Option<String> = None;
if offset < data.len() {
let block_length = read_u32_be(&data, offset) as usize;
offset += 4;
let block_end = offset + block_length;
while offset < block_end && offset < data.len() {
let tag = data[offset];
offset += 1;
match tag {
0x01 => {
if let Ok(s) = parse_string(&data, &mut offset) {
camera_metadata.sensor_make = Some(s);
}
}
0x02 => {
if let Ok(s) = parse_string(&data, &mut offset) {
camera_metadata.sensor_model = Some(s);
}
}
0x03 => {
if let Ok(s) = parse_string(&data, &mut offset) {
camera_metadata.camera_model = Some(s);
}
}
0x04 => {
if let Ok(s) = parse_string(&data, &mut offset) {
camera_metadata.lens_model = Some(s);
}
}
0x05 => {
if let Ok(v) = parse_f64(&data, &mut offset) {
camera_metadata.focal_length = Some(v);
}
}
0x06 => {
if let Ok(v) = parse_f64(&data, &mut offset) {
camera_metadata.aperture = Some(v);
}
}
0x07 => {
if let Ok(v) = parse_u32_be(&data, &mut offset) {
camera_metadata.iso = Some(v);
}
}
0x08 => {
if let Ok(v) = parse_f64(&data, &mut offset) {
camera_metadata.exposure_time = Some(v);
}
}
0x09 => {
if let Ok(v) = parse_f64(&data, &mut offset) {
camera_metadata.white_balance = Some(v);
}
}
0x0A => {
if let Ok(s) = parse_string(&data, &mut offset) {
camera_metadata.capture_date = Some(s);
}
}
0x0B => {
let matrix = parse_f64_array(&data, &mut offset, 9);
if matrix.len() == 9 {
let arr: [f64; 9] = matrix.try_into().ok().unwrap_or([0.0; 9]);
_color_matrix = Some(arr);
}
}
0x0C => {
if let Ok(s) = parse_string(&data, &mut offset) {
_calibration_illuminant = Some(s);
}
}
0x10 => {
let count = parse_u32_be(&data, &mut offset);
if let Ok(n) = count {
let mut offsets = Vec::with_capacity(n as usize);
for _ in 0..n {
if offset + 8 <= data.len() {
let val = u64::from_be_bytes([
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
]);
offsets.push(val);
offset += 8;
} else {
break;
}
}
frame_offsets = offsets;
}
}
0x11 => {
if has_audio && offset + 8 <= data.len() {
audio_offset = Some(u64::from_be_bytes([
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
]));
offset += 8;
}
}
0x12 => {
if has_audio && offset + 8 <= data.len() {
audio_length = Some(u64::from_be_bytes([
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
]));
offset += 8;
}
}
0x13 => {
if offset + 2 <= data.len() {
sensor_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
}
}
0x14 => {
if offset + 2 <= data.len() {
sensor_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
}
}
0x15 => {
if offset + 2 <= data.len() {
active_offset_x = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
}
}
0x16 => {
if offset + 2 <= data.len() {
active_offset_y = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
}
}
0x17 => {
if offset + 2 <= data.len() {
active_width = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
}
}
0x18 => {
if offset + 2 <= data.len() {
active_height = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
}
}
_ => {
offset += 1;
}
}
}
}
Ok(McrawFileInfo {
path: path.to_string_lossy().into_owned(),
size: data.len() as u64,
format_version,
frame_count,
width,
height,
fps,
has_audio,
audio_sample_rate,
audio_channels,
bit_depth,
bayer_pattern,
camera_metadata,
frame_offsets,
audio_offset,
audio_length,
sensor_width,
sensor_height,
active_offset_x,
active_offset_y,
active_width,
active_height,
white_level: 16383.0,
black_level: 0.0,
black_level_per_channel: [0.0; 4],
black_level_count: 0,
lens_shading_map: None,
dynamic_black_level: None,
dynamic_white_level: None,
first_timestamp: None,
})
}
fn read_u32_be(data: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
])
}
fn parse_u32_be(data: &[u8], offset: &mut usize) -> Result<u32> {
if *offset + 4 > data.len() {
return Err(anyhow::anyhow!("Unexpected end of data"));
}
let val = u32::from_be_bytes([
data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
]);
*offset += 4;
Ok(val)
}
fn parse_f64(data: &[u8], offset: &mut usize) -> Result<f64> {
if *offset + 8 > data.len() {
return Err(anyhow::anyhow!("Unexpected end of data"));
}
let val = f64::from_be_bytes([
data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
data[*offset + 4], data[*offset + 5], data[*offset + 6], data[*offset + 7],
]);
*offset += 8;
Ok(val)
}
fn parse_f64_array(data: &[u8], offset: &mut usize, len: usize) -> Vec<f64> {
let mut result = Vec::with_capacity(len);
for _ in 0..len {
if let Ok(v) = parse_f64(data, offset) {
result.push(v);
} else {
break;
}
}
result
}
fn parse_string(data: &[u8], offset: &mut usize) -> Result<String> {
if *offset + 4 > data.len() {
return Err(anyhow::anyhow!("Unexpected end of data"));
}
let str_len = u32::from_be_bytes([
data[*offset], data[*offset + 1], data[*offset + 2], data[*offset + 3],
]) as usize;
*offset += 4;
if *offset + str_len > data.len() {
return Err(anyhow::anyhow!("String extends beyond data"));
}
let s = std::str::from_utf8(&data[*offset..*offset + str_len])
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 string: {}", e))?;
*offset += str_len;
Ok(s.to_string())
}
pub fn detect_bit_depth_from_white_level(white_level: f64) -> u16 {
if white_level <= 1024.0 {
10
} else if white_level <= 4096.0 {
12
} else if white_level <= 16384.0 {
14
} else if white_level <= 65536.0 {
16
} else {
12
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bayer_pattern_from_u8() {
assert_eq!(BayerPattern::from_u8(0), BayerPattern::RGGB);
assert_eq!(BayerPattern::from_u8(1), BayerPattern::GRBG);
assert_eq!(BayerPattern::from_u8(2), BayerPattern::GBRG);
assert_eq!(BayerPattern::from_u8(3), BayerPattern::BGGR);
assert_eq!(BayerPattern::from_u8(4), BayerPattern::QuadBayerRGGB);
assert_eq!(BayerPattern::from_u8(5), BayerPattern::QuadBayerGRBG);
assert_eq!(BayerPattern::from_u8(6), BayerPattern::QuadBayerGBRG);
assert_eq!(BayerPattern::from_u8(7), BayerPattern::QuadBayerBGGR);
assert_eq!(BayerPattern::from_u8(99), BayerPattern::RGGB);
}
#[test]
fn test_bayer_pattern_to_u8() {
assert_eq!(BayerPattern::RGGB.to_u8(), 0);
assert_eq!(BayerPattern::GRBG.to_u8(), 1);
assert_eq!(BayerPattern::GBRG.to_u8(), 2);
assert_eq!(BayerPattern::BGGR.to_u8(), 3);
assert_eq!(BayerPattern::QuadBayerRGGB.to_u8(), 4);
assert_eq!(BayerPattern::QuadBayerGRBG.to_u8(), 5);
assert_eq!(BayerPattern::QuadBayerGBRG.to_u8(), 6);
assert_eq!(BayerPattern::QuadBayerBGGR.to_u8(), 7);
}
#[test]
fn test_detect_bit_depth_from_white_level() {
assert_eq!(detect_bit_depth_from_white_level(1023.0), 10);
assert_eq!(detect_bit_depth_from_white_level(1024.0), 10);
assert_eq!(detect_bit_depth_from_white_level(1025.0), 12);
assert_eq!(detect_bit_depth_from_white_level(4095.0), 12);
assert_eq!(detect_bit_depth_from_white_level(4096.0), 12);
assert_eq!(detect_bit_depth_from_white_level(4097.0), 14);
assert_eq!(detect_bit_depth_from_white_level(16383.0), 14);
assert_eq!(detect_bit_depth_from_white_level(16384.0), 14);
assert_eq!(detect_bit_depth_from_white_level(16385.0), 16);
assert_eq!(detect_bit_depth_from_white_level(65535.0), 16);
assert_eq!(detect_bit_depth_from_white_level(65536.0), 16);
assert_eq!(detect_bit_depth_from_white_level(65537.0), 12);
assert_eq!(detect_bit_depth_from_white_level(0.0), 10);
}
#[test]
fn test_parse_header_minimal() {
let mut data = vec![0u8; 36];
data[0..5].copy_from_slice(b"MCRAW");
data[5..9].copy_from_slice(&2u32.to_be_bytes());
data[9..13].copy_from_slice(&10u32.to_be_bytes());
data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
data[25] = 0;
let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
assert_eq!(info.format_version, 2);
assert_eq!(info.frame_count, 10);
assert_eq!(info.width, 1920);
assert_eq!(info.height, 1080);
assert!((info.fps - 30.0).abs() < 0.001);
assert!(!info.has_audio);
}
#[test]
fn test_duration_seconds() {
let mut data = vec![0u8; 36];
data[0..5].copy_from_slice(b"MCRAW");
data[9..13].copy_from_slice(&600u32.to_be_bytes());
data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
assert!((info.duration_seconds() - 20.0).abs() < 0.001);
}
fn make_test_info(w: u16, h: u16) -> McrawFileInfo {
McrawFileInfo {
path: String::new(),
size: 0,
format_version: 2,
frame_count: 0,
width: w,
height: h,
fps: 30.0,
has_audio: false,
audio_sample_rate: 0,
audio_channels: 0,
bit_depth: 0,
bayer_pattern: BayerPattern::RGGB,
camera_metadata: CameraMetadata::default(),
frame_offsets: Vec::new(),
audio_offset: None,
audio_length: None,
sensor_width: 0,
sensor_height: 0,
active_offset_x: 0,
active_offset_y: 0,
active_width: 0,
active_height: 0,
white_level: 16383.0,
black_level: 0.0,
black_level_per_channel: [0.0; 4],
black_level_count: 0,
lens_shading_map: None,
dynamic_black_level: None,
dynamic_white_level: None,
first_timestamp: None,
}
}
#[test]
fn test_resolution_label() {
assert_eq!(make_test_info(1920, 1080).resolution_label(), "1080p");
assert_eq!(make_test_info(2560, 1440).resolution_label(), "1440p");
assert_eq!(make_test_info(3840, 2160).resolution_label(), "4K");
assert_eq!(make_test_info(4096, 2160).resolution_label(), "4K DCI");
assert_eq!(make_test_info(1280, 720).resolution_label(), "Custom");
}
#[test]
fn test_parse_header_with_string_metadata() {
let mut data = vec![0u8; 64];
data[0..5].copy_from_slice(b"MCRAW");
data[5] = 2;
data[9..13].copy_from_slice(&1u32.to_be_bytes());
data[13..15].copy_from_slice(&(1920u16).to_be_bytes());
data[15..17].copy_from_slice(&(1080u16).to_be_bytes());
data[17..25].copy_from_slice(&(30.0f64).to_be_bytes());
data[25] = 0;
let camera_model = "TestCamera";
let block_offset = 36;
let str_len = camera_model.len() as u32;
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());
data[block_offset + 4] = 0x03;
data[block_offset + 5..block_offset + 9].copy_from_slice(&str_len.to_be_bytes());
data[block_offset + 9..block_offset + 9 + camera_model.len()]
.copy_from_slice(camera_model.as_bytes());
let info = parse_header(&data, std::path::Path::new("test.mcraw")).unwrap();
assert_eq!(info.camera_metadata.camera_model, Some("TestCamera".to_string()));
}
#[test]
fn test_parse_header_invalid_magic() {
let data = vec![b'X'; 36];
let result = parse_header(&data, std::path::Path::new("test.mcraw"));
assert!(result.is_err());
}
#[test]
fn test_parse_header_too_small() {
let data = vec![0u8; 10];
let result = parse_header(&data, std::path::Path::new("test.mcraw"));
assert!(result.is_err());
}
}