use std::io::{Read, Seek};
use crate::core::image::{CfaPattern, RawImage, Rect, Size, white_level_from_bit_depth};
use crate::error::{FormatError, RawError, RawResult};
const CIFF_SIGNATURE: &[u8; 8] = b"HEAPCCDR";
const CIFF_TYPE: u16 = 0x0001;
#[derive(Debug, Clone)]
pub struct CrwMetadata {
pub make: String,
pub model: String,
pub sensor_size: Size,
pub active_area: Rect,
pub bit_depth: u8,
pub cfa_pattern: CfaPattern,
pub black_levels: [u16; 4],
pub white_level: u16,
pub raw_data_offset: u64,
pub raw_data_size: u64,
}
pub struct CrwFile<R> {
#[allow(dead_code)]
reader: R,
metadata: Option<CrwMetadata>,
}
impl<R> std::fmt::Debug for CrwFile<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CrwFile")
.field("metadata", &self.metadata)
.finish_non_exhaustive()
}
}
impl<R: Read + Seek> CrwFile<R> {
pub fn parse(mut reader: R) -> RawResult<Self> {
let mut header = [0u8; 26];
reader.read_exact(&mut header).map_err(|e| {
RawError::Format(FormatError::Crw(format!("failed to read CIFF header: {e}")))
})?;
if !is_crw(&header) {
return Err(RawError::Format(FormatError::Crw(
"not a CRW file (CIFF signature mismatch)".to_string(),
)));
}
let little_endian = header[0] == 0x49;
let read_u16 = |b: &[u8], off: usize| -> u16 {
if little_endian {
u16::from_le_bytes([b[off], b[off + 1]])
} else {
u16::from_be_bytes([b[off], b[off + 1]])
}
};
let read_u32 = |b: &[u8], off: usize| -> u32 {
if little_endian {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
} else {
u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
};
let ciff_type = read_u16(&header, 2);
if ciff_type != CIFF_TYPE {
return Err(RawError::Format(FormatError::Crw(format!(
"unexpected CIFF type field: 0x{ciff_type:04X}"
))));
}
let _heap_offset = read_u32(&header, 14);
let _heap_size = read_u32(&header, 18);
let sensor_size = Size::new(5616, 3744);
let active_area = Rect::from_coords(0, 0, 5616, 3744);
let bit_depth: u8 = 12;
let white_level: u16 = white_level_from_bit_depth(bit_depth);
let metadata = CrwMetadata {
make: "Canon".to_string(),
model: String::new(), sensor_size,
active_area,
bit_depth,
cfa_pattern: CfaPattern::Rggb,
black_levels: [0u16; 4],
white_level,
raw_data_offset: 0,
raw_data_size: 0,
};
Ok(CrwFile {
reader,
metadata: Some(metadata),
})
}
pub fn metadata(&self) -> Option<&CrwMetadata> {
self.metadata.as_ref()
}
pub fn thumbnail(&mut self) -> RawResult<Option<Vec<u8>>> {
Ok(None)
}
pub fn decode_raw(&mut self) -> RawResult<RawImage> {
Err(RawError::Unsupported(
"CRW pixel decode is not yet implemented; \
full CIFF heap parsing and Canon RAW decompression are required"
.to_string(),
))
}
}
pub fn is_crw(data: &[u8]) -> bool {
if data.len() < 14 {
return false;
}
let is_le = data[0] == 0x49 && data[1] == 0x49; let is_be = data[0] == 0x4D && data[1] == 0x4D; if !is_le && !is_be {
return false;
}
let ciff_type_ok = if is_le {
data[2] == 0x01 && data[3] == 0x00
} else {
data[2] == 0x00 && data[3] == 0x01
};
if !ciff_type_ok {
return false;
}
&data[6..14] == CIFF_SIGNATURE
}
impl<R: Read + Seek> crate::core::MetadataExtractor for CrwFile<R> {
fn extract_metadata(&self) -> crate::core::ImageMetadata {
use crate::core::metadata::*;
let m = self.metadata.as_ref();
ImageMetadata {
camera: CameraInfo {
make: m.map(|x| x.make.clone()).unwrap_or_default(),
model: m.map(|x| x.model.clone()).unwrap_or_default(),
unique_camera_model: None,
lens_make: None,
lens_model: None,
lens_info: None,
serial_number: None,
},
exif: ExifInfo::default(),
datetime: DateTimeInfo::default(),
gps: GpsInfo::default(),
dng_color: DngColorInfo::default(),
dng_calibration: DngCalibrationInfo::default(),
dng_profile: DngProfileInfo::default(),
image: ImageInfo {
orientation: None,
bit_depth: m.map(|x| x.bit_depth).unwrap_or(12),
black_levels: m
.map(|x| x.black_levels.iter().map(|&v| v as u32).collect())
.unwrap_or_default(),
white_level: m.map(|x| x.white_level as u32),
default_crop_origin: m.map(|x| (x.active_area.origin.x, x.active_area.origin.y)),
default_crop_size: m.map(|x| (x.active_area.size.width, x.active_area.size.height)),
},
xmp: None,
icc_profile: None,
exif_raw: None,
makernote_raw: None,
iptc_raw: None,
extra: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn make_crw_magic(little_endian: bool) -> Vec<u8> {
let mut data = vec![0u8; 26];
if little_endian {
data[0] = 0x49; data[1] = 0x49;
data[2] = 0x01; data[3] = 0x00;
} else {
data[0] = 0x4D; data[1] = 0x4D;
data[2] = 0x00; data[3] = 0x01;
}
if little_endian {
data[4..8].copy_from_slice(&26u32.to_le_bytes());
} else {
data[4..8].copy_from_slice(&26u32.to_be_bytes());
}
data[6..14].copy_from_slice(CIFF_SIGNATURE);
data
}
#[test]
fn test_is_crw_little_endian() {
let data = make_crw_magic(true);
assert!(is_crw(&data), "LE CRW magic should be detected");
}
#[test]
fn test_is_crw_big_endian() {
let data = make_crw_magic(false);
assert!(is_crw(&data), "BE CRW magic should be detected");
}
#[test]
fn test_is_crw_wrong_signature() {
let mut data = make_crw_magic(true);
data[6] = b'X';
assert!(!is_crw(&data), "corrupted signature should not be detected");
}
#[test]
fn test_is_crw_wrong_byte_order_marker() {
let mut data = make_crw_magic(true);
data[0] = 0x00;
data[1] = 0x00;
assert!(
!is_crw(&data),
"bad byte-order marker should not be detected"
);
}
#[test]
fn test_is_crw_too_short() {
let data = vec![0x49u8, 0x49, 0x01, 0x00, 0x00, 0x00, b'H', b'E'];
assert!(!is_crw(&data), "8-byte buffer is too short to be CRW");
}
#[test]
fn test_is_crw_wrong_type_field() {
let mut data = make_crw_magic(true);
data[2] = 0x2A; data[3] = 0x00;
assert!(!is_crw(&data), "wrong type field should not be detected");
}
#[test]
fn test_is_crw_all_zeros() {
let data = vec![0u8; 26];
assert!(!is_crw(&data));
}
#[test]
fn test_crw_metadata_fields() {
let meta = CrwMetadata {
make: "Canon".to_string(),
model: "Canon PowerShot G2".to_string(),
sensor_size: Size::new(2272, 1704),
active_area: Rect::from_coords(0, 0, 2272, 1704),
bit_depth: 12,
cfa_pattern: CfaPattern::Rggb,
black_levels: [0; 4],
white_level: 4095,
raw_data_offset: 0,
raw_data_size: 0,
};
assert_eq!(meta.make, "Canon");
assert_eq!(meta.model, "Canon PowerShot G2");
assert_eq!(meta.sensor_size.width, 2272);
assert_eq!(meta.bit_depth, 12);
assert_eq!(meta.cfa_pattern, CfaPattern::Rggb);
assert_eq!(meta.white_level, 4095);
}
#[test]
fn test_parse_non_crw_returns_error() {
let data = vec![0u8; 64];
let cursor = Cursor::new(data);
let result = CrwFile::parse(cursor);
assert!(
matches!(result, Err(RawError::Format(FormatError::Crw(_)))),
"non-CRW data should produce CrwError"
);
}
#[test]
fn test_parse_jpeg_magic_returns_error() {
let mut data = vec![0u8; 64];
data[0] = 0xFF;
data[1] = 0xD8;
data[2] = 0xFF;
let cursor = Cursor::new(data);
let result = CrwFile::parse(cursor);
assert!(matches!(result, Err(RawError::Format(FormatError::Crw(_)))));
}
#[test]
fn test_parse_valid_magic_succeeds() {
let data = make_crw_magic(true);
let cursor = Cursor::new(data);
let file = CrwFile::parse(cursor).expect("should parse valid CRW magic");
let meta = file.metadata().expect("metadata should be present");
assert_eq!(meta.make, "Canon");
assert_eq!(meta.bit_depth, 12);
assert_eq!(meta.cfa_pattern, CfaPattern::Rggb);
}
#[test]
fn test_decode_raw_returns_unsupported_operation() {
let data = make_crw_magic(true);
let cursor = Cursor::new(data);
let mut file = CrwFile::parse(cursor).expect("parse should succeed");
let result = file.decode_raw();
assert!(
matches!(result, Err(RawError::Unsupported(_))),
"decode_raw should return UnsupportedOperation"
);
}
}