use crate::error::{Error, Result};
use crate::metadata::{ExifReader, IptcReader, XmpReader};
use crate::tag::Tag;
const MARKER_SOI: u8 = 0xD8;
const MARKER_SOS: u8 = 0xDA;
const MARKER_APP1: u8 = 0xE1;
const MARKER_APP3: u8 = 0xE3;
const MARKER_APP4: u8 = 0xE4;
const MARKER_APP5: u8 = 0xE5;
const MARKER_APP7: u8 = 0xE7;
const MARKER_APP12: u8 = 0xEC;
const MARKER_APP13: u8 = 0xED;
const MARKER_COM: u8 = 0xFE;
const CANON_VRD_SIG: &[u8] = b"CANON OPTIONAL DATA\0";
const EXIF_HEADER: &[u8] = b"Exif\0\0";
const XMP_HEADER: &[u8] = b"http://ns.adobe.com/xap/1.0/\0";
const PHOTOSHOP_HEADER: &[u8] = b"Photoshop 3.0\0";
pub fn read_jpeg(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 2 || data[0] != 0xFF || data[1] != MARKER_SOI {
return Err(Error::InvalidData("not a JPEG file".into()));
}
let mut tags = Vec::new();
let mut pos = 2;
let mut ext_xmp_chunks: Vec<(u32, Vec<u8>)> = Vec::new();
let mut ext_xmp_total: u32 = 0;
let mut flir_chunks: Vec<Option<Vec<u8>>> = Vec::new();
let mut flir_count: usize = 0;
let mut flir_total: Option<usize> = None;
let mut is_infray = false;
let mut fpxr_contents: Vec<FpxrEntry> = Vec::new();
let mut fpxr_seen = false;
while pos + 4 <= data.len() {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
pos += 2;
if marker == 0xFF || marker == 0x00 {
continue;
}
if marker == MARKER_SOS {
break;
}
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xCC {
if pos + 2 <= data.len() {
let sof_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
if pos + sof_len <= data.len() && sof_len >= 8 {
let sof = &data[pos + 2..pos + sof_len];
let precision = sof[0];
let height = u16::from_be_bytes([sof[1], sof[2]]);
let width = u16::from_be_bytes([sof[3], sof[4]]);
let components = sof[5];
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("ImageWidth".into()),
name: "ImageWidth".into(),
description: "Image Width".into(),
group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
raw_value: crate::value::Value::U16(width),
print_value: width.to_string(),
priority: 0,
});
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("ImageHeight".into()),
name: "ImageHeight".into(),
description: "Image Height".into(),
group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
raw_value: crate::value::Value::U16(height),
print_value: height.to_string(),
priority: 0,
});
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("BitsPerSample".into()),
name: "BitsPerSample".into(),
description: "Bits Per Sample".into(),
group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
raw_value: crate::value::Value::U8(precision),
print_value: precision.to_string(),
priority: 0,
});
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("ColorComponents".into()),
name: "ColorComponents".into(),
description: "Color Components".into(),
group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
raw_value: crate::value::Value::U8(components),
print_value: components.to_string(),
priority: 0,
});
let enc_process = match marker {
0xC0 => "Baseline DCT, Huffman coding",
0xC1 => "Extended sequential DCT, Huffman coding",
0xC2 => "Progressive DCT, Huffman coding",
0xC3 => "Lossless, Huffman coding",
0xC9 => "Extended sequential DCT, arithmetic coding",
0xCA => "Progressive DCT, arithmetic coding",
0xCB => "Lossless, arithmetic coding",
_ => "Unknown",
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("EncodingProcess".into()),
name: "EncodingProcess".into(),
description: "Encoding Process".into(),
group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
raw_value: crate::value::Value::U8(marker - 0xC0),
print_value: enc_process.to_string(),
priority: 0,
});
if components >= 3 && sof.len() >= 6 + components as usize * 3 {
let h_sample = (sof[7] >> 4) & 0x0F;
let v_sample = sof[7] & 0x0F;
let subsampling = if h_sample == 2 && v_sample == 2 { "YCbCr4:2:0".to_string() }
else if h_sample == 2 && v_sample == 1 { "YCbCr4:2:2".to_string() }
else if h_sample == 1 && v_sample == 1 { "YCbCr4:4:4".to_string() }
else { format!("YCbCr {}:{}", h_sample, v_sample) };
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("YCbCrSubSampling".into()),
name: "YCbCrSubSampling".into(),
description: "YCbCr Sub Sampling".into(),
group: crate::tag::TagGroup { family0: "File".into(), family1: "File".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(format!("{} {}", h_sample, v_sample)),
print_value: subsampling,
priority: 0,
});
}
}
}
}
if marker == MARKER_SOI || (0xD0..=0xD7).contains(&marker) {
continue;
}
if pos + 2 > data.len() {
break;
}
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
if seg_len < 2 || pos + seg_len > data.len() {
break;
}
let seg_data = &data[pos + 2..pos + seg_len];
pos += seg_len;
match marker {
0xE0 => {
if seg_data.len() >= 5 && seg_data.starts_with(b"JFIF\0") {
let major = seg_data[5] as u16;
let minor = if seg_data.len() > 6 { seg_data[6] as u16 } else { 0 };
let jfif_mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "JFIF".into(), family1: "JFIF".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: -1,
};
tags.push(jfif_mk("JFIFVersion", format!("{}.{:02}", major, minor)));
if seg_data.len() > 7 {
let unit = match seg_data[7] { 0 => "None", 1 => "inches", 2 => "cm", _ => "" };
if !unit.is_empty() { tags.push(jfif_mk("ResolutionUnit", unit.into())); }
}
if seg_data.len() > 11 {
let xres = u16::from_be_bytes([seg_data[8], seg_data[9]]);
let yres = u16::from_be_bytes([seg_data[10], seg_data[11]]);
tags.push(jfif_mk("XResolution", xres.to_string()));
tags.push(jfif_mk("YResolution", yres.to_string()));
}
} else if seg_data.len() >= 5 && seg_data.starts_with(b"JFXX\0") {
if seg_data.len() > 5 {
let ext_code = seg_data[5];
let thumb_data = &seg_data[6..];
if !thumb_data.is_empty() {
let (mime, fmt) = match ext_code {
0x10 => ("image/jpeg", "JPEG"),
0x11 => ("image/x-rgb", "RGB"),
0x13 => ("image/x-rgb", "RGB Palette"),
_ => ("image/jpeg", "JPEG"),
};
let _ = mime;
let _ = fmt;
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("ThumbnailImage".into()),
name: "ThumbnailImage".into(),
description: "Thumbnail Image".into(),
group: crate::tag::TagGroup { family0: "JFIF".into(), family1: "JFIF".into(), family2: "Image".into() },
raw_value: crate::value::Value::Binary(thumb_data.to_vec()),
print_value: format!("(Binary data {} bytes, use -b option to extract)", thumb_data.len()),
priority: 0,
});
}
}
} else if seg_data.len() >= 14 && {
(seg_data.starts_with(b"II") || seg_data.starts_with(b"MM"))
&& seg_data.len() > 10 && &seg_data[6..10] == b"HEAP"
} {
if let Ok(ciff_tags) = crate::formats::canon_raw::read_crw(seg_data) {
tags.extend(ciff_tags);
}
tags.extend(extract_ciff_freebytes(seg_data));
} else if seg_data.starts_with(b"AVI1") && seg_data.len() > 4 {
let d = &seg_data[4..];
if !d.is_empty() {
let val = d[0];
let print_val = match val {
0 => "Not Interleaved",
1 => "Odd",
2 => "Even",
_ => "",
};
if !print_val.is_empty() {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Numeric(0),
name: "InterleavedField".into(),
description: "Interleaved Field".into(),
group: crate::tag::TagGroup { family0: "APP0".into(), family1: "AVI1".into(), family2: "Image".into() },
raw_value: crate::value::Value::U8(val),
print_value: print_val.into(),
priority: 0,
});
}
}
}
}
MARKER_APP1 => {
if seg_data.len() > EXIF_HEADER.len()
&& seg_data.starts_with(EXIF_HEADER)
{
let exif_data = &seg_data[EXIF_HEADER.len()..];
match ExifReader::read(exif_data) {
Ok(exif_tags) => tags.extend(exif_tags),
Err(_) => {} }
}
else if seg_data.len() > XMP_HEADER.len()
&& seg_data.starts_with(XMP_HEADER)
{
let xmp_data = &seg_data[XMP_HEADER.len()..];
match XmpReader::read(xmp_data) {
Ok(xmp_tags) => tags.extend(xmp_tags),
Err(_) => {}
}
}
else if seg_data.starts_with(b"QVCI\0") && seg_data.len() > 0x80 {
let d = seg_data;
let mk = |name: &str, val: String| -> crate::tag::Tag {
crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "MakerNotes".into(), family1: "Casio".into(), family2: "Camera".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
}
};
let quality = match d[0x2C] {
1 => "Economy", 2 => "Normal", 3 => "Fine", 4 => "Super Fine", _ => "",
};
if !quality.is_empty() { tags.push(mk("CasioQuality", quality.into())); }
if d.len() > 0x61 {
let dt = String::from_utf8_lossy(&d[0x4D..0x61]).trim_end_matches('\0').replace('.', ":").to_string();
if !dt.is_empty() { tags.push(mk("DateTimeOriginal", dt)); }
}
if d.len() > 0x66 {
let mt = u32::from_le_bytes([d[0x62], d[0x63], d[0x64], d[0x65]]);
tags.push(mk("ModelType", mt.to_string()));
}
if d.len() > 0x7E {
let mi = u32::from_le_bytes([d[0x76], d[0x77], d[0x78], d[0x79]]);
let mc = u32::from_le_bytes([d[0x7A], d[0x7B], d[0x7C], d[0x7D]]);
tags.push(mk("ManufactureIndex", mi.to_string()));
tags.push(mk("ManufactureCode", mc.to_string()));
}
}
else if seg_data.starts_with(b"FLIR\0") && seg_data.len() >= 8 {
let chunk_num = seg_data[6] as usize;
let chunks_tot = seg_data[7] as usize + 1; let chunk_data = &seg_data[8..];
if let Some(prev_total) = flir_total {
if chunks_tot != prev_total {
flir_total = None;
flir_chunks.clear();
flir_count = 0;
}
}
if flir_total.is_none() && flir_count == 0 {
flir_total = Some(chunks_tot);
flir_chunks.resize(chunks_tot, None);
}
if flir_total.is_some() {
if chunk_num < flir_chunks.len() {
if flir_chunks[chunk_num].is_some() {
flir_chunks[chunk_num].as_mut().unwrap().extend_from_slice(chunk_data);
} else {
flir_chunks[chunk_num] = Some(chunk_data.to_vec());
flir_count += 1;
}
}
if flir_count >= flir_total.unwrap() {
let mut flir_data = Vec::new();
for chunk in &flir_chunks {
if let Some(c) = chunk {
flir_data.extend_from_slice(c);
}
}
flir_chunks.clear();
flir_count = 0;
flir_total = None;
if flir_data.starts_with(b"FFF\0") || flir_data.starts_with(b"AFF\0") {
tags.extend(decode_flir_fff(&flir_data));
}
}
}
}
else if seg_data.len() > 75
&& seg_data.starts_with(b"http://ns.adobe.com/xmp/extension/\0")
{
let rest = &seg_data[35..];
if rest.len() >= 40 {
let total = u32::from_be_bytes([rest[32], rest[33], rest[34], rest[35]]);
let offset = u32::from_be_bytes([rest[36], rest[37], rest[38], rest[39]]);
let chunk = &rest[40..];
ext_xmp_total = total;
ext_xmp_chunks.push((offset, chunk.to_vec()));
}
}
}
0xE2 => {
if seg_data.len() > 10 && &seg_data[4..10] == b"IJPEG\0" {
is_infray = true;
tags.extend(decode_infray_version(seg_data));
}
else if seg_data.starts_with(b"FPXR\0") && seg_data.len() > 7 {
fpxr_seen = true;
accumulate_fpxr(seg_data, &mut fpxr_contents);
}
else if seg_data.starts_with(b"MPF\0") {
tags.extend(parse_mpf(seg_data, data));
}
else if seg_data.starts_with(b"ICC_PROFILE\0") && seg_data.len() > 14 {
let icc_data = &seg_data[14..];
let icc_tags = crate::formats::icc::parse_icc_tags(icc_data);
tags.extend(icc_tags);
}
}
MARKER_APP3 => {
if is_infray && !seg_data.is_empty() {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("ImagingData".into()),
name: "ImagingData".into(), description: "Imaging Data".into(),
group: crate::tag::TagGroup { family0: "APP3".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::Binary(seg_data.to_vec()),
print_value: format!("(Binary data {} bytes, use -b option to extract)", seg_data.len()),
priority: 0,
});
}
else if seg_data.starts_with(b"_JPSJPS_") && seg_data.len() >= 14 {
tags.extend(parse_jps(seg_data));
}
else if seg_data.len() > 8
&& (seg_data.starts_with(b"Meta\0\0")
|| seg_data.starts_with(b"META\0\0")
|| seg_data.starts_with(b"Exif\0\0"))
{
let meta_data = &seg_data[6..];
tags.extend(parse_meta_ifd(meta_data));
}
}
MARKER_APP4 => {
if is_infray {
tags.extend(decode_infray_factory(seg_data));
}
}
MARKER_APP5 => {
if is_infray {
tags.extend(decode_infray_picture(seg_data));
} else if seg_data.starts_with(b"RMETA\0") && seg_data.len() > 6 {
tags.extend(parse_ricoh_rmeta(&seg_data[6..]));
}
}
0xEE => {
if seg_data.starts_with(b"Adobe") && seg_data.len() >= 12 {
let d = &seg_data[5..]; let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP14".into(), family1: "Adobe".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
if d.len() >= 2 {
tags.push(mk("DCTEncodeVersion", u16::from_be_bytes([d[0], d[1]]).to_string()));
}
if d.len() >= 4 {
tags.push(mk("APP14Flags0", u16::from_be_bytes([d[2], d[3]]).to_string()));
}
if d.len() >= 6 {
tags.push(mk("APP14Flags1", u16::from_be_bytes([d[4], d[5]]).to_string()));
}
if d.len() >= 7 {
let ct = match d[6] { 0 => "Unknown", 1 => "YCbCr", 2 => "YCCK", _ => "" };
if !ct.is_empty() { tags.push(mk("ColorTransform", ct.into())); }
}
}
}
MARKER_APP13 => {
if seg_data.starts_with(b"Adobe_CM") && seg_data.len() >= 10 {
let val = u16::from_be_bytes([seg_data[8], seg_data[9]]);
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Numeric(0),
name: "AdobeCMType".into(),
description: "Adobe CM Type".into(),
group: crate::tag::TagGroup { family0: "APP13".into(), family1: "AdobeCM".into(), family2: "Image".into() },
raw_value: crate::value::Value::U16(val),
print_value: val.to_string(),
priority: 0,
});
}
else if seg_data.len() > PHOTOSHOP_HEADER.len()
&& seg_data.starts_with(PHOTOSHOP_HEADER)
{
let (iptc_data, irb_tags) = extract_photoshop_irbs(
&seg_data[PHOTOSHOP_HEADER.len()..],
);
tags.extend(irb_tags);
if let Some(iptc_data) = iptc_data {
let digest = crate::md5::md5_hex(&iptc_data);
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("CurrentIPTCDigest".into()),
name: "CurrentIPTCDigest".into(),
description: "Current IPTC Digest".into(),
group: crate::tag::TagGroup { family0: "Photoshop".into(), family1: "Photoshop".into(), family2: "Other".into() },
raw_value: crate::value::Value::String(digest.clone()),
print_value: digest, priority: 0,
});
match IptcReader::read(&iptc_data) {
Ok(iptc_tags) => tags.extend(iptc_tags),
Err(_) => {}
}
}
}
}
0xE6 => {
if is_infray {
tags.extend(decode_infray_mixmode(seg_data));
} else if seg_data.starts_with(b"GoPro\0") && seg_data.len() > 6 {
tags.extend(parse_gopro_gpmf(&seg_data[6..]));
} else if seg_data.starts_with(b"NITF\0") || seg_data.starts_with(b"NTIF\0") {
tags.extend(process_nitf(&seg_data[5..]));
} else if seg_data.starts_with(b"EPPIM\0") && seg_data.len() > 6 {
let tiff_data = &seg_data[6..];
tags.extend(process_eppim(tiff_data));
}
}
MARKER_APP7 => {
if is_infray {
tags.extend(decode_infray_opmode(seg_data));
} else if seg_data.len() > 27
&& seg_data[0] == 0x1a
&& seg_data[1..27].starts_with(b"Qualcomm Camera Attributes")
{
tags.extend(parse_qualcomm(&seg_data[27..]));
}
}
0xE8 => {
if is_infray {
tags.extend(decode_infray_isothermal(seg_data));
} else if seg_data.starts_with(b"SPIFF\0") {
tags.extend(process_spiff(&seg_data[6..]));
}
}
0xE9 => {
if is_infray {
tags.extend(decode_infray_sensor(seg_data));
} else if seg_data.starts_with(b"Media Jukebox\0") {
let xml_start = seg_data.iter().position(|&b| b == b'<').unwrap_or(seg_data.len());
if xml_start < seg_data.len() {
tags.extend(process_media_jukebox_xml(&seg_data[xml_start..]));
}
}
}
0xEB => {
if seg_data.starts_with(b"HDR_RI ") {
tags.extend(process_jpeg_hdr(seg_data));
} else if seg_data.len() >= 2 && seg_data.starts_with(b"JP") {
tags.extend(process_jumbf_app11(seg_data));
}
}
MARKER_APP12 => {
if seg_data.starts_with(b"Ducky") {
tags.extend(process_ducky(&seg_data[5..]));
} else {
tags.extend(process_app12_picture_info(seg_data));
}
}
0xEF => {
tags.extend(process_graphicconverter(seg_data));
}
MARKER_COM => {
let comment = String::from_utf8_lossy(seg_data)
.trim_end_matches('\0')
.to_string();
if !comment.is_empty() {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("Comment".into()),
name: "Comment".into(),
description: "JPEG Comment".into(),
group: crate::tag::TagGroup {
family0: "File".into(),
family1: "Comment".into(),
family2: "Image".into(),
},
raw_value: crate::value::Value::String(comment.clone()),
print_value: comment,
priority: 0,
});
}
}
_ => {
}
}
}
if fpxr_seen {
tags.extend(process_fpxr_segments(&fpxr_contents));
}
if !ext_xmp_chunks.is_empty() {
ext_xmp_chunks.sort_by_key(|(off, _)| *off);
let mut assembled = Vec::with_capacity(ext_xmp_total as usize);
for (_, chunk) in &ext_xmp_chunks {
assembled.extend_from_slice(chunk);
}
if let Ok(ext_tags) = XmpReader::read(&assembled) {
tags.extend(ext_tags);
}
}
if data.len() > 24 {
let trailer_check = &data[data.len().saturating_sub(12)..];
if trailer_check.starts_with(b"AXS!") || trailer_check.starts_with(b"AXS*") {
let le = trailer_check[3] == b'*';
let rd32_afcp = |d: &[u8], off: usize| -> u32 {
if le { u32::from_le_bytes([d[off], d[off+1], d[off+2], d[off+3]]) }
else { u32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]]) }
};
let rd16_afcp = |d: &[u8], off: usize| -> u16 {
if le { u16::from_le_bytes([d[off], d[off+1]]) }
else { u16::from_be_bytes([d[off], d[off+1]]) }
};
let start_pos = rd32_afcp(trailer_check, 4) as usize;
if start_pos + 18 < data.len() {
let afcp = &data[start_pos..];
let _num_entries = rd16_afcp(afcp, 18) as usize; let num_entries = rd16_afcp(&data, start_pos + 6) as usize;
let dir_start = start_pos + 12;
for i in 0..num_entries.min(20) {
let eoff = dir_start + i * 12;
if eoff + 12 > data.len() { break; }
let tag = &data[eoff..eoff + 4];
let size = rd32_afcp(&data, eoff + 4) as usize;
let offset = rd32_afcp(&data, eoff + 8) as usize;
if tag == b"IPTC" && offset + size <= data.len() {
let iptc_raw = &data[offset..offset + size];
let iptc_start = iptc_raw.iter().position(|&b| b == 0x1C).unwrap_or(0);
if let Ok(iptc_tags) = IptcReader::read(&iptc_raw[iptc_start..]) {
tags.extend(iptc_tags);
}
}
}
}
}
}
if let Some(pm_sig_pos) = data.windows(8).position(|w| w == b"cbipcbbl") {
if pm_sig_pos >= 12 {
let size = u32::from_be_bytes([
data[pm_sig_pos-4], data[pm_sig_pos-3], data[pm_sig_pos-2], data[pm_sig_pos-1]
]) as usize;
if size > 0 && pm_sig_pos >= 4 + size {
let pm_data = &data[pm_sig_pos - 4 - size..pm_sig_pos - 4];
if let Some(start) = pm_data.iter().position(|&b| b == 0x1C) {
if let Ok(iptc_tags) = IptcReader::read(&pm_data[start..]) {
for tag in &iptc_tags {
tags.push(tag.clone());
}
}
let mut pos = start;
while pos + 5 <= pm_data.len() {
if pm_data[pos] != 0x1C { break; }
let rec = pm_data[pos + 1];
let ds = pm_data[pos + 2];
let len = u16::from_be_bytes([pm_data[pos+3], pm_data[pos+4]]) as usize;
pos += 5;
if pos + len > pm_data.len() { break; }
let val_bytes = &pm_data[pos..pos+len];
let name = match (rec, ds) {
(2, 216) => "Rotation",
(2, 217) => "CropLeft",
(2, 218) => "CropTop",
(2, 219) => "CropRight",
(2, 220) => "CropBottom",
(2, 221) => "Tagged",
(2, 222) => "ColorClass",
_ => { pos += len; continue; },
};
let raw_int = if len == 4 {
i32::from_be_bytes([val_bytes[0], val_bytes[1], val_bytes[2], val_bytes[3]])
} else if len == 2 {
i16::from_be_bytes([val_bytes[0], val_bytes[1]]) as i32
} else { 0 };
let raw_val = raw_int.to_string();
let print_val = match (rec, ds) {
(2, 221) => match raw_int { 0 => "No".to_string(),
1 => "Yes".to_string(),
_ => raw_val.clone(),
},
(2, 222) => match raw_int { 0 => "0 (None)".to_string(),
1 => "1 (Winner)".to_string(),
2 => "2 (Winner alt)".to_string(),
3 => "3 (Superior)".to_string(),
4 => "4 (Superior alt)".to_string(),
5 => "5 (Typical)".to_string(),
6 => "6 (Typical alt)".to_string(),
7 => "7 (Extras)".to_string(),
8 => "8 (Trash)".to_string(),
_ => raw_val.clone(),
},
_ => raw_val.clone(),
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "PhotoMechanic".into(), family1: "PhotoMechanic".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(raw_val), print_value: print_val, priority: 0,
});
pos += len;
}
}
}
}
}
{
let fs_sig = [0xa1u8, 0xb2, 0xc3, 0xd4];
let mut search_start = 0usize;
while search_start + 4 <= data.len() {
let found = data[search_start..].windows(4).position(|w| w == fs_sig);
let sig_pos = match found {
Some(p) => search_start + p,
None => break,
};
search_start = sig_pos + 4;
if sig_pos < 6 { continue; }
let footer_start = sig_pos - 6;
let tag = u16::from_be_bytes([data[footer_start], data[footer_start+1]]);
let size = u32::from_be_bytes([data[footer_start+2], data[footer_start+3],
data[footer_start+4], data[footer_start+5]]) as usize;
if size < 10 { continue; }
let block_end = sig_pos + 4; if block_end < size { continue; }
let block_start = block_end - size;
let rec_data = &data[block_start..block_start + size - 10];
match tag {
0x01 => {
if let Some(start) = rec_data.iter().position(|&b| b == 0x1C) {
if let Ok(iptc_tags) = IptcReader::read(&rec_data[start..]) {
tags.extend(iptc_tags);
}
}
}
0x02 => {
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "FotoStation".into(), family1: "FotoStation".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let rd32s = |idx: usize| -> i32 {
let off = idx * 4;
if off + 4 > rec_data.len() { return 0; }
i32::from_be_bytes([rec_data[off], rec_data[off+1], rec_data[off+2], rec_data[off+3]])
};
if rec_data.len() >= 16 {
tags.push(mk("OriginalImageWidth", rd32s(0).to_string()));
tags.push(mk("OriginalImageHeight", rd32s(1).to_string()));
tags.push(mk("ColorPlanes", rd32s(2).to_string()));
}
if rec_data.len() >= 20 {
let xy_raw = rd32s(3);
let xy_val = xy_raw as f64 / 1000.0;
let xy_str = if xy_val == xy_val.floor() && xy_val.fract() == 0.0 {
format!("{}", xy_val as i64)
} else {
format!("{}", xy_val)
};
tags.push(mk("XYResolution", xy_str));
}
if rec_data.len() >= 24 {
let rot_raw = rd32s(4);
let rot_val = if rot_raw != 0 { 360.0 - rot_raw as f64 / 100.0 } else { 0.0 };
let rot_str = if rot_val == rot_val.floor() {
format!("{}", rot_val as i64)
} else {
format!("{}", rot_val)
};
tags.push(mk("Rotation", rot_str));
}
if rec_data.len() >= 40 {
let fmt_crop = |v: i32| -> String {
let f = v as f64 / 1000.0;
let s = format!("{}", f);
format!("{}%", s)
};
tags.push(mk("CropLeft", fmt_crop(rd32s(6))));
tags.push(mk("CropTop", fmt_crop(rd32s(7))));
tags.push(mk("CropRight", fmt_crop(rd32s(8))));
tags.push(mk("CropBottom", fmt_crop(rd32s(9))));
}
if rec_data.len() >= 48 {
let cr_raw = rd32s(11);
let cr_val = -(cr_raw as f64) / 100.0;
let cr_str = if cr_val == cr_val.floor() && cr_val.fract() == 0.0 {
format!("{}", cr_val as i64)
} else {
format!("{}", cr_val)
};
tags.push(mk("CropRotation", cr_str));
}
}
_ => {}
}
}
}
{
let sos_pos = data.windows(2).position(|w| w == [0xFF, 0xDA]);
if let Some(sp) = sos_pos {
let rest = &data[sp..];
if let Some(ps_pos) = rest.windows(14).position(|w| w == PHOTOSHOP_HEADER) {
let ps_data = &rest[ps_pos + PHOTOSHOP_HEADER.len()..];
let (iptc2, irb2) = extract_photoshop_irbs(ps_data);
tags.extend(irb2);
if let Some(iptc2_data) = iptc2 {
let _digest = crate::md5::md5_hex(&iptc2_data);
if let Ok(iptc_tags) = IptcReader::read(&iptc2_data) {
tags.extend(iptc_tags);
}
}
}
}
}
{
let sig = CANON_VRD_SIG;
let sig_len = sig.len(); let mut search_end = data.len();
'vrd_scan: while search_end >= sig_len + 0x40 {
let found = data[..search_end].windows(sig_len)
.rposition(|w| w == sig);
let candidate = match found {
Some(p) => p,
None => break,
};
search_end = candidate; let footer_end = candidate + 0x40;
if footer_end > data.len() { continue; }
let footer = &data[candidate..footer_end];
if footer.len() < 24 { continue; }
let contained_len = u32::from_be_bytes([
footer[20], footer[21], footer[22], footer[23],
]) as usize;
let total_len = contained_len.saturating_add(0x5c);
if total_len < 0x60 || total_len > 0x800000 { continue; }
if footer_end < total_len { continue; }
let vrd_start = footer_end - total_len;
if !data[vrd_start..].starts_with(sig) { continue; }
if vrd_start + 0x1c + contained_len != candidate { continue; }
let vrd_data = &data[vrd_start..footer_end];
tags.extend(parse_canon_vrd(vrd_data, total_len));
break 'vrd_scan;
}
}
if let Some(qdiobs_pos) = data.windows(6).rposition(|w| w == b"QDIOBS") {
let after = &data[qdiobs_pos + 6..];
if let Some(json_start) = after.iter().position(|&b| b == b'{') {
let json_data = &after[json_start..];
let mut depth = 0usize;
let mut json_end = None;
for (i, &b) in json_data.iter().enumerate() {
match b {
b'{' => depth += 1,
b'}' => {
depth = depth.saturating_sub(1);
if depth == 0 { json_end = Some(i + 1); break; }
}
_ => {}
}
}
if let Some(end) = json_end {
let json_str = String::from_utf8_lossy(&json_data[..end]).to_string();
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("JSONInfo".into()),
name: "JSONInfo".into(),
description: "JSON Info".into(),
group: crate::tag::TagGroup { family0: "Trailer".into(), family1: "Samsung".into(), family2: "Other".into() },
raw_value: crate::value::Value::String(json_str.clone()),
print_value: json_str, priority: 0,
});
}
}
let block_end = qdiobs_pos + 4; tags.extend(parse_samsung_seft(data, block_end));
}
if let Some(mie_pos) = data.windows(4).rposition(|w| w == b"\x7e\x10\x04\xfe") {
let mie_data = &data[mie_pos..];
tags.extend(parse_mie_trailer(mie_data));
}
{
let stored = tags.iter().find(|t| t.name == "IPTCDigest").map(|t| t.print_value.clone());
let current = tags.iter().find(|t| t.name == "CurrentIPTCDigest").map(|t| t.print_value.clone());
if let (Some(stored_val), Some(current_val)) = (stored, current) {
if stored_val != current_val {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("Warning".into()),
name: "Warning".into(),
description: "Warning".into(),
group: crate::tag::TagGroup {
family0: "ExifTool".into(),
family1: "ExifTool".into(),
family2: "Other".into(),
},
raw_value: crate::value::Value::String("IPTCDigest is not current".into()),
print_value: "IPTCDigest is not current".into(),
priority: 0,
});
}
}
}
{
let is_flir = tags.iter().any(|t| t.name == "Make"
&& t.print_value.to_uppercase().contains("FLIR"));
if is_flir {
for t in tags.iter_mut() {
if t.name == "CPUVersions" && t.group.family0 == "MakerNotes" {
t.id = crate::tag::TagId::Text("ImageTemperatureMin".into());
t.name = "ImageTemperatureMin".into();
t.description = "ImageTemperatureMin".into();
t.priority = -1;
}
}
tags.retain(|t| {
!(t.name == "Quality" && t.group.family0 == "MakerNotes")
&& !(t.name == "LensID" && t.group.family0 == "Composite")
});
if !tags.iter().any(|t| t.name == "ImageTemperatureMax") {
let (max_t, _) = read_flir_makernote_temps(data);
if let Some(v) = max_t {
tags.insert(0, crate::tag::Tag {
id: crate::tag::TagId::Text("ImageTemperatureMax".into()),
name: "ImageTemperatureMax".into(),
description: "ImageTemperatureMax".into(),
group: crate::tag::TagGroup {
family0: "MakerNotes".into(),
family1: "FLIR".into(),
family2: "Camera".into(),
},
raw_value: crate::value::Value::String(v.to_string()),
print_value: v.to_string(),
priority: -1,
});
}
}
}
}
Ok(tags)
}
fn process_nitf(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 14 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP6".into(), family1: "NITF".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
tags.push(mk("NITFVersion", format!("{}.{:02}", data[0], data[1])));
let fmt_byte = data[2] & 0xFF;
tags.push(mk("ImageFormat", if fmt_byte == b'B' { "IMode B".into() } else { format!("{}", fmt_byte as char) }));
if data.len() > 4 {
tags.push(mk("BlocksPerRow", u16::from_be_bytes([data[3], data[4]]).to_string()));
}
if data.len() > 6 {
tags.push(mk("BlocksPerColumn", u16::from_be_bytes([data[5], data[6]]).to_string()));
}
if data.len() > 7 {
tags.push(mk("ImageColor", match data[7] { 0 => "Monochrome".into(), v => v.to_string() }));
}
if data.len() > 8 { tags.push(mk("BitDepth", data[8].to_string())); }
if data.len() > 9 {
tags.push(mk("ImageClass", match data[9] {
0 => "General Purpose".into(), 4 => "Tactical Imagery".into(), v => v.to_string(),
}));
}
if data.len() > 10 {
tags.push(mk("JPEGProcess", match data[10] {
1 => "Baseline sequential DCT, Huffman coding, 8-bit samples".into(),
4 => "Extended sequential DCT, Huffman coding, 12-bit samples".into(),
v => v.to_string(),
}));
}
if data.len() > 12 {
tags.push(mk("StreamColor", match data[12] { 0 => "Monochrome".into(), v => v.to_string() }));
}
if data.len() > 13 { tags.push(mk("StreamBitDepth", data[13].to_string())); }
if data.len() > 17 {
let flags = u32::from_be_bytes([data[14], data[15], data[16], data[17]]);
tags.push(mk("Flags", format!("0x{:x}", flags)));
}
tags
}
fn process_jumbf_app11(seg_data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if seg_data.len() < 12 { return tags; }
let boxes_data = &seg_data[8..]; parse_jumbf_boxes(boxes_data, &mut tags);
tags
}
fn parse_jumbf_boxes(data: &[u8], tags: &mut Vec<crate::tag::Tag>) {
let mut pos = 0;
while pos + 8 <= data.len() {
let lbox = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
let tbox = &data[pos+4..pos+8];
if lbox < 8 { break; }
let content_end = pos + lbox;
if content_end > data.len() { break; }
let content = &data[pos+8..content_end];
if tbox == b"jumb" {
parse_jumbf_boxes_jumd(content, tags);
}
pos += lbox;
if pos >= data.len() { break; }
}
}
fn parse_jumbf_boxes_jumd(data: &[u8], tags: &mut Vec<crate::tag::Tag>) {
let mut pos = 0;
while pos + 8 <= data.len() {
let lbox = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
let tbox = &data[pos+4..pos+8];
if lbox < 8 { break; }
let content_end = pos + lbox;
if content_end > data.len() { break; }
let content = &data[pos+8..content_end];
if tbox == b"jumd" {
if content.len() >= 17 {
let type_bytes = &content[..16];
let _toggles = content[16];
let label_data = &content[17..];
let null_pos = label_data.iter().position(|&b| b == 0).unwrap_or(label_data.len());
let label = String::from_utf8_lossy(&label_data[..null_pos]).to_string();
let type_hex = type_bytes.iter().map(|b| format!("{:02x}", b)).collect::<String>();
let print_type = {
let a0 = &type_hex[..8];
let a1 = &type_hex[8..12];
let a2 = &type_hex[12..16];
let a3 = &type_hex[16..32];
let ascii4 = &type_bytes[..4];
let is_printable_alpha = ascii4.iter().all(|&b| b.is_ascii_alphanumeric());
if is_printable_alpha {
let ascii_str = String::from_utf8_lossy(ascii4);
format!("({})-{}-{}-{}", ascii_str, a1, a2, a3)
} else {
format!("{}-{}-{}-{}", a0, a1, a2, a3)
}
};
let jumbf_group = crate::tag::TagGroup {
family0: "JUMBF".into(),
family1: "JUMBF".into(),
family2: "Image".into(),
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("JUMDType".into()),
name: "JUMDType".into(),
description: "JUMD Type".into(),
group: jumbf_group.clone(),
raw_value: crate::value::Value::String(type_hex),
print_value: print_type,
priority: 0,
});
if !label.is_empty() {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("JUMDLabel".into()),
name: "JUMDLabel".into(),
description: "JUMD Label".into(),
group: jumbf_group,
raw_value: crate::value::Value::String(label.clone()),
print_value: label,
priority: 0,
});
}
}
return;
} else if tbox == b"jumb" {
parse_jumbf_boxes_jumd(content, tags);
}
pos += lbox;
if pos >= data.len() { break; }
}
}
fn process_eppim(tiff_data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if tiff_data.len() < 8 { return tags; }
let is_le = tiff_data[0] == b'I' && tiff_data[1] == b'I';
if !is_le && !(tiff_data[0] == b'M' && tiff_data[1] == b'M') {
return tags;
}
let r16 = |d: &[u8], off: usize| -> u16 {
if off + 2 > d.len() { return 0; }
if is_le { u16::from_le_bytes([d[off], d[off+1]]) } else { u16::from_be_bytes([d[off], d[off+1]]) }
};
let r32 = |d: &[u8], off: usize| -> u32 {
if off + 4 > d.len() { return 0; }
if is_le { u32::from_le_bytes([d[off], d[off+1], d[off+2], d[off+3]]) } else { u32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]]) }
};
let ifd0 = r32(tiff_data, 4) as usize;
if ifd0 + 2 > tiff_data.len() { return tags; }
let n = r16(tiff_data, ifd0) as usize;
for i in 0..n {
let off = ifd0 + 2 + i * 12;
if off + 12 > tiff_data.len() { break; }
let tag = r16(tiff_data, off);
let dt = r16(tiff_data, off + 2);
let count = r32(tiff_data, off + 4) as usize;
let voff_raw = r32(tiff_data, off + 8) as usize;
if tag == 0xC4A5 {
let voff = voff_raw;
let size = match dt { 1 | 6 | 7 => count, 2 => count, _ => 0 };
if size >= 11 && voff + size <= tiff_data.len() {
let pm = &tiff_data[voff..voff + size];
if pm.starts_with(b"PrintIM") {
let ver = String::from_utf8_lossy(&pm[7..11]).trim_end_matches('\0').to_string();
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("PrintIMVersion".into()),
name: "PrintIMVersion".into(),
description: "PrintIM Version".into(),
group: crate::tag::TagGroup {
family0: "PrintIM".into(),
family1: "PrintIM".into(),
family2: "Printing".into(),
},
raw_value: crate::value::Value::String(ver.clone()),
print_value: ver,
priority: 0,
});
}
}
}
}
tags
}
fn process_spiff(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 2 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP8".into(), family1: "SPIFF".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
tags.push(mk("SPIFFVersion", format!("{}.{}", data[0], data[1])));
if data.len() > 14 {
let cs = match data[14] {
0 => "Bi-level", 1 => "YCbCr, ITU-R BT 709, video",
2 => "No color space specified", 3 => "YCbCr, ITU-R BT 601-1, RGB",
4 => "YCbCr, ITU-R BT 601-1, video", 8 => "Gray-scale",
9 => "PhotoYCC", 10 => "RGB", 11 => "CMY", 12 => "CMYK",
13 => "YCCK", 14 => "CIELab", _ => "",
};
if !cs.is_empty() { tags.push(mk("ColorSpace", cs.into())); }
}
if data.len() > 16 {
let comp = match data[16] {
0 => "Uncompressed, interleaved, 8 bits per sample", 1 => "Modified Huffman",
2 => "Modified READ", 3 => "Modified Modified READ", 4 => "JBIG", 5 => "JPEG", _ => "",
};
if !comp.is_empty() { tags.push(mk("Compression", comp.into())); }
}
if data.len() > 17 {
let ru = match data[17] { 0 => "None", 1 => "inches", 2 => "cm", _ => "" };
if !ru.is_empty() { tags.push(mk("ResolutionUnit", ru.into())); }
}
if data.len() > 21 {
tags.push(mk("YResolution", u32::from_be_bytes([data[18], data[19], data[20], data[21]]).to_string()));
}
if data.len() > 25 {
tags.push(mk("XResolution", u32::from_be_bytes([data[22], data[23], data[24], data[25]]).to_string()));
}
tags
}
fn process_media_jukebox_xml(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
let xml = String::from_utf8_lossy(data);
let mk_xml = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "XML".into(), family1: "MediaJukebox".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let extract_xml_tag = |xml: &str, tag: &str| -> Option<String> {
let open = format!("<{}>", tag);
let close = format!("</{}>", tag);
let start = xml.find(&open)?;
let after_open = start + open.len();
let end = xml[after_open..].find(&close)? + after_open;
if after_open <= end { Some(xml[after_open..end].trim().to_string()) } else { None }
};
for tag_name in &["Tool_Name", "Tool_Version", "People", "Places", "Album", "Name"] {
if let Some(val) = extract_xml_tag(&xml, tag_name) {
if !val.is_empty() { tags.push(mk_xml(tag_name, val)); }
}
}
if let Some(date_str) = extract_xml_tag(&xml, "Date") {
if let Ok(days) = date_str.parse::<f64>() {
let unix_secs = ((days - 25569.0) * 86400.0) as i64;
let formatted = unix_to_exiftool_datetime(unix_secs);
let mut dt = mk_xml("Date", formatted);
dt.group.family2 = "Time".into();
tags.push(dt);
}
}
tags
}
fn unix_to_exiftool_datetime(unix_secs: i64) -> String {
let secs_per_day = 86400i64;
let days = unix_secs.div_euclid(secs_per_day);
let tod = unix_secs.rem_euclid(secs_per_day);
let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
let z = days + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y0 = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
let yr = if mo <= 2 { y0 + 1 } else { y0 };
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", yr, mo, d, h, m, s)
}
fn process_jpeg_hdr(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 9 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP11".into(), family1: "JPEG-HDR".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let tilde_pos = match data.windows(2).position(|w| w == b"~\x00") {
Some(p) => p, None => return tags,
};
let meta_len = tilde_pos.saturating_sub(7);
let meta = String::from_utf8_lossy(&data[7..7 + meta_len]);
let meta_bytes = meta.as_bytes();
let mut i = 0usize;
while i < meta_bytes.len() {
if !meta_bytes[i].is_ascii_alphanumeric() && meta_bytes[i] != b'_' { i += 1; continue; }
let key_start = i;
while i < meta_bytes.len() && (meta_bytes[i].is_ascii_alphanumeric() || meta_bytes[i] == b'_') { i += 1; }
let key = std::str::from_utf8(&meta_bytes[key_start..i]).unwrap_or("");
if i >= meta_bytes.len() || meta_bytes[i] != b'=' { continue; }
i += 1;
let val_start = i;
while i < meta_bytes.len() && meta_bytes[i] != b',' && !meta_bytes[i].is_ascii_whitespace() { i += 1; }
let val = std::str::from_utf8(&meta_bytes[val_start..i]).unwrap_or("");
let tag_name = match key {
"ver" => "JPEG-HDRVersion",
"ln0" => "Ln0", "ln1" => "Ln1", "s2n" => "S2n",
"alp" => "Alpha", "bet" => "Beta", "cor" => "CorrectionMethod",
other => other,
};
tags.push(mk(tag_name, val.to_string()));
}
let ratio_data = &data[tilde_pos + 2..];
if !ratio_data.is_empty() {
let display = format!("(Binary data {} bytes, use -b option to extract)", ratio_data.len());
let mut t = mk("RatioImage", display);
t.raw_value = crate::value::Value::Binary(ratio_data.to_vec());
t.group.family2 = "Preview".into();
tags.push(t);
}
tags
}
fn process_graphicconverter(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.is_empty() || data[0] != b'Q' { return tags; }
let rest = String::from_utf8_lossy(&data[1..]);
let trimmed = rest.trim_start();
let num_end = trimmed.find(|c: char| !c.is_ascii_digit()).unwrap_or(trimmed.len());
let quality_str = trimmed[..num_end].to_string();
if quality_str.is_empty() { return tags; }
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("Quality".into()),
name: "Quality".into(),
description: "Quality".into(),
group: crate::tag::TagGroup { family0: "APP15".into(), family1: "GraphConv".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(quality_str.clone()),
print_value: quality_str,
priority: 0,
});
tags
}
fn parse_samsung_seft(data: &[u8], block_end: usize) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if block_end < 8 { return tags; }
let get_u32le = |d: &[u8], off: usize| -> u32 {
if off + 4 > d.len() { return 0; }
u32::from_le_bytes([d[off], d[off+1], d[off+2], d[off+3]])
};
let get_u16le = |d: &[u8], off: usize| -> u16 {
if off + 2 > d.len() { return 0; }
u16::from_le_bytes([d[off], d[off+1]])
};
let mut cur_end = block_end;
let mut dir_pos: Option<usize> = None;
for _ in 0..10 { if cur_end < 8 { break; }
let footer_pos = cur_end - 8;
let size = get_u32le(data, footer_pos) as usize;
let type_bytes = &data[footer_pos+4..footer_pos+8];
if footer_pos < size { break; }
let block_start = footer_pos - size;
if type_bytes == b"SEFT" {
dir_pos = Some(block_start);
break;
}
cur_end = block_start;
}
let dir_start = match dir_pos { Some(p) => p, None => return tags };
let sefh_off = if data.len() > dir_start + 4 {
let block_data = &data[dir_start..cur_end.saturating_sub(8)];
block_data.windows(4).position(|w| w == b"SEFH").map(|p| dir_start + p)
} else {
None
};
let sefh_abs = match sefh_off { Some(p) => p, None => return tags };
if sefh_abs + 12 > data.len() { return tags; }
let count = get_u32le(data, sefh_abs + 8) as usize;
if count > 100 { return tags; }
let mk_sam = |name: &str, raw: crate::value::Value, print: String| -> crate::tag::Tag {
crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "MakerNotes".into(),
family1: "Samsung".into(),
family2: "Other".into(),
},
raw_value: raw,
print_value: print,
priority: 0,
}
};
for i in 0..count {
let entry = sefh_abs + 12 + i * 12;
if entry + 12 > data.len() { break; }
let entry_type = get_u16le(data, entry + 2);
let noff = get_u32le(data, entry + 4) as usize;
let size = get_u32le(data, entry + 8) as usize;
if noff > dir_start || size < 8 { continue; }
let block_abs = dir_start - noff;
if block_abs + size > data.len() { continue; }
let buf2 = &data[block_abs..block_abs + size];
let name_len = get_u32le(buf2, 4) as usize;
if 8 + name_len > size { continue; }
let name_bytes = &buf2[8..8 + name_len];
let name_str = String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string();
let value_bytes = &buf2[8 + name_len..];
match entry_type {
0x0100 => {
if !name_str.is_empty() {
tags.push(mk_sam(
"EmbeddedAudioFileName",
crate::value::Value::String(name_str.clone()),
name_str,
));
}
if !value_bytes.is_empty() {
tags.push(mk_sam(
"EmbeddedAudioFile",
crate::value::Value::Binary(value_bytes.to_vec()),
format!("(Binary data {} bytes, use -b option to extract)", value_bytes.len()),
));
}
}
_ => {} }
}
tags
}
fn parse_mie_trailer(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
let mut pos = 0usize;
parse_mie_elements(data, &mut pos, &mut tags, 0);
tags
}
fn parse_mie_elements(data: &[u8], pos: &mut usize, tags: &mut Vec<crate::tag::Tag>, depth: usize) {
if depth > 8 { return; }
while *pos + 4 <= data.len() {
if data[*pos] != 0x7E { *pos += 1; continue; } *pos += 1;
if *pos + 3 > data.len() { break; }
let format = data[*pos]; *pos += 1;
let name_len = data[*pos] as usize; *pos += 1;
let raw_vlen = data[*pos] as usize; *pos += 1;
if name_len == 0 {
let val_len: usize = if raw_vlen <= 252 {
raw_vlen
} else {
let extra = 1usize << (256 - raw_vlen);
if *pos + extra > data.len() { break; }
let mut v = 0usize;
for k in 0..extra { v = (v << 8) | (data[*pos + k] as usize); }
*pos += extra;
v
};
*pos += val_len;
return; }
if *pos + name_len > data.len() { break; }
let name = String::from_utf8_lossy(&data[*pos..*pos + name_len]).to_string();
*pos += name_len;
let val_len: usize = if raw_vlen <= 252 {
raw_vlen
} else {
let extra = 1usize << (256 - raw_vlen);
if *pos + extra > data.len() { break; }
let mut v = 0usize;
for k in 0..extra { v = (v << 8) | (data[*pos + k] as usize); }
*pos += extra;
v
};
let type_nibble = (format >> 4) & 0x0F;
if type_nibble == 0x1 {
if val_len == 0 {
parse_mie_elements(data, pos, tags, depth + 1);
} else {
if *pos + val_len > data.len() { break; }
let sub_data = &data[*pos..*pos + val_len];
*pos += val_len;
let mut sub_pos = 0usize;
parse_mie_elements(sub_data, &mut sub_pos, tags, depth + 1);
}
} else {
if *pos + val_len > data.len() { break; }
let val_bytes = &data[*pos..*pos + val_len];
*pos += val_len;
if name.starts_with(|c: char| c.is_ascii_digit()) { continue; }
if name.is_empty() { continue; }
let name = match name.as_str() {
"zmie" => "TrailerSignature".to_string(),
_ => name,
};
let val_str = String::from_utf8_lossy(val_bytes).trim_end_matches('\0').to_string();
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(name.clone()),
name: name.clone(),
description: name.clone(),
group: crate::tag::TagGroup {
family0: "MIE".into(), family1: "MIE".into(), family2: "Other".into(),
},
raw_value: crate::value::Value::String(val_str.clone()),
print_value: val_str,
priority: 0,
});
}
}
}
fn decode_infray_version(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 0x50 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP2".into(), family1: "InfiRay".into(), family2: "Camera".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let ru16 = |off: usize| u16::from_le_bytes([data[off], data[off+1]]);
let _ru32 = |off: usize| u32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]);
let _rf32 = |off: usize| f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]);
tags.push(mk("IJPEGVersion", format!("{}.{}.{}.{}", data[0], data[1], data[2], data[3])));
if data.len() > 0x11 {
tags.push(mk("IJPEGOrgType", data[0x0C].to_string()));
tags.push(mk("IJPEGDispType", data[0x0D].to_string()));
tags.push(mk("IJPEGRotate", data[0x0E].to_string()));
tags.push(mk("IJPEGMirrorFlip", data[0x0F].to_string()));
tags.push(mk("ImageColorSwitchable", data[0x10].to_string()));
tags.push(mk("ThermalColorPalette", ru16(0x11).to_string()));
}
if data.len() > 0x30 {
tags.push(mk("IRDataSize", format!("{}", u64::from_le_bytes([data[0x20],data[0x21],data[0x22],data[0x23],data[0x24],data[0x25],data[0x26],data[0x27]]))));
tags.push(mk("IRDataFormat", ru16(0x28).to_string()));
tags.push(mk("IRImageWidth", ru16(0x2A).to_string()));
tags.push(mk("IRImageHeight", ru16(0x2C).to_string()));
tags.push(mk("IRImageBpp", data[0x2E].to_string()));
}
if data.len() > 0x48 {
tags.push(mk("TempDataSize", format!("{}", u64::from_le_bytes([data[0x30],data[0x31],data[0x32],data[0x33],data[0x34],data[0x35],data[0x36],data[0x37]]))));
tags.push(mk("TempDataFormat", ru16(0x38).to_string()));
tags.push(mk("TempImageWidth", ru16(0x3A).to_string()));
tags.push(mk("TempImageHeight", ru16(0x3C).to_string()));
tags.push(mk("TempImageBpp", data[0x3E].to_string()));
}
if data.len() > 0x4E {
tags.push(mk("VisibleDataSize", format!("{}", u64::from_le_bytes([data[0x40],data[0x41],data[0x42],data[0x43],data[0x44],data[0x45],data[0x46],data[0x47]]))));
tags.push(mk("VisibleDataFormat", ru16(0x48).to_string()));
tags.push(mk("VisibleImageWidth", ru16(0x4A).to_string()));
tags.push(mk("VisibleImageHeight", ru16(0x4C).to_string()));
tags.push(mk("VisibleImageBpp", data[0x4E].to_string()));
}
if data.len() > 0x54 {
tags.push(mk("IJPEGTempVersion", format!("{}.{}.{}.{}", data[0x50], data[0x51], data[0x52], data[0x53])));
}
tags
}
fn float8g(v: f32) -> String {
let v = v as f64;
let formatted = format!("{:.8e}", v);
if let Some(e_pos) = formatted.find('e') {
let exp: i32 = formatted[e_pos+1..].parse().unwrap_or(0);
if exp >= -4 && exp < 8 {
let decimals = (7 - exp).max(0) as usize;
let s = format!("{:.*}", decimals, v);
if s.contains('.') {
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
return s.to_string();
}
return s;
}
}
format!("{:.7e}", v)
}
fn decode_flir_fff(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 0x40 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP1".into(), family1: "FLIR".into(), family2: "Camera".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let _ver_be = u32::from_be_bytes([data[0x14], data[0x15], data[0x16], data[0x17]]);
let ver_le = u32::from_le_bytes([data[0x14], data[0x15], data[0x16], data[0x17]]);
let le = ver_le >= 100 && ver_le < 200;
let rd32 = |off: usize| -> u32 {
if off + 4 > data.len() { return 0; }
if le { u32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
else { u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
let _rd_f32 = |off: usize| -> f32 {
if off + 4 > data.len() { return 0.0; }
let bits = rd32(off);
f32::from_bits(bits)
};
let dir_offset = rd32(0x18) as usize;
let num_entries = rd32(0x1C) as usize;
tags.push(mk("CreatorSoftware", String::from_utf8_lossy(&data[4..20]).trim_end_matches('\0').to_string()));
for i in 0..num_entries.min(50) {
let entry_off = dir_offset + i * 0x20;
if entry_off + 0x20 > data.len() { break; }
let rec_type = if le { u16::from_le_bytes([data[entry_off], data[entry_off + 1]]) } else { u16::from_be_bytes([data[entry_off], data[entry_off + 1]]) };
let rec_offset = rd32(entry_off + 0x0C) as usize;
let rec_size = rd32(entry_off + 0x10) as usize;
if rec_offset + rec_size > data.len() { continue; }
let rec = &data[rec_offset..rec_offset + rec_size];
match rec_type {
0x20 => {
if rec.len() >= 200 {
let ci_le = rec.len() > 2 && rec[0] == 2; let rf = |off: usize| -> f32 {
if off + 4 > rec.len() { return 0.0; }
let bits = if ci_le {
u32::from_le_bytes([rec[off], rec[off+1], rec[off+2], rec[off+3]])
} else {
u32::from_be_bytes([rec[off], rec[off+1], rec[off+2], rec[off+3]])
};
f32::from_bits(bits)
};
let fmt_celsius = |off: usize| -> String {
let c = rf(off) - 273.15;
let c = if c == 0.0 { 0.0 } else { c }; format!("{:.1} C", c)
};
tags.push(mk("Emissivity", format!("{:.2}", rf(32))));
tags.push(mk("ObjectDistance", format!("{:.2} m", rf(36))));
tags.push(mk("ReflectedApparentTemperature", fmt_celsius(40)));
tags.push(mk("AtmosphericTemperature", fmt_celsius(44)));
tags.push(mk("IRWindowTemperature", fmt_celsius(48)));
tags.push(mk("IRWindowTransmission", format!("{:.2}", rf(52))));
tags.push(mk("RelativeHumidity", format!("{:.1} %", rf(60) * 100.0)));
tags.push(mk("PlanckR1", float8g(rf(88))));
tags.push(mk("PlanckB", float8g(rf(92))));
tags.push(mk("PlanckF", float8g(rf(96))));
tags.push(mk("AtmosphericTransAlpha1", format!("{:.6}", rf(112))));
tags.push(mk("AtmosphericTransAlpha2", format!("{:.6}", rf(116))));
tags.push(mk("AtmosphericTransBeta1", format!("{:.6}", rf(120))));
tags.push(mk("AtmosphericTransBeta2", format!("{:.6}", rf(124))));
tags.push(mk("AtmosphericTransX", format!("{:.6}", rf(128))));
tags.push(mk("CameraTemperatureRangeMax", fmt_celsius(144)));
tags.push(mk("CameraTemperatureRangeMin", fmt_celsius(148)));
tags.push(mk("CameraTemperatureMaxClip", fmt_celsius(152)));
tags.push(mk("CameraTemperatureMinClip", fmt_celsius(156)));
tags.push(mk("CameraTemperatureMaxSaturated", fmt_celsius(160)));
tags.push(mk("CameraTemperatureMinSaturated", fmt_celsius(164)));
tags.push(mk("CameraTemperatureMaxWarn", fmt_celsius(168)));
tags.push(mk("CameraTemperatureMinWarn", fmt_celsius(172)));
if rec.len() >= 260 {
let cam_model = String::from_utf8_lossy(&rec[212..244]).trim_end_matches('\0').to_string();
if !cam_model.is_empty() { tags.push(mk("CameraModel", cam_model)); }
let cam_pn = String::from_utf8_lossy(&rec[244..260]).trim_end_matches('\0').to_string();
if !cam_pn.is_empty() { tags.push(mk("CameraPartNumber", cam_pn)); }
let cam_sn = String::from_utf8_lossy(&rec[260..276]).trim_end_matches('\0').to_string();
if !cam_sn.is_empty() { tags.push(mk("CameraSerialNumber", cam_sn)); }
}
if rec.len() >= 572 {
let cam_sw = String::from_utf8_lossy(&rec[276..292]).trim_end_matches('\0').to_string();
if !cam_sw.is_empty() { tags.push(mk("CameraSoftware", cam_sw)); }
let lens_model = String::from_utf8_lossy(&rec[368..400]).trim_end_matches('\0').to_string();
if !lens_model.is_empty() { tags.push(mk("LensModel", lens_model)); }
let lens_pn = String::from_utf8_lossy(&rec[400..416]).trim_end_matches('\0').to_string();
tags.push(mk("LensPartNumber", lens_pn));
let lens_sn = String::from_utf8_lossy(&rec[416..432]).trim_end_matches('\0').to_string();
tags.push(mk("LensSerialNumber", lens_sn));
let fov = rf(436);
if fov > 0.0 { tags.push(mk("FieldOfView", format!("{:.1} deg", fov))); }
let filter_model = String::from_utf8_lossy(&rec[492..508]).trim_end_matches('\0').to_string();
tags.push(mk("FilterModel", filter_model));
let filter_pn = String::from_utf8_lossy(&rec[508..540]).trim_end_matches('\0').to_string();
tags.push(mk("FilterPartNumber", filter_pn));
let filter_sn = String::from_utf8_lossy(&rec[540..572]).trim_end_matches('\0').to_string();
tags.push(mk("FilterSerialNumber", filter_sn));
}
tags.push(mk("PeakSpectralSensitivity", format!("{:.1} um", rf(440))));
tags.push(mk("FocusStepCount", rd32(444).to_string()));
tags.push(mk("FocusDistance", format!("{:.1} m", rf(448))));
if rec.len() >= 784 {
let planck_o = if ci_le { i32::from_le_bytes([rec[776], rec[777], rec[778], rec[779]]) } else { i32::from_be_bytes([rec[776], rec[777], rec[778], rec[779]]) };
tags.push(mk("PlanckO", planck_o.to_string()));
tags.push(mk("PlanckR2", float8g(rf(780))));
}
tags.push(mk("FrameRate", format!("{}", u16::from_le_bytes([rec[452], rec[453]]))));
if rec.len() >= 830 {
let ru16 = |off: usize| -> u16 {
if ci_le { u16::from_le_bytes([rec[off], rec[off+1]]) }
else { u16::from_be_bytes([rec[off], rec[off+1]]) }
};
tags.push(mk("RawValueRangeMin", ru16(784).to_string()));
tags.push(mk("RawValueRangeMax", ru16(786).to_string()));
tags.push(mk("RawValueMedian", ru16(824).to_string()));
tags.push(mk("RawValueRange", ru16(828).to_string()));
}
}
}
0x22 => {
if rec.len() >= 28 {
tags.push(mk("PaletteColors", rec[0].to_string()));
let color = |off: usize| -> String {
if off + 3 <= rec.len() { format!("{} {} {}", rec[off], rec[off+1], rec[off+2]) }
else { String::new() }
};
tags.push(mk("AboveColor", color(6)));
tags.push(mk("BelowColor", color(9)));
tags.push(mk("OverflowColor", color(12)));
tags.push(mk("UnderflowColor", color(15)));
tags.push(mk("Isotherm1Color", color(18)));
tags.push(mk("Isotherm2Color", color(21)));
tags.push(mk("PaletteMethod", rec[26].to_string()));
tags.push(mk("PaletteStretch", rec[27].to_string()));
if rec.len() >= 128 {
let fname = String::from_utf8_lossy(&rec[48..80]).trim_end_matches('\0').to_string();
if !fname.is_empty() { tags.push(mk("PaletteFileName", fname)); }
let pname = String::from_utf8_lossy(&rec[80..112]).trim_end_matches('\0').to_string();
if !pname.is_empty() { tags.push(mk("PaletteName", pname)); }
}
let pc = rec[0] as usize;
if pc > 0 && 112 + pc * 3 <= rec.len() {
tags.push(mk("Palette", format!("(Binary data {} bytes)", pc * 3)));
}
}
}
0x01 => {
if rec.len() >= 34 {
let rec_le = u16::from_le_bytes([rec[0], rec[1]]) == 0x0002;
let rw = |off: usize| -> u16 {
if rec_le { u16::from_le_bytes([rec[off], rec[off+1]]) }
else { u16::from_be_bytes([rec[off], rec[off+1]]) }
};
let w = rw(2);
let h = rw(4);
tags.push(mk("RawThermalImageWidth", w.to_string()));
tags.push(mk("RawThermalImageHeight", h.to_string()));
let img_data = &rec[0x20..];
let type_str = if img_data.starts_with(b"\x89PNG\r\n\x1a\n") {
"PNG"
} else if img_data.starts_with(b"\xff\xd8\xff") {
"JPG"
} else if img_data.len() == (w as usize) * (h as usize) * 2 {
"TIFF"
} else {
"DAT"
};
tags.push(mk("RawThermalImageType", type_str.into()));
tags.push(mk("RawThermalImage", format!("(Binary data {} bytes)", rec.len())));
}
}
_ => {}
}
}
tags
}
fn read_flir_makernote_temps(jpeg_data: &[u8]) -> (Option<i64>, Option<i64>) {
let mut pos = 2usize;
while pos + 4 <= jpeg_data.len() {
if jpeg_data[pos] != 0xFF { pos += 1; continue; }
let marker = jpeg_data[pos + 1];
pos += 2;
if marker == 0xD8 || (0xD0..=0xD7).contains(&marker) { continue; }
if pos + 2 > jpeg_data.len() { break; }
let seg_len = u16::from_be_bytes([jpeg_data[pos], jpeg_data[pos + 1]]) as usize;
if seg_len < 2 || pos + seg_len > jpeg_data.len() { break; }
let seg = &jpeg_data[pos + 2..pos + seg_len];
pos += seg_len;
if marker == 0xE1 && seg.len() > 6 && seg.starts_with(b"Exif\0\0") {
let exif = &seg[6..];
if exif.len() < 8 { break; }
let le = &exif[..2] == b"II";
let ru16 = |d: &[u8], off: usize| -> usize {
if off + 2 > d.len() { return 0; }
if le { u16::from_le_bytes([d[off], d[off+1]]) as usize }
else { u16::from_be_bytes([d[off], d[off+1]]) as usize }
};
let ru32 = |d: &[u8], off: usize| -> usize {
if off + 4 > d.len() { return 0; }
if le { u32::from_le_bytes([d[off], d[off+1], d[off+2], d[off+3]]) as usize }
else { u32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]]) as usize }
};
let ri32 = |d: &[u8], off: usize| -> i64 {
if off + 4 > d.len() { return 0; }
if le { i32::from_le_bytes([d[off], d[off+1], d[off+2], d[off+3]]) as i64 }
else { i32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]]) as i64 }
};
let ifd0_off = ru32(exif, 4);
if ifd0_off + 2 > exif.len() { break; }
let ifd0_count = ru16(exif, ifd0_off);
let mut exif_ifd_off = 0usize;
for i in 0..ifd0_count {
let eoff = ifd0_off + 2 + i * 12;
if eoff + 12 > exif.len() { break; }
let tag = ru16(exif, eoff);
if tag == 0x8769 {
exif_ifd_off = ru32(exif, eoff + 8);
break;
}
}
if exif_ifd_off == 0 || exif_ifd_off + 2 > exif.len() { break; }
let exif_count = ru16(exif, exif_ifd_off);
let mut mn_off = 0usize;
let mut mn_count = 0usize;
for j in 0..exif_count {
let etoff = exif_ifd_off + 2 + j * 12;
if etoff + 12 > exif.len() { break; }
let tag = ru16(exif, etoff);
if tag == 0x927C {
mn_count = ru32(exif, etoff + 4);
mn_off = ru32(exif, etoff + 8);
break;
}
}
if mn_off == 0 || mn_off + mn_count > exif.len() { break; }
let mn = &exif[mn_off..mn_off + mn_count];
if mn.len() < 2 { break; }
let mn_entries = ru16(mn, 0);
if mn_entries > 500 { break; }
let mut max_t: Option<i64> = None;
let mut min_t: Option<i64> = None;
for k in 0..mn_entries {
let koff = 2 + k * 12;
if koff + 12 > mn.len() { break; }
let ktag = ru16(mn, koff);
let ktype = ru16(mn, koff + 2);
let kcount_val = ru32(mn, koff + 4);
if (ktype == 5 || ktype == 10) && kcount_val == 1 {
let val_off = ru32(mn, koff + 8); if val_off + 8 <= exif.len() {
let num = ri32(exif, val_off);
let den = ri32(exif, val_off + 4);
let rational_val = if den != 0 { num / den } else { 0 };
match ktag {
0x0001 => max_t = Some(rational_val),
0x0002 => min_t = Some(rational_val),
_ => {}
}
}
}
if max_t.is_some() && min_t.is_some() { break; }
}
return (max_t, min_t);
}
}
(None, None)
}
fn extract_photoshop_irbs(data: &[u8]) -> (Option<Vec<u8>>, Vec<crate::tag::Tag>) {
let mut iptc = None;
let mut tags = Vec::new();
let mut pos = 0;
while pos + 12 <= data.len() {
if &data[pos..pos + 4] != b"8BIM" { break; }
pos += 4;
let resource_id = u16::from_be_bytes([data[pos], data[pos + 1]]);
pos += 2;
let name_len = data[pos] as usize;
pos += 1 + name_len;
if (name_len + 1) % 2 != 0 { pos += 1; }
if pos + 4 > data.len() { break; }
let data_len = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
pos += 4;
if pos + data_len > data.len() { break; }
let irb_data = &data[pos..pos + data_len];
if resource_id == 0x0404 {
iptc = Some(irb_data.to_vec());
} else {
decode_photoshop_irb_subtags(resource_id, irb_data, &mut tags);
let name = photoshop_irb_name(resource_id);
if !name.is_empty() && data_len <= 256 {
let value = decode_photoshop_irb(resource_id, irb_data);
if !value.is_empty() {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Numeric(resource_id),
name: name.to_string(),
description: name.to_string(),
group: crate::tag::TagGroup {
family0: "Photoshop".into(),
family1: "Photoshop".into(),
family2: "Image".into(),
},
raw_value: crate::value::Value::String(value.clone()),
print_value: value,
priority: 0,
});
}
}
}
pos += data_len;
if data_len % 2 != 0 { pos += 1; }
}
(iptc, tags)
}
fn photoshop_irb_name(id: u16) -> &'static str {
match id {
0x03ED => "ResolutionInfo", 0x03F3 => "PrintFlags",
0x0408 => "GridGuidesInfo",
0x040A => "CopyrightFlag",
0x040B => "URL",
0x040C => "ThumbnailImage",
0x0419 => "GlobalAltitude",
0x041A => "ICC_Profile",
0x0421 => "VersionInfo",
0x0425 => "IPTCDigest",
0x0426 => "PrintScale",
0x043C => "MeasurementScale",
0x043D => "TimelineInfo",
0x043E => "SheetDisclosure",
0x043F => "DisplayInfo",
0x0440 => "OnionSkins",
0x2710 => "PrintInfo2",
0x041B => "SpotHalftone",
0x041D => "AlphaIdentifiers",
0x041F => "PrintFlagsInfo",
_ => "",
}
}
fn decode_photoshop_irb_subtags(id: u16, data: &[u8], tags: &mut Vec<crate::tag::Tag>) {
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "Photoshop".into(), family1: "Photoshop".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
match id {
0x03ED if data.len() >= 14 => {
let xres = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as f64 / 65536.0;
tags.push(mk("XResolution", format!("{}", (xres * 100.0).round() / 100.0)));
let units_x = match u16::from_be_bytes([data[4], data[5]]) { 1 => "inches", 2 => "cm", _ => "" };
if !units_x.is_empty() { tags.push(mk("DisplayedUnitsX", units_x.into())); }
let yres = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as f64 / 65536.0;
tags.push(mk("YResolution", format!("{}", (yres * 100.0).round() / 100.0)));
let units_y = match u16::from_be_bytes([data[12], data[13]]) { 1 => "inches", 2 => "cm", _ => "" };
if !units_y.is_empty() { tags.push(mk("DisplayedUnitsY", units_y.into())); }
}
0x0406 if data.len() >= 4 => {
let quality = i16::from_be_bytes([data[0], data[1]]);
tags.push(mk("PhotoshopQuality", (quality + 4).to_string()));
let format = i16::from_be_bytes([data[2], data[3]]);
let fmt_str = match format { 0 => "Standard", 1 => "Optimized", 0x101 => "Progressive", _ => "" };
if !fmt_str.is_empty() { tags.push(mk("PhotoshopFormat", fmt_str.into())); }
}
0x040D if data.len() >= 4 => {
let angle = i32::from_be_bytes([data[0], data[1], data[2], data[3]]);
tags.push(mk("GlobalAngle", angle.to_string()));
}
0x0419 if data.len() >= 4 => {
let alt = i32::from_be_bytes([data[0], data[1], data[2], data[3]]);
tags.push(mk("GlobalAltitude", alt.to_string()));
}
0x041A if data.len() >= 28 => {
if data.len() > 24 {
let name_len = u32::from_be_bytes([data[20], data[21], data[22], data[23]]) as usize;
if 24 + name_len * 2 <= data.len() {
let units: Vec<u16> = data[24..24 + name_len * 2].chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
let name = String::from_utf16_lossy(&units).trim_end_matches('\0').to_string();
if !name.is_empty() { tags.push(mk("SlicesGroupName", name)); }
}
let num_off = 24 + name_len * 2;
if num_off + 4 <= data.len() {
let num = u32::from_be_bytes([data[num_off], data[num_off+1], data[num_off+2], data[num_off+3]]);
tags.push(mk("NumSlices", num.to_string()));
}
}
}
0x0421 if data.len() >= 5 => {
let has_merged = if data[4] != 0 { "Yes" } else { "No" };
tags.push(mk("HasRealMergedData", has_merged.into()));
if data.len() > 9 {
let wname_len = u32::from_be_bytes([data[5], data[6], data[7], data[8]]) as usize;
if 9 + wname_len * 2 <= data.len() {
let units: Vec<u16> = data[9..9 + wname_len * 2].chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
let wname = String::from_utf16_lossy(&units).trim_end_matches('\0').to_string();
if !wname.is_empty() { tags.push(mk("WriterName", wname)); }
let rname_off = 9 + wname_len * 2;
if rname_off + 4 <= data.len() {
let rname_len = u32::from_be_bytes([data[rname_off], data[rname_off+1], data[rname_off+2], data[rname_off+3]]) as usize;
if rname_off + 4 + rname_len * 2 <= data.len() {
let units: Vec<u16> = data[rname_off+4..rname_off+4+rname_len*2].chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
let rname = String::from_utf16_lossy(&units).trim_end_matches('\0').to_string();
if !rname.is_empty() { tags.push(mk("ReaderName", rname)); }
}
}
}
}
}
0x0426 if data.len() >= 14 => {
let style = match u16::from_be_bytes([data[0], data[1]]) {
0 => "Centered", 1 => "Size to Fit", 2 => "User Defined", _ => "",
};
if !style.is_empty() { tags.push(mk("PrintStyle", style.into())); }
let x = f32::from_be_bytes([data[2], data[3], data[4], data[5]]);
let y = f32::from_be_bytes([data[6], data[7], data[8], data[9]]);
tags.push(mk("PrintPosition", format!("{} {}", x, y)));
let scale = f32::from_be_bytes([data[10], data[11], data[12], data[13]]);
tags.push(mk("PrintScale", format!("{}", scale)));
}
0x041E if data.len() >= 4 => {
let count = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
tags.push(mk("URL_List", String::new()));
let mut upos = 4;
for _ in 0..count.min(20) {
if upos + 12 > data.len() { break; }
upos += 8;
let slen = u32::from_be_bytes([data[upos], data[upos+1], data[upos+2], data[upos+3]]) as usize;
upos += 4;
if upos + slen * 2 > data.len() { break; }
let units: Vec<u16> = data[upos..upos+slen*2].chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
let url = String::from_utf16_lossy(&units).trim_end_matches('\0').to_string();
if !url.is_empty() { tags.push(mk("URL", url)); }
upos += slen * 2;
}
}
_ => {}
}
}
fn decode_photoshop_irb(id: u16, data: &[u8]) -> String {
match id {
0x040A => {
if !data.is_empty() {
if data[0] == 0 { "False".into() } else { "True".into() }
} else { String::new() }
}
0x0419 => {
if data.len() >= 4 {
u32::from_be_bytes([data[0], data[1], data[2], data[3]]).to_string()
} else { String::new() }
}
0x0406 => {
if data.len() >= 4 {
let quality = u16::from_be_bytes([data[0], data[1]]);
let format = u16::from_be_bytes([data[2], data[3]]);
let q_str = match quality { 1..=3 => "Low", 4..=6 => "Medium", 7..=9 => "High", 10..=12 => "Maximum", _ => "" };
let f_str = match format { 0 => "Standard", 1 => "Optimized", 2 => "Progressive", _ => "" };
format!("{} ({})", q_str, f_str)
} else { String::new() }
}
0x0425 => {
if data.len() >= 16 {
data[..16].iter().map(|b| format!("{:02x}", b)).collect()
} else { String::new() }
}
_ => {
if data.iter().all(|&b| b >= 0x20 && b < 0x7F || b == 0) {
String::from_utf8_lossy(data).trim_end_matches('\0').to_string()
} else if data.len() <= 4 {
format!("{}", u32::from_be_bytes({
let mut buf = [0u8; 4];
buf[4-data.len()..].copy_from_slice(data);
buf
}))
} else {
String::new()
}
}
}
}
fn process_ducky(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
let mut pos = 0;
let mk = |name: &str, val: String, family2: &str| -> crate::tag::Tag {
crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "Ducky".into(),
family1: "Ducky".into(),
family2: family2.into(),
},
raw_value: crate::value::Value::String(val.clone()),
print_value: val,
priority: 0,
}
};
while pos + 4 <= data.len() {
let tag = u16::from_be_bytes([data[pos], data[pos + 1]]);
let len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if tag == 0 {
break;
}
if pos + len > data.len() {
break;
}
let val_bytes = &data[pos..pos + len];
pos += len;
match tag {
1 => {
if val_bytes.len() >= 4 {
let q = u32::from_be_bytes([val_bytes[0], val_bytes[1], val_bytes[2], val_bytes[3]]);
tags.push(mk("Quality", format!("{}%", q), "Image"));
}
}
2 => {
if val_bytes.len() >= 4 {
let s = decode_utf16be(&val_bytes[4..]);
if !s.is_empty() {
tags.push(mk("Comment", s, "Image"));
}
}
}
3 => {
if val_bytes.len() >= 4 {
let s = decode_utf16be(&val_bytes[4..]);
if !s.is_empty() {
tags.push(mk("Copyright", s, "Author"));
}
}
}
_ => {}
}
}
tags
}
fn decode_utf16be(bytes: &[u8]) -> String {
let words: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.take_while(|&w| w != 0)
.collect();
String::from_utf16_lossy(&words).to_string()
}
fn make_app12_tag_name(raw: &str) -> String {
let cleaned: String = raw.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect();
if cleaned.is_empty() {
return String::new();
}
let mut name = String::new();
let mut chars = cleaned.chars();
if let Some(first) = chars.next() {
name.push(first.to_ascii_uppercase());
name.extend(chars);
}
if name.len() < 2 || name.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
name = format!("Tag{}", name);
}
name
}
fn app12_group2(tag_name: &str, section: &str) -> &'static str {
match tag_name {
"CameraType" | "SerialNumber" | "Version" | "ID" => "Camera",
_ if section.to_ascii_lowercase().contains("camera") => "Camera",
_ => "Image",
}
}
fn process_app12_picture_info(data: &[u8]) -> Vec<crate::tag::Tag> {
let text = String::from_utf8_lossy(data);
let mut tags = Vec::new();
let mut section = String::new();
for line in text.split(|c| c == '\r' || c == '\n' || c == '\0') {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with('[') {
if let Some(end) = line.find(']') {
section = line[1..end].to_string();
}
continue;
}
parse_app12_kv_line(line, §ion, &mut tags);
}
tags
}
fn parse_app12_kv_line(line: &str, section: &str, tags: &mut Vec<crate::tag::Tag>) {
let mut remaining = line;
loop {
let eq_pos = match app12_find_key_start(remaining) {
Some(p) => p,
None => break,
};
let kv = &remaining[eq_pos..];
let eq = match kv.find('=') {
Some(p) => p,
None => break,
};
let raw_key = &kv[..eq];
let after_eq = &kv[eq + 1..];
let val_end = app12_find_key_start(after_eq).unwrap_or(after_eq.len());
let raw_val = after_eq[..val_end].trim_end();
remaining = &after_eq[val_end..];
if !raw_key.is_empty() && !raw_val.is_empty() {
emit_app12_tag(raw_key, raw_val, section, tags);
}
}
}
fn app12_find_key_start(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if app12_is_key_char(bytes[i]) {
let start = i;
while i < len && app12_is_key_char(bytes[i]) {
i += 1;
}
if i < len && bytes[i] == b'=' {
return Some(start);
}
} else {
i += 1;
}
}
None
}
#[inline]
fn app12_is_key_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'#' || b == b'-'
}
fn emit_app12_tag(raw_key: &str, raw_val: &str, section: &str, tags: &mut Vec<crate::tag::Tag>) {
let (tag_name, print_val, group2): (String, String, &str) = match raw_key {
"Shutter" | "shtr" => {
let micros: f64 = raw_val.parse().unwrap_or(0.0);
let secs = micros * 1e-6_f64;
("ExposureTime".to_string(), app12_format_exposure_time(secs), "Image")
}
"Type" => ("CameraType".to_string(), raw_val.to_string(), "Camera"),
"Serial#" => ("SerialNumber".to_string(), raw_val.to_string(), "Camera"),
"Macro" => {
let print = match raw_val { "0" => "Off", "1" => "On", _ => raw_val };
("Macro".to_string(), print.to_string(), "Image")
}
"Flash" => {
let print = match raw_val { "0" => "Off", "1" => "On", _ => raw_val };
("Flash".to_string(), print.to_string(), "Image")
}
"FNumber" => {
let stripped: String = raw_val.chars().skip_while(|c| !c.is_ascii_digit()).collect();
let print = stripped.parse::<f64>().map(|v| format!("{:.1}", v)).unwrap_or(stripped);
("FNumber".to_string(), print, "Image")
}
"TimeDate" => {
let unix: i64 = raw_val.parse().unwrap_or(0);
("DateTimeOriginal".to_string(), app12_unix_to_datetime(unix), "Time")
}
"ExpBias" => ("ExposureCompensation".to_string(), raw_val.to_string(), "Image"),
"FWare" => ("FirmwareVersion".to_string(), raw_val.to_string(), "Camera"),
"Ytarget" => ("YTarget".to_string(), raw_val.to_string(), "Image"),
"ylevel" => ("YLevel".to_string(), raw_val.to_string(), "Image"),
"ImageSize" => {
("ImageSize".to_string(), raw_val.replace('-', "x"), "Image")
}
_ => {
let name = make_app12_tag_name(raw_key);
if name.is_empty() {
return;
}
let g2 = app12_group2(&name, section);
(name, raw_val.to_string(), g2)
}
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(tag_name.clone()),
name: tag_name.clone(),
description: tag_name,
group: crate::tag::TagGroup {
family0: "APP12".into(),
family1: "APP12".into(),
family2: group2.into(),
},
raw_value: crate::value::Value::String(raw_val.to_string()),
print_value: print_val,
priority: 0,
});
}
fn app12_format_exposure_time(secs: f64) -> String {
if secs <= 0.0 {
return "0".to_string();
}
if secs >= 1.0 {
return format!("{:.0}", secs);
}
let denom = (1.0 / secs).round() as u64;
format!("1/{}", denom)
}
fn app12_unix_to_datetime(unix: i64) -> String {
let secs_per_day = 86400i64;
let time_of_day = unix.rem_euclid(secs_per_day);
let days = unix.div_euclid(secs_per_day);
let h = time_of_day / 3600;
let mi = (time_of_day % 3600) / 60;
let s = time_of_day % 60;
let (year, month, day) = app12_days_to_ymd(days);
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", year, month, day, h, mi, s)
}
fn app12_days_to_ymd(days: i64) -> (i64, u32, u32) {
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mo <= 2 { y + 1 } else { y };
(y, mo as u32, d as u32)
}
fn parse_mpf(seg_data: &[u8], jpeg_data: &[u8]) -> Vec<crate::tag::Tag> {
parse_mpf_inner(seg_data, jpeg_data).unwrap_or_default()
}
fn parse_mpf_inner(
seg_data: &[u8],
jpeg_data: &[u8],
) -> Option<Vec<crate::tag::Tag>> {
let mut tags = Vec::new();
if seg_data.len() < 8 {
return None;
}
let mpf = &seg_data[4..];
let tiff_base = seg_data.as_ptr() as usize - jpeg_data.as_ptr() as usize + 4;
let big_endian = match (mpf.get(0)?, mpf.get(1)?) {
(b'M', b'M') => true,
(b'I', b'I') => false,
_ => return None,
};
let ru16 = |data: &[u8], off: usize| -> Option<u16> {
let b = data.get(off..off + 2)?;
Some(if big_endian {
u16::from_be_bytes([b[0], b[1]])
} else {
u16::from_le_bytes([b[0], b[1]])
})
};
let ru32 = |data: &[u8], off: usize| -> Option<u32> {
let b = data.get(off..off + 4)?;
Some(if big_endian {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
} else {
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
})
};
let magic = ru16(mpf, 2)?;
if magic != 42 {
return None;
}
let ifd0_off = ru32(mpf, 4)? as usize;
let mk_ifd_tag = |name: &str, raw: crate::value::Value, print: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "MPF".into(),
family1: "MPF".into(),
family2: "Image".into(),
},
raw_value: raw,
print_value: print,
priority: 0,
};
let entry_count = ru16(mpf, ifd0_off)? as usize;
let mut mp_list_offset: usize = 0;
let mut mp_list_byte_count: usize = 0;
for i in 0..entry_count {
let eoff = ifd0_off + 2 + i * 12;
if eoff + 12 > mpf.len() {
break;
}
let tag_id = ru16(mpf, eoff)?;
let count = ru32(mpf, eoff + 4)? as usize;
let val32 = ru32(mpf, eoff + 8)?;
match tag_id {
0xb000 => {
let bytes = mpf.get(eoff + 8..eoff + 12).unwrap_or(&[]);
let ver = String::from_utf8_lossy(bytes).into_owned();
tags.push(mk_ifd_tag(
"MPFVersion",
crate::value::Value::String(ver.clone()),
ver,
));
}
0xb001 => {
tags.push(mk_ifd_tag(
"NumberOfImages",
crate::value::Value::U32(val32),
val32.to_string(),
));
}
0xb002 => {
let off = val32 as usize;
if off + count <= mpf.len() {
mp_list_offset = off;
mp_list_byte_count = count;
}
}
_ => {} }
}
let num_entries = mp_list_byte_count / 16;
for idx in 0..num_entries {
let eoff = mp_list_offset + idx * 16;
if eoff + 16 > mpf.len() {
break;
}
let attr = ru32(mpf, eoff)?;
let img_len = ru32(mpf, eoff + 4)?;
let img_off = ru32(mpf, eoff + 8)?;
let dep1 = ru16(mpf, eoff + 12)?;
let dep2 = ru16(mpf, eoff + 14)?;
if img_off == 0 && idx == 0 {
continue;
}
let flags_raw = (attr >> 27) & 0x1F; let fmt_raw = (attr >> 24) & 0x07; let type_raw = attr & 0x00FF_FFFF;
let mk = |name: &str, raw: crate::value::Value, print: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "MPF".into(),
family1: "MPF".into(),
family2: "Image".into(),
},
raw_value: raw,
print_value: print,
priority: 0,
};
let flags_print = {
let mut parts: Vec<&str> = Vec::new();
if flags_raw & (1 << 2) != 0 { parts.push("Representative image"); }
if flags_raw & (1 << 3) != 0 { parts.push("Dependent child image"); }
if flags_raw & (1 << 4) != 0 { parts.push("Dependent parent image"); }
if parts.is_empty() {
flags_raw.to_string()
} else {
parts.join(", ")
}
};
tags.push(mk("MPImageFlags", crate::value::Value::U32(flags_raw), flags_print));
let fmt_print = match fmt_raw {
0 => "JPEG".to_string(),
_ => fmt_raw.to_string(),
};
tags.push(mk("MPImageFormat", crate::value::Value::U32(fmt_raw), fmt_print));
let type_print = match type_raw {
0x000000 => "Undefined".to_string(),
0x010001 => "Large Thumbnail (VGA equivalent)".to_string(),
0x010002 => "Large Thumbnail (full HD equivalent)".to_string(),
0x010003 => "Large Thumbnail (4K equivalent)".to_string(),
0x010004 => "Large Thumbnail (8K equivalent)".to_string(),
0x010005 => "Large Thumbnail (16K equivalent)".to_string(),
0x020001 => "Multi-frame Panorama".to_string(),
0x020002 => "Multi-frame Disparity".to_string(),
0x020003 => "Multi-angle".to_string(),
0x030000 => "Baseline MP Primary Image".to_string(),
0x040000 => "Original Preservation Image".to_string(),
0x050000 => "Gain Map Image".to_string(),
_ => format!("0x{:06X}", type_raw),
};
tags.push(mk("MPImageType", crate::value::Value::U32(type_raw), type_print));
tags.push(mk(
"MPImageLength",
crate::value::Value::U32(img_len),
img_len.to_string(),
));
let abs_start = tiff_base as u64 + img_off as u64;
tags.push(mk(
"MPImageStart",
crate::value::Value::U32(abs_start as u32),
abs_start.to_string(),
));
tags.push(mk(
"DependentImage1EntryNumber",
crate::value::Value::U16(dep1),
dep1.to_string(),
));
tags.push(mk(
"DependentImage2EntryNumber",
crate::value::Value::U16(dep2),
dep2.to_string(),
));
if (type_raw & 0x0F_0000) == 0x01_0000 && img_len > 0 {
let start = tiff_base + img_off as usize;
let end = start + img_len as usize;
let preview = if start < jpeg_data.len() {
jpeg_data[start..end.min(jpeg_data.len())].to_vec()
} else {
Vec::new()
};
let print = format!(
"(Binary data {} bytes, use -b option to extract)",
img_len
);
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("PreviewImage".into()),
name: "PreviewImage".into(),
description: "Preview Image".into(),
group: crate::tag::TagGroup {
family0: "MPF".into(),
family1: "MPF".into(),
family2: "Image".into(),
},
raw_value: crate::value::Value::Binary(preview),
print_value: print,
priority: 0,
});
}
}
Some(tags)
}
fn parse_meta_ifd(data: &[u8]) -> Vec<crate::tag::Tag> {
use crate::metadata::exif::{parse_tiff_header, ByteOrderMark};
let mut tags = Vec::new();
let header = match parse_tiff_header(data) {
Ok(h) => h,
Err(_) => return tags,
};
let ifd_offset = header.ifd0_offset as usize;
if ifd_offset + 2 > data.len() {
return tags;
}
let read_u16 = |d: &[u8], off: usize| -> u16 {
if off + 2 > d.len() { return 0; }
match header.byte_order {
ByteOrderMark::LittleEndian => u16::from_le_bytes([d[off], d[off+1]]),
ByteOrderMark::BigEndian => u16::from_be_bytes([d[off], d[off+1]]),
}
};
let read_u32 = |d: &[u8], off: usize| -> u32 {
if off + 4 > d.len() { return 0; }
match header.byte_order {
ByteOrderMark::LittleEndian => u32::from_le_bytes([d[off], d[off+1], d[off+2], d[off+3]]),
ByteOrderMark::BigEndian => u32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]]),
}
};
let entry_count = read_u16(data, ifd_offset) as usize;
let entries_start = ifd_offset + 2;
let entry_count = entry_count.min((data.len().saturating_sub(entries_start)) / 12);
for i in 0..entry_count {
let eoff = entries_start + i * 12;
if eoff + 12 > data.len() { break; }
let tag_id = read_u16(data, eoff);
let data_type = read_u16(data, eoff + 2);
let count = read_u32(data, eoff + 4);
let elem_size: usize = match data_type {
1 | 2 | 6 | 7 => 1,
3 | 8 => 2,
4 | 9 | 11 | 13 => 4,
5 | 10 | 12 => 8,
_ => continue,
};
let total_size = elem_size * count as usize;
let val_data: &[u8] = if total_size <= 4 {
let end = (eoff + 8 + total_size).min(eoff + 12);
&data[eoff + 8..end]
} else {
let offset = read_u32(data, eoff + 8) as usize;
if offset + total_size > data.len() { continue; }
&data[offset..offset + total_size]
};
let name: &str = match tag_id {
0xC350 => "FilmProductCode",
0xC351 => "ImageSourceEK",
0xC352 => "CaptureConditionsPAR",
0xC353 => "CameraOwner",
0xC354 => "SerialNumber",
0xC355 => "UserSelectGroupTitle",
0xC356 => "DealerIDNumber",
0xC357 => "CaptureDeviceFID",
0xC358 => "EnvelopeNumber",
0xC359 => "FrameNumber",
0xC35A => "FilmCategory",
0xC35B => "FilmGencode",
0xC35C => "ModelAndVersion",
0xC35D => "FilmSize",
0xC35E => "SBA_RGBShifts",
0xC35F => "SBAInputImageColorspace",
0xC360 => "SBAInputImageBitDepth",
0xC361 => "SBAExposureRecord",
0xC362 => "UserAdjSBA_RGBShifts",
0xC363 => "ImageRotationStatus",
0xC364 => "RollGuidElements",
0xC365 => "MetadataNumber",
0xC366 => "EditTagArray",
0xC367 => "Magnification",
0xC36C => "NativeXResolution",
0xC36D => "NativeYResolution",
0xC36E => "KodakEffectsIFD",
0xC36F => "KodakBordersIFD",
0xC37A => "NativeResolutionUnit",
0xC418 => "SourceImageDirectory",
0xC419 => "SourceImageFileName",
0xC41A => "SourceImageVolumeName",
0xC46C => "PrintQuality",
0xC46E => "ImagePrintStatus",
_ => continue,
};
if name == "KodakEffectsIFD" || name == "KodakBordersIFD" {
continue;
}
let is_binary = matches!(name, "SBAExposureRecord" | "UserAdjSBA_RGBShifts");
let print_value = if is_binary {
format!("(Binary data {} bytes, use -b option to extract)", total_size)
} else {
meta_ifd_value_string(data_type, count, val_data, header.byte_order)
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Numeric(tag_id),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "Meta".into(),
family1: "MetaIFD".into(),
family2: "Image".into(),
},
raw_value: crate::value::Value::String(print_value.clone()),
print_value,
priority: 0,
});
}
tags
}
fn meta_ifd_value_string(
data_type: u16,
count: u32,
val_data: &[u8],
byte_order: crate::metadata::exif::ByteOrderMark,
) -> String {
use crate::metadata::exif::ByteOrderMark;
let ru16 = |off: usize| -> u16 {
if off + 2 > val_data.len() { return 0; }
match byte_order {
ByteOrderMark::LittleEndian => u16::from_le_bytes([val_data[off], val_data[off+1]]),
ByteOrderMark::BigEndian => u16::from_be_bytes([val_data[off], val_data[off+1]]),
}
};
let _ri16 = |off: usize| -> i16 { ru16(off) as i16 };
let ru32 = |off: usize| -> u32 {
if off + 4 > val_data.len() { return 0; }
match byte_order {
ByteOrderMark::LittleEndian => u32::from_le_bytes([val_data[off], val_data[off+1], val_data[off+2], val_data[off+3]]),
ByteOrderMark::BigEndian => u32::from_be_bytes([val_data[off], val_data[off+1], val_data[off+2], val_data[off+3]]),
}
};
match data_type {
1 => { let n = count as usize;
if n == 1 { val_data[0].to_string() }
else { val_data[..n.min(val_data.len())].iter().map(|b| b.to_string()).collect::<Vec<_>>().join(" ") }
}
2 => { String::from_utf8_lossy(val_data).trim_end_matches('\0').to_string()
}
3 => { let n = count as usize;
if n == 1 { ru16(0).to_string() }
else { (0..n).map(|i| ru16(i * 2).to_string()).collect::<Vec<_>>().join(" ") }
}
4 | 13 => { let n = count as usize;
if n == 1 { ru32(0).to_string() }
else { (0..n).map(|i| ru32(i * 4).to_string()).collect::<Vec<_>>().join(" ") }
}
5 => { let n = count as usize;
(0..n).map(|i| {
let num = ru32(i * 8);
let den = ru32(i * 8 + 4);
if den == 0 { "0".into() } else { format!("{}", num as f64 / den as f64) }
}).collect::<Vec<_>>().join(" ")
}
7 => { let s = String::from_utf8_lossy(val_data);
let trimmed = s.trim_end_matches('\0');
if trimmed.chars().all(|c| c.is_ascii_graphic() || c == ' ') {
trimmed.to_string()
} else {
val_data.iter().map(|b| format!("{:02X}", b)).collect::<Vec<_>>().concat()
}
}
8 => { let n = count as usize;
if n == 1 { (ru16(0) as i16).to_string() }
else { (0..n).map(|i| (ru16(i * 2) as i16).to_string()).collect::<Vec<_>>().join(" ") }
}
9 => { let n = count as usize;
if n == 1 { (ru32(0) as i32).to_string() }
else { (0..n).map(|i| (ru32(i * 4) as i32).to_string()).collect::<Vec<_>>().join(" ") }
}
_ => String::new(),
}
}
fn parse_canon_vrd(data: &[u8], total_len: usize) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
let header_len = 0x1c_usize;
let footer_len = 0x40_usize;
if total_len < header_len + footer_len || total_len > data.len() {
return tags;
}
let footer_start = total_len - footer_len;
let _ru16be = |d: &[u8], off: usize| -> u16 {
if off + 2 > d.len() { return 0; }
u16::from_be_bytes([d[off], d[off+1]])
};
let ru32be = |d: &[u8], off: usize| -> u32 {
if off + 4 > d.len() { return 0; }
u32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]])
};
let mk = |name: &str, val: String| -> crate::tag::Tag {
crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "CanonVRD".into(),
family1: "CanonVRD".into(),
family2: "Image".into(),
},
raw_value: crate::value::Value::String(val.clone()),
print_value: val,
priority: 0,
}
};
let mut pos = header_len;
let blocks_end = footer_start;
while pos + 8 <= blocks_end {
let block_type = ru32be(data, pos);
let block_len = ru32be(data, pos + 4) as usize;
pos += 8;
if pos + block_len > blocks_end {
break;
}
let block = &data[pos..pos + block_len];
if block_type == 0xffff00f4 {
if block.len() >= 4 {
let rec0_len = ru32be(block, 0) as usize;
if 4 + rec0_len <= block.len() {
let rec0 = &block[4..4 + rec0_len];
let vrd1_size = 0x272_usize;
if rec0.len() >= vrd1_size {
let vrd1 = &rec0[..vrd1_size];
tags.extend(parse_vrd1(vrd1, &mk));
}
}
}
}
pos += block_len;
}
tags
}
fn parse_vrd1(
d: &[u8],
mk: &impl Fn(&str, String) -> crate::tag::Tag,
) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if d.len() < 0x272 { return tags; }
let ru16 = |off: usize| -> u16 {
if off + 2 > d.len() { return 0; }
u16::from_be_bytes([d[off], d[off+1]])
};
let ri16 = |off: usize| -> i16 { ru16(off) as i16 };
let ru32 = |off: usize| -> u32 {
if off + 4 > d.len() { return 0; }
u32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]])
};
let ri32 = |off: usize| -> i32 { ru32(off) as i32 };
let rf32 = |off: usize| -> f32 {
if off + 4 > d.len() { return 0.0; }
f32::from_be_bytes([d[off], d[off+1], d[off+2], d[off+3]])
};
let ver_raw = ru16(0x002);
let ver_str = {
let s = ver_raw.to_string();
if s.len() >= 3 {
format!("{}.{}.{}", &s[..s.len()-2], &s[s.len()-2..s.len()-1], &s[s.len()-1..])
} else {
s
}
};
tags.push(mk("VRDVersion", ver_str));
let wba: Vec<String> = (0..4).map(|i| ru16(0x006 + i*2).to_string()).collect();
tags.push(mk("WBAdjRGGBLevels", wba.join(" ")));
let wb_adj = match ru16(0x018) {
0 => "Auto", 1 => "Daylight", 2 => "Cloudy", 3 => "Tungsten",
4 => "Fluorescent", 5 => "Flash", 8 => "Shade", 9 => "Kelvin",
30 => "Manual (Click)", 31 => "Shot Settings", _ => "",
};
if !wb_adj.is_empty() { tags.push(mk("WhiteBalanceAdj", wb_adj.into())); }
tags.push(mk("WBAdjColorTemp", ru16(0x01a).to_string()));
tags.push(mk("WBFineTuneActive", if ru16(0x024) == 0 { "No" } else { "Yes" }.into()));
tags.push(mk("WBFineTuneSaturation", ru16(0x028).to_string()));
tags.push(mk("WBFineTuneTone", ru16(0x02c).to_string()));
let raw_color = match ru16(0x02e) {
0 => "Shot Settings", 1 => "Faithful", 2 => "Custom", _ => "",
};
if !raw_color.is_empty() { tags.push(mk("RawColorAdj", raw_color.into())); }
tags.push(mk("RawCustomSaturation", ri32(0x030).to_string()));
tags.push(mk("RawCustomTone", ri32(0x034).to_string()));
tags.push(mk("RawBrightnessAdj", format!("{:.2}", ri32(0x038) as f64 / 6000.0)));
let tcp = match ru16(0x03c) {
0 => "Shot Settings", 1 => "Linear", 2 => "Custom 1", 3 => "Custom 2",
4 => "Custom 3", 5 => "Custom 4", 6 => "Custom 5", _ => "",
};
if !tcp.is_empty() { tags.push(mk("ToneCurveProperty", tcp.into())); }
tags.push(mk("DynamicRangeMin", ru16(0x07a).to_string()));
tags.push(mk("DynamicRangeMax", ru16(0x07c).to_string()));
tags.push(mk("ToneCurveActive", if ru16(0x110) == 0 { "No" } else { "Yes" }.into()));
tags.push(mk("ToneCurveMode", if d[0x113] == 0 { "RGB" } else { "Luminance" }.into()));
tags.push(mk("BrightnessAdj", (d[0x114] as i8).to_string()));
tags.push(mk("ContrastAdj", (d[0x115] as i8).to_string()));
tags.push(mk("SaturationAdj", ri16(0x116).to_string()));
tags.push(mk("ColorToneAdj", ri32(0x11e).to_string()));
let tone_curve_str = |off: usize| -> String {
if off + 42 > d.len() { return String::new(); }
let count = ru16(off) as usize;
if count < 2 || count > 10 { return String::new(); }
let mut parts = Vec::new();
for i in 0..count {
let x = ru16(off + 2 + i * 4);
let y = ru16(off + 2 + i * 4 + 2);
parts.push(format!("({},{})", x, y));
}
parts.join(" ")
};
let curve_limits = |off: usize| -> String {
(0..4).map(|i| ru16(off + i*2).to_string()).collect::<Vec<_>>().join(" ")
};
tags.push(mk("LuminanceCurvePoints", tone_curve_str(0x126)));
tags.push(mk("LuminanceCurveLimits", curve_limits(0x150)));
tags.push(mk("ToneCurveInterpolation", if d[0x159] == 0 { "Curve" } else { "Straight" }.into()));
tags.push(mk("RedCurvePoints", tone_curve_str(0x160)));
tags.push(mk("RedCurveLimits", curve_limits(0x18a)));
tags.push(mk("GreenCurvePoints", tone_curve_str(0x19a)));
tags.push(mk("GreenCurveLimits", curve_limits(0x1c4)));
tags.push(mk("BlueCurvePoints", tone_curve_str(0x1d4)));
tags.push(mk("BlueCurveLimits", curve_limits(0x1fe)));
tags.push(mk("RGBCurvePoints", tone_curve_str(0x20e)));
tags.push(mk("RGBCurveLimits", curve_limits(0x238)));
tags.push(mk("CropActive", if ru16(0x244) == 0 { "No" } else { "Yes" }.into()));
tags.push(mk("CropWidth", ru16(0x24a).to_string()));
tags.push(mk("CropHeight", ru16(0x24c).to_string()));
tags.push(mk("SharpnessAdj", ru16(0x25a).to_string()));
let car = match ru16(0x260) {
0 => "Free", 1 => "3:2", 2 => "2:3", 3 => "4:3", 4 => "3:4",
5 => "A-size Landscape", 6 => "A-size Portrait",
7 => "Letter-size Landscape", 8 => "Letter-size Portrait",
9 => "4:5", 10 => "5:4", 11 => "1:1", 12 => "Circle",
65535 => "Custom", _ => "",
};
if !car.is_empty() { tags.push(mk("CropAspectRatio", car.into())); }
{
let v = rf32(0x262);
let s = if v == v.trunc() && v.abs() < 1e7 {
format!("{}", v as i64)
} else {
let _s7 = format!("{:.7e}", v);
format!("{:.7}", v).trim_end_matches('0').trim_end_matches('.').to_string()
};
tags.push(mk("ConstrainedCropWidth", s));
}
{
let v = rf32(0x266);
let s = if v == v.trunc() && v.abs() < 1e7 {
format!("{}", v as i64)
} else {
format!("{:.7}", v).trim_end_matches('0').trim_end_matches('.').to_string()
};
tags.push(mk("ConstrainedCropHeight", s));
}
let cm = match ru16(0x26a) {
0 => "Clear".to_string(),
v => v.to_string(),
};
tags.push(mk("CheckMark", cm));
let wcs = match ru16(0x270) {
0 => "sRGB", 1 => "Adobe RGB", 2 => "Wide Gamut RGB",
3 => "Apple RGB", 4 => "ColorMatch RGB", _ => "",
};
if !wcs.is_empty() { tags.push(mk("WorkColorSpace", wcs.into())); }
tags
}
#[allow(dead_code)]
fn gpmf_format_size(fmt: u8) -> usize {
match fmt {
0x62 | 0x42 => 1, 0x63 => 1, 0x73 | 0x53 => 2, 0x6c | 0x4c | 0x66 => 4, 0x64 => 8, 0x46 => 4, 0x47 | 0x55 => 16, 0x6a | 0x4a => 8, 0x71 | 0x51 => 4, _ => 0,
}
}
fn gpmf_tag_name(tag: &[u8; 4]) -> &'static str {
match tag {
b"CASN" => "CameraSerialNumber",
b"FMWR" => "FirmwareVersion",
b"MINF" => "Model",
b"DVNM" => "DeviceName",
b"SIUN" | b"UNIT" => "", b"SCAL" => "", b"TYPE" => "", b"TSMP" => "", b"TICK" => "", b"TOCK" => "", b"EMPT" => "", b"MTRX" => "", b"ORIN" => "", b"ORIO" => "", b"ACCL" => "Accelerometer",
b"GYRO" => "Gyroscope",
b"MAGN" => "Magnetometer",
b"ISOE" => "ISOSpeeds",
b"ISOG" => "ISO",
b"SHUT" => "ShutterSpeed",
b"WBAL" => "WhiteBalance",
b"WRGB" => "WhiteBalanceRGB",
b"FACE" => "FaceDetected",
b"FCNM" => "FaceNumbers",
b"GPSF" => "GPSMeasureMode",
b"GPSP" => "GPSHPositioningError",
b"GPSU" => "GPSDateTime",
b"GPS5" => "GPSInfo",
b"CDAT" => "CreationDate",
b"MDAT" => "ModifyDate",
b"EISA" => "ElectronicImageStabilization",
b"EISE" => "ElectronicStabilizationOn",
b"YAVG" => "AverageY",
b"HUES" => "HueCount",
b"UNIF" => "Uniformity",
b"SCEN" => "SceneClassification",
b"SROT" => "SensorReadOutTime",
b"MWET" => "WaterDetected",
b"AALP" => "AudioLevel",
b"APTS" => "AudioPTS",
b"MUID" => "MediaUniqueID",
b"EXPT" => "ExposureType",
b"OREN" => "AutoRotation",
b"DZOM" => "DigitalZoomOn",
b"SMTR" => "SpotMeter",
b"PRTN" => "Protune",
b"PIMX" => "AutoISOMax",
b"PIMN" => "AutoISOMin",
b"RATE" => "Rate",
b"PRES" => "PhotoResolution",
b"PHDR" => "HDRSetting",
b"PTEV" => "ExposureCompensation",
b"PTCL" => "ColorMode",
b"PTSH" => "Sharpness",
b"ZMKF" => "ZoomModePinch",
b"FWVS" => "OtherFirmware",
b"KBAT" => "BatteryStatus",
b"STMP" => "", b"STRM" => "", b"DEVC" => "", b"DZMX" => "DigitalZoomAmount",
b"DZST" => "DigitalZoom",
b"ABSC" => "AutoBoostScore",
b"ALLD" => "AutoLowLightDuration",
b"AUDO" => "AudioSetting",
b"BITR" => "BitrateSetting",
b"MMOD" => "MediaMode",
b"LOGS" => "HealthLogs",
_ => "",
}
}
fn parse_gopro_gpmf(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
parse_gpmf_records(data, &mut tags, 0);
tags
}
fn parse_gpmf_records(data: &[u8], tags: &mut Vec<crate::tag::Tag>, depth: usize) {
if depth > 8 { return; }
let mut pos = 0;
while pos + 8 <= data.len() {
let tag_bytes: [u8; 4] = [data[pos], data[pos+1], data[pos+2], data[pos+3]];
if tag_bytes == [0, 0, 0, 0] { break; }
if !tag_bytes.iter().all(|&b| b == b'-' || b == b'_' || b == b' ' || b.is_ascii_alphanumeric()) {
break;
}
let fmt = data[pos + 4];
let sample_size = data[pos + 5] as usize;
let count = u16::from_be_bytes([data[pos + 6], data[pos + 7]]) as usize;
let size = sample_size * count;
pos += 8;
if pos + size > data.len() { break; }
let val_data = &data[pos..pos + size];
let padded = (size + 3) & !3;
pos += padded;
if fmt == 0 {
parse_gpmf_records(val_data, tags, depth + 1);
continue;
}
let name = gpmf_tag_name(&tag_bytes);
if name.is_empty() { continue; }
let val_str = if fmt == 0x63 {
String::from_utf8_lossy(val_data).trim_end_matches('\0').to_string()
} else if fmt == 0x55 && val_data.len() >= 16 {
String::from_utf8_lossy(&val_data[..16]).trim_end_matches('\0').to_string()
} else if (fmt == 0x42 || fmt == 0x62) && size == 1 {
val_data[0].to_string()
} else if (fmt == 0x53 || fmt == 0x73) && size >= 2 {
if count == 1 {
let v = if fmt == 0x73 {
(i16::from_be_bytes([val_data[0], val_data[1]])).to_string()
} else {
u16::from_be_bytes([val_data[0], val_data[1]]).to_string()
};
v
} else {
format!("(Binary data {} bytes)", size)
}
} else if (fmt == 0x4c || fmt == 0x6c) && size >= 4 {
if count == 1 {
if fmt == 0x6c {
i32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]).to_string()
} else {
u32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]).to_string()
}
} else {
format!("(Binary data {} bytes)", size)
}
} else if fmt == 0x66 && size >= 4 {
if count == 1 {
let v = f32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]);
format!("{}", v)
} else {
format!("(Binary data {} bytes)", size)
}
} else if fmt == 0x64 && size >= 8 {
if count == 1 {
let v = f64::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3],
val_data[4], val_data[5], val_data[6], val_data[7]]);
format!("{}", v)
} else {
format!("(Binary data {} bytes)", size)
}
} else if size > 256 {
format!("(Binary data {} bytes)", size)
} else {
String::from_utf8_lossy(val_data).trim_end_matches('\0').to_string()
};
{
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "APP6".into(),
family1: "GoPro".into(),
family2: "Camera".into(),
},
raw_value: crate::value::Value::String(val_str.clone()),
print_value: val_str,
priority: 0,
});
}
}
}
fn parse_ricoh_rmeta(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 20 { return tags; }
let big_endian = match (data[0], data[1]) {
(b'M', b'M') => true,
(b'I', b'I') => false,
_ => true, };
let ru16 = |off: usize| -> u16 {
if off + 2 > data.len() { return 0; }
if big_endian { u16::from_be_bytes([data[off], data[off+1]]) }
else { u16::from_le_bytes([data[off], data[off+1]]) }
};
let rmeta_num = ru16(4);
if rmeta_num != 0 {
return tags;
}
let dir_offset = ru16(8) as usize;
if dir_offset + 2 > data.len() { return tags; }
let num_entries = ru16(dir_offset) as usize;
if num_entries > 100 { return tags; }
let mut section_tags: Vec<String> = Vec::new();
let mut section_vals: Vec<String> = Vec::new();
let mut section_nums: Vec<u16> = Vec::new();
let mut spos = dir_offset + 10; while spos + 4 <= data.len() {
let sec_type = ru16(spos);
let sec_size = ru16(spos + 2) as usize;
if sec_size == 0 { break; }
spos += 4;
let actual_size = sec_size.saturating_sub(2);
if actual_size == 0 || spos + actual_size > data.len() { break; }
let sec_data = &data[spos..spos + actual_size];
if sec_type == 1 {
section_tags = String::from_utf8_lossy(sec_data)
.split('\0')
.take(num_entries + 1)
.map(|s| s.to_string())
.collect();
} else if sec_type == 2 || sec_type == 18 {
section_vals = String::from_utf8_lossy(sec_data)
.split('\0')
.take(num_entries + 1)
.map(|s| s.to_string())
.collect();
} else if sec_type == 3 {
for i in 0..num_entries.min(actual_size / 2) {
section_nums.push(ru16(spos + i * 2));
}
}
spos += actual_size;
}
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: crate::tag::TagGroup {
family0: "APP5".into(),
family1: "RMETA".into(),
family2: "Image".into(),
},
raw_value: crate::value::Value::String(val.clone()),
print_value: val,
priority: 0,
};
for i in 0..num_entries {
let tag = section_tags.get(i).cloned().unwrap_or_default();
let val = section_vals.get(i).cloned().unwrap_or_default();
let num = section_nums.get(i).copied();
if tag.is_empty() && val.is_empty() { continue; }
let name = if tag.is_empty() {
"RMETA_Unknown".to_string()
} else {
tag.split_whitespace()
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().chain(c).collect(),
}
})
.collect::<Vec<_>>()
.join("")
};
let display = if !val.is_empty() {
val
} else if let Some(n) = num {
n.to_string()
} else {
continue;
};
tags.push(mk(&name, display));
}
tags
}
fn decode_infray_factory(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 4 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP4".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let ri16 = |off: usize| -> i16 { if off + 2 > data.len() { 0 } else { i16::from_le_bytes([data[off], data[off+1]]) } };
let ri32 = |off: usize| -> i32 { if off + 4 > data.len() { 0 } else { i32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) } };
let ri8 = |off: usize| -> i8 { if off < data.len() { data[off] as i8 } else { 0 } };
tags.push(mk("IJPEGTempVersion", format!("{}.{}.{}.{}", data[0], data[1], data[2], data[3])));
if data.len() > 0x05 { tags.push(mk("FactDefEmissivity", ri8(0x04).to_string())); }
if data.len() > 0x06 { tags.push(mk("FactDefTau", ri8(0x05).to_string())); }
if data.len() > 0x08 { tags.push(mk("FactDefTa", ri16(0x06).to_string())); }
if data.len() > 0x0A { tags.push(mk("FactDefTu", ri16(0x08).to_string())); }
if data.len() > 0x0C { tags.push(mk("FactDefDist", ri16(0x0A).to_string())); }
if data.len() > 0x10 { tags.push(mk("FactDefA0", ri32(0x0C).to_string())); }
if data.len() > 0x14 { tags.push(mk("FactDefB0", ri32(0x10).to_string())); }
if data.len() > 0x18 { tags.push(mk("FactDefA1", ri32(0x14).to_string())); }
if data.len() > 0x1C { tags.push(mk("FactDefB1", ri32(0x18).to_string())); }
if data.len() > 0x20 { tags.push(mk("FactDefP0", ri32(0x1C).to_string())); }
if data.len() > 0x24 { tags.push(mk("FactDefP1", ri32(0x20).to_string())); }
if data.len() > 0x28 { tags.push(mk("FactDefP2", ri32(0x24).to_string())); }
if data.len() > 0x46 { tags.push(mk("FactRelSensorTemp", ri16(0x44).to_string())); }
if data.len() > 0x48 { tags.push(mk("FactRelShutterTemp", ri16(0x46).to_string())); }
if data.len() > 0x4A { tags.push(mk("FactRelLensTemp", ri16(0x48).to_string())); }
if data.len() > 0x65 { tags.push(mk("FactStatusGain", ri8(0x64).to_string())); }
if data.len() > 0x66 { tags.push(mk("FactStatusEnvOK", ri8(0x65).to_string())); }
if data.len() > 0x67 { tags.push(mk("FactStatusDistOK", ri8(0x66).to_string())); }
if data.len() > 0x68 { tags.push(mk("FactStatusTempMap", ri8(0x67).to_string())); }
tags
}
fn decode_infray_picture(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 4 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP5".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let rf32 = |off: usize| -> f32 {
if off + 4 > data.len() { 0.0 } else { f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
tags.push(mk("EnvironmentTemp", format!("{:.2} C", rf32(0x00))));
tags.push(mk("Distance", format!("{:.2} m", rf32(0x04))));
tags.push(mk("Emissivity", format!("{:.2}", rf32(0x08))));
tags.push(mk("Humidity", format!("{:.1} %", rf32(0x0C) * 100.0)));
if data.len() > 0x14 { tags.push(mk("ReferenceTemp", format!("{:.2} C", rf32(0x10)))); }
if data.len() > 0x21 { tags.push(mk("TempUnit", data[0x20].to_string())); }
if data.len() > 0x22 { tags.push(mk("ShowCenterTemp", data[0x21].to_string())); }
if data.len() > 0x23 { tags.push(mk("ShowMaxTemp", data[0x22].to_string())); }
if data.len() > 0x24 { tags.push(mk("ShowMinTemp", data[0x23].to_string())); }
if data.len() > 0x26 {
let count = u16::from_le_bytes([data[0x24], data[0x25]]);
tags.push(mk("TempMeasureCount", count.to_string()));
}
tags
}
fn decode_infray_mixmode(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.is_empty() { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP6".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let rf32 = |off: usize| -> f32 {
if off + 4 > data.len() { 0.0 } else { f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
tags.push(mk("MixMode", data[0].to_string()));
if data.len() > 5 { tags.push(mk("FusionIntensity", format!("{:.1} %", rf32(0x01) * 100.0))); }
if data.len() > 9 { tags.push(mk("OffsetAdjustment", format!("{}", rf32(0x05)))); }
if data.len() >= 0x09 + 30 * 4 {
let vals: Vec<String> = (0..30).map(|i| format!("{}", rf32(0x09 + i * 4))).collect();
tags.push(mk("CorrectionAsix", vals.join(" ")));
}
tags
}
fn decode_infray_opmode(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.is_empty() { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP7".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let ru32 = |off: usize| -> u32 {
if off + 4 > data.len() { 0 } else { u32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
let rf32 = |off: usize| -> f32 {
if off + 4 > data.len() { 0.0 } else { f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
tags.push(mk("WorkingMode", data[0].to_string()));
if data.len() > 5 { tags.push(mk("IntegralTime", ru32(0x01).to_string())); }
if data.len() > 9 { tags.push(mk("IntegratTimeHdr", ru32(0x05).to_string())); }
if data.len() > 0x0A { tags.push(mk("GainStable", data[0x09].to_string())); }
if data.len() > 0x0B { tags.push(mk("TempControlEnable", data[0x0A].to_string())); }
if data.len() > 0x0F { tags.push(mk("DeviceTemp", format!("{:.2} C", rf32(0x0B)))); }
tags
}
fn decode_infray_isothermal(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 16 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP8".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let rf32 = |off: usize| -> f32 {
f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]])
};
tags.push(mk("IsothermalMax", format!("{}", rf32(0x00))));
tags.push(mk("IsothermalMin", format!("{}", rf32(0x04))));
tags.push(mk("ChromaBarMax", format!("{}", rf32(0x08))));
tags.push(mk("ChromaBarMin", format!("{}", rf32(0x0C))));
tags
}
fn decode_infray_sensor(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 0x100 { return tags; }
let mk = |name: &str, val: String| crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(), description: name.into(),
group: crate::tag::TagGroup { family0: "APP9".into(), family1: "InfiRay".into(), family2: "Image".into() },
raw_value: crate::value::Value::String(val.clone()), print_value: val, priority: 0,
};
let read_str = |off: usize, len: usize| -> String {
if off + len > data.len() { return String::new(); }
String::from_utf8_lossy(&data[off..off+len]).trim_end_matches('\0').to_string()
};
let rf32 = |off: usize| -> f32 {
if off + 4 > data.len() { 0.0 } else { f32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
let v = read_str(0x000, 12); if !v.is_empty() { tags.push(mk("IRSensorManufacturer", v)); }
let v = read_str(0x040, 12); if !v.is_empty() { tags.push(mk("IRSensorName", v)); }
let v = read_str(0x080, 32); if !v.is_empty() { tags.push(mk("IRSensorPartNumber", v)); }
let v = read_str(0x0C0, 32); if !v.is_empty() { tags.push(mk("IRSensorSerialNumber", v)); }
if data.len() > 0x10C {
let v = read_str(0x100, 12); if !v.is_empty() { tags.push(mk("IRSensorFirmware", v)); }
}
if data.len() > 0x144 { tags.push(mk("IRSensorAperture", format!("{:.2}", rf32(0x140)))); }
if data.len() > 0x148 { tags.push(mk("IRFocalLength", format!("{:.2}", rf32(0x144)))); }
if data.len() > 0x18C {
let v = read_str(0x180, 12); if !v.is_empty() { tags.push(mk("VisibleSensorManufacturer", v)); }
}
if data.len() > 0x1CC {
let v = read_str(0x1C0, 12); if !v.is_empty() { tags.push(mk("VisibleSensorName", v)); }
}
if data.len() > 0x220 {
let v = read_str(0x200, 32); if !v.is_empty() { tags.push(mk("VisibleSensorPartNumber", v)); }
}
if data.len() > 0x260 {
let v = read_str(0x240, 32); if !v.is_empty() { tags.push(mk("VisibleSensorSerialNumber", v)); }
}
if data.len() > 0x28C {
let v = read_str(0x280, 12); if !v.is_empty() { tags.push(mk("VisibleSensorFirmware", v)); }
}
if data.len() > 0x2C4 { tags.push(mk("VisibleSensorAperture", format!("{}", rf32(0x2C0)))); }
if data.len() > 0x2C8 { tags.push(mk("VisibleFocalLength", format!("{}", rf32(0x2C4)))); }
tags
}
struct FpxrEntry {
name: String,
#[allow(dead_code)]
size: u32,
stream: Vec<u8>,
}
fn accumulate_fpxr(seg_data: &[u8], contents: &mut Vec<FpxrEntry>) {
if seg_data.len() < 7 { return; }
let seg_type = seg_data[6];
if seg_type == 1 {
if seg_data.len() < 9 { return; }
let num_entries = u16::from_be_bytes([seg_data[7], seg_data[8]]) as usize;
let mut pos = 9;
contents.clear();
for _ in 0..num_entries.min(50) {
if pos + 5 > seg_data.len() { break; }
let size = u32::from_be_bytes([seg_data[pos], seg_data[pos+1], seg_data[pos+2], seg_data[pos+3]]);
let _default = seg_data[pos + 4];
pos += 5;
let name_start = pos;
let mut found_end = false;
while pos + 2 <= seg_data.len() {
let w = u16::from_le_bytes([seg_data[pos], seg_data[pos+1]]);
pos += 2;
if w == 0 { found_end = true; break; }
}
if !found_end { break; }
let name_bytes = &seg_data[name_start..pos.saturating_sub(2)];
let units: Vec<u16> = name_bytes.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
let mut name = String::from_utf16_lossy(&units);
if let Some(slash_pos) = name.rfind('/') {
name = name[slash_pos+1..].to_string();
}
if size == 0xFFFFFFFF {
if pos + 16 > seg_data.len() { break; }
pos += 16; }
contents.push(FpxrEntry {
name,
size,
stream: Vec::new(),
});
}
} else if seg_type == 2 {
if seg_data.len() < 13 { return; }
let index = u16::from_be_bytes([seg_data[7], seg_data[8]]) as usize;
let _offset = u32::from_be_bytes([seg_data[9], seg_data[10], seg_data[11], seg_data[12]]);
if index < contents.len() {
let stream_data = &seg_data[13..];
contents[index].stream.extend_from_slice(stream_data);
}
}
}
fn process_fpxr_segments(contents: &[FpxrEntry]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
for entry in contents {
if entry.stream.is_empty() { continue; }
let name = &entry.name;
if name.contains("Screen Nail") {
let payload = if entry.stream.len() > 0x1c { &entry.stream[0x1c..] } else { &entry.stream };
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("ScreenNail".into()),
name: "ScreenNail".into(), description: "Screen Nail".into(),
group: crate::tag::TagGroup { family0: "FlashPix".into(), family1: "FlashPix".into(), family2: "Other".into() },
raw_value: crate::value::Value::Binary(payload.to_vec()),
print_value: format!("(Binary data {} bytes, use -b option to extract)", payload.len()),
priority: 0,
});
continue;
}
if name.contains("Audio") && name.contains("Stream") {
let payload = if entry.stream.len() > 0x1c { &entry.stream[0x1c..] } else { &entry.stream };
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("AudioStream".into()),
name: "AudioStream".into(), description: "Audio Stream".into(),
group: crate::tag::TagGroup { family0: "FlashPix".into(), family1: "FlashPix".into(), family2: "Other".into() },
raw_value: crate::value::Value::Binary(payload.to_vec()),
print_value: format!("(Binary data {} bytes, use -b option to extract)", payload.len()),
priority: 0,
});
continue;
}
if name.contains("Extension List") {
tags.extend(parse_fpxr_extension_list(&entry.stream));
continue;
}
if name.contains("Audio") && name.contains("Info") {
tags.extend(parse_fpxr_audio_info(&entry.stream));
continue;
}
}
tags
}
fn parse_fpxr_extension_list(data: &[u8]) -> Vec<crate::tag::Tag> {
parse_ole_props(data, "FlashPix", &|id| match id & 0x0000ffff {
0x0001 => Some("ExtensionName"),
0x0002 => Some("ExtensionClassID"),
0x0003 => Some("ExtensionPersistence"),
0x0004 => Some("ExtensionCreateDate"),
0x0005 => Some("ExtensionModifyDate"),
0x0006 => Some("CreatingApplication"),
0x0007 => Some("ExtensionDescription"),
0x1000 => Some("Storage-StreamPathname"),
_ => match id {
0x10000000 => Some("UsedExtensionNumbers"),
_ => None,
}
})
}
fn parse_fpxr_audio_info(data: &[u8]) -> Vec<crate::tag::Tag> {
parse_ole_props(data, "FlashPix", &|id| match id {
0x01 => Some("CodePage"),
_ => None,
})
}
fn parse_ole_props<'a>(data: &[u8], family: &str, tag_map: &dyn Fn(u32) -> Option<&'a str>) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
if data.len() < 28 { return tags; }
let le = data[0] == 0xFE && data[1] == 0xFF;
let ru32 = |off: usize| -> u32 {
if off + 4 > data.len() { return 0; }
if le { u32::from_le_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
else { u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) }
};
let ru16 = |off: usize| -> u16 {
if off + 2 > data.len() { return 0; }
if le { u16::from_le_bytes([data[off], data[off+1]]) }
else { u16::from_be_bytes([data[off], data[off+1]]) }
};
let section_count = ru32(24) as usize;
if section_count == 0 || data.len() < 28 + section_count * 20 { return tags; }
for s in 0..section_count.min(4) {
let sec_offset = ru32(28 + 16 + s * 20) as usize;
if sec_offset + 8 > data.len() { continue; }
let prop_count = ru32(sec_offset + 4) as usize;
if prop_count > 500 { continue; }
for i in 0..prop_count.min(500) {
let entry_off = sec_offset + 8 + i * 8;
if entry_off + 8 > data.len() { break; }
let prop_id = ru32(entry_off);
let prop_offset = ru32(entry_off + 4) as usize;
let val_off = sec_offset + prop_offset;
if val_off + 4 > data.len() { continue; }
let vtype = ru32(val_off) & 0xFFF;
let tag_name = if prop_id == 1 { Some("CodePage") } else { tag_map(prop_id) };
let tag_name: &str = match tag_name { Some(n) => n, None => continue };
let val_str = match vtype {
2 | 18 => { if val_off + 6 > data.len() { continue; }
let v = ru16(val_off + 4) as i16;
if tag_name == "CodePage" {
match v as u16 { 1200 => "Unicode UTF-16, little endian".into(), 1252 => "Windows Latin 1".into(), _ => v.to_string() }
} else if tag_name == "ExtensionPersistence" {
match v { 0 => "Always Valid".into(), 1 => "Invalidated By Modification".into(), _ => v.to_string() }
} else { v.to_string() }
}
3 => { if val_off + 8 > data.len() { continue; } ru32(val_off + 4).to_string() }
30 => { if val_off + 8 > data.len() { continue; }
let slen = ru32(val_off + 4) as usize;
if val_off + 8 + slen > data.len() { continue; }
String::from_utf8_lossy(&data[val_off+8..val_off+8+slen]).trim_end_matches('\0').to_string()
}
31 => { if val_off + 8 > data.len() { continue; }
let chars = ru32(val_off + 4) as usize;
if val_off + 8 + chars * 2 > data.len() { continue; }
let u16s: Vec<u16> = (0..chars).map(|j| ru16(val_off + 8 + j * 2)).collect();
String::from_utf16_lossy(&u16s).trim_end_matches('\0').to_string()
}
64 => { if val_off + 12 > data.len() { continue; }
let lo = ru32(val_off + 4) as u64;
let hi = ru32(val_off + 8) as u64;
let ft = (hi << 32) | lo;
if ft == 0 { "0000:00:00 00:00:00".into() } else {
let secs = ft / 10_000_000;
let unix = secs.wrapping_sub(11644473600) as i64;
let s = (unix.rem_euclid(60)) as u32; let m = ((unix / 60).rem_euclid(60)) as u32;
let h = ((unix / 3600).rem_euclid(24)) as u32;
let days = unix.div_euclid(86400);
let d = days + 719468;
let era = if d >= 0 { d } else { d - 146096 } / 146097;
let doe = (d - era * 146097) as u32;
let yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365*yoe + yoe/4 - yoe/100);
let mp = (5*doy + 2) / 153;
let dd = doy - (153*mp + 2)/5 + 1;
let mm = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mm <= 2 { y + 1 } else { y };
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", y, mm, dd, h, m, s)
}
}
0x1002 | 0x1003 | 0x1012 | 0x1013 => { if val_off + 8 > data.len() { continue; }
let count = ru32(val_off + 4) as usize;
let esz = if vtype == 0x1002 || vtype == 0x1012 { 2usize } else { 4 };
(0..count.min(100)).filter_map(|j| {
let eoff = val_off + 8 + j * esz;
if eoff + esz > data.len() { return None; }
Some(if esz == 2 { ru16(eoff).to_string() } else { ru32(eoff).to_string() })
}).collect::<Vec<_>>().join(", ")
}
72 | 65 => { if val_off + 20 > data.len() { continue; }
let d1 = ru32(val_off+4); let d2 = ru16(val_off+8); let d3 = ru16(val_off+10);
let d4 = &data[val_off+12..val_off+20];
format!("{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
d1, d2, d3, d4[0], d4[1], d4[2], d4[3], d4[4], d4[5], d4[6], d4[7])
}
_ => continue,
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(tag_name.into()),
name: tag_name.into(), description: tag_name.into(),
group: crate::tag::TagGroup { family0: family.into(), family1: family.into(), family2: "Other".into() },
raw_value: crate::value::Value::String(val_str.clone()), print_value: val_str, priority: 0,
});
}
}
tags
}
fn parse_qualcomm(data: &[u8]) -> Vec<crate::tag::Tag> {
let mut tags = Vec::new();
let mut pos = 0;
let end = data.len();
while pos + 3 < end {
let val_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
let tag_len = data[pos + 2] as usize;
if pos + 3 + tag_len + 5 + val_len > end {
break;
}
let tag_bytes = &data[pos + 3..pos + 3 + tag_len];
let tag_str = String::from_utf8_lossy(tag_bytes).to_string();
pos += 3 + tag_len; let fmt = data[pos];
pos += 5;
let value_data = &data[pos..pos + val_len];
pos += val_len;
let (val_str, raw_value) = match fmt {
0 => { let v = if !value_data.is_empty() { value_data[0] as u64 } else { 0 };
(v.to_string(), crate::value::Value::U32(v as u32))
}
1 => { let v = if !value_data.is_empty() { value_data[0] as i8 as i64 } else { 0 };
(v.to_string(), crate::value::Value::String(v.to_string()))
}
2 => { if value_data.len() >= 2 {
let v = u16::from_le_bytes([value_data[0], value_data[1]]);
(v.to_string(), crate::value::Value::U16(v))
} else { continue; }
}
3 => { if value_data.len() >= 2 {
let v = i16::from_le_bytes([value_data[0], value_data[1]]);
(v.to_string(), crate::value::Value::String(v.to_string()))
} else { continue; }
}
4 => { if value_data.len() >= 4 {
let v = u32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
(v.to_string(), crate::value::Value::U32(v))
} else { continue; }
}
5 => { if value_data.len() >= 4 {
let v = i32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
(v.to_string(), crate::value::Value::String(v.to_string()))
} else { continue; }
}
6 => { if value_data.len() >= 4 {
let v = f32::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3]]);
(v.to_string(), crate::value::Value::String(v.to_string()))
} else { continue; }
}
7 => { if value_data.len() >= 8 {
let v = f64::from_le_bytes([value_data[0], value_data[1], value_data[2], value_data[3],
value_data[4], value_data[5], value_data[6], value_data[7]]);
(v.to_string(), crate::value::Value::String(v.to_string()))
} else { continue; }
}
_ => continue, };
let name = qualcomm_tag_to_name(&tag_str);
if name.is_empty() { continue; }
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(tag_str.clone()),
name: name.clone(),
description: name.clone(),
group: crate::tag::TagGroup {
family0: "Qualcomm".into(),
family1: "Qualcomm".into(),
family2: "Camera".into(),
},
raw_value,
print_value: val_str,
priority: 0,
});
}
tags
}
fn qualcomm_tag_to_name(tag: &str) -> String {
let mut s = tag.to_string();
let prefixes = ["asf", "awb", "aec", "afr", "af_", "la_", "r2_tl", "tl"];
let mut matched = false;
for pfx in &prefixes {
if s.starts_with(pfx) {
let upper = pfx.to_uppercase();
s = format!("{}{}", upper, &s[pfx.len()..]);
matched = true;
break;
}
}
if !matched {
let mut chars = s.chars();
if let Some(c) = chars.next() {
s = format!("{}{}", c.to_uppercase(), chars.as_str());
}
}
let mut result = String::new();
let mut prev_underscore = false;
for c in s.chars() {
if c == '_' {
prev_underscore = true;
result.push('_');
} else if prev_underscore && c.is_ascii_lowercase() {
result.push(c.to_ascii_uppercase());
prev_underscore = false;
} else {
result.push(c);
prev_underscore = false;
}
}
s = result;
if let Some(bracket_pos) = s.find('[') {
if s.ends_with(']') {
let inner = &s[bracket_pos + 1..s.len() - 1];
if let Ok(n) = inner.parse::<u32>() {
s = format!("{}_{:02}", &s[..bracket_pos], n);
}
}
}
s.retain(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
let has_underscore = s.contains('_');
if has_underscore {
let mut out = String::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '_' && i + 2 < chars.len()
&& chars[i + 1].is_ascii_uppercase()
&& chars[i + 2].is_ascii_lowercase()
{
i += 1;
continue;
}
out.push(chars[i]);
i += 1;
}
s = out;
let mut out2 = String::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '_' && i > 0 && i + 1 < chars.len()
&& (chars[i - 1].is_ascii_lowercase() || chars[i - 1].is_ascii_digit())
&& chars[i + 1].is_ascii_uppercase()
{
i += 1;
continue;
}
out2.push(chars[i]);
i += 1;
}
s = out2;
let mut out3 = String::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '_' && i > 0 && i + 1 < chars.len()
&& chars[i - 1].is_ascii_alphabetic()
&& chars[i + 1].is_ascii_digit()
{
i += 1;
continue;
}
out3.push(chars[i]);
i += 1;
}
s = out3;
}
s
}
fn extract_ciff_freebytes(data: &[u8]) -> Vec<crate::tag::Tag> {
if data.len() < 14 {
return Vec::new();
}
let is_le = data[0] == b'I' && data[1] == b'I';
if !is_le && !(data[0] == b'M' && data[1] == b'M') {
return Vec::new();
}
let hlen = if is_le {
u32::from_le_bytes([data[2], data[3], data[4], data[5]]) as usize
} else {
u32::from_be_bytes([data[2], data[3], data[4], data[5]]) as usize
};
if hlen < 14 || data.len() < hlen || &data[6..10] != b"HEAP" {
return Vec::new();
}
let mut tags = Vec::new();
ciff_find_freebytes(data, hlen, data.len(), is_le, &mut tags, 0);
tags
}
fn ciff_find_freebytes(
data: &[u8], block_start: usize, block_end: usize,
is_le: bool, tags: &mut Vec<crate::tag::Tag>, depth: u32,
) {
if depth > 10 || block_end <= block_start + 4 || block_end > data.len() {
return;
}
let ru16 = |off: usize| -> u16 {
if is_le { u16::from_le_bytes([data[off], data[off + 1]]) }
else { u16::from_be_bytes([data[off], data[off + 1]]) }
};
let ru32 = |off: usize| -> u32 {
if is_le { u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) }
else { u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) }
};
let dir_offset = ru32(block_end - 4) as usize + block_start;
if dir_offset + 2 > block_end { return; }
let num_entries = ru16(dir_offset) as usize;
let mut pos = dir_offset + 2;
for _ in 0..num_entries {
if pos + 10 > block_end { break; }
let raw_tag = ru16(pos);
let size_field = ru32(pos + 2) as usize;
let value_offset = ru32(pos + 6) as usize;
let entry_pos = pos;
pos += 10;
if (raw_tag & 0x8000) != 0 { continue; }
let tag_id = raw_tag & 0x3FFF;
let data_type = (raw_tag >> 8) & 0x38;
let value_in_dir = (raw_tag & 0x4000) != 0;
if (data_type == 0x28 || data_type == 0x30) && !value_in_dir {
let abs_offset = value_offset + block_start;
if abs_offset + size_field <= block_end {
ciff_find_freebytes(data, abs_offset, abs_offset + size_field, is_le, tags, depth + 1);
}
continue;
}
if tag_id != 0x0001 { continue; }
let value_data = if value_in_dir {
if entry_pos + 10 > data.len() { continue; }
&data[entry_pos + 2..entry_pos + 10]
} else {
let abs_offset = value_offset + block_start;
if abs_offset + size_field > data.len() { continue; }
&data[abs_offset..abs_offset + size_field]
};
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Numeric(0x0001),
name: "FreeBytes".into(),
description: "Free Bytes".into(),
group: crate::tag::TagGroup {
family0: "CanonRaw".into(),
family1: "CanonRaw".into(),
family2: "Camera".into(),
},
raw_value: crate::value::Value::Binary(value_data.to_vec()),
print_value: format!("(Binary data {} bytes, use -b option to extract)", value_data.len()),
priority: 0,
});
return; }
}
fn parse_jps(data: &[u8]) -> Vec<Tag> {
use crate::tag::{TagId, TagGroup};
use crate::value::Value;
let mut tags = Vec::new();
let mk = |name: &str, description: &str, val: Value| -> Tag {
let print_value = val.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: "APP3".into(),
family1: "JPS".into(),
family2: "Image".into(),
},
raw_value: val,
print_value,
priority: 0,
}
};
let hdr_length = if data.len() >= 10 {
u16::from_be_bytes([data[8], data[9]]) as usize
} else {
return tags;
};
if data.len() < 14 {
return tags;
}
let sep_raw = u32::from_be_bytes([data[10], data[11], data[12], data[13]]);
let media_type = sep_raw & 0xff;
let separation = (sep_raw >> 24) & 0xff;
if media_type == 1 {
tags.push(mk("JPSSeparation", "JPS Separation", Value::U32(separation)));
}
if data.len() > 11 {
let flags = data[11];
let mut flag_strs = Vec::new();
if flags & (1 << 0) != 0 { flag_strs.push("Half height"); }
if flags & (1 << 1) != 0 { flag_strs.push("Half width"); }
if flags & (1 << 2) != 0 { flag_strs.push("Left field first"); }
let flag_str = if flag_strs.is_empty() {
String::new()
} else {
flag_strs.join(", ")
};
tags.push(mk("JPSFlags", "JPS Flags", Value::String(flag_str)));
}
if data.len() > 12 {
let layout = data[12];
let layout_str = if media_type == 0 {
match layout {
0 => "Both Eyes",
1 => "Left Eye",
2 => "Right Eye",
_ => "Unknown",
}
} else {
match layout {
1 => "Interleaved",
2 => "Side By Side",
3 => "Over Under",
4 => "Anaglyph",
_ => "Unknown",
}
};
tags.push(mk("JPSLayout", "JPS Layout", Value::String(layout_str.to_string())));
}
if data.len() > 13 {
let jtype = data[13];
let type_str = match jtype {
0 => "Mono",
1 => "Stereo",
_ => "Unknown",
};
tags.push(mk("JPSType", "JPS Type", Value::String(type_str.to_string())));
}
let comment_offset = 0x10 + (hdr_length.saturating_sub(4));
if data.len() > comment_offset {
let comment_bytes = &data[comment_offset..];
let comment = String::from_utf8_lossy(comment_bytes)
.trim_end_matches('\0')
.to_string();
if !comment.is_empty() {
tags.push(mk("JPSComment", "JPS Comment", Value::String(comment)));
}
}
tags
}