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());
String::from_utf8_lossy(&data[..end]).into_owned()
}
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,
}
}