use crate::error::{Error, Result};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
pub fn read_x3f(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 40 {
return Err(Error::InvalidData("X3F too short".into()));
}
if &data[0..4] != b"FOVb" {
return Err(Error::InvalidData("not an X3F file".into()));
}
let mut tags = Vec::new();
let ver_raw = u32_le(data, 4);
let ver_major = (ver_raw >> 16) as f64;
let ver_minor = (ver_raw & 0xffff) as f64;
let ver_f = ver_major + ver_minor / 10000.0; let ver_str = format!("{}.{}", ver_raw >> 16, ver_raw & 0xffff);
tags.push(mk_tag_str(
"FileVersion",
"File Version",
ver_str,
"X3F",
"Main",
"Image",
));
if ver_raw >> 16 >= 4 {
parse_header4(data, &mut tags);
} else {
parse_header2(data, ver_f, &mut tags);
}
if data.len() < 4 {
return Ok(tags);
}
let dir_offset = u32_le(data, data.len() - 4) as usize;
if dir_offset + 12 > data.len() {
return Ok(tags);
}
if &data[dir_offset..dir_offset + 4] != b"SECd" {
return Ok(tags);
}
let entries = u32_le(data, dir_offset + 8) as usize;
if entries == 0 || entries > 100 {
return Ok(tags);
}
let dir_data_start = dir_offset + 12;
if dir_data_start + entries * 12 > data.len() {
return Ok(tags);
}
let mut found_jpg_from_raw = false;
for i in 0..entries {
let pos = dir_data_start + i * 12;
let sec_offset = u32_le(data, pos) as usize;
let sec_len = u32_le(data, pos + 4) as usize;
let tag_bytes = &data[pos + 8..pos + 12];
if sec_offset + sec_len > data.len() {
continue;
}
let sec_data = &data[sec_offset..sec_offset + sec_len];
match tag_bytes {
b"PROP" => {
parse_properties(sec_data, &mut tags);
}
b"IMA2" => {
if !found_jpg_from_raw {
if let Some(img_data) = extract_image_data(sec_data) {
if img_data.starts_with(b"\xff\xd8\xff\xe1") {
found_jpg_from_raw = true;
if let Ok(jpeg_tags) = crate::formats::jpeg::read_jpeg(img_data) {
tags.extend(jpeg_tags);
}
tags.push(mk_tag_binary(
"JpgFromRaw",
"Jpg From Raw",
img_data.to_vec(),
"X3F",
"Main",
"Preview",
));
} else {
tags.push(mk_tag_binary(
"PreviewImage",
"Preview Image",
img_data.to_vec(),
"X3F",
"Main",
"Preview",
));
}
}
} else {
if let Some(img_data) = extract_image_data(sec_data) {
tags.push(mk_tag_binary(
"PreviewImage",
"Preview Image",
img_data.to_vec(),
"X3F",
"Main",
"Preview",
));
}
}
}
b"IMAG" => {
if let Some(img_data) = extract_image_data(sec_data) {
tags.push(mk_tag_binary(
"PreviewImage",
"Preview Image",
img_data.to_vec(),
"X3F",
"Main",
"Preview",
));
}
}
_ => {}
}
}
Ok(tags)
}
fn extract_image_data(sec_data: &[u8]) -> Option<&[u8]> {
if sec_data.len() < 28 {
return None;
}
if &sec_data[0..4] != b"SECi" {
return None;
}
let sec_ver = u16_le(sec_data, 6); let img_type = u32_le(sec_data, 8);
let img_fmt = u32_le(sec_data, 12);
if sec_ver == 2 && img_type == 2 && img_fmt == 0x12 {
let payload = &sec_data[28..];
if !payload.is_empty() {
return Some(payload);
}
}
None
}
fn parse_header2(data: &[u8], ver_f: f64, tags: &mut Vec<Tag>) {
let hdr_len = if ver_f >= 2.0003 { 104usize } else { 72usize };
let has_extended = data.len() >= hdr_len + 160;
if data.len() >= 24 {
let uid = hex_bytes(&data[8..24]);
tags.push(mk_tag_str(
"ImageUniqueID",
"Image Unique ID",
uid,
"X3F",
"Header",
"Camera",
));
}
if data.len() >= 28 {
let mark = u32_le(data, 24);
let mark_str = if mark == 0 {
"(none)".to_string()
} else {
mark.to_string()
};
tags.push(mk_tag_str(
"MarkBits",
"Mark Bits",
mark_str,
"X3F",
"Header",
"Image",
));
}
if data.len() >= 32 {
let w = u32_le(data, 28);
tags.push(mk_tag_u32(
"ImageWidth",
"Image Width",
w,
"X3F",
"Header",
"Image",
));
}
if data.len() >= 36 {
let h = u32_le(data, 32);
tags.push(mk_tag_u32(
"ImageHeight",
"Image Height",
h,
"X3F",
"Header",
"Image",
));
}
if data.len() >= 40 {
let r = u32_le(data, 36);
tags.push(mk_tag_u32(
"Rotation", "Rotation", r, "X3F", "Header", "Image",
));
}
if data.len() >= 72 {
let wb = read_cstr(&data[40..72]);
if !wb.is_empty() {
tags.push(mk_tag_str(
"WhiteBalance",
"White Balance",
wb,
"X3F",
"Header",
"Camera",
));
}
}
if hdr_len >= 104 && data.len() >= 104 {
let sct = read_cstr(&data[72..104]);
if !sct.is_empty() {
tags.push(mk_tag_str(
"SceneCaptureType",
"Scene Capture Type",
sct,
"X3F",
"Header",
"Image",
));
}
}
if has_extended {
let ext_start = hdr_len;
let tag_indices = &data[ext_start..ext_start + 32];
for (i, &tidx) in tag_indices.iter().enumerate() {
if tidx == 0 {
continue;
}
let float_offset = ext_start + 32 + i * 4;
if float_offset + 4 > data.len() {
continue;
}
let val = f32_le(data, float_offset);
let val_str = format!("{:.1}", val);
let name = match tidx {
1 => "ExposureAdjust",
2 => "Contrast",
3 => "Shadow",
4 => "Highlight",
5 => "Saturation",
6 => "Sharpness",
7 => "RedAdjust",
8 => "GreenAdjust",
9 => "BlueAdjust",
10 => "X3FillLight",
_ => continue,
};
tags.push(mk_tag_str(
name,
name,
val_str,
"X3F",
"HeaderExt",
"Camera",
));
}
}
}
fn parse_header4(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() >= 44 {
let w = u32_le(data, 40);
tags.push(mk_tag_u32(
"ImageWidth",
"Image Width",
w,
"X3F",
"Header",
"Image",
));
}
if data.len() >= 48 {
let h = u32_le(data, 44);
tags.push(mk_tag_u32(
"ImageHeight",
"Image Height",
h,
"X3F",
"Header",
"Image",
));
}
if data.len() >= 52 {
let r = u32_le(data, 48);
tags.push(mk_tag_u32(
"Rotation", "Rotation", r, "X3F", "Header", "Image",
));
}
}
fn parse_properties(sec_data: &[u8], tags: &mut Vec<Tag>) {
if sec_data.len() < 24 {
return;
}
if &sec_data[0..4] != b"SECp" {
return;
}
let entries = u32_le(sec_data, 8) as usize;
let fmt = u32_le(sec_data, 12);
if fmt != 0 {
return; }
let char_len = u32_le(sec_data, 20) as usize;
let char_start = 24 + 8 * entries;
if char_start + char_len * 2 > sec_data.len() {
return;
}
let chars_bytes = &sec_data[char_start..char_start + char_len * 2];
let mut chars = Vec::with_capacity(char_len);
for i in 0..char_len {
chars.push(u16::from_le_bytes([
chars_bytes[i * 2],
chars_bytes[i * 2 + 1],
]));
}
for i in 0..entries {
let entry_off = 24 + i * 8;
if entry_off + 8 > sec_data.len() {
break;
}
let name_pos = u32_le(sec_data, entry_off) as usize;
let val_pos = u32_le(sec_data, entry_off + 4) as usize;
if name_pos >= chars.len() || val_pos >= chars.len() {
continue;
}
let prop_name = extract_utf16_str(&chars, name_pos);
let prop_val = extract_utf16_str(&chars, val_pos);
if let Some((tag_name, tag_desc, raw_val, print_val)) = map_prop(&prop_name, &prop_val) {
let pv = print_val.clone();
tags.push(Tag {
id: TagId::Text(tag_name.to_string()),
name: tag_name.to_string(),
description: tag_desc.to_string(),
group: TagGroup {
family0: "X3F".to_string(),
family1: "Properties".to_string(),
family2: "Camera".to_string(),
},
raw_value: raw_val,
print_value: pv,
priority: 0,
});
}
}
}
fn map_prop(key: &str, val: &str) -> Option<(&'static str, &'static str, Value, String)> {
let s = |v: &str| -> (Value, String) { (Value::String(v.to_string()), v.to_string()) };
let (name, desc, raw, pv): (&'static str, &'static str, Value, String) = match key {
"AEMODE" => {
let pv = match val {
"8" => "8-segment",
"C" => "Center-weighted average",
"A" => "Average",
_ => val,
};
let (r, p) = s(pv);
("MeteringMode", "Metering Mode", r, p)
}
"AFMODE" => {
let (r, p) = s(val);
("FocusMode", "Focus Mode", r, p)
}
"AP_DESC" => {
let (r, p) = s(val);
("ApertureDisplayed", "Aperture Displayed", r, p)
}
"APERTURE" => {
if let Ok(f) = val.parse::<f64>() {
let pv = format!("{:.1}", f);
("FNumber", "F Number", Value::F64(f), pv)
} else {
let (r, p) = s(val);
("FNumber", "F Number", r, p)
}
}
"BRACKET" => {
let (r, p) = s(val);
("BracketShot", "Bracket Shot", r, p)
}
"BURST" => {
let (r, p) = s(val);
("BurstShot", "Burst Shot", r, p)
}
"CAMMANUF" => {
let (r, p) = s(val);
("Make", "Make", r, p)
}
"CAMMODEL" => {
let (r, p) = s(val);
("Model", "Model", r, p)
}
"CAMNAME" => {
let (r, p) = s(val);
("CameraName", "Camera Name", r, p)
}
"CAMSERIAL" => {
let (r, p) = s(val);
("SerialNumber", "Serial Number", r, p)
}
"CM_DESC" => {
let (r, p) = s(val);
("SceneCaptureType", "Scene Capture Type", r, p)
}
"COLORSPACE" => {
let (r, p) = s(val);
("ColorSpace", "Color Space", r, p)
}
"DRIVE" => {
let pv = match val {
"SINGLE" => "Single Shot",
"MULTI" => "Multi Shot",
"2S" => "2 s Timer",
"10S" => "10 s Timer",
"UP" => "Mirror Up",
"AB" => "Auto Bracket",
"OFF" => "Off",
_ => val,
};
let (r, p) = s(pv);
("DriveMode", "Drive Mode", r, p)
}
"EXPCOMP" => {
let (r, p) = s(val);
("ExposureCompensation", "Exposure Compensation", r, p)
}
"EXPNET" => {
let (r, p) = s(val);
("NetExposureCompensation", "Net Exposure Compensation", r, p)
}
"EXPTIME" => {
if let Ok(usec) = val.parse::<f64>() {
let secs = usec * 1e-6;
let print = print_exposure_time(secs);
(
"IntegrationTime",
"Integration Time",
Value::F64(secs),
print,
)
} else {
let (r, p) = s(val);
("IntegrationTime", "Integration Time", r, p)
}
}
"FIRMVERS" => {
let (r, p) = s(val);
("FirmwareVersion", "Firmware Version", r, p)
}
"FLASH" => {
let pv = ucfirst_lc(val);
let (r, _) = s(val);
("FlashMode", "Flash Mode", r, pv)
}
"FLASHEXPCOMP" => {
let (r, p) = s(val);
("FlashExposureComp", "Flash Exposure Comp", r, p)
}
"FLASHPOWER" => {
let (r, p) = s(val);
("FlashPower", "Flash Power", r, p)
}
"FLASHTTLMODE" => {
let (r, p) = s(val);
("FlashTTLMode", "Flash TTL Mode", r, p)
}
"FLASHTYPE" => {
let (r, p) = s(val);
("FlashType", "Flash Type", r, p)
}
"FLENGTH" => {
if let Ok(f) = val.parse::<f64>() {
let pv = format!("{:.1} mm", f);
("FocalLength", "Focal Length", Value::F64(f), pv)
} else {
let (r, p) = s(val);
("FocalLength", "Focal Length", r, p)
}
}
"FLEQ35MM" => {
if let Ok(f) = val.parse::<f64>() {
let pv = format!("{:.1} mm", f);
(
"FocalLengthIn35mmFormat",
"Focal Length In 35mm Format",
Value::F64(f),
pv,
)
} else {
let (r, p) = s(val);
(
"FocalLengthIn35mmFormat",
"Focal Length In 35mm Format",
r,
p,
)
}
}
"FOCUS" => {
let pv = match val {
"AF" => "Auto-focus Locked",
"NO LOCK" => "Auto-focus Didn't Lock",
"M" => "Manual",
_ => val,
};
let (r, p) = s(pv);
("Focus", "Focus", r, p)
}
"IMAGERBOARDID" => {
let (r, p) = s(val);
("ImagerBoardID", "Imager Board ID", r, p)
}
"IMAGERTEMP" => {
let pv = format!("{} C", val);
let (r, _) = s(val);
("SensorTemperature", "Sensor Temperature", r, pv)
}
"IMAGEBOARDID" => {
let (r, p) = s(val);
("ImageBoardID", "Image Board ID", r, p)
}
"ISO" => {
if let Ok(f) = val.parse::<f64>() {
let pv = format!("{}", f as u64);
("ISO", "ISO", Value::F64(f), pv)
} else {
let (r, p) = s(val);
("ISO", "ISO", r, p)
}
}
"LENSARANGE" => {
let (r, p) = s(val);
("LensApertureRange", "Lens Aperture Range", r, p)
}
"LENSFRANGE" => {
let (r, p) = s(val);
("LensFocalRange", "Lens Focal Range", r, p)
}
"LENSMODEL" => {
let val_trimmed = val.trim();
let pv =
if !val_trimmed.is_empty() && val_trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
if let Ok(hex_val) = u32::from_str_radix(val_trimmed, 16) {
lens_type_name(hex_val)
} else {
val.to_string()
}
} else {
val.to_string()
};
let (r, _) = s(val);
("LensType", "Lens Type", r, pv)
}
"PMODE" => {
let pv = match val {
"P" => "Program",
"A" => "Aperture Priority",
"S" => "Shutter Priority",
"M" => "Manual",
_ => val,
};
let (r, p) = s(pv);
("ExposureProgram", "Exposure Program", r, p)
}
"RESOLUTION" => {
let pv = match val {
"LOW" => "Low",
"MED" => "Medium",
"HI" => "High",
_ => val,
};
let (r, p) = s(pv);
("Quality", "Quality", r, p)
}
"SENSORID" => {
let (r, p) = s(val);
("SensorID", "Sensor ID", r, p)
}
"SH_DESC" => {
let (r, p) = s(val);
("ShutterSpeedDisplayed", "Shutter Speed Displayed", r, p)
}
"SHUTTER" => {
if let Ok(f) = val.parse::<f64>() {
let print = print_exposure_time(f);
("ExposureTime", "Exposure Time", Value::F64(f), print)
} else {
let (r, p) = s(val);
("ExposureTime", "Exposure Time", r, p)
}
}
"TIME" => {
if let Ok(ts) = val.parse::<i64>() {
let dt = unix_to_datetime(ts);
(
"DateTimeOriginal",
"Date/Time Original",
Value::String(dt.clone()),
dt,
)
} else {
let (r, p) = s(val);
("DateTimeOriginal", "Date/Time Original", r, p)
}
}
"WB_DESC" => {
let (r, p) = s(val);
("WhiteBalance", "White Balance", r, p)
}
"VERSION_BF" => {
let (r, p) = s(val);
("VersionBF", "Version BF", r, p)
}
_ => return None,
};
Some((name, desc, raw, pv))
}
fn u32_le(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn u16_le(data: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
fn f32_le(data: &[u8], offset: usize) -> f32 {
f32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn hex_bytes(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect()
}
fn read_cstr(data: &[u8]) -> String {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
crate::encoding::decode_utf8_or_latin1(&data[..end])
}
fn extract_utf16_str(chars: &[u16], pos: usize) -> String {
let mut end = pos;
while end < chars.len() && chars[end] != 0 {
end += 1;
}
chars[pos..end]
.iter()
.map(|&c| char::from_u32(c as u32).unwrap_or('?'))
.collect()
}
fn print_exposure_time(secs: f64) -> String {
if secs <= 0.0 {
return secs.to_string();
}
if secs >= 1.0 {
return format!("{}", secs as u32);
}
let inv = 1.0 / secs;
let rounded = inv.round() as u64;
format!("1/{}", rounded)
}
fn unix_to_datetime(ts: i64) -> String {
let mut secs = ts;
if secs < 0 {
return "0000:00:00 00:00:00".to_string();
}
let s = (secs % 60) as u32;
secs /= 60;
let m = (secs % 60) as u32;
secs /= 60;
let h = (secs % 24) as u32;
secs /= 24;
let (y, mo, d) = days_to_ymd(secs as u32);
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", y, mo, d, h, m, s)
}
fn days_to_ymd(mut days: u32) -> (u32, u32, u32) {
let mut year = 1970u32;
loop {
let leap = is_leap(year);
let days_in_year = if leap { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let leap = is_leap(year);
let month_days = [
31u32,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut month = 1u32;
for &md in &month_days {
if days < md {
break;
}
days -= md;
month += 1;
}
(year, month, days + 1)
}
fn is_leap(year: u32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn ucfirst_lc(s: &str) -> String {
let lower = s.to_lowercase();
let mut chars = lower.chars();
match chars.next() {
None => String::new(),
Some(c) => {
let upper: String = c.to_uppercase().collect();
upper + chars.as_str()
}
}
}
fn lens_type_name(hex_val: u32) -> String {
let name = match hex_val {
0x000 => "Sigma Lens (0x0)",
0x100 => "Sigma Lens (0x100)",
0x101 => "Sigma 18-50mm F3.5-5.6 DC",
0x103 => "Sigma 18-125mm F3.5-5.6 DC",
0x104 => "Sigma 18-200mm F3.5-6.3 DC",
0x105 => "Sigma 24-60mm F2.8 EX DG",
0x106 => "Sigma 17-70mm F2.8-4.5 DC Macro",
0x107 => "Sigma 18-50mm F2.8 EX DC",
0x108 => "Sigma 70-200mm F2.8 II EX DG APO Macro",
0x109 => "Sigma 50-150mm F2.8 EX DC APO HSM",
0x10a => "Sigma 28mm F1.8 EX DG",
0x10b => "Sigma 70mm F2.8 EX DG Macro",
0x10c => "Sigma 18-50mm F2.8 EX DC Macro",
0x129 => "Sigma 14mm F2.8 EX Aspherical HSM",
0x12c => "Sigma 20mm F1.8 EX DG Aspherical RF",
0x130 => "Sigma 30mm F1.4 EX DC HSM",
0x145 => "Sigma 15-30mm F3.5-4.5 EX DG Aspherical",
0x146 => "Sigma 18-35mm F3.5-4.5 Aspherical",
0x150 => "Sigma 50mm F2.8 EX DG Macro",
0x151 => "Sigma 105mm F2.8 EX DG Macro",
0x152 => "Sigma 180mm F3.5 EX DG APO HSM Macro",
0x153 => "Sigma 150mm F2.8 EX DG HSM APO Macro",
0x154 => "Sigma 10-20mm F4-5.6 EX DC",
0x155 => "Sigma 12-24mm F4.5-5.6 EX DG Aspherical",
0x156 => "Sigma 17-35mm F2.8-4 EX DG Aspherical",
0x157 => "Sigma 24mm F1.8 EX DG Aspherical Macro",
0x158 => "Sigma 28-70mm F2.8-4 DG",
0x169 => "Sigma 70-300mm F4-5.6 APO DG Macro",
0x184 => "Sigma 24-70mm F2.8 EX DG Macro",
0x190 => "Sigma APO 70-200mm F2.8 EX DG",
0x194 => "Sigma 300mm F2.8 APO EX DG HSM",
0x195 => "Sigma 500mm F4.5 APO EX DG HSM",
0x1a0 => "Sigma 24-135mm F2.8-4.5",
_ => "",
};
if name.is_empty() {
format!("Unknown ({:#x})", hex_val)
} else {
name.to_string()
}
}
fn mk_tag_str(name: &str, desc: &str, value: String, f0: &str, f1: &str, f2: &str) -> Tag {
let pv = value.clone();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: desc.to_string(),
group: TagGroup {
family0: f0.to_string(),
family1: f1.to_string(),
family2: f2.to_string(),
},
raw_value: Value::String(value),
print_value: pv,
priority: 0,
}
}
fn mk_tag_u32(name: &str, desc: &str, value: u32, f0: &str, f1: &str, f2: &str) -> Tag {
let pv = value.to_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: desc.to_string(),
group: TagGroup {
family0: f0.to_string(),
family1: f1.to_string(),
family2: f2.to_string(),
},
raw_value: Value::U32(value),
print_value: pv,
priority: 0,
}
}
fn mk_tag_binary(name: &str, desc: &str, data: Vec<u8>, f0: &str, f1: &str, f2: &str) -> Tag {
let pv = format!(
"(Binary data {} bytes, use -b option to extract)",
data.len()
);
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: desc.to_string(),
group: TagGroup {
family0: f0.to_string(),
family1: f1.to_string(),
family2: f2.to_string(),
},
raw_value: Value::Binary(data),
print_value: pv,
priority: 0,
}
}