use std::io::{Read, Seek, SeekFrom};
use crate::core::image::{CfaPattern, RawImage, Rect, Size, XTransPattern};
use crate::error::{FormatError, RawError, RawResult};
use tracing::instrument;
const RAF_MAGIC: &[u8; 16] = b"FUJIFILMCCD-RAW ";
pub const RAF_HEADER_SIZE: usize = 160;
const JPEG_OFFSET_FIELD: usize = 84;
const JPEG_SIZE_FIELD: usize = 88;
const RAW_DATA_OFFSET_FIELD: usize = 92;
#[allow(dead_code)]
const RAW_HEADER_SIZE_FIELD: usize = 96;
const RAW_DATA_SIZE_FIELD: usize = 100;
const MODEL_OFFSET: usize = 28;
const MODEL_LEN: usize = 32;
const DEFAULT_WIDTH: u32 = 6240;
const DEFAULT_HEIGHT: u32 = 4168;
#[derive(Debug, Clone)]
pub struct RafMetadata {
pub make: String,
pub model: String,
pub sensor_size: Size,
pub active_area: Rect,
pub bit_depth: u8,
pub cfa_pattern: CfaPattern,
pub xtrans_pattern: Option<XTransPattern>,
pub black_levels: [u16; 4],
pub white_level: u16,
pub jpeg_offset: u64,
pub jpeg_size: u64,
pub raw_data_offset: u64,
pub raw_data_size: u64,
}
pub struct RafFile<R> {
reader: R,
metadata: Option<RafMetadata>,
}
impl<R> std::fmt::Debug for RafFile<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RafFile")
.field("metadata", &self.metadata)
.finish_non_exhaustive()
}
}
impl<R: Read + Seek> RafFile<R> {
#[instrument(skip(reader))]
pub fn parse(mut reader: R) -> RawResult<Self> {
let mut header = [0u8; RAF_HEADER_SIZE];
reader.read_exact(&mut header).map_err(|e| {
RawError::Format(FormatError::Raf(format!("Failed to read RAF header: {e}")))
})?;
if &header[..16] != RAF_MAGIC {
return Err(RawError::Format(FormatError::Raf(
"Invalid RAF magic: not a Fujifilm RAF file".to_string(),
)));
}
let model_raw = &header[MODEL_OFFSET..MODEL_OFFSET + MODEL_LEN];
let model = extract_cstring(model_raw);
let jpeg_offset = read_be_u32(&header, JPEG_OFFSET_FIELD) as u64;
let jpeg_size = read_be_u32(&header, JPEG_SIZE_FIELD) as u64;
let raw_data_offset = read_be_u32(&header, RAW_DATA_OFFSET_FIELD) as u64;
let raw_data_size = read_be_u32(&header, RAW_DATA_SIZE_FIELD) as u64;
if raw_data_offset == 0 {
return Err(RawError::Format(FormatError::Raf(
"RAF header has zero raw data offset".to_string(),
)));
}
let xtrans = is_xtrans_model(&model);
let (width, height) = if jpeg_size > 0 {
extract_dimensions_from_jpeg(&mut reader, jpeg_offset, jpeg_size)
.unwrap_or((DEFAULT_WIDTH, DEFAULT_HEIGHT))
} else {
(DEFAULT_WIDTH, DEFAULT_HEIGHT)
};
let sensor_size = Size::new(width, height);
let active_area = Rect::from_coords(0, 0, width, height);
let bit_depth: u8 = 14;
let white_level: u16 = 16383; let black_levels: [u16; 4] = [512; 4];
let xtrans_pattern = if xtrans {
Some(XTransPattern::standard())
} else {
None
};
let metadata = RafMetadata {
make: "FUJIFILM".to_string(),
model,
sensor_size,
active_area,
bit_depth,
cfa_pattern: CfaPattern::Rggb,
xtrans_pattern,
black_levels,
white_level,
jpeg_offset,
jpeg_size,
raw_data_offset,
raw_data_size,
};
Ok(RafFile {
reader,
metadata: Some(metadata),
})
}
pub fn metadata(&self) -> Option<&RafMetadata> {
self.metadata.as_ref()
}
pub fn thumbnail(&mut self) -> RawResult<Option<Vec<u8>>> {
let metadata = match self.metadata.as_ref() {
Some(m) => m,
None => return Ok(None),
};
let offset = metadata.jpeg_offset;
let size = metadata.jpeg_size as usize;
if size == 0 {
return Ok(None);
}
self.reader.seek(std::io::SeekFrom::Start(offset))?;
let mut data = vec![0u8; size];
self.reader.read_exact(&mut data)?;
Ok(Some(data))
}
#[instrument(skip(self))]
pub fn decode_raw(&mut self) -> RawResult<RawImage> {
let metadata = self.metadata.as_ref().cloned().ok_or_else(|| {
RawError::Format(FormatError::Raf("Metadata not extracted".to_string()))
})?;
let raw_data_size = metadata.raw_data_size as usize;
if raw_data_size < 32 {
return Err(RawError::Format(FormatError::Raf(format!(
"RAF raw data size too small: {raw_data_size} bytes"
))));
}
self.reader
.seek(SeekFrom::Start(metadata.raw_data_offset))
.map_err(|e| {
RawError::Format(FormatError::Raf(format!("Failed to seek to raw data: {e}")))
})?;
let mut raw_bytes = vec![0u8; raw_data_size];
self.reader.read_exact(&mut raw_bytes).map_err(|e| {
RawError::Format(FormatError::Raf(format!("Failed to read raw data: {e}")))
})?;
const RAW_SUB_HEADER: usize = 32;
let pixel_bytes = if raw_bytes.len() > RAW_SUB_HEADER {
&raw_bytes[RAW_SUB_HEADER..]
} else {
&raw_bytes[..]
};
let pixels = unpack_raw_16bit(pixel_bytes);
let expected = metadata.sensor_size.pixel_count() as usize;
if pixels.len() != expected {
return Err(RawError::Format(FormatError::Raf(format!(
"Pixel count mismatch: got {} pixels, expected {} ({}×{})",
pixels.len(),
expected,
metadata.sensor_size.width,
metadata.sensor_size.height,
))));
}
{
let mut builder = RawImage::builder(
metadata.sensor_size,
metadata.active_area,
metadata.bit_depth,
metadata.cfa_pattern,
)
.black_levels(metadata.black_levels)
.white_level(metadata.white_level)
.data(pixels);
if let Some(xtrans) = metadata.xtrans_pattern {
builder = builder.xtrans_pattern(xtrans);
}
Ok(builder.build())
}
}
}
impl<R: Read + Seek> crate::core::MetadataExtractor for RafFile<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(14),
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: None,
default_crop_size: None,
},
xmp: None,
icc_profile: None,
exif_raw: None,
makernote_raw: None,
iptc_raw: None,
extra: Vec::new(),
}
}
}
pub fn is_raf(data: &[u8]) -> bool {
data.len() >= 16 && &data[..16] == RAF_MAGIC
}
pub fn is_xtrans_model(model: &str) -> bool {
let model_upper = model.to_uppercase();
model_upper.contains("X-T")
|| model_upper.contains("X-PRO")
|| model_upper.contains("X-E")
|| model_upper.contains("X-H")
|| model_upper.contains("X-S")
|| model_upper.contains("X100")
|| model_upper.contains("X-A") }
pub fn unpack_raw_16bit(raw_bytes: &[u8]) -> Vec<u16> {
raw_bytes
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect()
}
fn read_be_u32(data: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn extract_cstring(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
fn extract_dimensions_from_jpeg<R: Read + Seek>(
reader: &mut R,
jpeg_offset: u64,
jpeg_size: u64,
) -> Option<(u32, u32)> {
let read_size = jpeg_size.min(65536) as usize;
reader.seek(SeekFrom::Start(jpeg_offset)).ok()?;
let mut jpeg_bytes = vec![0u8; read_size];
reader.read_exact(&mut jpeg_bytes).ok()?;
if jpeg_bytes.len() < 4 {
return None;
}
if jpeg_bytes[0] != 0xFF || jpeg_bytes[1] != 0xD8 {
return None;
}
let mut pos = 2usize;
while pos + 3 < jpeg_bytes.len() {
if jpeg_bytes[pos] != 0xFF {
return None;
}
let marker = jpeg_bytes[pos + 1];
let seg_len = u16::from_be_bytes([jpeg_bytes[pos + 2], jpeg_bytes[pos + 3]]) as usize;
if marker == 0xE1 {
let app1_data = &jpeg_bytes[pos + 2..];
if app1_data.len() < 8 {
return None;
}
if &app1_data[2..8] != b"Exif\0\0" {
return None;
}
let tiff_start = pos + 4; let tiff_offset = tiff_start + 6; if tiff_offset >= jpeg_bytes.len() {
return None;
}
let tiff_data = &jpeg_bytes[tiff_offset..];
return parse_tiff_dimensions(tiff_data);
}
pos += 2 + seg_len;
}
None
}
fn parse_tiff_dimensions(data: &[u8]) -> Option<(u32, u32)> {
if data.len() < 8 {
return None;
}
let little_endian = match &data[0..2] {
b"II" => true,
b"MM" => false,
_ => return None,
};
let read_u16 = |offset: usize| -> Option<u16> {
if offset + 2 > data.len() {
return None;
}
let bytes = [data[offset], data[offset + 1]];
if little_endian {
Some(u16::from_le_bytes(bytes))
} else {
Some(u16::from_be_bytes(bytes))
}
};
let read_u32 = |offset: usize| -> Option<u32> {
if offset + 4 > data.len() {
return None;
}
let bytes = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
if little_endian {
Some(u32::from_le_bytes(bytes))
} else {
Some(u32::from_be_bytes(bytes))
}
};
let magic = read_u16(2)?;
if magic != 42 {
return None;
}
let ifd_offset = read_u32(4)? as usize;
if ifd_offset + 2 > data.len() {
return None;
}
let entry_count = read_u16(ifd_offset)? as usize;
let mut width: Option<u32> = None;
let mut height: Option<u32> = None;
for i in 0..entry_count {
let entry_offset = ifd_offset + 2 + i * 12;
if entry_offset + 12 > data.len() {
break;
}
let tag = read_u16(entry_offset)?;
let value_raw = read_u32(entry_offset + 8)?;
match tag {
256 => width = Some(value_raw), 257 => height = Some(value_raw), _ => {}
}
if width.is_some() && height.is_some() {
break;
}
}
match (width, height) {
(Some(w), Some(h)) if w > 0 && h > 0 => Some((w, h)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
#[test]
fn test_is_raf_correct_magic() {
let mut data = vec![0u8; 32];
data[..16].copy_from_slice(RAF_MAGIC);
assert!(is_raf(&data), "Correct RAF magic should be detected");
}
#[test]
fn test_is_raf_wrong_magic() {
let data = vec![0u8; 32];
assert!(!is_raf(&data), "All-zero bytes should not match RAF magic");
}
#[test]
fn test_is_raf_partial_magic() {
let data = b"FUJIFILM".to_vec();
assert!(
!is_raf(&data),
"Partial magic (8 bytes) should not be detected"
);
}
#[test]
fn test_is_raf_almost_correct() {
let mut data = vec![0u8; 32];
data[..16].copy_from_slice(b"FUJIFILMCCD-RAW!"); assert!(!is_raf(&data), "Near-match magic should not be detected");
}
#[test]
fn test_xtrans_detection_xt5() {
assert!(is_xtrans_model("X-T5"), "X-T5 should be X-Trans");
}
#[test]
fn test_xtrans_detection_xpro3() {
assert!(is_xtrans_model("X-Pro3"), "X-Pro3 should be X-Trans");
}
#[test]
fn test_xtrans_detection_x100v() {
assert!(is_xtrans_model("X100V"), "X100V should be X-Trans");
}
#[test]
fn test_xtrans_detection_xh2() {
assert!(is_xtrans_model("X-H2"), "X-H2 should be X-Trans");
}
#[test]
fn test_xtrans_detection_xe4() {
assert!(is_xtrans_model("X-E4"), "X-E4 should be X-Trans");
}
#[test]
fn test_bayer_detection_s_series() {
assert!(
!is_xtrans_model("S5 Pro"),
"S5 Pro should not be detected as X-Trans"
);
}
#[test]
fn test_bayer_detection_empty_model() {
assert!(!is_xtrans_model(""), "Empty model should default to Bayer");
}
#[test]
fn test_raf_metadata_fields() {
let meta = RafMetadata {
make: "FUJIFILM".to_string(),
model: "X-T5".to_string(),
sensor_size: Size::new(6240, 4168),
active_area: Rect::from_coords(0, 0, 6240, 4168),
bit_depth: 14,
cfa_pattern: CfaPattern::Rggb,
xtrans_pattern: Some(XTransPattern::standard()),
black_levels: [512; 4],
white_level: 16383,
jpeg_offset: 160,
jpeg_size: 1024,
raw_data_offset: 2048,
raw_data_size: 52_000_000,
};
assert_eq!(meta.make, "FUJIFILM");
assert_eq!(meta.model, "X-T5");
assert_eq!(meta.sensor_size.width, 6240);
assert_eq!(meta.sensor_size.height, 4168);
assert_eq!(meta.bit_depth, 14);
assert_eq!(meta.cfa_pattern, CfaPattern::Rggb);
assert!(meta.xtrans_pattern.is_some());
assert_eq!(meta.black_levels, [512; 4]);
assert_eq!(meta.white_level, 16383);
assert_eq!(meta.jpeg_offset, 160);
assert_eq!(meta.raw_data_offset, 2048);
assert_eq!(meta.raw_data_size, 52_000_000);
}
#[test]
fn test_header_size_constant() {
assert_eq!(RAF_HEADER_SIZE, 160, "RAF header must be exactly 160 bytes");
}
#[test]
fn test_unpack_raw_16bit_basic() {
let bytes = [0x12u8, 0x34, 0xAB, 0xCD];
let pixels = unpack_raw_16bit(&bytes);
assert_eq!(pixels.len(), 2);
assert_eq!(pixels[0], 0x1234);
assert_eq!(pixels[1], 0xABCD);
}
#[test]
fn test_unpack_raw_16bit_zeros() {
let bytes = [0u8; 8];
let pixels = unpack_raw_16bit(&bytes);
assert_eq!(pixels, vec![0, 0, 0, 0]);
}
#[test]
fn test_unpack_raw_16bit_max() {
let bytes = [0xFF, 0xFF, 0xFF, 0xFF];
let pixels = unpack_raw_16bit(&bytes);
assert_eq!(pixels, vec![65535, 65535]);
}
#[test]
fn test_unpack_raw_16bit_odd_bytes_ignored() {
let bytes = [0x00, 0x01, 0x00, 0x02, 0xFF];
let pixels = unpack_raw_16bit(&bytes);
assert_eq!(pixels.len(), 2);
assert_eq!(pixels[0], 1);
assert_eq!(pixels[1], 2);
}
#[test]
fn test_parse_rejects_non_raf() {
let data = vec![0u8; 256]; let cursor = Cursor::new(data);
let result = RafFile::parse(cursor);
assert!(
matches!(result, Err(RawError::Format(FormatError::Raf(_)))),
"Non-RAF data should produce RafError"
);
}
#[test]
fn test_parse_rejects_truncated_header() {
let data = b"FUJIFILM".to_vec();
let cursor = Cursor::new(data);
let result = RafFile::parse(cursor);
assert!(
matches!(result, Err(RawError::Format(FormatError::Raf(_)))),
"Truncated header should produce RafError"
);
}
fn make_minimal_raf_header(model: &str, raw_data_offset: u32, raw_data_size: u32) -> Vec<u8> {
let mut header = vec![0u8; RAF_HEADER_SIZE];
header[..16].copy_from_slice(RAF_MAGIC);
header[16..20].copy_from_slice(b"0200");
let model_bytes = model.as_bytes();
let copy_len = model_bytes.len().min(MODEL_LEN);
header[MODEL_OFFSET..MODEL_OFFSET + copy_len].copy_from_slice(&model_bytes[..copy_len]);
header[RAW_DATA_OFFSET_FIELD..RAW_DATA_OFFSET_FIELD + 4]
.copy_from_slice(&raw_data_offset.to_be_bytes());
header[RAW_DATA_SIZE_FIELD..RAW_DATA_SIZE_FIELD + 4]
.copy_from_slice(&raw_data_size.to_be_bytes());
header
}
#[test]
fn test_parse_xtrans_model_sets_xtrans_pattern() {
let raw_offset = RAF_HEADER_SIZE as u32;
let pixel_count = DEFAULT_WIDTH * DEFAULT_HEIGHT;
let raw_size = 32 + pixel_count * 2;
let mut data = make_minimal_raf_header("X-T5", raw_offset, raw_size);
data.resize(data.len() + raw_size as usize, 0);
let cursor = Cursor::new(data);
let raf = RafFile::parse(cursor).expect("Should parse minimal RAF header");
let meta = raf.metadata().expect("Metadata should be present");
assert_eq!(meta.make, "FUJIFILM");
assert_eq!(meta.model, "X-T5");
assert!(
meta.xtrans_pattern.is_some(),
"X-T5 should have an X-Trans pattern"
);
assert_eq!(meta.bit_depth, 14);
assert_eq!(meta.white_level, 16383);
assert_eq!(meta.black_levels, [512; 4]);
}
#[test]
fn test_parse_bayer_model_no_xtrans_pattern() {
let raw_offset = RAF_HEADER_SIZE as u32;
let pixel_count = DEFAULT_WIDTH * DEFAULT_HEIGHT;
let raw_size = 32 + pixel_count * 2;
let mut data = make_minimal_raf_header("S5 Pro", raw_offset, raw_size);
data.resize(data.len() + raw_size as usize, 0);
let cursor = Cursor::new(data);
let raf = RafFile::parse(cursor).expect("Should parse minimal RAF header");
let meta = raf.metadata().expect("Metadata should be present");
assert!(
meta.xtrans_pattern.is_none(),
"S5 Pro should use Bayer (no X-Trans pattern)"
);
}
fn make_tiff_ifd(little_endian: bool, width: u32, height: u32) -> Vec<u8> {
let mut data = Vec::new();
let write_u16 =
|v: u16, le: bool| -> [u8; 2] { if le { v.to_le_bytes() } else { v.to_be_bytes() } };
let write_u32 =
|v: u32, le: bool| -> [u8; 4] { if le { v.to_le_bytes() } else { v.to_be_bytes() } };
if little_endian {
data.extend_from_slice(b"II");
} else {
data.extend_from_slice(b"MM");
}
data.extend_from_slice(&write_u16(42, little_endian));
data.extend_from_slice(&write_u32(8, little_endian));
data.extend_from_slice(&write_u16(2, little_endian));
data.extend_from_slice(&write_u16(256, little_endian));
data.extend_from_slice(&write_u16(3, little_endian)); data.extend_from_slice(&write_u32(1, little_endian));
data.extend_from_slice(&write_u32(width, little_endian));
data.extend_from_slice(&write_u16(257, little_endian));
data.extend_from_slice(&write_u16(3, little_endian)); data.extend_from_slice(&write_u32(1, little_endian));
data.extend_from_slice(&write_u32(height, little_endian));
data.extend_from_slice(&write_u32(0, little_endian));
data
}
#[test]
fn test_parse_tiff_dimensions_le() {
let tiff = make_tiff_ifd(true, 6240, 4168);
let result = parse_tiff_dimensions(&tiff);
assert_eq!(result, Some((6240, 4168)));
}
#[test]
fn test_parse_tiff_dimensions_be() {
let tiff = make_tiff_ifd(false, 5640, 3760);
let result = parse_tiff_dimensions(&tiff);
assert_eq!(result, Some((5640, 3760)));
}
#[test]
fn test_parse_tiff_dimensions_invalid() {
let data = vec![0u8; 16];
assert_eq!(parse_tiff_dimensions(&data), None);
}
}