use crate::error::{Error, Result};
use crate::metadata::XmpReader;
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
pub fn read_dicom(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 136 || &data[128..132] != b"DICM" {
return Err(Error::InvalidData("not a DICOM file".into()));
}
let mut tags = Vec::new();
tags.push(mktag("DICOM", "FileFormat", "File Format", Value::String("DICOM".into())));
let mut pos = 132;
let mut count = 0;
while pos + 8 <= data.len() && count < 100 {
let group = u16::from_le_bytes([data[pos], data[pos + 1]]);
let element = u16::from_le_bytes([data[pos + 2], data[pos + 3]]);
let vr = &data[pos + 4..pos + 6];
let (val_len, hdr_size) = if vr[0].is_ascii_uppercase() && vr[1].is_ascii_uppercase() {
let len = u16::from_le_bytes([data[pos + 6], data[pos + 7]]) as usize;
(len, 8)
} else {
let len = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]) as usize;
(len, 8)
};
pos += hdr_size;
if val_len == 0 || val_len > 10000 || pos + val_len > data.len() {
pos += val_len.min(data.len() - pos);
count += 1;
continue;
}
let val_data = &data[pos..pos + val_len];
let text = String::from_utf8_lossy(val_data).trim().trim_end_matches('\0').to_string();
match (group, element) {
(0x0008, 0x0060) => tags.push(mktag("DICOM", "Modality", "Modality", Value::String(text))),
(0x0008, 0x0070) => tags.push(mktag("DICOM", "Manufacturer", "Manufacturer", Value::String(text))),
(0x0008, 0x1030) => tags.push(mktag("DICOM", "StudyDescription", "Study Description", Value::String(text))),
(0x0010, 0x0010) => tags.push(mktag("DICOM", "PatientName", "Patient Name", Value::String(text))),
(0x0010, 0x0020) => tags.push(mktag("DICOM", "PatientID", "Patient ID", Value::String(text))),
(0x0028, 0x0010) => {
if val_len == 2 {
let v = u16::from_le_bytes([val_data[0], val_data[1]]);
tags.push(mktag("DICOM", "Rows", "Image Rows", Value::U16(v)));
}
}
(0x0028, 0x0011) => {
if val_len == 2 {
let v = u16::from_le_bytes([val_data[0], val_data[1]]);
tags.push(mktag("DICOM", "Columns", "Image Columns", Value::U16(v)));
}
}
_ => {}
}
pos += val_len;
count += 1;
if group > 0x0028 { break; } }
Ok(tags)
}
pub fn read_fits(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 80 || !data.starts_with(b"SIMPLE =") {
return Err(Error::InvalidData("not a FITS file".into()));
}
let mut tags = Vec::new();
let mut pos = 0;
let mut continue_tag: Option<String> = None;
let mut continue_val: String = String::new();
while pos + 80 <= data.len() {
let record = &data[pos..pos + 80];
let keyword = String::from_utf8_lossy(&record[..8]).trim_end().to_string();
pos += 80;
if keyword == "END" { break; }
if keyword == "CONTINUE" {
if continue_tag.is_some() {
let val_raw = String::from_utf8_lossy(&record[8..]).to_string();
let (more, cont) = fits_parse_continued_value(&val_raw);
continue_val.push_str(&more);
if !cont {
let tag_name = continue_tag.take().unwrap();
let tag_desc = fits_tag_description(&tag_name);
tags.push(mktag("FITS", &tag_name, &tag_desc, Value::String(continue_val.clone())));
continue_val.clear();
}
}
continue;
}
if let Some(tag_name) = continue_tag.take() {
let tag_desc = fits_tag_description(&tag_name);
tags.push(mktag("FITS", &tag_name, &tag_desc, Value::String(continue_val.clone())));
continue_val.clear();
}
if keyword == "COMMENT" || keyword == "HISTORY" {
let val = String::from_utf8_lossy(&record[8..]).trim_end().to_string();
let name = if keyword == "COMMENT" { "Comment" } else { "History" };
tags.push(mktag("FITS", name, name, Value::String(val)));
continue;
}
if keyword.is_empty() { continue; }
if record.len() <= 10 || record[8] != b'=' { continue; }
let val_raw = String::from_utf8_lossy(&record[10..]).to_string();
let (value, is_continued) = fits_parse_value(&val_raw);
if value.is_empty() { continue; }
let tag_name = fits_keyword_to_name(&keyword);
let tag_desc = fits_tag_description(&tag_name);
if is_continued {
continue_tag = Some(tag_name);
continue_val = value;
} else {
tags.push(mktag("FITS", &tag_name, &tag_desc, Value::String(value)));
}
}
if let Some(tag_name) = continue_tag.take() {
let tag_desc = fits_tag_description(&tag_name);
tags.push(mktag("FITS", &tag_name, &tag_desc, Value::String(continue_val.clone())));
}
Ok(tags)
}
fn fits_parse_value(s: &str) -> (String, bool) {
let s = s.trim_start();
if s.starts_with('\'') {
let inner = &s[1..];
let mut result = String::new();
let mut chars = inner.chars().peekable();
loop {
match chars.next() {
None => break,
Some('\'') => {
if chars.peek() == Some(&'\'') {
chars.next();
result.push('\'');
} else {
break; }
}
Some(c) => result.push(c),
}
}
let trimmed = result.trim_end().to_string();
let is_cont = trimmed.ends_with('&');
let val = if is_cont { trimmed[..trimmed.len()-1].to_string() } else { trimmed };
(val, is_cont)
} else {
let val = s.splitn(2, '/').next().unwrap_or("").trim().to_string();
let val = val.replace('D', "e").replace('E', "e");
if val.is_empty() { return (String::new(), false); }
(val, false)
}
}
fn fits_parse_continued_value(s: &str) -> (String, bool) {
fits_parse_value(s)
}
fn fits_keyword_to_name(keyword: &str) -> String {
match keyword {
"SIMPLE" => return String::new(), "BITPIX" => "Bitpix".into(),
"NAXIS" => "Naxis".into(),
"NAXIS1" => "Naxis1".into(),
"NAXIS2" => "Naxis2".into(),
"EXTEND" => "Extend".into(),
"ORIGIN" => "Origin".into(),
"TELESCOP" => "Telescope".into(),
"BACKGRND" => "Background".into(),
"INSTRUME" => "Instrument".into(),
"OBJECT" => "Object".into(),
"OBSERVER" => "Observer".into(),
"DATE" => "CreateDate".into(),
"AUTHOR" => "Creator".into(),
"REFERENC" => "Reference".into(),
"DATE-OBS" => "ObservationDate".into(),
"TIME-OBS" => "ObservationTime".into(),
"DATE-END" => "ObservationDateEnd".into(),
"TIME-END" => "ObservationTimeEnd".into(),
"COMMENT" => "Comment".into(),
"HISTORY" => "History".into(),
_ => {
let lower = keyword.to_lowercase();
let mut result = String::new();
let mut capitalize_next = true;
for ch in lower.chars() {
if ch == '_' || ch == '-' {
capitalize_next = true;
} else if capitalize_next {
for c in ch.to_uppercase() { result.push(c); }
capitalize_next = false;
} else {
result.push(ch);
}
}
result
}
}
}
fn fits_tag_description(name: &str) -> String {
let mut desc = String::new();
for ch in name.chars() {
if ch.is_uppercase() && !desc.is_empty() {
desc.push(' ');
}
desc.push(ch);
}
desc
}
pub fn read_flv(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 9 || !data.starts_with(b"FLV\x01") {
return Err(Error::InvalidData("not an FLV file".into()));
}
let mut tags = Vec::new();
let flags = data[4];
let has_audio = flags & 0x04 != 0;
let has_video = flags & 0x01 != 0;
let header_offset = u32::from_be_bytes([data[5], data[6], data[7], data[8]]) as usize;
let mut pos = header_offset;
if pos + 4 <= data.len() {
pos += 4;
}
let mut found_meta = false;
let mut audio_info_found = false;
let mut video_info_found = false;
while pos + 11 <= data.len() && (!found_meta || (!audio_info_found && has_audio) || (!video_info_found && has_video)) {
let tag_type = data[pos];
let data_size = ((data[pos+1] as usize) << 16) | ((data[pos+2] as usize) << 8) | (data[pos+3] as usize);
let tag_start = pos + 11;
let tag_end = tag_start + data_size;
if tag_end > data.len() { break; }
match tag_type {
0x12 => {
if !found_meta {
let tag_data = &data[tag_start..tag_end];
flv_parse_amf_metadata(tag_data, &mut tags);
found_meta = true;
}
}
0x08 if !audio_info_found => {
if data_size >= 1 {
let info_byte = data[tag_start];
let codec_id = (info_byte >> 4) & 0x0f;
let sample_rate_idx = (info_byte >> 2) & 0x03;
let sample_size = (info_byte >> 1) & 0x01;
let stereo = info_byte & 0x01;
let codec_name = match codec_id {
0 => "Uncompressed",
1 => "ADPCM",
2 => "MP3",
3 => "Uncompressed LE",
4 => "Nellymoser 16kHz",
5 => "Nellymoser 8kHz",
6 => "Nellymoser",
7 => "G711 A-law",
8 => "G711 mu-law",
10 => "AAC",
11 => "Speex",
14 => "MP3 8kHz",
15 => "Device-specific",
_ => "Unknown",
};
let sample_rate = match sample_rate_idx {
0 => "5512", 1 => "11025", 2 => "22050", 3 => "44100", _ => "Unknown",
};
let channels = if stereo == 1 { "2 (stereo)" } else { "1 (mono)" };
let bits = if sample_size == 1 { "16" } else { "8" };
tags.push(mktag("FLV", "AudioCodecID", "Audio Codec ID", Value::String(format!("{}", codec_id))));
tags.push(mktag("FLV", "AudioSampleRate", "Audio Sample Rate", Value::String(sample_rate.to_string())));
tags.push(mktag("FLV", "AudioBitsPerSample", "Audio Bits Per Sample", Value::String(bits.to_string())));
tags.push(mktag("FLV", "AudioChannels", "Audio Channels", Value::String(channels.to_string())));
tags.push(mktag("FLV", "AudioEncoding", "Audio Encoding", Value::String(codec_name.to_string())));
audio_info_found = true;
}
}
0x09 if !video_info_found => {
if data_size >= 1 {
let info_byte = data[tag_start];
let codec_id = info_byte & 0x0f;
let codec_name = match codec_id {
2 => "Sorenson H.263",
3 => "Screen video",
4 => "On2 VP6",
5 => "On2 VP6 with alpha",
6 => "Screen video v2",
7 => "H.264",
_ => "Unknown",
};
tags.push(mktag("FLV", "VideoCodecID", "Video Codec ID", Value::String(format!("{}", codec_id))));
tags.push(mktag("FLV", "VideoEncoding", "Video Encoding", Value::String(codec_name.to_string())));
video_info_found = true;
}
}
_ => {}
}
pos = tag_end + 4;
}
if has_audio && !tags.iter().any(|t| t.name == "HasAudio") {
tags.push(mktag("FLV", "HasAudio", "Has Audio", Value::String("Yes".into())));
}
if has_video && !tags.iter().any(|t| t.name == "HasVideo") {
tags.push(mktag("FLV", "HasVideo", "Has Video", Value::String("Yes".into())));
}
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut deduped: Vec<Tag> = Vec::with_capacity(tags.len());
for tag in tags.into_iter().rev() {
if seen.insert(tag.name.clone()) {
deduped.push(tag);
}
}
deduped.reverse();
Ok(deduped)
}
fn flv_parse_amf_metadata(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 0;
if pos + 3 > data.len() || data[pos] != 0x02 { return; }
pos += 1;
let str_len = u16::from_be_bytes([data[pos], data[pos+1]]) as usize;
pos += 2;
if pos + str_len > data.len() { return; }
let name = String::from_utf8_lossy(&data[pos..pos+str_len]).to_string();
pos += str_len;
if name != "onMetaData" { return; }
if pos >= data.len() { return; }
let container_type = data[pos];
pos += 1;
if container_type == 0x08 {
if pos + 4 > data.len() { return; }
pos += 4; } else if container_type == 0x03 {
} else {
return;
}
flv_parse_amf_object(data, &mut pos, tags, "");
}
fn flv_parse_amf_value(
data: &[u8],
pos: &mut usize,
tags: &mut Vec<Tag>,
compound_key: &str,
struct_name: &str,
) {
if *pos >= data.len() { return; }
let val_type = data[*pos];
*pos += 1;
match val_type {
0x00 => {
if *pos + 8 > data.len() { return; }
let bytes: [u8; 8] = [data[*pos], data[*pos+1], data[*pos+2], data[*pos+3],
data[*pos+4], data[*pos+5], data[*pos+6], data[*pos+7]];
let val = f64::from_be_bytes(bytes);
*pos += 8;
let tag_name = flv_lookup_tag(compound_key);
let val_str = flv_apply_conv(&tag_name, val);
tags.push(mktag("FLV", &tag_name, &tag_name, Value::String(val_str)));
}
0x01 => {
if *pos >= data.len() { return; }
let b = data[*pos] != 0;
*pos += 1;
let tag_name = flv_lookup_tag(compound_key);
tags.push(mktag("FLV", &tag_name, &tag_name, Value::String(if b { "Yes" } else { "No" }.to_string())));
}
0x02 => {
if *pos + 2 > data.len() { return; }
let slen = u16::from_be_bytes([data[*pos], data[*pos+1]]) as usize;
*pos += 2;
if *pos + slen > data.len() { return; }
let s = String::from_utf8_lossy(&data[*pos..*pos+slen]).to_string();
*pos += slen;
let tag_name = flv_lookup_tag(compound_key);
let s = s.trim_end().to_string();
tags.push(mktag("FLV", &tag_name, &tag_name, Value::String(s)));
}
0x03 | 0x08 => {
if val_type == 0x08 {
if *pos + 4 > data.len() { return; }
*pos += 4;
}
flv_parse_amf_object(data, pos, tags, struct_name);
}
0x09 => { }
0x0a => {
if *pos + 4 > data.len() { return; }
let count = u32::from_be_bytes([data[*pos], data[*pos+1], data[*pos+2], data[*pos+3]]) as usize;
*pos += 4;
let mut items: Vec<String> = Vec::new();
for i in 0..count {
if *pos >= data.len() { break; }
let item_type = data[*pos];
if item_type == 0x03 || item_type == 0x08 {
let indexed_name = format!("{}{}", struct_name, i);
*pos += 1;
if item_type == 0x08 {
if *pos + 4 > data.len() { break; }
*pos += 4;
}
flv_parse_amf_object(data, pos, tags, &indexed_name);
} else {
*pos += 1;
match item_type {
0x00 => {
if *pos + 8 > data.len() { break; }
let bytes: [u8; 8] = [data[*pos], data[*pos+1], data[*pos+2], data[*pos+3],
data[*pos+4], data[*pos+5], data[*pos+6], data[*pos+7]];
let v = f64::from_be_bytes(bytes);
*pos += 8;
items.push(flv_format_number(v));
}
0x01 => {
if *pos >= data.len() { break; }
let b = data[*pos] != 0;
*pos += 1;
items.push(if b { "Yes" } else { "No" }.to_string());
}
0x02 => {
if *pos + 2 > data.len() { break; }
let slen = u16::from_be_bytes([data[*pos], data[*pos+1]]) as usize;
*pos += 2;
if *pos + slen > data.len() { break; }
let s = String::from_utf8_lossy(&data[*pos..*pos+slen]).to_string();
*pos += slen;
items.push(s);
}
_ => { *pos = data.len(); break; }
}
}
}
if !items.is_empty() {
let tag_name = flv_lookup_tag(compound_key);
tags.push(mktag("FLV", &tag_name, &tag_name, Value::String(items.join(", "))));
}
}
0x0b => {
if *pos + 10 > data.len() { return; }
let ms = f64::from_be_bytes([data[*pos], data[*pos+1], data[*pos+2], data[*pos+3],
data[*pos+4], data[*pos+5], data[*pos+6], data[*pos+7]]);
let tz_offset = i16::from_be_bytes([data[*pos+8], data[*pos+9]]) as i32;
*pos += 10;
let s = flv_format_date(ms, tz_offset);
let tag_name = flv_lookup_tag(compound_key);
tags.push(mktag("FLV", &tag_name, &tag_name, Value::String(s)));
}
0x0c | 0x0f => {
if *pos + 4 > data.len() { return; }
let slen = u32::from_be_bytes([data[*pos], data[*pos+1], data[*pos+2], data[*pos+3]]) as usize;
*pos += 4;
if *pos + slen > data.len() { return; }
let s = String::from_utf8_lossy(&data[*pos..*pos+slen]).to_string();
*pos += slen;
let tag_name = flv_lookup_tag(compound_key);
tags.push(mktag("FLV", &tag_name, &tag_name, Value::String(s)));
}
0x05 | 0x06 => { }
_ => { *pos = data.len(); }
}
}
fn flv_parse_amf_object(data: &[u8], pos: &mut usize, tags: &mut Vec<Tag>, struct_name: &str) {
while *pos + 3 <= data.len() {
if data[*pos] == 0x00 && data[*pos+1] == 0x00 && *pos + 2 < data.len() && data[*pos+2] == 0x09 {
*pos += 3;
break;
}
if *pos + 2 > data.len() { break; }
let key_len = u16::from_be_bytes([data[*pos], data[*pos+1]]) as usize;
*pos += 2;
if *pos + key_len > data.len() { break; }
let key = String::from_utf8_lossy(&data[*pos..*pos+key_len]).to_string();
*pos += key_len;
if *pos >= data.len() { break; }
let (compound_key, nested_struct) = flv_build_compound_key(struct_name, &key);
flv_parse_amf_value(data, pos, tags, &compound_key, &nested_struct);
}
}
fn flv_build_compound_key(struct_name: &str, raw_key: &str) -> (String, String) {
if struct_name.is_empty() {
let compound_key = raw_key.to_string();
let nested_struct = match raw_key {
"cuePoints" => "CuePoint".to_string(),
_ => raw_key.to_string(),
};
(compound_key, nested_struct)
} else {
let mapped_key = flv_map_sub_key(struct_name, raw_key);
let uckey = flv_ucfirst(&mapped_key);
let compound_key = format!("{}{}", struct_name, uckey);
let nested_struct = compound_key.clone();
(compound_key, nested_struct)
}
}
fn flv_map_sub_key(struct_name: &str, key: &str) -> String {
if let Some(rest) = struct_name.strip_prefix("CuePoint") {
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if !digits.is_empty() && !rest[digits.len()..].starts_with("Parameter") {
return match key {
"name" => "Name".to_string(),
"type" => "Type".to_string(),
"time" => "Time".to_string(),
"parameters" => "Parameter".to_string(),
_ => key.to_string(),
};
}
}
key.to_string()
}
fn flv_lookup_tag(key: &str) -> String {
match key {
"audiocodecid" => return "AudioCodecID".to_string(),
"audiodatarate" => return "AudioBitrate".to_string(),
"audiodelay" => return "AudioDelay".to_string(),
"audiosamplerate" => return "AudioSampleRate".to_string(),
"audiosamplesize" => return "AudioSampleSize".to_string(),
"audiosize" => return "AudioSize".to_string(),
"bytelength" => return "ByteLength".to_string(),
"canseekontime" => return "CanSeekOnTime".to_string(),
"canSeekToEnd" => return "CanSeekToEnd".to_string(),
"creationdate" => return "CreateDate".to_string(),
"createdby" => return "CreatedBy".to_string(),
"cuePoints" => return "CuePoint".to_string(),
"datasize" => return "DataSize".to_string(),
"duration" => return "Duration".to_string(),
"filesize" => return "FileSizeBytes".to_string(),
"framerate" => return "FrameRate".to_string(),
"hasAudio" => return "HasAudio".to_string(),
"hasCuePoints" => return "HasCuePoints".to_string(),
"hasKeyframes" => return "HasKeyFrames".to_string(),
"hasMetadata" => return "HasMetadata".to_string(),
"hasVideo" => return "HasVideo".to_string(),
"height" => return "ImageHeight".to_string(),
"httphostheader" => return "HTTPHostHeader".to_string(),
"keyframesTimes" => return "KeyFramesTimes".to_string(),
"keyframesFilepositions" => return "KeyFramePositions".to_string(),
"lasttimestamp" => return "LastTimeStamp".to_string(),
"lastkeyframetimestamp" => return "LastKeyFrameTime".to_string(),
"metadatacreator" => return "MetadataCreator".to_string(),
"metadatadate" => return "MetadataDate".to_string(),
"purl" => return "URL".to_string(),
"pmsg" => return "Message".to_string(),
"sourcedata" => return "SourceData".to_string(),
"starttime" => return "StartTime".to_string(),
"stereo" => return "Stereo".to_string(),
"totaldatarate" => return "TotalDataRate".to_string(),
"totalduration" => return "TotalDuration".to_string(),
"videocodecid" => return "VideoCodecID".to_string(),
"videodatarate" => return "VideoBitrate".to_string(),
"videosize" => return "VideoSize".to_string(),
"width" => return "ImageWidth".to_string(),
_ => {}
}
flv_ucfirst(key)
}
fn flv_apply_conv(tag_name: &str, val: f64) -> String {
match tag_name {
"AudioBitrate" => flv_convert_bitrate(val * 1000.0),
"VideoBitrate" => flv_convert_bitrate(val * 1000.0),
"Duration" | "StartTime" | "TotalDuration" => flv_convert_duration(val),
"FrameRate" => {
let rounded = (val * 1000.0 + 0.5).floor() / 1000.0;
flv_format_number(rounded)
}
_ => flv_format_number(val),
}
}
fn flv_convert_bitrate(bps: f64) -> String {
let mut val = bps;
let mut units = "bps";
for u in &["bps", "kbps", "Mbps", "Gbps"] {
units = u;
if val < 1000.0 { break; }
val /= 1000.0;
}
if val >= 100.0 {
format!("{:.0} {}", val, units)
} else {
let s = format_3g(val);
format!("{} {}", s, units)
}
}
fn format_3g(val: f64) -> String {
if val == 0.0 { return "0".to_string(); }
let magnitude = val.abs().log10().floor() as i32;
let factor = 10f64.powi(2 - magnitude);
let rounded = (val * factor).round() / factor;
if rounded.fract() == 0.0 {
format!("{:.0}", rounded)
} else {
let s = format!("{:.6}", rounded);
let s = s.trim_end_matches('0').trim_end_matches('.');
s.to_string()
}
}
fn flv_convert_duration(secs: f64) -> String {
if secs == 0.0 {
return "0 s".to_string();
}
let sign = if secs < 0.0 { "-" } else { "" };
let t = secs.abs();
if t < 30.0 {
return format!("{}{:.2} s", sign, t);
}
let t = t + 0.5; let h = (t / 3600.0) as u64;
let t = t - (h as f64) * 3600.0;
let m = (t / 60.0) as u64;
let t = t - (m as f64) * 60.0;
if h > 24 {
let d = h / 24;
let h = h - d * 24;
format!("{}{}d {}:{:02}:{:02}", sign, d, h, m, t as u64)
} else {
format!("{}{}:{:02}:{:02}", sign, h, m, t as u64)
}
}
fn flv_format_number(val: f64) -> String {
if val.fract() == 0.0 && val.abs() < 1e15 {
format!("{}", val as i64)
} else {
let s = format!("{:.10}", val);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_string()
}
}
fn flv_ucfirst(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => {
let upper: String = c.to_uppercase().collect();
upper + chars.as_str()
}
}
}
fn flv_format_date(ms: f64, tz_offset_minutes: i32) -> String {
let unix_secs = ms / 1000.0;
let whole_secs = unix_secs.floor() as i64;
let usec = ((unix_secs - unix_secs.floor()) * 1_000_000.0).round() as u64;
let epoch_to_ymdhms = |ts: i64| -> (i32, u32, u32, u32, u32, u32) {
let days = ts / 86400;
let rem_secs = ts % 86400;
let hours = rem_secs / 3600;
let mins = (rem_secs % 3600) / 60;
let secs = rem_secs % 60;
let mut year = 1970i32;
let mut remaining_days = days;
loop {
let days_in_year = if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 { 366 } else { 365 };
if remaining_days < days_in_year { break; }
remaining_days -= days_in_year;
year += 1;
}
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
let month_days: [i64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 1u32;
let mut day = remaining_days + 1;
for &md in &month_days {
if day > md { day -= md; month += 1; } else { break; }
}
(year, month, day as u32, hours as u32, mins as u32, secs as u32)
};
let (year, month, day, hours, mins, secs) = epoch_to_ymdhms(whole_secs);
let tz_hours = tz_offset_minutes.abs() / 60;
let tz_mins = tz_offset_minutes.abs() % 60;
let tz_sign = if tz_offset_minutes >= 0 { "+" } else { "-" };
if usec != 0 {
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}.{:06}{}{:02}:{:02}",
year, month, day, hours, mins, secs, usec, tz_sign, tz_hours, tz_mins)
} else {
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}{}{:02}:{:02}",
year, month, day, hours, mins, secs, tz_sign, tz_hours, tz_mins)
}
}
pub fn read_swf(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 8 {
return Err(Error::InvalidData("not a SWF file".into()));
}
let compressed = match data[0] {
b'F' => false,
b'C' => true, b'Z' => true, _ => return Err(Error::InvalidData("not a SWF file".into())),
};
if data[1] != b'W' || data[2] != b'S' {
return Err(Error::InvalidData("not a SWF file".into()));
}
let mut tags = Vec::new();
let version = data[3];
let _file_length = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
tags.push(mktag("SWF", "FlashVersion", "Flash Version", Value::U8(version)));
tags.push(mktag("SWF", "Compressed", "Compressed",
Value::String(if compressed { "False" } else { "False" }.into())));
if !compressed && data.len() > 8 {
parse_swf_body(&data[8..], &mut tags);
}
Ok(tags)
}
fn parse_swf_body(body: &[u8], tags: &mut Vec<Tag>) {
if body.is_empty() { return; }
let n_bits = (body[0] >> 3) as usize;
let total_bits = 5 + n_bits * 4;
let n_bytes = (total_bits + 7) / 8;
if body.len() < n_bytes + 4 { return; }
let mut bit_str = 0u64;
let bytes_to_read = n_bytes.min(8);
for i in 0..bytes_to_read {
bit_str = (bit_str << 8) | body[i] as u64;
}
let total_64 = bytes_to_read * 8;
let shift = total_64.saturating_sub(total_bits);
bit_str >>= shift;
let mask = if n_bits >= 64 { u64::MAX } else { (1u64 << n_bits) - 1 };
let ymax_raw = (bit_str & mask) as i32;
let ymin_raw = ((bit_str >> n_bits) & mask) as i32;
let xmax_raw = ((bit_str >> (n_bits * 2)) & mask) as i32;
let xmin_raw = ((bit_str >> (n_bits * 3)) & mask) as i32;
let sign_extend = |v: i32, bits: usize| -> i32 {
if bits > 0 && bits < 32 && (v & (1 << (bits - 1))) != 0 {
v | (!0i32 << bits)
} else { v }
};
let xmin = sign_extend(xmin_raw, n_bits);
let xmax = sign_extend(xmax_raw, n_bits);
let ymin = sign_extend(ymin_raw, n_bits);
let ymax = sign_extend(ymax_raw, n_bits);
let width = ((xmax - xmin) as f64) / 20.0;
let height = ((ymax - ymin) as f64) / 20.0;
if width >= 0.0 && height >= 0.0 {
tags.push(mktag("SWF", "ImageWidth", "Image Width", Value::F64(width)));
tags.push(mktag("SWF", "ImageHeight", "Image Height", Value::F64(height)));
}
let fr_offset = n_bytes;
if fr_offset + 4 > body.len() { return; }
let frame_rate_raw = u16::from_le_bytes([body[fr_offset], body[fr_offset + 1]]);
let frame_count = u16::from_le_bytes([body[fr_offset + 2], body[fr_offset + 3]]);
let frame_rate = frame_rate_raw as f64 / 256.0;
tags.push(mktag("SWF", "FrameRate", "Frame Rate", Value::F64(frame_rate)));
tags.push(mktag("SWF", "FrameCount", "Frame Count", Value::U16(frame_count)));
if frame_rate > 0.0 && frame_count > 0 {
let duration = frame_count as f64 / frame_rate;
tags.push(mktag("SWF", "Duration", "Duration",
Value::String(format!("{:.2} s", duration))));
}
let mut tag_pos = fr_offset + 4;
let mut found_attributes = false;
while tag_pos + 2 <= body.len() {
let code = u16::from_le_bytes([body[tag_pos], body[tag_pos + 1]]);
let tag_type = (code >> 6) as u16;
let short_len = (code & 0x3F) as usize;
tag_pos += 2;
let tag_len = if short_len == 0x3F {
if tag_pos + 4 > body.len() { break; }
let l = u32::from_le_bytes([body[tag_pos], body[tag_pos+1], body[tag_pos+2], body[tag_pos+3]]) as usize;
tag_pos += 4;
l
} else {
short_len
};
if tag_pos + tag_len > body.len() { break; }
match tag_type {
69 => {
if tag_len >= 1 {
let flags = body[tag_pos];
found_attributes = true;
if flags & 0x10 == 0 { break; } }
}
77 => {
let xmp_data = &body[tag_pos..tag_pos + tag_len];
if let Ok(xmp_tags) = crate::metadata::XmpReader::read(xmp_data) {
for t in xmp_tags {
if !tags.iter().any(|e| e.name == t.name) {
tags.push(t);
}
}
}
tags.push(mktag("SWF", "XMPToolkit", "XMP Toolkit",
Value::String(extract_xmp_toolkit(xmp_data))));
break;
}
_ => {}
}
tag_pos += tag_len;
}
let _ = found_attributes;
}
fn extract_xmp_toolkit(xmp: &[u8]) -> String {
let text = String::from_utf8_lossy(xmp);
if let Some(start) = text.find("xmptk=\"") {
let after = &text[start + 7..];
if let Some(end) = after.find('"') {
return after[..end].to_string();
}
}
if let Some(start) = text.find("<xmp:CreatorTool>") {
let after = &text[start + 17..];
if let Some(end) = after.find("</") {
return after[..end].to_string();
}
}
String::new()
}
pub fn read_hdr(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 10 || (!data.starts_with(b"#?RADIANCE") && !data.starts_with(b"#?RGBE")) {
return Err(Error::InvalidData("not a Radiance HDR file".into()));
}
let mut tags = Vec::new();
let text = String::from_utf8_lossy(&data[..data.len().min(8192)]);
let mut kv_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut last_command: Option<String> = None;
let mut found_dims = false;
for line in text.lines() {
let line = line.trim_end_matches('\r');
if line.starts_with("#?") { continue; }
if line.starts_with('#') { continue; }
if line.is_empty() { continue; }
if line.starts_with("-Y ") || line.starts_with("+Y ") || line.starts_with("-X ") || line.starts_with("+X ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let axis1 = parts[0]; let axis3 = parts[2]; let orient = format!("{} {}", axis1, axis3);
let orient_name = match orient.as_str() {
"-Y +X" => "Horizontal (normal)",
"-Y -X" => "Mirror horizontal",
"+Y -X" => "Rotate 180",
"+Y +X" => "Mirror vertical",
"+X -Y" => "Mirror horizontal and rotate 270 CW",
"+X +Y" => "Rotate 90 CW",
"-X +Y" => "Mirror horizontal and rotate 90 CW",
"-X -Y" => "Rotate 270 CW",
_ => &orient,
};
kv_map.insert("_orient".to_string(), orient_name.to_string());
if let Ok(dim1) = parts[1].parse::<u32>() {
if axis1 == "-Y" || axis1 == "+Y" {
kv_map.insert("ImageHeight".to_string(), dim1.to_string());
} else {
kv_map.insert("ImageWidth".to_string(), dim1.to_string());
}
}
if let Ok(dim2) = parts[3].parse::<u32>() {
if axis3 == "-X" || axis3 == "+X" {
kv_map.insert("ImageWidth".to_string(), dim2.to_string());
} else {
kv_map.insert("ImageHeight".to_string(), dim2.to_string());
}
}
}
found_dims = true;
break;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim().to_lowercase();
let val = line[eq_pos+1..].trim().to_string();
let mapped_key = match key.as_str() {
"software" => "Software",
"view" => "View",
"format" => "Format",
"exposure" => "Exposure",
"gamma" => "Gamma",
"colorcorr" => "ColorCorrection",
"pixaspect" => "PixelAspectRatio",
"primaries" => "ColorPrimaries",
_ => "",
};
if !mapped_key.is_empty() {
kv_map.insert(mapped_key.to_string(), val);
}
} else {
last_command = Some(line.to_string());
}
}
if let Some(cmd) = last_command {
tags.push(mktag("HDR", "Command", "Command", Value::String(cmd)));
}
if let Some(v) = kv_map.get("Exposure") {
tags.push(mktag("HDR", "Exposure", "Exposure", Value::String(v.clone())));
}
if let Some(v) = kv_map.get("Format") {
tags.push(mktag("HDR", "Format", "Format", Value::String(v.clone())));
}
if let Some(h) = kv_map.get("ImageHeight") {
if let Ok(hv) = h.parse::<u32>() {
tags.push(mktag("HDR", "ImageHeight", "Image Height", Value::U32(hv)));
}
}
if let Some(w) = kv_map.get("ImageWidth") {
if let Ok(wv) = w.parse::<u32>() {
tags.push(mktag("HDR", "ImageWidth", "Image Width", Value::U32(wv)));
}
}
if let Some(v) = kv_map.get("_orient") {
tags.push(mktag("HDR", "Orientation", "Orientation", Value::String(v.clone())));
}
if let Some(v) = kv_map.get("Software") {
tags.push(mktag("HDR", "Software", "Software", Value::String(v.clone())));
}
if let Some(v) = kv_map.get("View") {
tags.push(mktag("HDR", "View", "View", Value::String(v.clone())));
}
let _ = found_dims;
Ok(tags)
}
pub fn read_pfm(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() >= 2 && data[0] == 0x00 && (data[1] == 0x01 || data[1] == 0x02) {
return read_printer_font_metrics(data);
}
read_ppm(data)
}
fn read_printer_font_metrics(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 117 {
return Err(Error::InvalidData("PFM file too short".into()));
}
let stored_size = u32::from_le_bytes([data[2], data[3], data[4], data[5]]) as usize;
if stored_size != data.len() {
return Err(Error::InvalidData("PFM file size mismatch".into()));
}
let mut tags: Vec<Tag> = Vec::new();
let pfm_ver = u16::from_le_bytes([data[0], data[1]]);
let ver_str = format!("{:x}.{:02x}", pfm_ver >> 8, pfm_ver & 0xff);
tags.push(mktag_font("PFMVersion", "PFM Version", Value::String(ver_str)));
let copyright = pfm_str(data, 6, 60);
if !copyright.is_empty() {
tags.push(mktag_font("Copyright", "Copyright", Value::String(copyright)));
}
let font_type = u16::from_le_bytes([data[66], data[67]]);
tags.push(mktag_font("FontType", "Font Type", Value::String(format!("{}", font_type))));
let point_size = u16::from_le_bytes([data[68], data[69]]);
tags.push(mktag_font("PointSize", "Point Size", Value::String(format!("{}", point_size))));
let y_res = u16::from_le_bytes([data[70], data[71]]);
tags.push(mktag_font("YResolution", "Y Resolution", Value::String(format!("{}", y_res))));
let x_res = u16::from_le_bytes([data[72], data[73]]);
tags.push(mktag_font("XResolution", "X Resolution", Value::String(format!("{}", x_res))));
let ascent = u16::from_le_bytes([data[74], data[75]]);
tags.push(mktag_font("Ascent", "Ascent", Value::String(format!("{}", ascent))));
let int_lead = u16::from_le_bytes([data[76], data[77]]);
tags.push(mktag_font("InternalLeading", "Internal Leading", Value::String(format!("{}", int_lead))));
let ext_lead = u16::from_le_bytes([data[78], data[79]]);
tags.push(mktag_font("ExternalLeading", "External Leading", Value::String(format!("{}", ext_lead))));
tags.push(mktag_font("Italic", "Italic", Value::String(format!("{}", data[80]))));
tags.push(mktag_font("Underline", "Underline", Value::String(format!("{}", data[81]))));
tags.push(mktag_font("Strikeout", "Strikeout", Value::String(format!("{}", data[82]))));
let weight = u16::from_le_bytes([data[83], data[84]]);
tags.push(mktag_font("Weight", "Weight", Value::String(format!("{}", weight))));
tags.push(mktag_font("CharacterSet", "Character Set", Value::String(format!("{}", data[85]))));
let pix_w = u16::from_le_bytes([data[86], data[87]]);
tags.push(mktag_font("PixWidth", "Pix Width", Value::String(format!("{}", pix_w))));
let pix_h = u16::from_le_bytes([data[88], data[89]]);
tags.push(mktag_font("PixHeight", "Pix Height", Value::String(format!("{}", pix_h))));
tags.push(mktag_font("PitchAndFamily", "Pitch And Family", Value::String(format!("{}", data[90]))));
let avg_w = u16::from_le_bytes([data[91], data[92]]);
tags.push(mktag_font("AvgWidth", "Avg Width", Value::String(format!("{}", avg_w))));
let max_w = u16::from_le_bytes([data[93], data[94]]);
tags.push(mktag_font("MaxWidth", "Max Width", Value::String(format!("{}", max_w))));
tags.push(mktag_font("FirstChar", "First Char", Value::String(format!("{}", data[95]))));
tags.push(mktag_font("LastChar", "Last Char", Value::String(format!("{}", data[96]))));
tags.push(mktag_font("DefaultChar", "Default Char", Value::String(format!("{}", data[97]))));
tags.push(mktag_font("BreakChar", "Break Char", Value::String(format!("{}", data[98]))));
let width_bytes = u16::from_le_bytes([data[99], data[100]]);
tags.push(mktag_font("WidthBytes", "Width Bytes", Value::String(format!("{}", width_bytes))));
if data.len() >= 109 {
let name_off = u32::from_le_bytes([data[105], data[106], data[107], data[108]]) as usize;
if name_off > 0 && name_off < data.len() {
let rest = &data[name_off..];
if let Some(null_pos) = rest.iter().position(|&b| b == 0) {
let font_name: String = rest[..null_pos].iter()
.filter(|&&b| b >= 0x20)
.map(|&b| b as char)
.collect();
if !font_name.is_empty() {
tags.push(mktag_font("FontName", "Font Name", Value::String(font_name)));
}
let rest2 = &rest[null_pos + 1..];
if let Some(null_pos2) = rest2.iter().position(|&b| b == 0) {
let ps_name: String = rest2[..null_pos2].iter()
.filter(|&&b| b >= 0x20)
.map(|&b| b as char)
.collect();
if !ps_name.is_empty() {
tags.push(mktag_font("PostScriptFontName", "PostScript Font Name", Value::String(ps_name)));
}
}
}
}
}
Ok(tags)
}
fn pfm_str(data: &[u8], offset: usize, max_len: usize) -> String {
let end = (offset + max_len).min(data.len());
if offset >= data.len() {
return String::new();
}
let slice = &data[offset..end];
let slice = if let Some(null_pos) = slice.iter().position(|&b| b == 0) {
&slice[..null_pos]
} else {
slice
};
slice.iter()
.filter(|&&b| b >= 0x20)
.map(|&b| b as char)
.collect()
}
fn mktag_font(name: &str, description: &str, value: Value) -> Tag {
let pv = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: "File".to_string(),
family1: "Font".to_string(),
family2: "Document".to_string(),
},
raw_value: value,
print_value: pv,
priority: 0,
}
}
pub fn read_ppm(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 3 || data[0] != b'P' {
return Err(Error::InvalidData("not a PBM/PGM/PPM file".into()));
}
let type_byte = data[1];
let is_pfm = type_byte == b'F' || type_byte == b'f';
let mut tags = Vec::new();
if is_pfm {
let text = String::from_utf8_lossy(&data[..data.len().min(256)]);
let re_str = text.as_ref();
let mut lines = re_str.lines();
let header_line = lines.next().unwrap_or("");
let cs_char = if header_line.ends_with('F') || header_line == "PF" { b'F' } else { b'f' };
let color_space = if cs_char == b'F' { "RGB" } else { "Monochrome" };
tags.push(mktag("PFM", "ColorSpace", "Color Space", Value::String(color_space.into())));
if let Some(wh_line) = lines.next() {
let parts: Vec<&str> = wh_line.split_whitespace().collect();
if parts.len() >= 2 {
if let (Ok(w), Ok(h)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
tags.push(mktag("PFM", "ImageWidth", "Image Width", Value::U32(w)));
tags.push(mktag("PFM", "ImageHeight", "Image Height", Value::U32(h)));
}
}
}
if let Some(scale_line) = lines.next() {
let scale_str = scale_line.trim();
if let Ok(scale) = scale_str.parse::<f64>() {
let byte_order = if scale > 0.0 { "Big-endian" } else { "Little-endian" };
tags.push(mktag("PFM", "ByteOrder", "Byte Order", Value::String(byte_order.into())));
}
}
} else {
let text = String::from_utf8_lossy(&data[2..data.len().min(1024)]);
let mut comment_lines: Vec<String> = Vec::new();
let mut width: Option<u32> = None;
let mut height: Option<u32> = None;
let mut maxval: Option<u32> = None;
let mut found_dims = false;
let mut remaining = text.as_ref();
remaining = remaining.trim_start();
while !remaining.is_empty() {
if remaining.starts_with('#') {
let end = remaining.find('\n').unwrap_or(remaining.len());
let comment = &remaining[1..end];
let comment = comment.strip_prefix(' ').unwrap_or(comment);
comment_lines.push(comment.to_string());
remaining = &remaining[end..];
remaining = remaining.trim_start_matches('\n').trim_start_matches('\r');
} else if !found_dims {
let parts: Vec<&str> = remaining.split_whitespace().collect();
if parts.len() >= 2 {
if let (Ok(w), Ok(h)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
width = Some(w);
height = Some(h);
found_dims = true;
let skip1 = remaining.find(parts[0]).unwrap_or(0) + parts[0].len();
remaining = &remaining[skip1..];
remaining = remaining.trim_start();
let skip2 = remaining.find(parts[1]).unwrap_or(0) + parts[1].len();
remaining = &remaining[skip2..];
remaining = remaining.trim_start();
} else {
break;
}
} else {
break;
}
} else {
if remaining.starts_with('#') {
let end = remaining.find('\n').unwrap_or(remaining.len());
let comment = &remaining[1..end];
let comment = comment.strip_prefix(' ').unwrap_or(comment);
comment_lines.push(comment.to_string());
remaining = &remaining[end..];
remaining = remaining.trim_start_matches('\n').trim_start_matches('\r');
continue;
}
let is_pbm = type_byte == b'1' || type_byte == b'4';
if !is_pbm {
let parts: Vec<&str> = remaining.splitn(2, char::is_whitespace).collect();
if let Some(v) = parts.first() {
if let Ok(mv) = v.parse::<u32>() {
maxval = Some(mv);
}
}
}
break;
}
}
if !comment_lines.is_empty() {
let comment = comment_lines.join("\n");
let comment = comment.trim_end_matches('\n').trim_end_matches('\r').to_string();
tags.push(mktag("PPM", "Comment", "Comment", Value::String(comment)));
}
if let Some(w) = width {
tags.push(mktag("PPM", "ImageWidth", "Image Width", Value::U32(w)));
}
if let Some(h) = height {
tags.push(mktag("PPM", "ImageHeight", "Image Height", Value::U32(h)));
}
if let Some(mv) = maxval {
tags.push(mktag("PPM", "MaxVal", "Max Val", Value::U32(mv)));
}
}
Ok(tags)
}
pub fn read_pcx(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 128 || data[0] != 0x0A {
return Err(Error::InvalidData("not a PCX file".into()));
}
let mut tags = Vec::new();
let manufacturer = data[0x00];
let software_ver = data[0x01];
let encoding = data[0x02];
let bpp = data[0x03];
let xmin = u16::from_le_bytes([data[0x04], data[0x05]]);
let ymin = u16::from_le_bytes([data[0x06], data[0x07]]);
let xmax = u16::from_le_bytes([data[0x08], data[0x09]]);
let ymax = u16::from_le_bytes([data[0x0a], data[0x0b]]);
let hdpi = u16::from_le_bytes([data[0x0c], data[0x0d]]);
let vdpi = u16::from_le_bytes([data[0x0e], data[0x0f]]);
let num_planes = data[0x41];
let bytes_per_line = u16::from_le_bytes([data[0x42], data[0x43]]);
let color_mode = u16::from_le_bytes([data[0x44], data[0x45]]);
let mfr_str = match manufacturer {
10 => "ZSoft",
_ => "Unknown",
};
tags.push(mktag("PCX", "Manufacturer", "Manufacturer", Value::String(mfr_str.into())));
let sw_str = match software_ver {
0 => "PC Paintbrush 2.5",
2 => "PC Paintbrush 2.8 (with palette)",
3 => "PC Paintbrush 2.8 (without palette)",
4 => "PC Paintbrush for Windows",
5 => "PC Paintbrush 3.0+",
_ => "Unknown",
};
tags.push(mktag("PCX", "Software", "Software", Value::String(sw_str.into())));
let enc_str = match encoding {
1 => "RLE",
_ => "Unknown",
};
tags.push(mktag("PCX", "Encoding", "Encoding", Value::String(enc_str.into())));
tags.push(mktag("PCX", "BitsPerPixel", "Bits Per Pixel", Value::U8(bpp)));
tags.push(mktag("PCX", "LeftMargin", "Left Margin", Value::U16(xmin)));
tags.push(mktag("PCX", "TopMargin", "Top Margin", Value::U16(ymin)));
tags.push(mktag("PCX", "ImageWidth", "Image Width", Value::U16(xmax - xmin + 1)));
tags.push(mktag("PCX", "ImageHeight", "Image Height", Value::U16(ymax - ymin + 1)));
tags.push(mktag("PCX", "XResolution", "X Resolution", Value::U16(hdpi)));
tags.push(mktag("PCX", "YResolution", "Y Resolution", Value::U16(vdpi)));
tags.push(mktag("PCX", "ColorPlanes", "Color Planes", Value::U8(num_planes)));
tags.push(mktag("PCX", "BytesPerLine", "Bytes Per Line", Value::U16(bytes_per_line)));
let cm_str = match color_mode {
0 => "n/a",
1 => "Color Palette",
2 => "Grayscale",
_ => "Unknown",
};
tags.push(mktag("PCX", "ColorMode", "Color Mode", Value::String(cm_str.into())));
Ok(tags)
}
pub fn read_djvu(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 16 || !data.starts_with(b"AT&TFORM") {
return Err(Error::InvalidData("not a DjVu file".into()));
}
let mut tags = Vec::new();
let form_type = &data[12..16];
let doc_type = match form_type {
b"DJVU" => "DjVu Single-Page",
b"DJVM" => "DjVu Multi-Page",
b"PM44" | b"BM44" => "DjVu Photo/Bitmap",
_ => "DjVu",
};
tags.push(mktag("DjVu", "DocumentType", "Document Type", Value::String(doc_type.into())));
let mut pos = 16;
while pos + 8 <= data.len() {
let chunk_id = &data[pos..pos + 4];
let chunk_size = u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]) as usize;
pos += 8;
if chunk_id == b"INFO" && chunk_size >= 10 && pos + 10 <= data.len() {
let width = u16::from_be_bytes([data[pos], data[pos + 1]]);
let height = u16::from_be_bytes([data[pos + 2], data[pos + 3]]);
let dpi = u16::from_le_bytes([data[pos + 6], data[pos + 7]]);
tags.push(mktag("DjVu", "ImageWidth", "Image Width", Value::U16(width)));
tags.push(mktag("DjVu", "ImageHeight", "Image Height", Value::U16(height)));
if dpi > 0 {
tags.push(mktag("DjVu", "Resolution", "Resolution", Value::U16(dpi)));
}
break;
}
pos += chunk_size;
if chunk_size % 2 != 0 { pos += 1; }
}
Ok(tags)
}
pub fn read_flif(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 6 || !data.starts_with(b"FLIF") {
return Err(Error::InvalidData("not a FLIF file".into()));
}
let mut tags = Vec::new();
let byte4 = data[4];
let type_char = byte4 as char;
let bpc_char = data[5] as char;
let image_type = match type_char {
'1' => "Grayscale (non-interlaced)",
'3' => "RGB (non-interlaced)",
'4' => "RGBA (non-interlaced)",
'A' => "Grayscale (interlaced)",
'C' => "RGB (interlaced)",
'D' => "RGBA (interlaced)",
'Q' => "Grayscale Animation (non-interlaced)",
'S' => "RGB Animation (non-interlaced)",
'T' => "RGBA Animation (non-interlaced)",
'a' => "Grayscale Animation (interlaced)",
'c' => "RGB Animation (interlaced)",
'd' => "RGBA Animation (interlaced)",
_ => "Unknown",
};
tags.push(mktag("FLIF", "ImageType", "Image Type", Value::String(image_type.into())));
let bit_depth = match bpc_char {
'0' => "Custom",
'1' => "8",
'2' => "16",
_ => "Unknown",
};
tags.push(mktag("FLIF", "BitDepth", "Bit Depth", Value::String(bit_depth.into())));
let mut pos = 6;
if let Some((w, consumed)) = read_flif_varint(data, pos) {
let width = (w + 1) as u32;
tags.push(mktag("FLIF", "ImageWidth", "Image Width", Value::U32(width)));
pos += consumed;
if let Some((h, consumed2)) = read_flif_varint(data, pos) {
let height = (h + 1) as u32;
tags.push(mktag("FLIF", "ImageHeight", "Image Height", Value::U32(height)));
pos += consumed2;
if byte4 > 0x48 {
if let Some((frames, consumed3)) = read_flif_varint(data, pos) {
let frame_count = (frames + 2) as u32;
tags.push(mktag("FLIF", "AnimationFrames", "Animation Frames", Value::U32(frame_count)));
pos += consumed3;
}
}
}
}
loop {
if pos + 4 >= data.len() { break; }
let chunk_tag = &data[pos..pos + 4];
let first_byte = chunk_tag[0];
if first_byte < 32 {
let encoding = match first_byte {
0 => "FLIF16",
_ => "Unknown",
};
tags.push(mktag("FLIF", "Encoding", "Encoding", Value::String(encoding.into())));
break;
}
pos += 4;
let chunk_tag = std::str::from_utf8(chunk_tag).unwrap_or("").to_string();
let size = match read_flif_varint(data, pos) {
Some((s, consumed)) => {
pos += consumed;
s as usize
}
None => break,
};
if pos + size > data.len() { break; }
let chunk_data = &data[pos..pos + size];
pos += size;
let inflated = flif_inflate(chunk_data);
let payload = if let Some(ref d) = inflated { d.as_slice() } else { chunk_data };
match chunk_tag.as_str() {
"iCCP" => {
if let Ok(icc_tags) = crate::formats::icc::read_icc(payload) {
tags.extend(icc_tags);
}
}
"eXif" => {
let exif_data = if payload.starts_with(b"Exif\x00\x00") {
&payload[6..]
} else {
payload
};
if let Ok(exif_tags) = crate::metadata::ExifReader::read(exif_data) {
tags.extend(exif_tags);
}
}
"eXmp" => {
if let Ok(xmp_tags) = crate::metadata::XmpReader::read(payload) {
tags.extend(xmp_tags);
}
}
_ => {}
}
}
Ok(tags)
}
fn flif_inflate(data: &[u8]) -> Option<Vec<u8>> {
use std::io::Read;
{
use flate2::read::DeflateDecoder;
let mut decoder = DeflateDecoder::new(data);
let mut output = Vec::new();
if decoder.read_to_end(&mut output).is_ok() && !output.is_empty() {
return Some(output);
}
}
{
use flate2::read::ZlibDecoder;
let mut decoder = ZlibDecoder::new(data);
let mut output = Vec::new();
if decoder.read_to_end(&mut output).is_ok() && !output.is_empty() {
return Some(output);
}
}
None
}
fn read_flif_varint(data: &[u8], mut pos: usize) -> Option<(u64, usize)> {
let start = pos;
let mut result = 0u64;
loop {
if pos >= data.len() { return None; }
let byte = data[pos];
result = (result << 7) | (byte & 0x7F) as u64;
pos += 1;
if byte & 0x80 == 0 { break; }
if pos - start > 8 { return None; }
}
Some((result, pos - start))
}
pub fn read_bpg(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 6 || !data.starts_with(&[0x42, 0x50, 0x47, 0xFB]) {
return Err(Error::InvalidData("not a BPG file".into()));
}
let mut tags = Vec::new();
let word = u16::from_be_bytes([data[4], data[5]]);
let pixel_format = (word & 0xe000) >> 13;
let alpha_raw = word & 0x1004;
let bit_depth = ((word & 0x0f00) >> 8) + 8;
let flags = word & 0x000b;
let pf_name = match pixel_format {
0 => "Grayscale",
1 => "4:2:0 (chroma at 0.5, 0.5)",
2 => "4:2:2 (chroma at 0.5, 0)",
3 => "4:4:4",
4 => "4:2:0 (chroma at 0, 0.5)",
5 => "4:2:2 (chroma at 0, 0)",
_ => "Unknown",
};
tags.push(mktag("BPG", "PixelFormat", "Pixel Format", Value::String(pf_name.into())));
let alpha_name = match alpha_raw {
0x1000 => "Alpha Exists (color not premultiplied)",
0x1004 => "Alpha Exists (color premultiplied)",
0x0004 => "Alpha Exists (W color component)",
_ => "No Alpha Plane",
};
tags.push(mktag("BPG", "Alpha", "Alpha", Value::String(alpha_name.into())));
tags.push(mktag("BPG", "BitDepth", "Bit Depth", Value::U32(bit_depth as u32)));
let mut flag_parts: Vec<&str> = Vec::new();
if flags & 0x0001 != 0 { flag_parts.push("Animation"); }
if flags & 0x0002 != 0 { flag_parts.push("Limited Range"); }
if flags & 0x0008 != 0 { flag_parts.push("Extension Present"); }
let flags_str = flag_parts.join(", ");
tags.push(mktag("BPG", "Flags", "Flags", Value::String(flags_str.into())));
let mut pos = 6;
if let Some((w, consumed)) = read_bpg_ue(data, pos) {
tags.push(mktag("BPG", "ImageWidth", "Image Width", Value::U32(w as u32)));
pos += consumed;
if let Some((h, consumed)) = read_bpg_ue(data, pos) {
tags.push(mktag("BPG", "ImageHeight", "Image Height", Value::U32(h as u32)));
pos += consumed;
if let Some((img_len, consumed)) = read_bpg_ue(data, pos) {
tags.push(mktag("BPG", "ImageLength", "Image Length", Value::U32(img_len as u32)));
pos += consumed;
if flags & 0x0008 != 0 {
if let Some((ext_size, n)) = read_bpg_ue(data, pos) {
pos += n;
let ext_end = pos + ext_size as usize;
if ext_end <= data.len() {
bpg_parse_extensions(data, pos, ext_end, &mut tags);
}
}
}
}
}
}
Ok(tags)
}
fn bpg_parse_extensions(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>) {
let mut pos = start;
while pos < end {
if pos >= data.len() { break; }
let ext_type = data[pos];
pos += 1;
let (ext_len, n) = match read_bpg_ue(data, pos) {
Some(v) => v,
None => break,
};
pos += n;
let ext_len = ext_len as usize;
if pos + ext_len > end { break; }
let ext_data = &data[pos..pos + ext_len];
pos += ext_len;
match ext_type {
1 => {
let exif_data = if ext_len > 3 {
let b0 = ext_data[0];
let b1 = ext_data[1];
let b2 = ext_data[2];
if b0 != b'I' && b0 != b'M' && (b1 == b'I' || b1 == b'M') && b1 == b2 {
tags.push(mktag("ExifTool", "Warning", "Warning",
Value::String("[minor] Ignored extra byte at start of EXIF extension".into())));
&ext_data[1..]
} else {
ext_data
}
} else {
ext_data
};
if let Ok(exif_tags) = crate::metadata::ExifReader::read(exif_data) {
tags.extend(exif_tags);
}
}
2 => {
if let Ok(icc_tags) = crate::formats::icc::read_icc(ext_data) {
tags.extend(icc_tags);
}
}
3 => {
if let Ok(xmp_tags) = crate::metadata::XmpReader::read(ext_data) {
tags.extend(xmp_tags);
}
}
_ => {
}
}
}
}
fn read_bpg_ue(data: &[u8], mut pos: usize) -> Option<(u64, usize)> {
let start = pos;
let mut result = 0u64;
loop {
if pos >= data.len() { return None; }
let byte = data[pos];
result = (result << 7) | (byte & 0x7F) as u64;
pos += 1;
if byte & 0x80 == 0 { break; }
if pos - start > 8 { return None; }
}
Some((result, pos - start))
}
pub fn read_pict(data: &[u8]) -> Result<Vec<Tag>> {
let offset = if data.len() > 522 && data[..512].iter().all(|&b| b == 0) {
512
} else {
0
};
if offset + 10 > data.len() {
return Err(Error::InvalidData("not a PICT file".into()));
}
let mut tags = Vec::new();
let d = &data[offset..];
let top = i16::from_be_bytes([d[2], d[3]]);
let left = i16::from_be_bytes([d[4], d[5]]);
let bottom = i16::from_be_bytes([d[6], d[7]]);
let right = i16::from_be_bytes([d[8], d[9]]);
let mut h_res: Option<f64> = None;
let mut v_res: Option<f64> = None;
let mut w = (right - left) as i32;
let mut h = (bottom - top) as i32;
if d.len() >= 40 && d[10] == 0x00 && d[11] == 0x11 {
if d.len() >= 18 && d[12] == 0x02 && d[13] == 0xff {
if d[16] == 0xff && d[17] == 0xfe && d.len() >= 36 {
let h_fixed = i32::from_be_bytes([d[20], d[21], d[22], d[23]]);
let v_fixed = i32::from_be_bytes([d[24], d[25], d[26], d[27]]);
if h_fixed != 0 && v_fixed != 0 {
h_res = Some(h_fixed as f64 / 65536.0);
v_res = Some(v_fixed as f64 / 65536.0);
w = (w as f64 * h_res.unwrap() / 72.0 + 0.5) as i32;
h = (h as f64 * v_res.unwrap() / 72.0 + 0.5) as i32;
}
}
}
}
tags.push(mktag("PICT", "ImageWidth", "Image Width", Value::I32(w)));
tags.push(mktag("PICT", "ImageHeight", "Image Height", Value::I32(h)));
if let Some(hr) = h_res {
tags.push(mktag("PICT", "XResolution", "X Resolution", Value::String(format!("{}", hr as i64))));
}
if let Some(vr) = v_res {
tags.push(mktag("PICT", "YResolution", "Y Resolution", Value::String(format!("{}", vr as i64))));
}
Ok(tags)
}
pub fn read_kyocera_raw(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 156 {
return Err(Error::InvalidData("too short for Kyocera RAW".into()));
}
if &data[0x19..0x20] != b"ARECOYK" {
return Err(Error::InvalidData("not a Kyocera RAW file".into()));
}
let mut tags = Vec::new();
let group = "KyoceraRaw";
let fw_bytes: Vec<u8> = data[0x01..0x0b].iter().rev().copied().collect();
let fw = String::from_utf8_lossy(&fw_bytes).trim_matches('\0').to_string();
if !fw.is_empty() {
tags.push(mktag(group, "FirmwareVersion", "Firmware Version", Value::String(fw)));
}
let model_bytes: Vec<u8> = data[0x0c..0x18].iter().rev().copied().collect();
let model = String::from_utf8_lossy(&model_bytes).trim_matches('\0').to_string();
if !model.is_empty() {
tags.push(mktag(group, "Model", "Camera Model Name", Value::String(model)));
}
let make_bytes: Vec<u8> = data[0x19..0x20].iter().rev().copied().collect();
let make = String::from_utf8_lossy(&make_bytes).trim_matches('\0').to_string();
if !make.is_empty() {
tags.push(mktag(group, "Make", "Camera Make", Value::String(make)));
}
let dt_bytes: Vec<u8> = data[0x21..0x35].iter().rev().copied().collect();
let dt_str = String::from_utf8_lossy(&dt_bytes).trim_matches('\0').to_string();
if !dt_str.is_empty() {
tags.push(mktag(group, "DateTimeOriginal", "Date/Time Original", Value::String(dt_str)));
}
if data.len() >= 0x38 {
let iso_idx = u32::from_be_bytes([data[0x34], data[0x35], data[0x36], data[0x37]]);
let iso_val = kyocera_iso(iso_idx);
if iso_val > 0 {
let mut t = mktag(group, "ISO", "ISO", Value::String(iso_idx.to_string()));
t.print_value = iso_val.to_string();
tags.push(t);
}
}
if data.len() >= 0x3c {
let et_idx = u32::from_be_bytes([data[0x38], data[0x39], data[0x3a], data[0x3b]]);
let et_val = f64::powf(2.0, et_idx as f64 / 8.0) / 16000.0;
let print_val = format_exposure_time(et_val);
let mut t = mktag(group, "ExposureTime", "Exposure Time", Value::String(format!("{:.10}", et_val)));
t.print_value = print_val;
tags.push(t);
}
if data.len() >= 0x4c {
let r = u32::from_be_bytes([data[0x3c], data[0x3d], data[0x3e], data[0x3f]]);
let g1 = u32::from_be_bytes([data[0x40], data[0x41], data[0x42], data[0x43]]);
let g2 = u32::from_be_bytes([data[0x44], data[0x45], data[0x46], data[0x47]]);
let b = u32::from_be_bytes([data[0x48], data[0x49], data[0x4a], data[0x4b]]);
let wb_str = format!("{} {} {} {}", r, g1, g2, b);
tags.push(mktag(group, "WB_RGGBLevels", "WB RGGB Levels", Value::String(wb_str)));
}
if data.len() >= 0x5c {
let fn_idx = u32::from_be_bytes([data[0x58], data[0x59], data[0x5a], data[0x5b]]);
let fn_val = f64::powf(2.0, fn_idx as f64 / 16.0);
let print_val = format!("{}", (fn_val * 10000.0).round() / 10000.0);
let mut t = mktag(group, "FNumber", "F Number", Value::String(format!("{}", fn_val)));
t.print_value = print_val;
tags.push(t);
}
if data.len() >= 0x6c {
let ma_idx = u32::from_be_bytes([data[0x68], data[0x69], data[0x6a], data[0x6b]]);
let ma_val = f64::powf(2.0, ma_idx as f64 / 16.0);
let print_val = format!("{}", (ma_val * 100.0).round() / 100.0);
let mut t = mktag(group, "MaxAperture", "Max Aperture Value", Value::String(format!("{}", ma_val)));
t.print_value = print_val;
tags.push(t);
}
if data.len() >= 0x74 {
let fl = u32::from_be_bytes([data[0x70], data[0x71], data[0x72], data[0x73]]);
let mut t = mktag(group, "FocalLength", "Focal Length", Value::String(fl.to_string()));
t.print_value = format!("{} mm", fl);
tags.push(t);
}
if data.len() >= 0x9c {
let lens_bytes = &data[0x7c..0x9c];
let lens = String::from_utf8_lossy(lens_bytes).trim_matches('\0').to_string();
if !lens.is_empty() {
tags.push(mktag(group, "Lens", "Lens", Value::String(lens)));
}
}
Ok(tags)
}
fn kyocera_iso(idx: u32) -> u32 {
match idx {
7 => 25, 8 => 32, 9 => 40, 10 => 50, 11 => 64, 12 => 80,
13 => 100, 14 => 125, 15 => 160, 16 => 200, 17 => 250,
18 => 320, 19 => 400, _ => 0,
}
}
fn format_exposure_time(val: f64) -> String {
if val == 0.0 { return "0".to_string(); }
if val >= 1.0 {
format!("{}", val)
} else {
let recip = (1.0 / val).round() as u32;
format!("1/{}", recip)
}
}
struct M2tsBitReader<'a> {
data: &'a [u8],
byte_pos: usize,
bit_pos: u8,
current: u8,
}
impl<'a> M2tsBitReader<'a> {
fn new(data: &'a [u8]) -> Self {
let (byte_pos, bit_pos, current) = if data.is_empty() {
(0, 0, 0)
} else {
(1, 8, data[0])
};
M2tsBitReader { data, byte_pos, bit_pos, current }
}
fn read_bit(&mut self) -> Option<u32> {
if self.bit_pos == 0 {
if self.byte_pos >= self.data.len() { return None; }
self.current = self.data[self.byte_pos];
self.byte_pos += 1;
self.bit_pos = 8;
}
self.bit_pos -= 1;
Some(((self.current >> self.bit_pos) & 1) as u32)
}
fn read_bits(&mut self, n: u32) -> Option<u32> {
let mut val = 0u32;
for _ in 0..n { val = (val << 1) | self.read_bit()?; }
Some(val)
}
fn skip_bits(&mut self, n: u32) {
for _ in 0..n { let _ = self.read_bit(); }
}
fn read_ue(&mut self) -> Option<u32> {
let mut leading = 0u32;
while self.read_bit()? == 0 {
leading += 1;
if leading > 31 { return None; }
}
let mut info = 0u32;
for _ in 0..leading { info = (info << 1) | self.read_bit()?; }
Some((1 << leading) + info - 1)
}
fn read_se(&mut self) -> Option<i32> {
let ue = self.read_ue()?;
let abs_val = ((ue + 1) >> 1) as i32;
Some(if ue & 1 != 0 { abs_val } else { -abs_val })
}
}
#[derive(Clone)]
struct M2tsMdpmData {
datetime_original: Option<String>,
aperture_setting: Option<String>,
gain: Option<String>,
image_stabilization: Option<String>,
exposure_time: Option<String>,
shutter_speed: Option<String>,
make: Option<String>,
recording_mode: Option<String>,
}
fn m2ts_parse_sei(nal_data: &[u8]) -> Option<M2tsMdpmData> {
let mut rbsp = Vec::with_capacity(nal_data.len());
let mut i = 0;
while i < nal_data.len() {
if i + 2 < nal_data.len() && nal_data[i] == 0 && nal_data[i+1] == 0 && nal_data[i+2] == 3 {
rbsp.push(0); rbsp.push(0); i += 3;
} else {
rbsp.push(nal_data[i]); i += 1;
}
}
let data = &rbsp;
let end = data.len();
let mut pos = 1;
while pos < end {
let mut sei_type: u32 = 0;
loop {
if pos >= end { return None; }
let t = data[pos]; pos += 1;
sei_type += t as u32;
if t != 0xFF { break; }
}
if sei_type == 0x80 { return None; }
let mut sei_size: usize = 0;
loop {
if pos >= end { return None; }
let t = data[pos]; pos += 1;
sei_size += t as usize;
if t != 0xFF { break; }
}
if pos + sei_size > end { return None; }
if sei_type == 5 {
let payload = &data[pos..pos + sei_size];
if sei_size > 20 {
let uuid_mdpm = b"\x17\xee\x8c\x60\xf8\x4d\x11\xd9\x8c\xd6\x08\x00\x20\x0c\x9a\x66MDPM";
if payload.len() >= 20 && &payload[..20] == uuid_mdpm {
return m2ts_parse_mdpm(&payload[20..]);
}
}
}
pos += sei_size;
}
None
}
fn m2ts_parse_mdpm(data: &[u8]) -> Option<M2tsMdpmData> {
if data.is_empty() { return None; }
let mut result = M2tsMdpmData {
datetime_original: None,
aperture_setting: None,
gain: None,
image_stabilization: None,
exposure_time: None,
shutter_speed: None,
make: None,
recording_mode: None,
};
let num = data[0] as usize;
let mut pos = 1;
let end = data.len();
let mut last_tag: u8 = 0;
let mut index = 0;
while index < num && pos + 5 <= end {
let tag = data[pos];
if tag <= last_tag && index > 0 { break; } last_tag = tag;
let val4 = [data[pos+1], data[pos+2], data[pos+3], data[pos+4]];
pos += 5;
index += 1;
match tag {
0x18 => {
let mut combined = val4.to_vec();
if pos + 5 <= end && data[pos] == 0x19 {
combined.extend_from_slice(&data[pos+1..pos+5]);
pos += 5; index += 1; last_tag = 0x19;
}
if combined.len() >= 8 {
let tz = combined[0];
let yh = combined[1]; let yl = combined[2]; let mo = combined[3];
let dy = combined[4];
let hh = combined[5];
let mm = combined[6];
let ss = combined[7];
let sign = if tz & 0x20 != 0 { '-' } else { '+' };
let tz_h = (tz >> 1) & 0x0f;
let tz_m = if tz & 0x01 != 0 { "30" } else { "00" };
let dst = if tz & 0x40 != 0 { " DST" } else { "" };
let s = format!("{:02x}{:02x}:{:02x}:{:02x} {:02x}:{:02x}:{:02x}{}{:02}:{}{}", yh, yl, mo, dy, hh, mm, ss, sign, tz_h, tz_m, dst);
result.datetime_original = Some(s);
}
}
0x70 => {
let aperture_raw = val4[0];
let aperture = match aperture_raw {
0xFF => "Auto".to_string(),
0xFE => "Closed".to_string(),
v => format!("{:.1}", 2f64.powf((v & 0x3f) as f64 / 8.0)),
};
result.aperture_setting = Some(aperture);
let gain_raw = val4[1] & 0x0f;
let gain_val = (gain_raw as i32 - 1) * 3;
result.gain = if gain_val == 42 {
Some("Out of range".to_string())
} else {
Some(format!("{} dB", gain_val))
};
}
0x71 => {
let is_raw = val4[1];
let is_str = match is_raw {
0x00 => "Off".to_string(),
0x3F => "On (0x3f)".to_string(),
0xBF => "Off (0xbf)".to_string(),
0xFF => "n/a".to_string(),
v => {
let state = if v & 0x10 != 0 { "On" } else { "Off" };
format!("{} (0x{:02x})", state, v)
}
};
result.image_stabilization = Some(is_str);
}
0x7F => {
let val_le = u16::from_le_bytes([val4[0], val4[1]]);
let val_le2 = u16::from_le_bytes([val4[2], val4[3]]);
let shutter_raw = val_le2 & 0x7fff;
let _ = val_le; if shutter_raw != 0x7fff {
let exp_f = shutter_raw as f64 / 28125.0;
let et_str = m2ts_format_exposure_time(exp_f);
result.exposure_time = Some(et_str.clone());
result.shutter_speed = Some(et_str);
}
}
0xE0 => {
let make_code = u16::from_be_bytes([val4[0], val4[1]]);
let make_str = match make_code {
0x0103 => "Panasonic",
0x0108 => "Sony",
0x1011 => "Canon",
0x1104 => "JVC",
_ => "Unknown",
};
result.make = Some(make_str.to_string());
}
0xE1 => {
let rec_mode = val4[0];
let mode_str = match rec_mode {
0x02 => "XP+",
0x04 => "SP",
0x05 => "LP",
0x06 => "FXP",
0x07 => "MXP",
_ => "Unknown",
};
result.recording_mode = Some(mode_str.to_string());
}
_ => {}
}
}
if result.datetime_original.is_some() || result.aperture_setting.is_some()
|| result.gain.is_some() || result.make.is_some() {
Some(result)
} else {
None
}
}
fn m2ts_format_exposure_time(val: f64) -> String {
if val <= 0.0 { return "0".to_string(); }
if val >= 1.0 {
if (val - val.round()).abs() < 0.005 {
return format!("{}", val.round() as i64);
}
return format!("{:.1}", val);
}
let n = (1.0 / val).round() as i64;
if n > 0 { format!("1/{}", n) } else { format!("{}", val) }
}
fn m2ts_find_packet_size(data: &[u8]) -> Option<(usize, usize)> {
for &(pkt, tco) in &[(192usize, 4usize), (188, 0)] {
if data.len() >= pkt * 3 && (0..3).all(|i| data[i * pkt + tco] == 0x47) {
return Some((pkt, tco));
}
}
None
}
fn m2ts_get_payload(pkt: &[u8], tco: usize) -> Option<(bool, u16, &[u8])> {
if pkt.len() < tco + 4 { return None; }
let hdr = &pkt[tco..];
if hdr[0] != 0x47 { return None; }
let pusi = (hdr[1] & 0x40) != 0;
let pid = (((hdr[1] & 0x1F) as u16) << 8) | hdr[2] as u16;
let afc = (hdr[3] >> 4) & 0x3;
if afc == 0 || afc == 2 { return None; }
let mut ps = 4;
if afc == 3 {
if hdr.len() <= ps { return None; }
ps += 1 + hdr[ps] as usize;
}
if ps >= hdr.len() { return None; }
Some((pusi, pid, &hdr[ps..]))
}
fn m2ts_parse_pat(section: &[u8]) -> Vec<u16> {
let mut pmt_pids = Vec::new();
if section.len() < 8 { return pmt_pids; }
let section_length = (((section[1] & 0x0F) as usize) << 8) | section[2] as usize;
let entries_end = (3 + section_length).saturating_sub(4).min(section.len());
let mut i = 8;
while i + 4 <= entries_end {
let prog_num = ((section[i] as u16) << 8) | section[i+1] as u16;
let pmt_pid = (((section[i+2] & 0x1F) as u16) << 8) | section[i+3] as u16;
if prog_num != 0 { pmt_pids.push(pmt_pid); }
i += 4;
}
pmt_pids
}
struct M2tsStreamInfo {
video_type: Option<String>,
audio_type: Option<String>,
audio_bitrate_idx: Option<u8>,
audio_surround_mode: Option<u8>,
audio_channels: Option<u8>,
h264_pid: Option<u16>,
audio_pid: Option<u16>,
}
fn m2ts_parse_pmt(section: &[u8]) -> Option<M2tsStreamInfo> {
if section.len() < 12 || section[0] != 0x02 { return None; }
let section_length = (((section[1] & 0x0F) as usize) << 8) | section[2] as usize;
let section_end = (3 + section_length).saturating_sub(4).min(section.len());
let prog_info_len = (((section[10] & 0x0F) as usize) << 8) | section[11] as usize;
let mut es_pos = 12 + prog_info_len;
if es_pos >= section_end { return None; }
let mut info = M2tsStreamInfo {
video_type: None, audio_type: None,
audio_bitrate_idx: None, audio_surround_mode: None, audio_channels: None,
h264_pid: None, audio_pid: None,
};
while es_pos + 5 <= section_end {
let stream_type = section[es_pos];
let es_pid = (((section[es_pos+1] & 0x1F) as u16) << 8) | section[es_pos+2] as u16;
let es_info_len = (((section[es_pos+3] & 0x0F) as usize) << 8) | section[es_pos+4] as usize;
let es_info_end = (es_pos + 5 + es_info_len).min(section_end);
match stream_type {
0x01 | 0x02 if info.video_type.is_none() => {
info.video_type = Some(m2ts_stream_type_name(stream_type).to_string());
}
0x10 if info.video_type.is_none() => {
info.video_type = Some(m2ts_stream_type_name(stream_type).to_string());
}
0x1b if info.video_type.is_none() => {
info.video_type = Some("H.264 (AVC) Video".to_string());
info.h264_pid = Some(es_pid);
}
0x24 if info.video_type.is_none() => {
info.video_type = Some(m2ts_stream_type_name(stream_type).to_string());
}
0x03 | 0x04 if info.audio_type.is_none() => {
info.audio_type = Some(m2ts_stream_type_name(stream_type).to_string());
info.audio_pid = Some(es_pid);
}
0x0f if info.audio_type.is_none() => {
info.audio_type = Some(m2ts_stream_type_name(stream_type).to_string());
info.audio_pid = Some(es_pid);
}
0x81 if info.audio_type.is_none() => {
info.audio_type = Some("A52/AC-3 Audio".to_string());
info.audio_pid = Some(es_pid);
let mut di = es_pos + 5;
while di + 2 <= es_info_end {
let dtag = section[di];
let dlen = section[di+1] as usize;
if di + 2 + dlen > es_info_end { break; }
if dtag == 0x81 && dlen >= 3 {
let d0 = section[di+2];
let d1 = section[di+3];
let d2 = section[di+4];
info.audio_bitrate_idx = Some(d1 >> 2);
info.audio_surround_mode = Some(d1 & 0x03);
info.audio_channels = Some((d2 >> 1) & 0x0f);
let _ = d0; }
di += 2 + dlen;
}
}
_ => {}
}
es_pos = es_info_end;
}
if info.video_type.is_some() || info.audio_type.is_some() {
Some(info)
} else {
None
}
}
fn m2ts_parse_sps(sps_nal: &[u8]) -> Option<(u32, u32)> {
let mut rbsp = Vec::with_capacity(sps_nal.len());
let mut i = 0;
while i < sps_nal.len() {
if i + 2 < sps_nal.len() && sps_nal[i] == 0 && sps_nal[i+1] == 0 && sps_nal[i+2] == 3 {
rbsp.push(0); rbsp.push(0); i += 3;
} else {
rbsp.push(sps_nal[i]); i += 1;
}
}
let mut br = M2tsBitReader::new(&rbsp);
br.skip_bits(8); let profile_idc = br.read_bits(8)?;
br.skip_bits(16); br.read_ue()?;
if matches!(profile_idc, 100|110|122|244|44|83|86|118|128) {
let chroma = br.read_ue()?;
if chroma == 3 { br.skip_bits(1); }
br.read_ue()?; br.read_ue()?; br.skip_bits(1);
let scaling = br.read_bit()?;
if scaling != 0 {
let count = if chroma != 3 { 8 } else { 12 };
for ci in 0..count {
if br.read_bit()? != 0 {
let sz = if ci < 6 { 16 } else { 64 };
let (mut last, mut next) = (8i32, 8i32);
for _ in 0..sz {
if next != 0 { let d = br.read_se()?; next = (last + d + 256) % 256; }
last = if next == 0 { last } else { next };
}
}
}
}
}
br.read_ue()?; let poc_type = br.read_ue()?;
if poc_type == 0 { br.read_ue()?; }
else if poc_type == 1 {
br.skip_bits(1); br.read_se()?; br.read_se()?;
let n = br.read_ue()?;
for _ in 0..n { br.read_se()?; }
}
br.read_ue()?; br.skip_bits(1);
let pic_w = br.read_ue()?;
let pic_h = br.read_ue()?;
let frame_mbs_only = br.read_bit()?;
if frame_mbs_only == 0 { br.skip_bits(1); }
br.skip_bits(1);
let crop = br.read_bit()?;
let (cl, cr, ct, cb) = if crop != 0 {
(br.read_ue()?, br.read_ue()?, br.read_ue()?, br.read_ue()?)
} else { (0, 0, 0, 0) };
let m = 4 - frame_mbs_only * 2;
let w = (pic_w + 1) * 16 - 4 * cl - 4 * cr;
let h = ((pic_h + 1) * (2 - frame_mbs_only)) * 16 - m * ct - m * cb;
if w >= 160 && w <= 4096 && h >= 120 && h <= 3072 {
Some((w, h))
} else {
None
}
}
fn m2ts_parse_h264_pes(payload: &[u8]) -> (Option<(u32, u32)>, Option<M2tsMdpmData>) {
let mut dims = None;
let mut mdpm = None;
let mut i = 0;
while i + 3 <= payload.len() {
let nal_start = if payload[i] == 0 && payload[i+1] == 0 && i + 3 < payload.len() && payload[i+2] == 1 {
i + 3
} else if i + 4 < payload.len() && payload[i] == 0 && payload[i+1] == 0 && payload[i+2] == 0 && payload[i+3] == 1 {
i + 4
} else {
i += 1; continue;
};
if nal_start >= payload.len() { break; }
let nal_type = payload[nal_start] & 0x1F;
match nal_type {
7 if dims.is_none() => {
dims = m2ts_parse_sps(&payload[nal_start..]);
}
6 if mdpm.is_none() => {
mdpm = m2ts_parse_sei(&payload[nal_start..]);
}
_ => {}
}
i = nal_start + 1;
}
(dims, mdpm)
}
fn m2ts_parse_ac3_sample_rate(payload: &[u8]) -> Option<u32> {
let pos = payload.windows(2).position(|w| w == [0x0B, 0x77])?;
if pos + 5 > payload.len() { return None; }
let fscod = payload[pos + 4] >> 6;
let rates = [48000u32, 44100, 32000, 0];
Some(rates.get(fscod as usize).copied().unwrap_or(0))
}
fn m2ts_stream_type_name(st: u8) -> &'static str {
match st {
0x01 => "MPEG1Video",
0x02 => "MPEG2Video",
0x03 => "MPEG1Audio",
0x04 => "MPEG2Audio",
0x0f => "ADTS AAC",
0x10 => "MPEG4Video",
0x1b => "H.264 (AVC) Video",
0x24 => "HEVC Video",
0x81 => "A52/AC-3 Audio",
0x82 => "DTS Audio",
_ => "Unknown",
}
}
fn m2ts_format_bitrate(kbps: u32) -> String {
format!("{} kbps", kbps)
}
fn m2ts_format_duration(first: u64, last: u64) -> String {
if last <= first { return "0 s".to_string(); }
let ticks = last - first;
let total_secs = ticks / 27_000_000;
if total_secs == 0 {
return "0 s".to_string();
}
let h = total_secs / 3600;
let m = (total_secs % 3600) / 60;
let s = total_secs % 60;
if h > 0 {
format!("{}:{:02}:{:02}", h, m, s)
} else {
format!("{}:{:02}", m, s)
}
}
pub fn read_m2ts(data: &[u8], extract_embedded: u8) -> Result<Vec<Tag>> {
if data.is_empty() {
return Err(Error::InvalidData("empty file".into()));
}
let (packet_size, tco) = m2ts_find_packet_size(data)
.ok_or_else(|| Error::InvalidData("not an MPEG-2 TS file".into()))?;
let mut tags = Vec::new();
let num_packets = data.len() / packet_size;
let scan_count = if extract_embedded > 0 { num_packets } else { num_packets.min(2000) };
let mut pmt_pids: Vec<u16> = Vec::new();
let mut pmt_buf: std::collections::HashMap<u16, Vec<u8>> = std::collections::HashMap::new();
let mut pat_done = false;
let mut stream_info: Option<M2tsStreamInfo> = None;
let mut h264_dims: Option<(u32, u32)> = None;
let mut mdpm_data: Option<M2tsMdpmData> = None;
let mut all_mdpm: Vec<M2tsMdpmData> = Vec::new();
let mut ac3_sample_rate: Option<u32> = None;
let mut pcr_first: Option<u64> = None;
let mut pcr_last: Option<u64> = None;
for pkt_idx in 0..scan_count {
let pkt = &data[pkt_idx * packet_size..(pkt_idx+1) * packet_size];
let hdr = &pkt[tco..];
if hdr.len() >= 12 && hdr[0] == 0x47 {
let afc = (hdr[3] >> 4) & 0x3;
if (afc == 2 || afc == 3) && hdr.len() > 5 {
let af_len = hdr[4] as usize;
if af_len >= 7 && hdr.len() >= 12 {
let af_flags = hdr[5];
if af_flags & 0x10 != 0 {
let pb = ((hdr[6] as u64) << 25) | ((hdr[7] as u64) << 17)
| ((hdr[8] as u64) << 9) | ((hdr[9] as u64) << 1)
| ((hdr[10] as u64) >> 7);
let pe = (((hdr[10] as u64) & 1) << 8) | hdr[11] as u64;
let pcr = pb * 300 + pe;
if pcr_first.is_none() { pcr_first = Some(pcr); }
pcr_last = Some(pcr);
}
}
}
}
if let Some((pusi, pid, payload)) = m2ts_get_payload(pkt, tco) {
if pid == 0x0000 && !pat_done {
let section = if pusi && !payload.is_empty() {
let ptr = payload[0] as usize;
&payload[(ptr + 1).min(payload.len())..]
} else { payload };
let new_pmts = m2ts_parse_pat(section);
if !new_pmts.is_empty() {
pmt_pids = new_pmts;
pat_done = true;
}
} else if stream_info.is_none() && pmt_pids.contains(&pid) {
let buf = pmt_buf.entry(pid).or_default();
if pusi {
buf.clear();
let ptr = if !payload.is_empty() { payload[0] as usize } else { 0 };
buf.extend_from_slice(&payload[(ptr + 1).min(payload.len())..]);
} else {
buf.extend_from_slice(payload);
}
let buf_clone = buf.clone();
if let Some(si) = m2ts_parse_pmt(&buf_clone) {
stream_info = Some(si);
}
} else if let Some(ref si) = stream_info {
let need_first = h264_dims.is_none() || mdpm_data.is_none();
if (need_first || extract_embedded > 0) && Some(pid) == si.h264_pid {
let es = m2ts_skip_pes_header(payload);
let (dims, mdpm) = m2ts_parse_h264_pes(es);
if dims.is_some() && h264_dims.is_none() { h264_dims = dims; }
if let Some(ref m) = mdpm {
if mdpm_data.is_none() { mdpm_data = mdpm.clone(); }
if extract_embedded > 0 { all_mdpm.push(m.clone()); }
}
}
if ac3_sample_rate.is_none() && Some(pid) == si.audio_pid {
let es = m2ts_skip_pes_header(payload);
if let Some(sr) = m2ts_parse_ac3_sample_rate(es) {
if sr > 0 { ac3_sample_rate = Some(sr); }
}
}
}
}
}
if num_packets > scan_count {
for pkt_idx in (num_packets - 500).max(scan_count)..num_packets {
let pkt = &data[pkt_idx * packet_size..(pkt_idx+1) * packet_size];
let hdr = &pkt[tco..];
if hdr.len() >= 12 && hdr[0] == 0x47 {
let afc = (hdr[3] >> 4) & 0x3;
if (afc == 2 || afc == 3) && hdr.len() > 5 {
let af_len = hdr[4] as usize;
if af_len >= 7 {
let af_flags = hdr[5];
if af_flags & 0x10 != 0 && hdr.len() >= 12 {
let pb = ((hdr[6] as u64) << 25) | ((hdr[7] as u64) << 17)
| ((hdr[8] as u64) << 9) | ((hdr[9] as u64) << 1)
| ((hdr[10] as u64) >> 7);
let pe = (((hdr[10] as u64) & 1) << 8) | hdr[11] as u64;
pcr_last = Some(pb * 300 + pe);
}
}
}
}
}
}
if let Some(ref si) = stream_info {
if let Some(ref vt) = si.video_type {
tags.push(mktag("M2TS", "VideoStreamType", "Video Stream Type", Value::String(vt.clone())));
}
if let Some(ref at) = si.audio_type {
tags.push(mktag("M2TS", "AudioStreamType", "Audio Stream Type", Value::String(at.clone())));
}
if si.audio_bitrate_idx.is_some() || si.audio_surround_mode.is_some() || si.audio_channels.is_some() {
let bitrates = [
32u32,40,48,56,64,80,96,112,128,160,192,224,256,320,384,448,512,576,640
];
if let Some(bi) = si.audio_bitrate_idx {
let idx = bi as usize;
if idx < bitrates.len() {
tags.push(mktag("M2TS", "AudioBitrate", "Audio Bitrate",
Value::String(m2ts_format_bitrate(bitrates[idx]))));
}
}
if let Some(sm) = si.audio_surround_mode {
let s = match sm {
0 => "Not indicated",
1 => "Not Dolby surround",
2 => "Dolby surround",
_ => "Reserved",
};
tags.push(mktag("M2TS", "SurroundMode", "Surround Mode", Value::String(s.into())));
}
if let Some(ch) = si.audio_channels {
let cs = match ch {
0 => "1 + 1",
1 => "1",
2 => "2",
3 => "3",
4 => "2/1",
5 => "3/1",
6 => "2/2",
7 => "3/2",
_ => "Unknown",
};
tags.push(mktag("M2TS", "AudioChannels", "Audio Channels", Value::String(cs.into())));
}
}
}
if let Some((w, h)) = h264_dims {
tags.push(mktag("M2TS", "ImageWidth", "Image Width", Value::U32(w)));
tags.push(mktag("M2TS", "ImageHeight", "Image Height", Value::U32(h)));
}
if let Some(sr) = ac3_sample_rate {
tags.push(mktag("M2TS", "AudioSampleRate", "Audio Sample Rate", Value::U32(sr)));
}
if let (Some(first), Some(last)) = (pcr_first, pcr_last) {
let dur = m2ts_format_duration(first, last);
tags.push(mktag("M2TS", "Duration", "Duration", Value::String(dur)));
}
if let Some(ref mdpm) = mdpm_data {
if let Some(ref v) = mdpm.make {
tags.push(mktag("H264", "Make", "Make", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.datetime_original {
tags.push(mktag("H264", "DateTimeOriginal", "Date/Time Original", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.aperture_setting {
tags.push(mktag("H264", "ApertureSetting", "Aperture Setting", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.gain {
tags.push(mktag("H264", "Gain", "Gain", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.image_stabilization {
tags.push(mktag("H264", "ImageStabilization", "Image Stabilization", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.exposure_time {
tags.push(mktag("H264", "ExposureTime", "Exposure Time", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.shutter_speed {
tags.push(mktag("H264", "ShutterSpeed", "Shutter Speed", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.recording_mode {
tags.push(mktag("H264", "RecordingMode", "Recording Mode", Value::String(v.clone())));
}
if extract_embedded == 0 {
tags.push(mktag("M2TS", "Warning", "Warning",
Value::String("[minor] The ExtractEmbedded option may find more tags in the video data".to_string())));
}
}
if extract_embedded > 0 && all_mdpm.len() > 1 {
for mdpm in &all_mdpm[1..] {
if let Some(ref v) = mdpm.datetime_original {
tags.push(mktag("H264", "DateTimeOriginal", "Date/Time Original", Value::String(v.clone())));
}
if let Some(ref v) = mdpm.make {
tags.push(mktag("H264", "Make", "Make", Value::String(v.clone())));
}
}
}
Ok(tags)
}
fn m2ts_skip_pes_header(payload: &[u8]) -> &[u8] {
if payload.len() < 9 || payload[0] != 0x00 || payload[1] != 0x00 || payload[2] != 0x01 {
return payload;
}
let stream_id = payload[3];
if stream_id == 0xBC || stream_id == 0xBE || stream_id == 0xBF
|| stream_id == 0xF0 || stream_id == 0xF1 || stream_id == 0xFF
|| stream_id == 0xF2 || stream_id == 0xF8 {
return &payload[6..];
}
if payload.len() < 9 { return payload; }
let header_data_length = payload[8] as usize;
let es_start = 9 + header_data_length;
if es_start <= payload.len() { &payload[es_start..] } else { payload }
}
pub fn read_gzip(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 10 || data[0] != 0x1F || data[1] != 0x8B || data[2] != 0x08 {
return Err(Error::InvalidData("not a GZIP file".into()));
}
let mut tags = Vec::new();
let method = data[2];
let flags = data[3];
let xflags = data[8];
let os_byte = data[9];
let compress_str = if method == 8 { "Deflated" } else { "Unknown" };
tags.push(mktag("GZIP", "Compression", "Compression", Value::String(compress_str.into())));
let flag_names = [(0, "Text"), (1, "CRC16"), (2, "ExtraFields"), (3, "FileName"), (4, "Comment")];
let mut flag_parts: Vec<&str> = Vec::new();
for (bit, name) in &flag_names {
if flags & (1 << bit) != 0 {
flag_parts.push(name);
}
}
let flags_str = if flag_parts.is_empty() {
"(none)".to_string()
} else {
flag_parts.join(", ")
};
tags.push(mktag("GZIP", "Flags", "Flags", Value::String(flags_str)));
let mtime = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
if mtime > 0 {
let dt = gzip_unix_to_datetime(mtime as i64);
tags.push(mktag("GZIP", "ModifyDate", "Modify Date", Value::String(dt)));
}
let extra_flags_str = match xflags {
0 => "(none)".to_string(),
2 => "Maximum Compression".to_string(),
4 => "Fastest Algorithm".to_string(),
_ => format!("{}", xflags),
};
tags.push(mktag("GZIP", "ExtraFlags", "Extra Flags", Value::String(extra_flags_str)));
let os_str = match os_byte {
0 => "FAT filesystem (MS-DOS, OS/2, NT/Win32)",
1 => "Amiga",
2 => "VMS (or OpenVMS)",
3 => "Unix",
4 => "VM/CMS",
5 => "Atari TOS",
6 => "HPFS filesystem (OS/2, NT)",
7 => "Macintosh",
8 => "Z-System",
9 => "CP/M",
10 => "TOPS-20",
11 => "NTFS filesystem (NT)",
12 => "QDOS",
13 => "Acorn RISCOS",
255 => "unknown",
_ => "Other",
};
tags.push(mktag("GZIP", "OperatingSystem", "Operating System", Value::String(os_str.into())));
let mut pos = 10usize;
if flags & 0x18 != 0 {
if flags & 0x04 != 0 && pos + 2 <= data.len() {
let xlen = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2 + xlen;
}
if flags & 0x08 != 0 && pos < data.len() {
let name_end = data[pos..].iter().position(|&b| b == 0)
.unwrap_or(data.len() - pos);
let filename = String::from_utf8_lossy(&data[pos..pos + name_end]).to_string();
if !filename.is_empty() {
tags.push(mktag("GZIP", "ArchivedFileName", "Archived File Name", Value::String(filename)));
}
pos += name_end + 1;
}
if flags & 0x10 != 0 && pos < data.len() {
let comment_end = data[pos..].iter().position(|&b| b == 0)
.unwrap_or(data.len() - pos);
let comment = String::from_utf8_lossy(&data[pos..pos + comment_end]).to_string();
if !comment.is_empty() {
tags.push(mktag("GZIP", "Comment", "Comment", Value::String(comment)));
}
}
} else {
if flags & 0x04 != 0 && pos + 2 <= data.len() {
let xlen = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2 + xlen;
}
if flags & 0x08 != 0 && pos < data.len() {
let name_end = data[pos..].iter().position(|&b| b == 0)
.unwrap_or(data.len() - pos);
let filename = String::from_utf8_lossy(&data[pos..pos + name_end]).to_string();
if !filename.is_empty() {
tags.push(mktag("GZIP", "ArchivedFileName", "Archived File Name", Value::String(filename)));
}
}
}
Ok(tags)
}
fn gzip_unix_to_datetime(secs: i64) -> String {
let tz_offset = get_local_tz_offset_for_timestamp(secs);
let local_secs = secs + tz_offset;
let days = local_secs / 86400;
let time = local_secs % 86400;
let (time, days) = if time < 0 { (time + 86400, days - 1) } else { (time, days) };
let h = time / 3600;
let m = (time % 3600) / 60;
let s = time % 60;
let mut y = 1970i32;
let mut rem = days;
loop {
let dy: i64 = if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 { 366 } else { 365 };
if rem < dy { break; }
rem -= dy;
y += 1;
}
let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
let months: [i64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut mo = 1;
for &dm in &months {
if rem < dm { break; }
rem -= dm;
mo += 1;
}
let tz_h = tz_offset / 3600;
let tz_m = (tz_offset.abs() % 3600) / 60;
let tz_sign = if tz_offset >= 0 { "+" } else { "-" };
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}{}{:02}:{:02}",
y, mo, rem + 1, h, m, s, tz_sign, tz_h.abs(), tz_m)
}
fn get_local_tz_offset_for_timestamp(ts: i64) -> i64 {
#[cfg(target_os = "linux")]
{
use std::mem;
extern "C" {
fn localtime_r(timep: *const LibcTimeT, result: *mut TmStruct) -> *mut TmStruct;
}
type LibcTimeT = i64;
#[repr(C)]
struct TmStruct {
tm_sec: i32, tm_min: i32, tm_hour: i32, tm_mday: i32,
tm_mon: i32, tm_year: i32, tm_wday: i32, tm_yday: i32,
tm_isdst: i32, tm_gmtoff: i64, tm_zone: *const i8,
}
unsafe {
let mut tm: TmStruct = mem::zeroed();
let t = ts;
if !localtime_r(&t, &mut tm).is_null() {
return tm.tm_gmtoff;
}
}
}
if let Ok(tz) = std::fs::read_to_string("/etc/timezone") {
let tz = tz.trim();
if tz == "UTC" || tz == "UTC0" { return 0; }
}
if let Ok(link) = std::fs::read_link("/etc/localtime") {
let path = link.to_string_lossy();
if path.contains("UTC") || path.ends_with("/UTC") { return 0; }
if path.contains("Europe/") || path.contains("/CET") { return 3600; }
if path.contains("America/New_York") { return -5 * 3600; }
if path.contains("America/Los_Angeles") { return -8 * 3600; }
if path.contains("America/Chicago") { return -6 * 3600; }
if path.contains("Asia/Tokyo") { return 9 * 3600; }
}
0
}
pub fn read_macos(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 26 || data[0] != 0x00 || data[1] != 0x05 || data[2] != 0x16 || data[3] != 0x07 {
return Err(Error::InvalidData("not a MacOS sidecar file".into()));
}
let ver = data[5];
if ver != 2 {
return Ok(Vec::new());
}
let entries = u16::from_be_bytes([data[24], data[25]]) as usize;
if 26 + entries * 12 > data.len() {
return Ok(Vec::new());
}
let mut tags = Vec::new();
for i in 0..entries {
let pos = 26 + i * 12;
let tag_id = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]);
let off = u32::from_be_bytes([data[pos+4], data[pos+5], data[pos+6], data[pos+7]]) as usize;
let len = u32::from_be_bytes([data[pos+8], data[pos+9], data[pos+10], data[pos+11]]) as usize;
if tag_id == 9 && off + len <= data.len() {
let entry_data = &data[off..off + len];
parse_attr_block(data, entry_data, &mut tags);
}
}
Ok(tags)
}
fn parse_attr_block(full_data: &[u8], entry_data: &[u8], tags: &mut Vec<Tag>) {
if entry_data.len() < 70 {
return;
}
if &entry_data[34..38] != b"ATTR" {
return;
}
let xattr_entries = u32::from_be_bytes([entry_data[66], entry_data[67], entry_data[68], entry_data[69]]) as usize;
let mut pos = 70;
for _i in 0..xattr_entries {
if pos + 11 > entry_data.len() {
break;
}
let off = u32::from_be_bytes([entry_data[pos], entry_data[pos+1], entry_data[pos+2], entry_data[pos+3]]) as usize;
let len = u32::from_be_bytes([entry_data[pos+4], entry_data[pos+5], entry_data[pos+6], entry_data[pos+7]]) as usize;
let n = entry_data[pos+10] as usize;
if pos + 11 + n > entry_data.len() {
break;
}
let name_bytes = &entry_data[pos+11..pos+11+n];
let name = String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string();
let val_data = if off + len <= full_data.len() {
&full_data[off..off + len]
} else {
pos += ((11 + n + 3) & !3).max(1);
continue;
};
let tag_name = xattr_name_to_tag(&name);
if val_data.starts_with(b"bplist0") {
if let Some(value) = parse_simple_bplist(val_data) {
tags.push(mktag("MacOS", &tag_name, &tag_name, Value::String(value)));
} else {
tags.push(mktag("MacOS", &tag_name, &tag_name, Value::Binary(val_data.to_vec())));
}
} else if len > 100 || val_data.contains(&0u8) && !val_data.starts_with(b"0082") {
tags.push(mktag("MacOS", &tag_name, &tag_name, Value::Binary(val_data.to_vec())));
} else {
let s = String::from_utf8_lossy(val_data).trim_end_matches('\0').to_string();
let display = if name == "com.apple.quarantine" {
format_quarantine(&s)
} else {
s
};
if !display.is_empty() {
tags.push(mktag("MacOS", &tag_name, &tag_name, Value::String(display)));
}
}
pos += ((11 + n + 3) & !3).max(4);
}
}
fn xattr_name_to_tag(name: &str) -> String {
let known = match name {
"com.apple.quarantine" => Some("XAttrQuarantine"),
"com.apple.lastuseddate#PS" => Some("XAttrLastUsedDate"),
"com.apple.metadata:kMDItemDownloadedDate" => Some("XAttrMDItemDownloadedDate"),
"com.apple.metadata:kMDItemWhereFroms" => Some("XAttrMDItemWhereFroms"),
"com.apple.metadata:kMDLabel" => Some("XAttrMDLabel"),
"com.apple.metadata:kMDItemFinderComment" => Some("XAttrMDItemFinderComment"),
"com.apple.metadata:_kMDItemUserTags" => Some("XAttrMDItemUserTags"),
_ => None,
};
if name.starts_with("org.") || name.starts_with("net.") || (!name.starts_with("com.apple.") && name.contains(':')) {
let mut tag = String::from("XAttr");
let mut cap_next = true;
for c in name.chars() {
if c == '.' || c == ':' || c == '_' || c == '-' {
cap_next = true;
} else if cap_next {
for uc in c.to_uppercase() {
tag.push(uc);
}
cap_next = false;
} else {
tag.push(c);
}
}
return tag;
}
if let Some(n) = known {
return n.to_string();
}
let name = if let Some(p) = name.find("kMDLabel_") {
&name[..p + 8] } else {
name
};
let basename = if let Some(rest) = name.strip_prefix("com.apple.") {
let rest = if let Some(r) = rest.strip_prefix("metadata:k") {
r
} else if let Some(r) = rest.strip_prefix("metadata:_k") {
r
} else if let Some(r) = rest.strip_prefix("metadata:") {
r
} else {
rest
};
rest.to_string()
} else {
name.to_string()
};
let base_ucfirst = ucfirst_str_misc(&basename);
let mut result = String::from("XAttr");
let chars: Vec<char> = base_ucfirst.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if (c == '.' || c == ':' || c == '_' || c == '#') && i + 1 < chars.len() && chars[i+1].is_ascii_lowercase() {
result.push(chars[i+1].to_ascii_uppercase());
i += 2;
} else if c == '.' || c == ':' || c == '_' || c == '#' {
i += 1; } else {
result.push(c);
i += 1;
}
}
result
}
fn ucfirst_str_misc(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
fn format_quarantine(s: &str) -> String {
let parts: Vec<&str> = s.split(';').collect();
if parts.len() >= 3 {
let flags = parts[0];
let time_hex = parts[1];
let app = parts[2];
let time_str = if let Ok(ts) = i64::from_str_radix(time_hex, 16) {
format!("(ts={})", ts)
} else {
time_hex.to_string()
};
if !app.is_empty() {
return format!("Flags={} set at {} by {}", flags, time_str, app);
}
return format!("Flags={} set at {}", flags, time_str);
}
s.to_string()
}
fn parse_simple_bplist(data: &[u8]) -> Option<String> {
if data.len() < 32 || !data.starts_with(b"bplist00") {
return None;
}
let trailer_start = data.len() - 32;
let trailer = &data[trailer_start..];
let offset_int_size = trailer[6] as usize;
let obj_ref_size = trailer[7] as usize;
let num_objects = u64::from_be_bytes([trailer[8], trailer[9], trailer[10], trailer[11],
trailer[12], trailer[13], trailer[14], trailer[15]]) as usize;
let top_object = u64::from_be_bytes([trailer[16], trailer[17], trailer[18], trailer[19],
trailer[20], trailer[21], trailer[22], trailer[23]]) as usize;
let offset_table_offset = u64::from_be_bytes([trailer[24], trailer[25], trailer[26], trailer[27],
trailer[28], trailer[29], trailer[30], trailer[31]]) as usize;
if offset_int_size == 0 || offset_int_size > 8 || num_objects == 0 {
return None;
}
let mut objects_offset = Vec::with_capacity(num_objects);
for i in 0..num_objects {
let ot_pos = offset_table_offset + i * offset_int_size;
if ot_pos + offset_int_size > data.len() {
return None;
}
let mut off: usize = 0;
for j in 0..offset_int_size {
off = (off << 8) | data[ot_pos + j] as usize;
}
objects_offset.push(off);
}
let read_object = |obj_idx: usize| -> Option<String> {
let off = *objects_offset.get(obj_idx)?;
if off >= data.len() {
return None;
}
let marker = data[off];
let type_nibble = (marker & 0xF0) >> 4;
let info_nibble = marker & 0x0F;
match type_nibble {
0x5 => {
let len = info_nibble as usize;
if off + 1 + len > data.len() { return None; }
Some(String::from_utf8_lossy(&data[off+1..off+1+len]).to_string())
}
0x6 => {
let len = info_nibble as usize;
let byte_len = len * 2;
if off + 1 + byte_len > data.len() { return None; }
let chars: Vec<u16> = data[off+1..off+1+byte_len]
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&chars).ok()
}
0x3 => {
if off + 9 > data.len() { return None; }
let bits = u64::from_be_bytes([data[off+1], data[off+2], data[off+3], data[off+4],
data[off+5], data[off+6], data[off+7], data[off+8]]);
let secs = f64::from_bits(bits);
let unix_secs = secs as i64 + 978307200;
let days = unix_secs / 86400;
let time = unix_secs % 86400;
let hour = time / 3600;
let min = (time % 3600) / 60;
let sec = time % 60;
let mut year = 1970i32;
let mut rem_days = days;
loop {
let dy = if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 { 366 } else { 365 };
if rem_days < dy { break; }
rem_days -= dy;
year += 1;
}
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
let month_days = [31i64, if leap {29} else {28}, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 1i32;
for &md in &month_days {
if rem_days < md { break; }
rem_days -= md;
month += 1;
}
let day = rem_days + 1;
Some(format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", year, month, day, hour, min, sec))
}
0xA => {
let count = if info_nibble == 0xF {
if off + 2 > data.len() { return None; }
let ext_marker = data[off+1];
(1 << (ext_marker & 0xF)) as usize
} else {
info_nibble as usize
};
Some(format!("({} items)", count))
}
_ => None,
}
};
let result = read_object(top_object)?;
if let Some(off) = objects_offset.get(top_object) {
let off = *off;
if off < data.len() {
let marker = data[off];
let type_nibble = (marker & 0xF0) >> 4;
if type_nibble == 0xA {
let count = (marker & 0x0F) as usize;
let mut items = Vec::new();
for j in 0..count {
let ref_pos = off + 1 + j * obj_ref_size;
if ref_pos + obj_ref_size > data.len() { break; }
let mut obj_ref: usize = 0;
for k in 0..obj_ref_size {
obj_ref = (obj_ref << 8) | data[ref_pos + k] as usize;
}
if let Some(item_val) = read_object(obj_ref) {
items.push(item_val);
}
}
if !items.is_empty() {
return Some(items.join(", "));
}
}
}
}
Some(result)
}
pub fn read_moi(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 256 || !data.starts_with(b"V6") {
return Err(Error::InvalidData("not a MOI file".into()));
}
let mut tags = Vec::new();
let version = String::from_utf8_lossy(&data[0..2]).to_string();
tags.push(mktag("MOI", "MOIVersion", "MOI Version", Value::String(version)));
if data.len() >= 14 {
let year = u16::from_be_bytes([data[6], data[7]]);
let month = data[8];
let day = data[9];
let hour = data[10];
let min = data[11];
let ms = u16::from_be_bytes([data[12], data[13]]);
let sec_f = ms as f64 / 1000.0;
let dt = format!("{:04}:{:02}:{:02} {:02}:{:02}:{:06.3}", year, month, day, hour, min, sec_f);
tags.push(mktag("MOI", "DateTimeOriginal", "Date/Time Original", Value::String(dt)));
}
if data.len() >= 0x12 {
let dur_ms = u32::from_be_bytes([data[0x0e], data[0x0f], data[0x10], data[0x11]]);
let dur_s = dur_ms as f64 / 1000.0;
let dur_str = format!("{:.2} s", dur_s);
tags.push(mktag("MOI", "Duration", "Duration", Value::String(dur_str)));
}
if data.len() > 0x80 {
let aspect = data[0x80];
let lo = aspect & 0x0F;
let hi = aspect >> 4;
let aspect_str = match lo {
0 | 1 => "4:3",
4 | 5 => "16:9",
_ => "Unknown",
};
let sys_str = match hi {
4 => " NTSC",
5 => " PAL",
_ => "",
};
let full = format!("{}{}", aspect_str, sys_str);
tags.push(mktag("MOI", "AspectRatio", "Aspect Ratio", Value::String(full)));
}
if data.len() > 0x86 {
let ac = u16::from_be_bytes([data[0x84], data[0x85]]);
let codec = match ac {
0x00c1 => "AC3",
0x4001 => "MPEG",
_ => "Unknown",
};
tags.push(mktag("MOI", "AudioCodec", "Audio Codec", Value::String(codec.into())));
}
if data.len() > 0x86 {
let ab = data[0x86];
let bitrate = ab as u32 * 16000 + 48000;
let bitrate_str = format!("{} kbps", bitrate / 1000);
tags.push(mktag("MOI", "AudioBitrate", "Audio Bitrate", Value::String(bitrate_str)));
}
if data.len() > 0xdc {
let vb = u16::from_be_bytes([data[0xda], data[0xdb]]);
let vbps: Option<u32> = match vb {
0x5896 => Some(8500000),
0x813d => Some(5500000),
_ => None,
};
if let Some(bps) = vbps {
let vb_str = format!("{:.1} Mbps", bps as f64 / 1_000_000.0);
tags.push(mktag("MOI", "VideoBitrate", "Video Bitrate", Value::String(vb_str)));
}
}
Ok(tags)
}
fn read_uleb128(data: &[u8], pos: &mut usize) -> Option<u64> {
let mut result: u64 = 0;
let mut shift = 0u32;
loop {
if *pos >= data.len() {
return None;
}
let byte = data[*pos];
*pos += 1;
result |= ((byte & 0x7F) as u64) << shift;
if byte & 0x80 == 0 {
break;
}
shift += 7;
if shift >= 64 {
return None;
}
}
Some(result)
}
pub fn read_rar(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 8 || !data.starts_with(b"Rar!\x1A\x07") {
return Err(Error::InvalidData("not a RAR file".into()));
}
let mut tags = Vec::new();
if data[6] == 0x00 {
tags.push(mktag("ZIP", "FileVersion", "File Version", Value::String("RAR v4".into())));
read_rar4_entries(data, &mut tags);
} else if data[6] == 0x01 && data[7] == 0x00 {
tags.push(mktag("ZIP", "FileVersion", "File Version", Value::String("RAR v5".into())));
read_rar5_entries(data, &mut tags);
}
Ok(tags)
}
fn read_rar5_entries(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 8;
loop {
if pos + 4 > data.len() {
break;
}
pos += 4;
let head_size = match read_uleb128(data, &mut pos) {
Some(v) if v > 0 => v as usize,
_ => break,
};
if pos + head_size > data.len() {
break;
}
let header = &data[pos..pos + head_size];
pos += head_size;
let mut hpos = 0;
let head_type = match read_uleb128(header, &mut hpos) {
Some(v) => v,
None => break,
};
let head_flag = match read_uleb128(header, &mut hpos) {
Some(v) => v,
None => break,
};
if head_type != 2 && head_type != 3 {
if head_flag & 0x0002 != 0 {
if let Some(data_size) = read_uleb128(data, &mut pos) {
pos += data_size as usize;
}
}
continue;
}
let _extra_size = read_uleb128(header, &mut hpos);
let data_size: u64 = if head_flag & 0x0002 != 0 {
match read_uleb128(header, &mut hpos) {
Some(v) => v,
None => break,
}
} else {
0
};
if head_type == 3 {
pos += data_size as usize;
continue;
}
if head_type == 2 {
tags.push(mktag("ZIP", "CompressedSize", "Compressed Size", Value::U32(data_size as u32)));
}
let file_flag = match read_uleb128(header, &mut hpos) {
Some(v) => v,
None => { pos += data_size as usize; continue; }
};
let uncompressed_size = match read_uleb128(header, &mut hpos) {
Some(v) => v,
None => { pos += data_size as usize; continue; }
};
if file_flag & 0x0008 == 0 {
tags.push(mktag("ZIP", "UncompressedSize", "Uncompressed Size", Value::U32(uncompressed_size as u32)));
}
let _attrs = read_uleb128(header, &mut hpos);
if file_flag & 0x0002 != 0 {
hpos += 4;
}
if file_flag & 0x0004 != 0 {
hpos += 4;
}
let _comp_info = read_uleb128(header, &mut hpos);
if let Some(os_val) = read_uleb128(header, &mut hpos) {
let os_name = match os_val {
0 => "Win32",
1 => "Unix",
_ => "Unknown",
};
tags.push(mktag("ZIP", "OperatingSystem", "Operating System", Value::String(os_name.into())));
}
if hpos < header.len() {
let name_len = header[hpos] as usize;
hpos += 1;
if hpos + name_len <= header.len() {
let name = String::from_utf8_lossy(&header[hpos..hpos + name_len])
.trim_end_matches('\0')
.to_string();
if !name.is_empty() {
tags.push(mktag("ZIP", "ArchivedFileName", "Archived File Name", Value::String(name)));
}
}
}
pos += data_size as usize;
}
}
fn read_rar4_entries(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 7;
loop {
if pos + 7 > data.len() {
break;
}
let block_type = data[pos + 2];
let flags = u16::from_le_bytes([data[pos + 3], data[pos + 4]]);
let mut size = u16::from_le_bytes([data[pos + 5], data[pos + 6]]) as usize;
size = size.saturating_sub(7);
if flags & 0x8000 != 0 {
if pos + 11 > data.len() {
break;
}
let add_size = u32::from_le_bytes([data[pos + 7], data[pos + 8], data[pos + 9], data[pos + 10]]) as usize;
size = size.saturating_add(add_size).saturating_sub(4);
}
pos += 7;
if block_type == 0x74 && size > 0 {
let n = size.min(4096).min(data.len() - pos);
if n >= 16 {
let file_data = &data[pos..pos + n];
let compressed = u32::from_le_bytes([file_data[0], file_data[1], file_data[2], file_data[3]]) as u64;
let uncompressed = u32::from_le_bytes([file_data[4], file_data[5], file_data[6], file_data[7]]) as u64;
let os_byte = file_data[14];
let name_len = u16::from_le_bytes([file_data[10], file_data[11]]) as usize;
if n >= 25 + name_len {
let name = String::from_utf8_lossy(&file_data[25..25 + name_len]).to_string();
tags.push(mktag("ZIP", "CompressedSize", "Compressed Size", Value::U32(compressed as u32)));
tags.push(mktag("ZIP", "UncompressedSize", "Uncompressed Size", Value::U32(uncompressed as u32)));
let os_name = match os_byte {
0 => "MS-DOS",
1 => "OS/2",
2 => "Win32",
3 => "Unix",
_ => "Unknown",
};
tags.push(mktag("ZIP", "OperatingSystem", "Operating System", Value::String(os_name.into())));
tags.push(mktag("ZIP", "ArchivedFileName", "Archived File Name", Value::String(name)));
}
}
}
if size == 0 {
break;
}
pos += size;
}
}
pub fn read_svg(data: &[u8]) -> Result<Vec<Tag>> {
let text = String::from_utf8_lossy(data);
if !text.contains("<svg") {
return Err(Error::InvalidData("not an SVG file".into()));
}
let mut tags = Vec::new();
use xml::reader::{EventReader, XmlEvent};
let _parser = EventReader::from_str(&text);
let mut path: Vec<String> = Vec::new(); let mut current_text = String::new();
let mut in_metadata = false; let mut in_rdf = 0_usize; let mut in_c2pa = 0_usize; let mut in_svg_body = false; let mut had_child: Vec<bool> = Vec::new();
for event in EventReader::from_str(text.as_ref()) {
match event {
Ok(XmlEvent::StartElement { name, attributes, namespace, .. }) => {
let local = &name.local_name;
let ns = name.namespace.as_deref().unwrap_or("");
if local == "svg" && path.is_empty() {
path.push("Svg".into());
had_child.push(false);
for attr in &attributes {
match attr.name.local_name.as_str() {
"width" => tags.push(mktag("SVG", "ImageWidth", "Image Width", Value::String(attr.value.clone()))),
"height" => tags.push(mktag("SVG", "ImageHeight", "Image Height", Value::String(attr.value.clone()))),
"version" => tags.push(mktag("SVG", "SVGVersion", "SVG Version", Value::String(attr.value.clone()))),
"viewBox" | "viewbox" => tags.push(mktag("SVG", "ViewBox", "View Box", Value::String(attr.value.clone()))),
"id" => tags.push(mktag("SVG", "ID", "ID", Value::String(attr.value.clone()))),
_ => {}
}
}
if let Some(default_ns) = namespace.get("") {
if !default_ns.is_empty() {
tags.push(mktag("SVG", "Xmlns", "XMLNS", Value::String(default_ns.to_string())));
}
}
current_text.clear();
continue;
}
if local == "metadata" && !in_metadata && in_rdf == 0 && in_c2pa == 0 {
in_metadata = true;
if let Some(last) = had_child.last_mut() { *last = true; }
path.push("Metadata".into());
had_child.push(false);
current_text.clear();
continue;
}
if in_metadata {
if in_rdf > 0 {
in_rdf += 1;
current_text.clear();
continue;
}
if in_c2pa > 0 {
in_c2pa += 1;
current_text.clear();
continue;
}
if local == "RDF" && ns == "http://www.w3.org/1999/02/22-rdf-syntax-ns#" {
in_rdf = 1;
current_text.clear();
continue;
}
if name.prefix.as_deref() == Some("c2pa") || local == "manifest" {
in_c2pa = 1;
current_text.clear();
continue;
}
current_text.clear();
continue;
}
if !in_metadata && path.len() >= 1 {
in_svg_body = true;
if let Some(last) = had_child.last_mut() { *last = true; }
let ucfirst_local = svg_ucfirst(local);
path.push(ucfirst_local);
had_child.push(false);
current_text.clear();
continue;
}
path.push(svg_ucfirst(local));
had_child.push(false);
current_text.clear();
}
Ok(XmlEvent::Characters(t)) | Ok(XmlEvent::CData(t)) => {
current_text.push_str(&t);
}
Ok(XmlEvent::EndElement { name }) => {
let local = &name.local_name;
if in_rdf > 0 {
in_rdf -= 1;
current_text.clear();
continue;
}
if in_c2pa > 0 {
in_c2pa -= 1;
if in_c2pa == 0 {
let b64 = current_text.chars().filter(|c| !c.is_whitespace()).collect::<String>();
if !b64.is_empty() {
if let Ok(jumbf_data) = base64_decode(&b64) {
let jumbf_group = crate::tag::TagGroup {
family0: "JUMBF".into(),
family1: "JUMBF".into(),
family2: "Image".into(),
};
let print = format!("(Binary data {} bytes, use -b option to extract)", jumbf_data.len());
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("JUMBF".into()),
name: "JUMBF".into(),
description: "JUMBF".into(),
group: jumbf_group,
raw_value: Value::Binary(jumbf_data.clone()),
print_value: print,
priority: 0,
});
parse_jumbf_for_svg(&jumbf_data, &mut tags);
}
}
}
current_text.clear();
continue;
}
if local == "metadata" && in_metadata {
in_metadata = false;
path.pop();
had_child.pop();
current_text.clear();
continue;
}
if in_metadata {
current_text.clear();
continue;
}
if in_svg_body && path.len() >= 2 {
let this_had_child = had_child.pop().unwrap_or(false);
let t = current_text.trim().to_string();
if !t.is_empty() && !this_had_child {
let tag_name = path.iter().skip(1).cloned().collect::<String>();
if !tag_name.is_empty() {
tags.push(mktag("SVG", &tag_name, &tag_name, Value::String(t)));
}
}
path.pop();
if path.len() <= 1 {
in_svg_body = false;
}
current_text.clear();
continue;
}
path.pop();
had_child.pop();
current_text.clear();
}
Err(_) => break,
_ => {}
}
}
if let Some(rdf_start) = text.find("<rdf:RDF") {
if let Some(rdf_end) = text.find("</rdf:RDF>") {
let rdf_section = &text[rdf_start..rdf_end + "</rdf:RDF>".len()];
if let Ok(xmp_tags) = XmpReader::read(rdf_section.as_bytes()) {
tags.extend(xmp_tags);
}
}
}
if let Some(mstart) = text.find("<c2pa:manifest>") {
let content_start = mstart + "<c2pa:manifest>".len();
if let Some(mend) = text[content_start..].find("</c2pa:manifest>") {
let b64_content = &text[content_start..content_start + mend];
let b64: String = b64_content.chars().filter(|c| !c.is_whitespace()).collect();
if !b64.is_empty() {
if let Ok(jumbf_data) = base64_decode(&b64) {
let jumbf_group = crate::tag::TagGroup {
family0: "JUMBF".into(),
family1: "JUMBF".into(),
family2: "Image".into(),
};
let print = format!("(Binary data {} bytes, use -b option to extract)", jumbf_data.len());
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text("JUMBF".into()),
name: "JUMBF".into(),
description: "JUMBF".into(),
group: jumbf_group,
raw_value: Value::Binary(jumbf_data.clone()),
print_value: print,
priority: 0,
});
parse_jumbf_for_svg(&jumbf_data, &mut tags);
}
}
}
}
Ok(tags)
}
fn svg_ucfirst(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
fn base64_decode(s: &str) -> std::result::Result<Vec<u8>, ()> {
let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut table = [0u8; 256];
for (i, &c) in alphabet.iter().enumerate() {
table[c as usize] = i as u8;
}
let bytes: Vec<u8> = s.bytes().filter(|&b| b != b'=' && b != b'\n' && b != b'\r' && b != b' ').collect();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
for chunk in bytes.chunks(4) {
if chunk.len() < 2 { break; }
let b0 = table[chunk[0] as usize];
let b1 = table[chunk[1] as usize];
out.push((b0 << 2) | (b1 >> 4));
if chunk.len() >= 3 {
let b2 = table[chunk[2] as usize];
out.push((b1 << 4) | (b2 >> 2));
if chunk.len() >= 4 {
let b3 = table[chunk[3] as usize];
out.push((b2 << 6) | b3);
}
}
}
Ok(out)
}
fn parse_jumbf_for_svg(data: &[u8], tags: &mut Vec<Tag>) {
parse_jumbf_boxes_svg(data, tags, true);
}
fn parse_jumbf_boxes_svg(data: &[u8], tags: &mut Vec<Tag>, top_level: bool) {
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 || pos + lbox > data.len() { break; }
let content = &data[pos+8..pos+lbox];
if tbox == b"jumb" {
parse_jumbf_jumd_svg(content, tags, top_level);
}
pos += lbox;
}
}
fn parse_jumbf_jumd_svg(data: &[u8], tags: &mut Vec<Tag>, emit_desc: bool) {
let jumbf_group = crate::tag::TagGroup {
family0: "JUMBF".into(),
family1: "JUMBF".into(),
family2: "Image".into(),
};
let mut pos = 0;
let mut found_jumd = false;
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 || pos + lbox > data.len() { break; }
let content = &data[pos+8..pos+lbox];
if tbox == b"jumd" && !found_jumd {
found_jumd = true;
if content.len() >= 17 {
let type_bytes = &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();
if emit_desc {
let type_hex: String = type_bytes.iter().map(|b| format!("{:02x}", b)).collect();
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 = ascii4.iter().all(|&b| b.is_ascii_alphanumeric());
let print_type = if is_printable {
let ascii_str = String::from_utf8_lossy(ascii4);
format!("({})-{}-{}-{}", ascii_str, a1, a2, a3)
} else {
format!("{}-{}-{}-{}", &type_hex[..8], a1, a2, a3)
};
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: 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.clone(),
raw_value: Value::String(label.clone()),
print_value: label.clone(),
priority: 0,
});
}
}
}
} else if tbox == b"json" {
if let Ok(json_str) = std::str::from_utf8(content) {
parse_jumbf_json_svg(json_str.trim(), tags, &jumbf_group);
}
} else if tbox == b"jumb" {
parse_jumbf_jumd_svg(content, tags, false);
}
pos += lbox;
}
}
fn parse_jumbf_json_svg(json: &str, tags: &mut Vec<Tag>, group: &crate::tag::TagGroup) {
let mut i = 0;
let bytes = json.as_bytes();
while i < bytes.len() {
if bytes[i] == b'"' {
i += 1;
let key_start = i;
while i < bytes.len() && bytes[i] != b'"' { i += 1; }
let key = &json[key_start..i];
i += 1; while i < bytes.len() && (bytes[i] == b':' || bytes[i] == b' ') { i += 1; }
if i < bytes.len() && bytes[i] == b'"' {
i += 1;
let val_start = i;
while i < bytes.len() && bytes[i] != b'"' { i += 1; }
let val = &json[val_start..i];
i += 1;
let tag_name = match key {
"location" => Some("Location"),
"copyright" => Some("Copyright"),
_ => None,
};
if let Some(name) = tag_name {
tags.push(crate::tag::Tag {
id: crate::tag::TagId::Text(name.into()),
name: name.into(),
description: name.into(),
group: group.clone(),
raw_value: Value::String(val.to_string()),
print_value: val.to_string(),
priority: 0,
});
}
}
} else {
i += 1;
}
}
}
pub fn read_json(data: &[u8]) -> Result<Vec<Tag>> {
let text = String::from_utf8_lossy(data);
let trimmed = text.trim();
if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
return Err(Error::InvalidData("not a JSON file".into()));
}
let mut tags = Vec::new();
if trimmed.starts_with('{') {
let mut collected: Vec<(String, String)> = Vec::new();
parse_json_object(trimmed, "", &mut collected);
for (key, value) in collected {
let tag_name = json_key_to_tag_name(&key);
if tag_name.is_empty() { continue; }
tags.push(mktag("JSON", &tag_name, &tag_name, Value::String(value)));
}
}
Ok(tags)
}
fn parse_json_object(json: &str, prefix: &str, out: &mut Vec<(String, String)>) {
let mut pos = 0;
let chars: Vec<char> = json.chars().collect();
if pos < chars.len() && chars[pos] == '{' {
pos += 1;
}
loop {
while pos < chars.len() && (chars[pos].is_whitespace() || chars[pos] == ',') {
pos += 1;
}
if pos >= chars.len() || chars[pos] == '}' {
break;
}
if chars[pos] != '"' {
break;
}
let key = read_json_string(&chars, &mut pos);
while pos < chars.len() && (chars[pos].is_whitespace() || chars[pos] == ':') {
pos += 1;
}
if pos >= chars.len() {
break;
}
let full_key = if prefix.is_empty() { key.clone() } else { format!("{}{}", prefix, ucfirst_str(&key)) };
match chars[pos] {
'"' => {
let val = read_json_string(&chars, &mut pos);
out.push((full_key, val));
}
'{' => {
let obj_start = pos;
let obj_end = find_matching_bracket(&chars, pos, '{', '}');
let obj_str: String = chars[obj_start..obj_end + 1].iter().collect();
parse_json_object(&obj_str, &full_key, out);
pos = obj_end + 1;
}
'[' => {
let arr_start = pos;
let arr_end = find_matching_bracket(&chars, pos, '[', ']');
let arr_str: String = chars[arr_start..arr_end + 1].iter().collect();
if array_contains_objects(&arr_str) {
let mut sub_map: Vec<(String, Vec<String>)> = Vec::new();
parse_json_array_of_objects(&arr_str, &full_key, &mut sub_map);
for (sub_key, vals) in sub_map {
if !vals.is_empty() {
out.push((sub_key, vals.join(", ")));
}
}
} else {
let values = parse_json_array(&arr_str);
if !values.is_empty() {
out.push((full_key, values.join(", ")));
}
}
pos = arr_end + 1;
}
'n' => {
pos += 4;
out.push((full_key, "null".into()));
}
't' => {
pos += 4;
out.push((full_key, "1".into()));
}
'f' => {
pos += 5;
out.push((full_key, "0".into()));
}
_ => {
let num_start = pos;
while pos < chars.len() && !chars[pos].is_whitespace() && chars[pos] != ',' && chars[pos] != '}' {
pos += 1;
}
let num: String = chars[num_start..pos].iter().collect();
out.push((full_key, num));
}
}
}
}
fn parse_json_array(json: &str) -> Vec<String> {
let mut results = Vec::new();
let chars: Vec<char> = json.chars().collect();
let mut pos = 0;
if pos < chars.len() && chars[pos] == '[' {
pos += 1;
}
loop {
while pos < chars.len() && (chars[pos].is_whitespace() || chars[pos] == ',') {
pos += 1;
}
if pos >= chars.len() || chars[pos] == ']' {
break;
}
match chars[pos] {
'"' => {
let val = read_json_string(&chars, &mut pos);
results.push(val);
}
'[' => {
let end = find_matching_bracket(&chars, pos, '[', ']');
let sub: String = chars[pos..end + 1].iter().collect();
let sub_vals = parse_json_array(&sub);
results.extend(sub_vals);
pos = end + 1;
}
'{' => {
let end = find_matching_bracket(&chars, pos, '{', '}');
pos = end + 1;
}
'n' => { pos += 4; results.push("null".into()); }
't' => { pos += 4; results.push("1".into()); }
'f' => { pos += 5; results.push("0".into()); }
_ => {
let start = pos;
while pos < chars.len() && !chars[pos].is_whitespace() && chars[pos] != ',' && chars[pos] != ']' {
pos += 1;
}
results.push(chars[start..pos].iter().collect());
}
}
}
results
}
fn array_contains_objects(json: &str) -> bool {
let chars: Vec<char> = json.chars().collect();
let mut pos = 0;
if pos < chars.len() && chars[pos] == '[' { pos += 1; }
while pos < chars.len() {
if chars[pos].is_whitespace() || chars[pos] == ',' { pos += 1; continue; }
if chars[pos] == ']' { break; }
if chars[pos] == '{' { return true; }
break;
}
false
}
fn parse_json_array_of_objects(json: &str, prefix: &str, sub_map: &mut Vec<(String, Vec<String>)>) {
let chars: Vec<char> = json.chars().collect();
let mut pos = 0;
if pos < chars.len() && chars[pos] == '[' { pos += 1; }
loop {
while pos < chars.len() && (chars[pos].is_whitespace() || chars[pos] == ',') { pos += 1; }
if pos >= chars.len() || chars[pos] == ']' { break; }
if chars[pos] == '{' {
let end = find_matching_bracket(&chars, pos, '{', '}');
let obj_str: String = chars[pos..end + 1].iter().collect();
let mut obj_fields: Vec<(String, String)> = Vec::new();
parse_json_object(&obj_str, prefix, &mut obj_fields);
for (k, v) in obj_fields {
if let Some(entry) = sub_map.iter_mut().find(|(sk, _)| sk == &k) {
for part in v.split(", ") {
entry.1.push(part.to_string());
}
} else {
let vals: Vec<String> = v.split(", ").map(|s| s.to_string()).collect();
sub_map.push((k, vals));
}
}
pos = end + 1;
} else {
while pos < chars.len() && chars[pos] != ',' && chars[pos] != ']' { pos += 1; }
}
}
}
fn read_json_string(chars: &[char], pos: &mut usize) -> String {
if *pos >= chars.len() || chars[*pos] != '"' {
return String::new();
}
*pos += 1; let mut result = String::new();
while *pos < chars.len() && chars[*pos] != '"' {
if chars[*pos] == '\\' && *pos + 1 < chars.len() {
*pos += 1;
match chars[*pos] {
'"' => result.push('"'),
'\\' => result.push('\\'),
'/' => result.push('/'),
'n' => result.push('\n'),
'r' => result.push('\r'),
't' => result.push('\t'),
_ => result.push(chars[*pos]),
}
} else {
result.push(chars[*pos]);
}
*pos += 1;
}
if *pos < chars.len() { *pos += 1; } result
}
fn find_matching_bracket(chars: &[char], start: usize, open: char, close: char) -> usize {
let mut level = 0;
let mut pos = start;
let mut in_string = false;
while pos < chars.len() {
if chars[pos] == '"' && (pos == 0 || chars[pos - 1] != '\\') {
in_string = !in_string;
}
if !in_string {
if chars[pos] == open { level += 1; }
else if chars[pos] == close {
level -= 1;
if level == 0 { return pos; }
}
}
pos += 1;
}
pos.saturating_sub(1)
}
fn ucfirst_str(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
fn json_key_to_tag_name(key: &str) -> String {
let key = ucfirst_str(key);
let mut result = String::new();
let chars: Vec<char> = key.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
result.push(c);
if !c.is_ascii_alphabetic() && i + 1 < chars.len() {
if chars[i + 1].is_ascii_lowercase() {
let uc = chars[i + 1].to_ascii_uppercase();
result.push(uc);
i += 2;
continue;
}
}
i += 1;
}
result
}
pub fn read_real_audio(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 8 || !data.starts_with(b".ra\xfd") {
return Err(Error::InvalidData("not a RealAudio file".into()));
}
let mut tags = Vec::new();
let version = u16::from_be_bytes([data[4], data[5]]);
if version != 4 {
return Ok(tags);
}
let d = &data[8..];
if d.len() < 40 {
return Ok(tags);
}
let mut pos = 0;
pos += 4;
pos += 4;
pos += 2;
pos += 4;
pos += 2;
pos += 4;
if pos + 4 > d.len() { return Ok(tags); }
let audio_bytes = u32::from_be_bytes([d[pos], d[pos+1], d[pos+2], d[pos+3]]);
pos += 4;
tags.push(mktag("Real", "AudioBytes", "Audio Bytes", Value::U32(audio_bytes)));
if pos + 4 > d.len() { return Ok(tags); }
let bpm = u32::from_be_bytes([d[pos], d[pos+1], d[pos+2], d[pos+3]]);
pos += 4;
tags.push(mktag("Real", "BytesPerMinute", "Bytes Per Minute", Value::U32(bpm)));
pos += 4;
pos += 2;
if pos + 2 > d.len() { return Ok(tags); }
let afs = u16::from_be_bytes([d[pos], d[pos+1]]);
pos += 2;
tags.push(mktag("Real", "AudioFrameSize", "Audio Frame Size", Value::U16(afs)));
pos += 2;
pos += 2;
if pos + 2 > d.len() { return Ok(tags); }
let sr = u16::from_be_bytes([d[pos], d[pos+1]]);
pos += 2;
tags.push(mktag("Real", "SampleRate", "Sample Rate", Value::U16(sr)));
pos += 2;
if pos + 2 > d.len() { return Ok(tags); }
let bps = u16::from_be_bytes([d[pos], d[pos+1]]);
pos += 2;
tags.push(mktag("Real", "BitsPerSample", "Bits Per Sample", Value::U16(bps)));
if pos + 2 > d.len() { return Ok(tags); }
let ch = u16::from_be_bytes([d[pos], d[pos+1]]);
pos += 2;
tags.push(mktag("Real", "Channels", "Channels", Value::U16(ch)));
if pos >= d.len() { return Ok(tags); }
let fc2l = d[pos] as usize;
pos += 1;
pos += fc2l;
if pos >= d.len() { return Ok(tags); }
let fc3l = d[pos] as usize;
pos += 1;
pos += fc3l;
if pos >= d.len() { return Ok(tags); }
pos += 1;
if pos + 2 > d.len() { return Ok(tags); }
pos += 2;
if pos >= d.len() { return Ok(tags); }
let title_len = d[pos] as usize;
pos += 1;
if pos + title_len <= d.len() && title_len > 0 {
let title = String::from_utf8_lossy(&d[pos..pos + title_len]).to_string();
tags.push(mktag("Real", "Title", "Title", Value::String(title)));
}
pos += title_len;
if pos >= d.len() { return Ok(tags); }
let artist_len = d[pos] as usize;
pos += 1;
if pos + artist_len <= d.len() && artist_len > 0 {
let artist = String::from_utf8_lossy(&d[pos..pos + artist_len]).to_string();
tags.push(mktag("Real", "Artist", "Artist", Value::String(artist)));
}
pos += artist_len;
if pos >= d.len() { return Ok(tags); }
let copy_len = d[pos] as usize;
pos += 1;
if pos + copy_len <= d.len() && copy_len > 0 {
let copyright = String::from_utf8_lossy(&d[pos..pos + copy_len]).to_string();
tags.push(mktag("Real", "Copyright", "Copyright", Value::String(copyright)));
}
Ok(tags)
}
pub fn read_aac(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 7 || data[0] != 0xFF || (data[1] != 0xF0 && data[1] != 0xF1) {
return Err(Error::InvalidData("not an AAC ADTS file".into()));
}
let t0 = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let t1 = u16::from_be_bytes([data[4], data[5]]);
let _t2 = data[6];
let profile_type = (t0 >> 16) & 0x03; if profile_type == 3 {
return Err(Error::InvalidData("reserved AAC profile type".into()));
}
let sr_index = (t0 >> 10) & 0x0F;
if sr_index > 12 {
return Err(Error::InvalidData("invalid AAC sampling rate index".into()));
}
let channel_config = (t0 >> 6) & 0x07;
let mut tags = Vec::new();
let profile_name = match profile_type {
0 => "Main",
1 => "Low Complexity",
2 => "Scalable Sampling Rate",
_ => "Unknown",
};
tags.push(mktag("AAC", "ProfileType", "Profile Type", Value::String(profile_name.into())));
let sample_rates = [96000u32, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
if let Some(&sr) = sample_rates.get(sr_index as usize) {
tags.push(mktag("AAC", "SampleRate", "Sample Rate", Value::U32(sr)));
}
let channels_str = match channel_config {
0 => "?",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "5+1",
7 => "7+1",
_ => "?",
};
tags.push(mktag("AAC", "Channels", "Channels", Value::String(channels_str.into())));
let len = (((t0 as u64) << 11) & 0x1800) | (((t1 as u64) >> 5) & 0x07FF);
let len = len as usize;
if len >= 8 && data.len() >= len {
let frame_data = &data[7..len];
let mut i = 0;
while i < frame_data.len() {
while i < frame_data.len() && frame_data[i] == 0 { i += 1; }
let start = i;
while i < frame_data.len() && frame_data[i] >= 0x20 && frame_data[i] <= 0x7e { i += 1; }
let end = i;
if end - start >= 4 {
if let Ok(enc) = std::str::from_utf8(&frame_data[start..end]) {
let enc = enc.trim();
if enc.len() >= 4 {
tags.push(mktag("AAC", "Encoder", "Encoder", Value::String(enc.into())));
break;
}
}
}
i += 1;
}
}
Ok(tags)
}
pub fn read_wpg(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 16 || &data[0..4] != b"\xff\x57\x50\x43" {
return Err(Error::InvalidData("not a WPG file".into()));
}
let mut tags = Vec::new();
let offset = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
let ver = data[10];
let rev = data[11];
tags.push(mktag("WPG", "WPGVersion", "WPG Version", Value::String(format!("{}.{}", ver, rev))));
if ver < 1 || ver > 2 {
return Ok(tags);
}
let mut pos = if offset > 16 { offset } else { 16 };
if pos > data.len() { pos = data.len(); }
let mut records: Vec<String> = Vec::new();
let mut last_type: Option<u32> = None;
let mut count = 0usize;
let mut image_width_inches: Option<f64> = None;
let mut image_height_inches: Option<f64> = None;
let v1_map: std::collections::HashMap<u32, &str> = [
(0x01, "Fill Attributes"), (0x02, "Line Attributes"), (0x03, "Marker Attributes"),
(0x04, "Polymarker"), (0x05, "Line"), (0x06, "Polyline"), (0x07, "Rectangle"),
(0x08, "Polygon"), (0x09, "Ellipse"), (0x0a, "Reserved"), (0x0b, "Bitmap (Type 1)"),
(0x0c, "Graphics Text (Type 1)"), (0x0d, "Graphics Text Attributes"),
(0x0e, "Color Map"), (0x0f, "Start WPG (Type 1)"), (0x10, "End WPG"),
(0x11, "PostScript Data (Type 1)"), (0x12, "Output Attributes"),
(0x13, "Curved Polyline"), (0x14, "Bitmap (Type 2)"), (0x15, "Start Figure"),
(0x16, "Start Chart"), (0x17, "PlanPerfect Data"), (0x18, "Graphics Text (Type 2)"),
(0x19, "Start WPG (Type 2)"), (0x1a, "Graphics Text (Type 3)"),
(0x1b, "PostScript Data (Type 2)"),
].iter().cloned().collect();
let v2_map: std::collections::HashMap<u32, &str> = [
(0x00, "End Marker"), (0x01, "Start WPG"), (0x02, "End WPG"),
(0x03, "Form Settings"), (0x04, "Ruler Settings"), (0x05, "Grid Settings"),
(0x06, "Layer"), (0x08, "Pen Style Definition"), (0x09, "Pattern Definition"),
(0x0a, "Comment"), (0x0b, "Color Transfer"), (0x0c, "Color Palette"),
(0x0d, "DP Color Palette"), (0x0e, "Bitmap Data"), (0x0f, "Text Data"),
(0x10, "Chart Style"), (0x11, "Chart Data"), (0x12, "Object Image"),
(0x15, "Polyline"), (0x16, "Polyspline"), (0x17, "Polycurve"),
(0x18, "Rectangle"), (0x19, "Arc"), (0x1a, "Compound Polygon"),
(0x1b, "Bitmap"), (0x1c, "Text Line"), (0x1d, "Text Block"),
(0x1e, "Text Path"), (0x1f, "Chart"), (0x20, "Group"),
(0x21, "Object Capsule"), (0x22, "Font Settings"), (0x25, "Pen Fore Color"),
(0x26, "DP Pen Fore Color"), (0x27, "Pen Back Color"), (0x28, "DP Pen Back Color"),
(0x29, "Pen Style"), (0x2a, "Pen Pattern"), (0x2b, "Pen Size"),
(0x2c, "DP Pen Size"), (0x2d, "Line Cap"), (0x2e, "Line Join"),
(0x2f, "Brush Gradient"), (0x30, "DP Brush Gradient"), (0x31, "Brush Fore Color"),
(0x32, "DP Brush Fore Color"), (0x33, "Brush Back Color"), (0x34, "DP Brush Back Color"),
(0x35, "Brush Pattern"), (0x36, "Horizontal Line"), (0x37, "Vertical Line"),
(0x38, "Poster Settings"), (0x39, "Image State"), (0x3a, "Envelope Definition"),
(0x3b, "Envelope"), (0x3c, "Texture Definition"), (0x3d, "Brush Texture"),
(0x3e, "Texture Alignment"), (0x3f, "Pen Texture "),
].iter().cloned().collect();
let mut safety = 0;
loop {
if pos >= data.len() || safety > 10000 { break; }
safety += 1;
let (record_type, len, get_size) = if ver == 1 {
if pos >= data.len() { break; }
let rtype = data[pos] as u32;
pos += 1;
let (l, advance) = read_wpg_varint(data, pos);
pos += advance;
let gs = rtype == 0x0f; (rtype, l, gs)
} else {
if pos + 1 >= data.len() { break; }
let rtype = data[pos + 1] as u32;
pos += 2;
let (_, adv) = read_wpg_varint(data, pos);
pos += adv;
let (l, adv2) = read_wpg_varint(data, pos);
pos += adv2;
let gs = rtype == 0x01; let rtype_opt = if rtype > 0x3f { u32::MAX } else { rtype };
(rtype_opt, l, gs)
};
if record_type == u32::MAX {
pos += len;
continue;
}
if get_size {
let rec_end = pos + len;
if rec_end > data.len() { break; }
let rec = &data[pos..rec_end];
pos = rec_end;
if ver == 1 && rec.len() >= 6 {
let w = u16::from_le_bytes([rec[2], rec[3]]) as f64;
let h = u16::from_le_bytes([rec[4], rec[5]]) as f64;
image_width_inches = Some(w / 1200.0);
image_height_inches = Some(h / 1200.0);
} else if ver == 2 && rec.len() >= 21 {
let xres = u16::from_le_bytes([rec[0], rec[1]]) as f64;
let yres = u16::from_le_bytes([rec[2], rec[3]]) as f64;
let precision = rec[4];
let (x1, y1, x2, y2) = if precision == 0 && rec.len() >= 21 {
let x1 = i16::from_le_bytes([rec[13], rec[14]]) as f64;
let y1 = i16::from_le_bytes([rec[15], rec[16]]) as f64;
let x2 = i16::from_le_bytes([rec[17], rec[18]]) as f64;
let y2 = i16::from_le_bytes([rec[19], rec[20]]) as f64;
(x1, y1, x2, y2)
} else if precision == 1 && rec.len() >= 29 {
let x1 = i32::from_le_bytes([rec[13], rec[14], rec[15], rec[16]]) as f64;
let y1 = i32::from_le_bytes([rec[17], rec[18], rec[19], rec[20]]) as f64;
let x2 = i32::from_le_bytes([rec[21], rec[22], rec[23], rec[24]]) as f64;
let y2 = i32::from_le_bytes([rec[25], rec[26], rec[27], rec[28]]) as f64;
(x1, y1, x2, y2)
} else {
pos += 0; if let Some(lt) = last_type.take() {
let _val = if count > 1 { format!("{}x{}", lt, count) } else { format!("{}", lt) };
records.push(format_wpg_record(lt, count, if ver == 1 { &v1_map } else { &v2_map }));
}
last_type = Some(record_type);
count = 1;
continue;
};
let w = (x2 - x1).abs();
let h = (y2 - y1).abs();
let xres_div = if xres == 0.0 { 1200.0 } else { xres };
let yres_div = if yres == 0.0 { 1200.0 } else { yres };
image_width_inches = Some(w / xres_div);
image_height_inches = Some(h / yres_div);
}
} else {
pos += len;
}
if last_type == Some(record_type) {
count += 1;
} else {
if let Some(lt) = last_type.take() {
records.push(format_wpg_record(lt, count, if ver == 1 { &v1_map } else { &v2_map }));
}
if record_type == 0 && ver == 2 { break; } last_type = Some(record_type);
count = 1;
}
}
if let Some(lt) = last_type.take() {
records.push(format_wpg_record(lt, count, if ver == 1 { &v1_map } else { &v2_map }));
}
if let Some(w) = image_width_inches {
tags.push(mktag("WPG", "ImageWidthInches", "Image Width Inches", Value::String(format!("{:.2}", w))));
}
if let Some(h) = image_height_inches {
tags.push(mktag("WPG", "ImageHeightInches", "Image Height Inches", Value::String(format!("{:.2}", h))));
}
if !records.is_empty() {
let joined = records.join(", ");
tags.push(mktag("WPG", "Records", "Records", Value::String(joined)));
}
Ok(tags)
}
fn format_wpg_record(rtype: u32, count: usize, map: &std::collections::HashMap<u32, &str>) -> String {
let name = map.get(&rtype)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("Unknown (0x{:02x})", rtype));
if count > 1 {
format!("{} x {}", name, count)
} else {
name
}
}
fn read_wpg_varint(data: &[u8], pos: usize) -> (usize, usize) {
if pos >= data.len() { return (0, 0); }
let first = data[pos] as usize;
if first != 0xFF {
return (first, 1);
}
if pos + 2 >= data.len() { return (0, 1); }
let val = u16::from_le_bytes([data[pos + 1], data[pos + 2]]) as usize;
if val & 0x8000 != 0 {
if pos + 4 >= data.len() { return (val & 0x7FFF, 3); }
let hi = u16::from_le_bytes([data[pos + 3], data[pos + 4]]) as usize;
let full = ((val & 0x7FFF) << 16) | hi;
return (full, 5);
}
(val, 3)
}
pub fn read_ram(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 4 {
return Err(Error::InvalidData("not a RAM file".into()));
}
let text = String::from_utf8_lossy(data);
let _first_line = text.lines().next().unwrap_or("").trim();
let valid_protocols = ["rtsp://", "pnm://", "http://", "rtspt://", "rtspu://", "mmst://", "file://"];
let has_valid = text.lines().any(|line| {
let l = line.trim();
valid_protocols.iter().any(|p| l.starts_with(p))
});
if !has_valid && !text.starts_with(".RMF") && !data.starts_with(b".ra\xfd") {
return Err(Error::InvalidData("not a Real RAM file".into()));
}
let mut tags = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() { continue; }
if line.starts_with("http://") {
if !line.ends_with(".ra") && !line.ends_with(".rm") && !line.ends_with(".rv")
&& !line.ends_with(".rmvb") && !line.ends_with(".smil") {
continue;
}
}
if valid_protocols.iter().any(|p| line.starts_with(p)) {
tags.push(mktag("Real", "URL", "URL", Value::String(line.into())));
}
}
Ok(tags)
}
pub fn read_dss(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 68 {
return Err(Error::InvalidData("DSS file too small".into()));
}
if !(data[0] == 0x02 || data[0] == 0x03)
|| data[1] != b'd'
|| data[2] != b's'
|| (data[3] != b's' && data[3] != b'2')
{
return Err(Error::InvalidData("not a DSS/DS2 file".into()));
}
let mut tags = Vec::new();
if data.len() >= 28 {
let model_bytes = &data[12..28];
let model = String::from_utf8_lossy(model_bytes)
.trim_end_matches('\0')
.trim()
.to_string();
if !model.is_empty() {
tags.push(mktag("Olympus", "Model", "Camera Model Name", Value::String(model)));
}
}
if data.len() >= 50 {
let st_bytes = &data[38..50];
let st_str = String::from_utf8_lossy(st_bytes);
if let Some(dt) = parse_dss_time(&st_str) {
tags.push(mktag("Olympus", "StartTime", "Start Time", Value::String(dt)));
}
}
if data.len() >= 62 {
let et_bytes = &data[50..62];
let et_str = String::from_utf8_lossy(et_bytes);
if let Some(dt) = parse_dss_time(&et_str) {
tags.push(mktag("Olympus", "EndTime", "End Time", Value::String(dt)));
}
}
if data.len() >= 68 {
let dur_bytes = &data[62..68];
let dur_str = String::from_utf8_lossy(dur_bytes);
if let Some(dur_secs) = parse_dss_duration(&dur_str) {
let dur_display = dss_convert_duration(dur_secs);
tags.push(mktag("Olympus", "Duration", "Duration", Value::String(dur_display)));
}
}
Ok(tags)
}
fn parse_dss_time(s: &str) -> Option<String> {
let s = s.trim_matches('\0');
if s.len() < 12 {
return None;
}
let yy = &s[0..2];
let mm = &s[2..4];
let dd = &s[4..6];
let hh = &s[6..8];
let mi = &s[8..10];
let ss = &s[10..12];
if !yy.chars().all(|c| c.is_ascii_digit()) { return None; }
Some(format!("20{}:{}:{} {}:{}:{}", yy, mm, dd, hh, mi, ss))
}
fn parse_dss_duration(s: &str) -> Option<f64> {
let s = s.trim_matches('\0');
if s.len() < 6 { return None; }
let hh: u64 = s[0..2].parse().ok()?;
let mm: u64 = s[2..4].parse().ok()?;
let ss: u64 = s[4..6].parse().ok()?;
Some(((hh * 60 + mm) * 60 + ss) as f64)
}
fn dss_convert_duration(secs: f64) -> String {
if secs == 0.0 {
return "0 s".to_string();
}
if secs < 30.0 {
return format!("{:.2} s", secs);
}
let secs_u = (secs + 0.5) as u64;
let h = secs_u / 3600;
let m = (secs_u % 3600) / 60;
let s = secs_u % 60;
format!("{}:{:02}:{:02}", h, m, s)
}
fn mktag(family: &str, name: &str, description: &str, value: Value) -> Tag {
let pv = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: family.into(),
family1: family.into(),
family2: "Other".into(),
},
raw_value: value,
print_value: pv,
priority: 0,
}
}
pub fn read_indesign(data: &[u8]) -> Result<Vec<Tag>> {
let master_guid = &[0x06u8, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5,
0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D];
let object_header_guid = &[0xDE, 0x39, 0x39, 0x79, 0x51, 0x88, 0x4B, 0x6C,
0x8E, 0x63, 0xEE, 0xF8, 0xAE, 0xE0, 0xDD, 0x38];
if data.len() < 4096 || !data.starts_with(master_guid) {
return Err(crate::error::Error::InvalidData("not an InDesign file".into()));
}
if data.len() < 8192 {
return Ok(vec![]);
}
let page1 = &data[..4096];
let page2 = &data[4096..8192];
let cur_page = {
let seq1 = u64::from_le_bytes(page1[264..272].try_into().unwrap_or([0;8]));
let seq2 = if page2.starts_with(master_guid) {
u64::from_le_bytes(page2[264..272].try_into().unwrap_or([0;8]))
} else { 0 };
if seq2 > seq1 { page2 } else { page1 }
};
let _stream_is_le = cur_page[24] == 1;
let pages = u32::from_le_bytes(cur_page[280..284].try_into().unwrap_or([0;4]));
let start_pos = (pages as usize) * 4096;
if start_pos >= data.len() {
return Ok(vec![]);
}
let mut pos = start_pos;
while pos + 32 <= data.len() {
if &data[pos..pos+16] != object_header_guid {
break;
}
let obj_len = u32::from_le_bytes(
data[pos+24..pos+28].try_into().unwrap_or([0;4])
) as usize;
pos += 32;
if obj_len == 0 || pos + obj_len > data.len() { break; }
let obj_data = &data[pos..pos + obj_len];
if obj_len > 56 {
if let Some(xp_pos) = find_xpacket(obj_data) {
let xmp_data = &obj_data[xp_pos..];
if let Ok(xmp_tags) = crate::metadata::XmpReader::read(xmp_data) {
return Ok(xmp_tags);
}
}
}
pos += obj_len;
}
Ok(vec![])
}
fn find_xpacket(data: &[u8]) -> Option<usize> {
for i in 0..data.len().saturating_sub(10) {
if data[i..].starts_with(b"<?xpacket") || data[i..].starts_with(b"<x:xmpmeta") {
return Some(i);
}
}
None
}
pub fn read_pcap(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 24 {
return Err(crate::error::Error::InvalidData("not a PCAP file".into()));
}
let is_le = data[0] == 0xD4 && data[1] == 0xC3;
let r16 = |d: &[u8], o: usize| -> u16 {
if o+2 > d.len() { return 0; }
if is_le { u16::from_le_bytes([d[o], d[o+1]]) }
else { u16::from_be_bytes([d[o], d[o+1]]) }
};
let r32 = |d: &[u8], o: usize| -> u32 {
if o+4 > d.len() { return 0; }
if is_le { u32::from_le_bytes([d[o], d[o+1], d[o+2], d[o+3]]) }
else { u32::from_be_bytes([d[o], d[o+1], d[o+2], d[o+3]]) }
};
let maj = r16(data, 4);
let min = r16(data, 6);
let link_type = r32(data, 20);
let mut tags = Vec::new();
let bo_str = if is_le { "Little-endian (Intel, II)" } else { "Big-endian (Motorola, MM)" };
tags.push(mktag("PCAP", "ByteOrder", "Byte Order", Value::String(bo_str.into())));
tags.push(mktag("PCAP", "PCAPVersion", "PCAP Version",
Value::String(format!("PCAP {}.{}", maj, min))));
tags.push(mktag("PCAP", "LinkType", "Link Type",
Value::String(pcap_link_type_name(link_type))));
Ok(tags)
}
pub fn read_pcapng(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 28 || data[0] != 0x0A || data[1] != 0x0D || data[2] != 0x0D || data[3] != 0x0A {
return Err(crate::error::Error::InvalidData("not a PCAPNG file".into()));
}
let bo_magic_le = data.len() >= 12 &&
data[8] == 0x4D && data[9] == 0x3C && data[10] == 0x2B && data[11] == 0x1A;
let is_le = bo_magic_le;
let r16 = |d: &[u8], o: usize| -> u16 {
if o+2 > d.len() { return 0; }
if is_le { u16::from_le_bytes([d[o], d[o+1]]) }
else { u16::from_be_bytes([d[o], d[o+1]]) }
};
let r32 = |d: &[u8], o: usize| -> u32 {
if o+4 > d.len() { return 0; }
if is_le { u32::from_le_bytes([d[o], d[o+1], d[o+2], d[o+3]]) }
else { u32::from_be_bytes([d[o], d[o+1], d[o+2], d[o+3]]) }
};
let maj = r16(data, 12);
let min = r16(data, 14);
let blk_len = r32(data, 4) as usize;
let mut tags = Vec::new();
let bo_str = if is_le { "Little-endian (Intel, II)" } else { "Big-endian (Motorola, MM)" };
tags.push(mktag("PCAP", "ByteOrder", "Byte Order", Value::String(bo_str.into())));
tags.push(mktag("PCAP", "PCAPVersion", "PCAP Version",
Value::String(format!("PCAPNG {}.{}", maj, min))));
let opt_start = 24usize;
let opt_end = if blk_len > 4 && blk_len <= data.len() { blk_len - 4 } else { data.len() };
parse_pcapng_options(data, opt_start, opt_end, is_le, &mut tags, "shb");
let idb_start = if blk_len < data.len() { blk_len } else { return Ok(tags); };
if idb_start + 20 <= data.len() {
let idb_type = r32(data, idb_start);
if idb_type == 1 {
let idb_len = r32(data, idb_start + 4) as usize;
let link_type = r32(data, idb_start + 8) & 0xFFFF;
let link_name = pcap_link_type_name(link_type);
tags.push(mktag("PCAP", "LinkType", "Link Type", Value::String(link_name)));
let idb_opt_start = idb_start + 16;
let idb_opt_end = if idb_start + idb_len > 4 && idb_start + idb_len <= data.len() {
idb_start + idb_len - 4
} else { data.len() };
parse_pcapng_options(data, idb_opt_start, idb_opt_end, is_le, &mut tags, "idb");
let epb_start = idb_start + idb_len;
parse_pcapng_blocks(data, epb_start, is_le, &mut tags);
}
}
Ok(tags)
}
fn parse_pcapng_options(data: &[u8], start: usize, end: usize, is_le: bool,
tags: &mut Vec<Tag>, ctx: &str) {
let r16 = |d: &[u8], o: usize| -> u16 {
if o+2 > d.len() { return 0; }
if is_le { u16::from_le_bytes([d[o], d[o+1]]) }
else { u16::from_be_bytes([d[o], d[o+1]]) }
};
let mut pos = start;
while pos + 4 <= end.min(data.len()) {
let opt_code = r16(data, pos);
let opt_len = r16(data, pos + 2) as usize;
pos += 4;
if opt_code == 0 { break; } let padded_len = (opt_len + 3) & !3;
if pos + opt_len > data.len() { break; }
let opt_data = &data[pos..pos + opt_len];
match (ctx, opt_code) {
("shb", 2) => { let s = String::from_utf8_lossy(opt_data).to_string();
tags.push(mktag("PCAP", "Hardware", "Hardware", Value::String(s)));
}
("shb", 3) => { let s = String::from_utf8_lossy(opt_data).to_string();
tags.push(mktag("PCAP", "OperatingSystem", "Operating System", Value::String(s)));
}
("shb", 4) => { let s = String::from_utf8_lossy(opt_data).to_string();
tags.push(mktag("PCAP", "UserApplication", "User Application", Value::String(s)));
}
("idb", 2) => { let s = String::from_utf8_lossy(opt_data).to_string();
tags.push(mktag("PCAP", "DeviceName", "Device Name", Value::String(s)));
}
("idb", 9) => { if opt_len >= 1 {
let tsresol = opt_data[0];
let resolution = if tsresol & 0x80 != 0 {
let exp = tsresol & 0x7F;
format!("2^-{}", exp)
} else {
let exp = tsresol & 0x7F;
format!("1e-{:02}", exp)
};
tags.push(mktag("PCAP", "TimeStampResolution", "Time Stamp Resolution",
Value::String(resolution)));
}
}
("idb", 12) => { let s = String::from_utf8_lossy(opt_data).to_string();
if !tags.iter().any(|t| t.name == "OperatingSystem") {
tags.push(mktag("PCAP", "OperatingSystem", "Operating System", Value::String(s)));
}
}
_ => {}
}
pos += padded_len;
}
}
fn parse_pcapng_blocks(data: &[u8], start: usize, is_le: bool, tags: &mut Vec<Tag>) {
let r32 = |d: &[u8], o: usize| -> u32 {
if o+4 > d.len() { return 0; }
if is_le { u32::from_le_bytes([d[o], d[o+1], d[o+2], d[o+3]]) }
else { u32::from_be_bytes([d[o], d[o+1], d[o+2], d[o+3]]) }
};
let _r64 = |d: &[u8], o: usize| -> u64 {
if o+8 > d.len() { return 0; }
if is_le { u64::from_le_bytes([d[o], d[o+1], d[o+2], d[o+3], d[o+4], d[o+5], d[o+6], d[o+7]]) }
else { u64::from_be_bytes([d[o], d[o+1], d[o+2], d[o+3], d[o+4], d[o+5], d[o+6], d[o+7]]) }
};
let mut pos = start;
while pos + 8 <= data.len() {
let block_type = r32(data, pos);
let block_len = r32(data, pos + 4) as usize;
if block_len < 12 || pos + block_len > data.len() { break; }
if block_type == 6 && block_len >= 28 {
let ts_hi = r32(data, pos + 12) as u64;
let ts_lo = r32(data, pos + 16) as u64;
let ts_raw = (ts_hi << 32) | ts_lo;
let ts_secs = ts_raw / 1_000_000;
let ts_usecs = ts_raw % 1_000_000;
if let Some(dt) = format_unix_timestamp(ts_secs as i64, ts_usecs) {
tags.push(mktag("PCAP", "TimeStamp", "Time Stamp", Value::String(dt)));
}
break; }
pos += block_len;
}
}
fn format_unix_timestamp(secs: i64, usecs: u64) -> Option<String> {
let tz_offset_secs = get_local_tz_offset();
let adjusted = secs + tz_offset_secs as i64;
let (y, mo, d, h, mi, s) = unix_to_datetime(adjusted);
let tz_hours = tz_offset_secs / 3600;
let tz_mins = (tz_offset_secs.abs() % 3600) / 60;
let tz_sign = if tz_offset_secs >= 0 { '+' } else { '-' };
Some(format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}.{:06}{}{:02}:{:02}",
y, mo, d, h, mi, s, usecs, tz_sign, tz_hours.abs(), tz_mins))
}
fn get_local_tz_offset() -> i32 {
0
}
fn unix_to_datetime(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
const SECS_PER_DAY: i64 = 86400;
const DAYS_PER_400Y: i64 = 146097;
let (days, rem) = if secs >= 0 {
(secs / SECS_PER_DAY, secs % SECS_PER_DAY)
} else {
let d = (secs + 1) / SECS_PER_DAY - 1;
let r = secs - d * SECS_PER_DAY;
(d, r)
};
let h = (rem / 3600) as u32;
let m = ((rem % 3600) / 60) as u32;
let s = (rem % 60) as u32;
let z = days + 719468; let era = if z >= 0 { z } else { z - DAYS_PER_400Y + 1 } / DAYS_PER_400Y;
let doe = z - era * DAYS_PER_400Y;
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 as i32, mo as u32, d as u32, h, m, s)
}
fn pcap_link_type_name(link_type: u32) -> String {
match link_type {
0 => "BSD Loopback".into(),
1 => "IEEE 802.3 Ethernet".into(),
9 => "PPP".into(),
105 => "IEEE 802.11".into(),
108 => "OpenBSD Loopback".into(),
113 => "Linux SLL".into(),
127 => "IEEE 802.11 Radiotap".into(),
_ => format!("{}", link_type),
}
}
pub fn read_itc(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 8 {
return Err(Error::InvalidData("not an ITC file".into()));
}
let first_size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
if &data[4..8] != b"itch" || first_size < 0x1c || first_size >= 0x10000 {
return Err(Error::InvalidData("not an ITC file".into()));
}
let mut tags = Vec::new();
let mut pos = 0usize;
while pos + 8 <= data.len() {
let block_size = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
let block_tag = &data[pos+4..pos+8];
if block_size < 8 || block_size >= 0x80000000 {
break;
}
if pos + block_size > data.len() {
break;
}
if block_tag == b"itch" {
let body_start = pos + 8;
let body_end = pos + block_size;
let body = &data[body_start..body_end];
if body.len() >= 20 {
let _data_type = &body[0x10 - 8 + 8..]; let dt_bytes = &data[pos+16..pos+20];
let dt_str = match dt_bytes {
b"artw" => "Artwork",
_ => "Unknown",
};
tags.push(mktag("ITC", "DataType", "Data Type", Value::String(dt_str.into())));
}
} else if block_tag == b"item" {
if pos + 12 > data.len() { break; }
let inner_len = u32::from_be_bytes([data[pos+8], data[pos+9], data[pos+10], data[pos+11]]) as usize;
if inner_len < 0xd0 || inner_len > block_size { break; }
let image_size = block_size - inner_len;
let mut scan = pos + 12;
let mut remaining = inner_len - 12;
loop {
if remaining < 4 || scan + 4 > data.len() { break; }
let word = &data[scan..scan+4];
remaining -= 4;
scan += 4;
if word == b"\0\0\0\0" { break; }
}
if remaining < 4 { break; }
let hdr_start = scan;
let hdr_len = remaining;
if hdr_start + hdr_len > data.len() { break; }
let hdr = &data[hdr_start..hdr_start + hdr_len];
if hdr.len() < 0xb4 || &hdr[0xb0..0xb4] != b"data" { break; }
if hdr.len() >= 8 {
let lib_id = &hdr[0..8];
let hex: String = lib_id.iter().map(|b| format!("{:02X}", b)).collect();
tags.push(mktag("ITC", "LibraryID", "Library ID", Value::String(hex)));
}
if hdr.len() >= 16 {
let track_id = &hdr[8..16];
let hex: String = track_id.iter().map(|b| format!("{:02X}", b)).collect();
tags.push(mktag("ITC", "TrackID", "Track ID", Value::String(hex)));
}
if hdr.len() >= 20 {
let loc = &hdr[16..20];
let loc_str = match loc {
b"down" => "Downloaded Separately",
b"locl" => "Local Music File",
_ => "Unknown",
};
tags.push(mktag("ITC", "DataLocation", "Data Location", Value::String(loc_str.into())));
}
if hdr.len() >= 24 {
let img_type = &hdr[20..24];
let type_str = match img_type {
b"PNGf" => "PNG",
b"\0\0\0\x0d" => "JPEG",
_ => "Unknown",
};
tags.push(mktag("ITC", "ImageType", "Image Type", Value::String(type_str.into())));
}
if hdr.len() >= 32 {
let width = u32::from_be_bytes([hdr[28], hdr[29], hdr[30], hdr[31]]);
tags.push(mktag("ITC", "ImageWidth", "Image Width", Value::U32(width)));
}
if hdr.len() >= 36 {
let height = u32::from_be_bytes([hdr[32], hdr[33], hdr[34], hdr[35]]);
tags.push(mktag("ITC", "ImageHeight", "Image Height", Value::U32(height)));
}
if image_size > 0 {
let img_start = pos + block_size - image_size;
if img_start + image_size <= data.len() {
let img_data = data[img_start..img_start + image_size].to_vec();
tags.push(mktag("ITC", "ImageData", "Image Data", Value::Binary(img_data)));
}
}
}
pos += block_size;
}
Ok(tags)
}
pub fn read_czi(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 100 || !data.starts_with(b"ZISRAWFILE\x00\x00\x00\x00\x00\x00") {
return Err(Error::InvalidData("not a ZISRAW/CZI file".into()));
}
let mut tags = Vec::new();
if data.len() >= 0x28 {
let major = u32::from_le_bytes([data[0x20], data[0x21], data[0x22], data[0x23]]);
let minor = u32::from_le_bytes([data[0x24], data[0x25], data[0x26], data[0x27]]);
let version = format!("{}.{}", major, minor);
tags.push(mktag("ZISRAW", "ZISRAWVersion", "ZISRAW Version", Value::String(version)));
}
if data.len() >= 0x40 {
let guid = hex_encode(&data[0x30..0x40]);
tags.push(mktag("ZISRAW", "PrimaryFileGUID", "Primary File GUID", Value::String(guid)));
}
if data.len() >= 0x50 {
let guid = hex_encode(&data[0x40..0x50]);
tags.push(mktag("ZISRAW", "FileGUID", "File GUID", Value::String(guid)));
}
if data.len() >= 100 {
let meta_off = u64::from_le_bytes([data[92], data[93], data[94], data[95],
data[96], data[97], data[98], data[99]]) as usize;
if meta_off > 0 && meta_off + 288 <= data.len() {
if &data[meta_off..meta_off+16] == b"ZISRAWMETADATA\x00\x00" {
let xml_len = u32::from_le_bytes([data[meta_off+32], data[meta_off+33],
data[meta_off+34], data[meta_off+35]]) as usize;
let xml_start = meta_off + 288;
if xml_start + xml_len <= data.len() {
let xml_bytes = &data[xml_start..xml_start+xml_len];
tags.push(mktag("ZISRAW", "XML", "XML",
Value::String(format!("(Binary data {} bytes, use -b option to extract)", xml_len))));
if let Ok(xml_str) = std::str::from_utf8(xml_bytes) {
czi_parse_xml(xml_str, &mut tags);
}
}
}
}
}
Ok(tags)
}
fn czi_parse_xml(xml: &str, tags: &mut Vec<Tag>) {
use xml::reader::{EventReader, XmlEvent};
let parser = EventReader::from_str(xml);
let mut path: Vec<String> = Vec::new();
let mut ignored: Vec<bool> = Vec::new();
let mut current_text = String::new();
let mut has_child: Vec<bool> = Vec::new();
let ignore_elems = ["ImageDocument", "Metadata", "Information"];
for event in parser {
match event {
Ok(XmlEvent::StartElement { name, attributes, .. }) => {
let elem_name = &name.local_name;
let is_ignored = ignore_elems.contains(&elem_name.as_str());
ignored.push(is_ignored);
if let Some(last) = has_child.last_mut() { *last = true; }
if !is_ignored {
path.push(elem_name.clone());
has_child.push(false);
current_text.clear();
let path_str = path.join("");
for attr in &attributes {
let aname = &attr.name;
if aname.prefix.as_deref() == Some("xmlns")
|| aname.prefix.as_deref() == Some("xsi")
|| aname.local_name.starts_with("xmlns")
{
continue;
}
let raw_tag = format!("{}{}", path_str, aname.local_name);
let tag_name = czi_shorten_tag_name(&raw_tag);
if !tag_name.is_empty() {
let val = attr.value.trim().to_string();
tags.push(mktag("ZISRAW", &tag_name, &tag_name, Value::String(val)));
}
}
} else {
has_child.push(false);
current_text.clear();
}
}
Ok(XmlEvent::Characters(text)) | Ok(XmlEvent::CData(text)) => {
current_text.push_str(&text);
}
Ok(XmlEvent::EndElement { .. }) => {
let is_ignored = ignored.pop().unwrap_or(false);
let is_leaf = has_child.pop().unwrap_or(false) == false;
if !is_ignored {
if is_leaf {
let text = current_text.trim().to_string();
if !text.is_empty() {
let path_str = path.join("");
let tag_name = czi_shorten_tag_name(&path_str);
if !tag_name.is_empty() {
tags.push(mktag("ZISRAW", &tag_name, &tag_name, Value::String(text)));
}
}
}
path.pop();
} else {
}
current_text.clear();
}
_ => {}
}
}
}
fn czi_shorten_tag_name(name: &str) -> String {
let mut s = name.to_string();
s = s.strip_prefix("HardwareSetting").unwrap_or(&s).to_string();
s = regex_replace(&s, "^DevicesDevice", "Device");
s = s.replace("LightPathNode", "");
s = s.replace("Successors", "");
s = s.replace("ExperimentExperiment", "Experiment");
s = regex_replace(&s, "ObjectivesObjective", "Objective");
s = s.replace("ChannelsChannel", "Channel");
s = s.replace("TubeLensesTubeLens", "TubeLens");
s = regex_replace(&s, "^ExperimentHardwareSettingsPoolHardwareSetting", "HardwareSetting");
s = s.replace("SharpnessMeasureSetSharpnessMeasure", "Sharpness");
s = s.replace("FocusSetupAutofocusSetup", "Autofocus");
s = s.replace("TracksTrack", "Track");
s = s.replace("ChannelRefsChannelRef", "ChannelRef");
s = s.replace("ChangerChanger", "Changer");
s = s.replace("ElementsChangerElement", "Changer");
s = s.replace("ChangerElements", "Changer");
s = s.replace("ContrastChangerContrast", "Contrast");
s = s.replace("KeyFunctionsKeyFunction", "KeyFunction");
s = regex_replace(&s, "ManagerContrastManager(Contrast)?", "ManagerContrast");
s = s.replace("ObjectiveChangerObjective", "ObjectiveChanger");
s = s.replace("ManagerLightManager", "ManagerLight");
s = s.replace("WavelengthAreasWavelengthArea", "WavelengthArea");
s = s.replace("ReflectorChangerReflector", "ReflectorChanger");
s = regex_replace(&s, "^StageStageAxesStageAxis", "StageAxis");
s = s.replace("ShutterChangerShutter", "ShutterChanger");
s = s.replace("OnOffChangerOnOff", "OnOffChanger");
s = s.replace("UnsharpMaskStateUnsharpMask", "UnsharpMask");
s = s.replace("Acquisition", "Acq");
s = s.replace("Continuous", "Cont");
s = s.replace("Resolution", "Res");
s = s.replace("Experiment", "Expt");
s = s.replace("Threshold", "Thresh");
s = s.replace("Reference", "Ref");
s = s.replace("Magnification", "Mag");
s = s.replace("Original", "Orig");
s = s.replace("FocusSetupFocusStrategySetup", "Focus");
s = s.replace("ParametersParameter", "Parameter");
s = s.replace("IntervalInfo", "Interval");
s = s.replace("ExptBlocksAcqBlock", "AcqBlock");
s = s.replace("MicroscopesMicroscope", "Microscope");
s = s.replace("TimeSeriesInterval", "TimeSeries");
while let Some(idx) = s.find("Interval") {
let rest = &s[idx + "Interval".len()..];
if rest.contains("Interval") {
s = format!("{}{}", &s[..idx], &s[idx + "Interval".len()..]);
} else {
break;
}
}
s = s.replace("SingleTileRegionsSingleTileRegion", "SingleTileRegion");
s = s.replace("AcquisitionMode", ""); s = s.replace("DetectorsDetector", "Detector");
s = regex_replace(&s, "Setup[s]?", "");
s = s.replace("Setting", "");
s = s.replace("TrackTrack", "Track");
s = s.replace("AnalogOutMaximumsAnalogOutMaximum", "AnalogOutMaximum");
s = s.replace("AnalogOutMinimumsAnalogOutMinimum", "AnalogOutMinimum");
s = s.replace("DigitalOutLabelsDigitalOutLabelLabel", "DigitalOutLabelLabel");
s = s.replace("FocusDefiniteFocus", "FocusDefinite");
s = s.replace("ChangerChanger", "Changer");
s = s.replace("Calibration", "Cal");
s = s.replace("LightSwitchChangerRLTLSwitch", "LightSwitchChangerRLTL");
s = s.replace("Parameters", "");
s = s.replace("Fluorescence", "Fluor");
s = s.replace("CameraGeometryCameraGeometry", "CameraGeometry");
s = s.replace("CameraCamera", "Camera");
s = s.replace("DetectorsCamera", "Camera");
s = s.replace("FilterChangerLeftChangerEmissionFilter", "LeftChangerEmissionFilter");
s = s.replace("SwitchingStatesSwitchingState", "SwitchingState");
s = s.replace("Information", "Info");
s = s.replace("SubDimensions", "");
s = s.replace("SubDimension", "");
s = regex_replace_first(&s, "Setups?", "");
s = regex_replace_first(&s, "Parameters?", "");
s = s.replace("Calculate", "Calc");
s = s.replace("Visibility", "Vis");
s = s.replace("Orientation", "Orient");
s = s.replace("ListItems", "Items");
s = s.replace("Increment", "Incr");
s = s.replace("Parameter", "Param");
s = regex_replace(&s, "(ParfocalParcentralValues?)+ParfocalParcentralValues?", "Parcentral");
s = s.replace("ParcentralParcentral", "Parcentral");
s = s.replace("CorrFocusCorrection", "FocusCorr");
s = regex_replace(&s, "(ApoTomeDepthInfo)+Element", "ApoTomeDepth");
s = regex_replace(&s, "(ApoTomeClickStopInfo)+Element", "ApoTomeClickStop");
s = s.replace("DepthDepth", "Depth");
s = regex_replace(&s, "(Devices?)+Device", "Device");
s = regex_replace(&s, "(BeamPathNode)+", "BeamPathNode");
s = s.replace("BeamPathsBeamPath", "BeamPath");
s = s.replace("BeamPathBeamPath", "BeamPath");
s = s.replace("Configuration", "Config");
s = s.replace("StageAxesStageAxis", "StageAxis");
s = s.replace("RangesRange", "Range");
s = s.replace("DataGridDatasGridData", "DataGrid");
s = s.replace("DataMicroscopeDatasMicroscopeData", "DataMicroscope");
s = s.replace("DataWegaDatasWegaData", "DataWega");
s = s.replace("ClickStopPositionsClickStopPosition", "ClickStopPosition");
s = regex_replace(&s, "LightSourcess?LightSource(Settings)?(LightSource)?", "LightSource");
s = s.replace("FilterSetsFilterSet", "FilterSet");
s = s.replace("EmissionFiltersEmissionFilter", "EmissionFilter");
s = s.replace("ExcitationFiltersExcitationFilter", "ExcitationFilter");
s = s.replace("FiltersFilter", "Filter");
s = s.replace("DichroicsDichroic", "Dichronic");
s = s.replace("WavelengthsWavelength", "Wavelength");
s = s.replace("MultiTrackSetup", "MultiTrack");
s = s.replace("TrackTrack", "Track");
s = s.replace("DataGrabberSetup", "DataGrabber");
s = s.replace("CameraFrameSetup", "CameraFrame");
s = regex_replace(&s, "TimeSeries(TimeSeries|Setups)", "TimeSeries");
s = s.replace("FocusFocus", "Focus");
s = s.replace("FocusAutofocus", "Autofocus");
s = regex_replace(&s, "Focus(Hardware|Software)(Autofocus)+", "Autofocus$1");
s = s.replace("AutofocusAutofocus", "Autofocus");
s
}
fn regex_replace(s: &str, pat: &str, replacement: &str) -> String {
if pat.starts_with('^') {
let pat_body = &pat[1..];
if s.starts_with(pat_body) {
return format!("{}{}", replacement, &s[pat_body.len()..]);
}
return s.to_string();
}
if pat.contains('(') {
return czi_regex_replace_group(s, pat, replacement);
}
if let Some(idx) = s.find(pat) {
format!("{}{}{}", &s[..idx], replacement, &s[idx + pat.len()..])
} else {
s.to_string()
}
}
fn regex_replace_first(s: &str, pat: &str, replacement: &str) -> String {
let variants: Vec<&str> = if pat.ends_with('?') {
let base = &pat[..pat.len()-1];
let long = pat.trim_end_matches('?');
vec![long, base]
} else {
vec![pat]
};
if pat.ends_with('?') {
let long = pat.trim_end_matches('?'); let base = &long[..long.len()-1]; if let Some(idx) = s.find(long) {
return format!("{}{}{}", &s[..idx], replacement, &s[idx + long.len()..]);
}
if let Some(idx) = s.find(base) {
return format!("{}{}{}", &s[..idx], replacement, &s[idx + base.len()..]);
}
}
let _ = variants;
s.to_string()
}
fn czi_regex_replace_group(s: &str, pat: &str, _replacement: &str) -> String {
if pat == "(Devices?)+Device" {
let result = s.to_string();
let mut r = result.clone();
loop {
let prev = r.clone();
r = r.replace("DevicesDevice", "Device");
r = r.replace("DeviceDevice", "Device");
if r == prev { break; }
}
return r;
}
if pat == "(BeamPathNode)+" {
let mut r = s.to_string();
loop {
let prev = r.clone();
r = r.replace("BeamPathNodeBeamPathNode", "BeamPathNode");
if r == prev { break; }
}
return r;
}
if pat == "ManagerContrastManager(Contrast)?" {
let r = s.replace("ManagerContrastManagerContrast", "ManagerContrast");
let r = r.replace("ManagerContrastManager", "ManagerContrast");
return r;
}
if pat.starts_with("Focus(Hardware|Software)(Autofocus)+") {
for suffix in &["Hardware", "Software"] {
let search = format!("Focus{}Autofocus", suffix);
if let Some(idx) = s.find(&search) {
let mut end = idx + search.len();
while s[end..].starts_with("Autofocus") {
end += "Autofocus".len();
}
return format!("{}Autofocus{}{}", &s[..idx], suffix, &s[end..]);
}
}
return s.to_string();
}
if pat.starts_with("LightSourcess?LightSource") {
let r = s.replace("LightSourcessLightSourceSettingsLightSource", "LightSource");
let r = r.replace("LightSourcessLightSourceSettings", "LightSource");
let r = r.replace("LightSourcessLightSourceLightSource", "LightSource");
let r = r.replace("LightSourcessLightSource", "LightSource");
let r = r.replace("LightSourcesLightSourceSettingsLightSource", "LightSource");
let r = r.replace("LightSourcesLightSourceSettings", "LightSource");
let r = r.replace("LightSourcesLightSourceLightSource", "LightSource");
let r = r.replace("LightSourcesLightSource", "LightSource");
return r;
}
if pat.starts_with("TimeSeries(TimeSeries|Setups)") {
let r = s.replace("TimeSeriesTimeSeries", "TimeSeries");
let r = r.replace("TimeSeriesSetups", "TimeSeries");
return r;
}
if pat.starts_with("(ApoTomeDepthInfo)+Element") {
let mut r = s.to_string();
loop {
let prev = r.clone();
r = r.replace("ApoTomeDepthInfoApoTomeDepthInfoElement", "ApoTomeDepthInfoElement");
if r == prev { break; }
}
r = r.replace("ApoTomeDepthInfoElement", "ApoTomeDepth");
return r;
}
if pat.starts_with("(ApoTomeClickStopInfo)+Element") {
let mut r = s.to_string();
r = r.replace("ApoTomeClickStopInfoElement", "ApoTomeClickStop");
return r;
}
if pat.starts_with("(ParfocalParcentralValues?)+") {
let r = s.replace("ParfocalParcentralValuesParfocalParcentralValue", "Parcentral");
let r = r.replace("ParfocalParcentralValueParfocalParcentralValue", "Parcentral");
let r = r.replace("ParfocalParcentralValue", "Parcentral");
return r;
}
s.to_string()
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn read_real_media(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 8 || !data.starts_with(b".RMF") {
return Err(Error::InvalidData("not a RealMedia file".into()));
}
let mut tags = Vec::new();
if data.len() < 8 { return Ok(tags); }
let hdr_size = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
let hdr_size = hdr_size.max(8);
let mut pos = hdr_size;
let mut first_mdpr = true;
let rjmd_data_opt = real_find_rjmd(data);
while pos + 10 <= data.len() {
let chunk_id = &data[pos..pos+4];
if chunk_id == b"\x00\x00\x00\x00" { break; }
let chunk_size = u32::from_be_bytes([data[pos+4], data[pos+5], data[pos+6], data[pos+7]]) as usize;
if chunk_size < 10 || pos + chunk_size > data.len() { break; }
if chunk_id == b"DATA" { break; }
let chunk_data = &data[pos+10..pos+chunk_size];
match chunk_id {
b"PROP" => real_parse_prop(chunk_data, &mut tags),
b"MDPR" => {
real_parse_mdpr(chunk_data, &mut tags, first_mdpr);
first_mdpr = false;
}
b"CONT" => real_parse_cont(chunk_data, &mut tags),
_ => {}
}
pos += chunk_size;
}
if let Some(rjmd_data) = rjmd_data_opt {
real_parse_rjmd(&rjmd_data, &mut tags);
}
if data.len() >= 128 && data[data.len()-128..data.len()-125] == *b"TAG" {
let id3_data = &data[data.len()-128..];
real_parse_id3v1(id3_data, &mut tags);
}
Ok(tags)
}
fn real_find_rjmd(data: &[u8]) -> Option<Vec<u8>> {
if data.len() < 140 { return None; }
let rmje_pos = data.len() - 140;
if &data[rmje_pos..rmje_pos+4] != b"RMJE" { return None; }
let meta_size = u32::from_be_bytes([data[rmje_pos+8], data[rmje_pos+9], data[rmje_pos+10], data[rmje_pos+11]]) as usize;
if meta_size > rmje_pos { return None; }
let rjmd_start = rmje_pos - meta_size;
if rjmd_start + 4 > data.len() || &data[rjmd_start..rjmd_start+4] != b"RJMD" { return None; }
Some(data[rjmd_start..rjmd_start+meta_size].to_vec())
}
fn real_parse_prop(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 40 { return; }
let mut off = 0usize;
let max_bitrate = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let avg_bitrate = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let max_pkt = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let avg_pkt = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let num_pkts = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let duration_ms = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let preroll_ms = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
off += 4; off += 4; if data.len() < off + 4 { return; }
let num_streams = u16::from_be_bytes([data[off], data[off+1]]); off += 2;
if data.len() < off + 2 { return; }
let flags = u16::from_be_bytes([data[off], data[off+1]]);
tags.push(mktag("Real", "MaxBitrate", "Max Bitrate", Value::String(real_convert_bitrate(max_bitrate as f64))));
tags.push(mktag("Real", "AvgBitrate", "Avg Bitrate", Value::String(real_convert_bitrate(avg_bitrate as f64))));
tags.push(mktag("Real", "MaxPacketSize", "Max Packet Size", Value::U32(max_pkt)));
tags.push(mktag("Real", "AvgPacketSize", "Avg Packet Size", Value::U32(avg_pkt)));
tags.push(mktag("Real", "NumPackets", "Num Packets", Value::U32(num_pkts)));
let dur_secs = duration_ms as f64 / 1000.0;
tags.push(mktag("Real", "Duration", "Duration", Value::String(real_convert_duration(dur_secs))));
let preroll_secs = preroll_ms as f64 / 1000.0;
tags.push(mktag("Real", "Preroll", "Preroll", Value::String(real_convert_duration(preroll_secs))));
tags.push(mktag("Real", "NumStreams", "Num Streams", Value::U16(num_streams)));
let mut flag_strs = Vec::new();
if flags & 0x01 != 0 { flag_strs.push("Allow Recording"); }
if flags & 0x02 != 0 { flag_strs.push("Perfect Play"); }
if flags & 0x04 != 0 { flag_strs.push("Live"); }
if flags & 0x08 != 0 { flag_strs.push("Allow Download"); }
if !flag_strs.is_empty() {
tags.push(mktag("Real", "Flags", "Flags", Value::String(flag_strs.join(", "))));
}
}
fn real_parse_mdpr(data: &[u8], tags: &mut Vec<Tag>, is_first: bool) {
if data.len() < 30 { return; }
let mut off = 0usize;
let stream_num = u16::from_be_bytes([data[off], data[off+1]]); off += 2;
let max_bitrate = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let avg_bitrate = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let max_pkt = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let avg_pkt = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let start_time = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let preroll_ms = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let duration_ms = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
if off >= data.len() { return; }
let name_len = data[off] as usize; off += 1;
if off + name_len > data.len() { return; }
let stream_name = String::from_utf8_lossy(&data[off..off+name_len]).to_string(); off += name_len;
if off >= data.len() { return; }
let mime_len = data[off] as usize; off += 1;
if off + mime_len > data.len() { return; }
let mime_type = String::from_utf8_lossy(&data[off..off+mime_len]).to_string(); off += mime_len;
if is_first {
tags.push(mktag("Real", "StreamNumber", "Stream Number", Value::U16(stream_num)));
tags.push(mktag("Real", "StreamMaxBitrate", "Stream Max Bitrate", Value::String(real_convert_bitrate(max_bitrate as f64))));
tags.push(mktag("Real", "StreamAvgBitrate", "Stream Avg Bitrate", Value::String(real_convert_bitrate(avg_bitrate as f64))));
tags.push(mktag("Real", "StreamMaxPacketSize", "Stream Max Packet Size", Value::U32(max_pkt)));
tags.push(mktag("Real", "StreamAvgPacketSize", "Stream Avg Packet Size", Value::U32(avg_pkt)));
tags.push(mktag("Real", "StreamStartTime", "Stream Start Time", Value::U32(start_time)));
let preroll_secs = preroll_ms as f64 / 1000.0;
tags.push(mktag("Real", "StreamPreroll", "Stream Preroll", Value::String(real_convert_duration(preroll_secs))));
let dur_secs = duration_ms as f64 / 1000.0;
tags.push(mktag("Real", "StreamDuration", "Stream Duration", Value::String(real_convert_duration(dur_secs))));
tags.push(mktag("Real", "StreamName", "Stream Name", Value::String(stream_name)));
tags.push(mktag("Real", "StreamMimeType", "Stream Mime Type", Value::String(mime_type.clone())));
}
if mime_type == "logical-fileinfo" && off + 12 <= data.len() {
real_parse_fileinfo(&data[off..], tags);
}
}
fn real_parse_fileinfo(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 12 { return; }
let mut off = 0usize;
let _file_info_len = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let _file_info_len2 = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); off += 4;
let fi_ver = u16::from_be_bytes([data[off], data[off+1]]); off += 2;
let phys_streams = u16::from_be_bytes([data[off], data[off+1]]) as usize; off += 2;
off += phys_streams * 2 + phys_streams * 4;
if off + 2 > data.len() { return; }
let num_rules = u16::from_be_bytes([data[off], data[off+1]]) as usize; off += 2;
off += num_rules * 2;
if off + 2 > data.len() { return; }
let _num_props = u16::from_be_bytes([data[off], data[off+1]]); off += 2;
tags.push(mktag("Real", "FileInfoVersion", "File Info Version", Value::U16(fi_ver)));
real_parse_properties(&data[off..], tags);
}
fn real_parse_properties(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 0usize;
while pos + 7 <= data.len() {
let p_start = pos;
let p_size = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
let p_ver = u16::from_be_bytes([data[pos+4], data[pos+5]]);
if p_size < 7 || p_start + p_size > data.len() { break; }
if p_ver != 0 { pos = p_start + p_size; continue; }
pos += 6;
let tag_len = data[pos] as usize; pos += 1;
if pos + tag_len > data.len() { break; }
let tag_name = String::from_utf8_lossy(&data[pos..pos+tag_len]).to_string(); pos += tag_len;
if pos + 6 > data.len() { break; }
let prop_type = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]); pos += 4;
let val_len = u16::from_be_bytes([data[pos], data[pos+1]]) as usize; pos += 2;
if pos + val_len > data.len() { break; }
let val_data = &data[pos..pos+val_len];
let (exif_name, val_str) = real_file_info_tag(&tag_name, prop_type, val_data, val_len);
if let Some(val) = val_str {
if !exif_name.is_empty() {
tags.push(mktag("Real", &exif_name, &exif_name, Value::String(val)));
}
}
pos = p_start + p_size;
}
}
fn real_file_info_tag(tag: &str, prop_type: u32, val_data: &[u8], val_len: usize) -> (String, Option<String>) {
let tag_name = match tag {
"Content Rating" => "ContentRating",
"Audiences" => "Audiences",
"audioMode" => "AudioMode",
"Creation Date" => "CreateDate",
"Generated By" => "Software",
"Modification Date" => "ModifyDate",
"videoMode" => "VideoMode",
"Description" => "Description",
"Keywords" => "Keywords",
"Indexable" => "Indexable",
"File ID" => "FileID",
"Target Audiences" => "TargetAudiences",
"Audio Format" => "AudioFormat",
"Video Quality" => "VideoQuality",
_ => {
let s: String = tag.split_whitespace().collect::<Vec<_>>().join("");
return (ucfirst_first_char(&s), real_parse_prop_value(tag, prop_type, val_data, val_len));
}
};
let val = match tag_name {
"ContentRating" => {
if prop_type == 0 && val_len >= 4 {
let v = u32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]);
Some(match v {
0 => "No Rating".to_string(),
1 => "All Ages".to_string(),
2 => "Older Children".to_string(),
3 => "Younger Teens".to_string(),
4 => "Older Teens".to_string(),
5 => "Adult Supervision Recommended".to_string(),
6 => "Adults Only".to_string(),
_ => format!("{}", v),
})
} else { None }
}
"CreateDate" | "ModifyDate" => {
if prop_type == 2 {
let s = String::from_utf8_lossy(val_data).trim_matches('\0').to_string();
Some(real_parse_date(&s))
} else { None }
}
_ => real_parse_prop_value(tag_name, prop_type, val_data, val_len)
};
(tag_name.to_string(), val)
}
fn real_parse_prop_value(tag: &str, prop_type: u32, val_data: &[u8], val_len: usize) -> Option<String> {
let _ = tag;
if val_len == 0 { return Some(String::new()); }
match prop_type {
0 => { if val_len >= 4 {
Some(format!("{}", u32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]])))
} else { None }
}
2 => { let s = String::from_utf8_lossy(val_data).trim_matches('\0').to_string();
Some(s)
}
_ => None
}
}
fn real_parse_date(s: &str) -> String {
let parts: Vec<&str> = s.split(|c| c == '/' || c == ' ' || c == ':').collect();
if parts.len() >= 6 {
let day: u32 = parts[0].parse().unwrap_or(0);
let month: u32 = parts[1].parse().unwrap_or(0);
let year: u32 = parts[2].parse().unwrap_or(0);
let hour: u32 = parts[3].parse().unwrap_or(0);
let min: u32 = parts[4].parse().unwrap_or(0);
let sec: u32 = parts[5].parse().unwrap_or(0);
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}", year, month, day, hour, min, sec)
} else {
s.to_string()
}
}
fn real_parse_cont(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 8 { return; }
let mut off = 0usize;
let title_len = u16::from_be_bytes([data[off], data[off+1]]) as usize; off += 2;
if off + title_len > data.len() { return; }
let title = String::from_utf8_lossy(&data[off..off+title_len]).to_string(); off += title_len;
if off + 2 > data.len() { return; }
let author_len = u16::from_be_bytes([data[off], data[off+1]]) as usize; off += 2;
if off + author_len > data.len() { return; }
let author = String::from_utf8_lossy(&data[off..off+author_len]).to_string(); off += author_len;
if off + 2 > data.len() { return; }
let copyright_len = u16::from_be_bytes([data[off], data[off+1]]) as usize; off += 2;
if off + copyright_len > data.len() { return; }
let copyright = String::from_utf8_lossy(&data[off..off+copyright_len]).to_string(); off += copyright_len;
if off + 2 > data.len() { return; }
let comment_len = u16::from_be_bytes([data[off], data[off+1]]) as usize; off += 2;
if off + comment_len > data.len() { return; }
let comment = String::from_utf8_lossy(&data[off..off+comment_len]).to_string();
if !title.is_empty() { tags.push(mktag("Real", "Title", "Title", Value::String(title))); }
if !author.is_empty() { tags.push(mktag("Real", "Author", "Author", Value::String(author))); }
if !copyright.is_empty() { tags.push(mktag("Real", "Copyright", "Copyright", Value::String(copyright))); }
if !comment.is_empty() { tags.push(mktag("Real", "Comment", "Comment", Value::String(comment))); }
}
fn real_parse_rjmd(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 8 { return; }
let dir_start = 8usize;
let dir_end = data.len();
real_parse_rjmd_entries(data, dir_start, dir_end, "", tags);
}
fn real_parse_rjmd_entries(data: &[u8], pos_start: usize, dir_end: usize, prefix: &str, tags: &mut Vec<Tag>) {
let mut pos = pos_start;
while pos + 28 <= dir_end {
if pos + 28 > data.len() { break; }
let entry_size = u32::from_be_bytes([data[pos], data[pos+1], data[pos+2], data[pos+3]]) as usize;
let entry_type = u32::from_be_bytes([data[pos+4], data[pos+5], data[pos+6], data[pos+7]]);
let _flags = u32::from_be_bytes([data[pos+8], data[pos+9], data[pos+10], data[pos+11]]);
let value_pos_rel = u32::from_be_bytes([data[pos+12], data[pos+13], data[pos+14], data[pos+15]]) as usize;
let _sub_pos_rel = u32::from_be_bytes([data[pos+16], data[pos+17], data[pos+18], data[pos+19]]) as usize;
let num_sub = u32::from_be_bytes([data[pos+20], data[pos+21], data[pos+22], data[pos+23]]) as usize;
let name_len = u32::from_be_bytes([data[pos+24], data[pos+25], data[pos+26], data[pos+27]]) as usize;
if entry_size < 28 { break; }
if pos + entry_size > dir_end { break; }
if pos + 28 + name_len > dir_end { break; }
let name_bytes = &data[pos+28..pos+28+name_len];
let name = String::from_utf8_lossy(name_bytes).split('\0').next().unwrap_or("").to_string();
let full_name = if prefix.is_empty() {
name.clone()
} else {
format!("{}/{}", prefix, name)
};
let value_pos = value_pos_rel + pos;
if value_pos + 4 <= dir_end {
let value_len = u32::from_be_bytes([data[value_pos], data[value_pos+1], data[value_pos+2], data[value_pos+3]]) as usize;
let value_start = value_pos + 4;
if value_start + value_len <= dir_end && entry_type != 9 && entry_type != 10 {
let val_data = &data[value_start..value_start+value_len];
let val_str = match entry_type {
1 | 2 | 6 | 7 | 8 => {
let s = String::from_utf8_lossy(val_data).trim_matches('\0').to_string();
let s = s.trim_end().to_string();
if entry_type == 7 {
real_parse_rjmd_date(&s)
} else {
s
}
}
4 => { if value_len >= 4 {
format!("{}", u32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]))
} else { String::new() }
}
3 => { if value_len == 4 {
format!("{}", u32::from_be_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]))
} else if value_len >= 1 {
format!("{}", val_data[0])
} else { String::new() }
}
_ => String::new()
};
if !full_name.is_empty() {
let tag_name = real_rjmd_tag_name(&full_name);
if !tag_name.is_empty() {
tags.push(mktag("Real", &tag_name, &tag_name, Value::String(val_str)));
}
}
}
if num_sub > 0 {
let sub_dir_start = value_pos + 4 + value_len + num_sub * 8;
let sub_dir_len = pos + entry_size - sub_dir_start;
if sub_dir_start + sub_dir_len <= dir_end && sub_dir_len > 0 {
real_parse_rjmd_entries(data, sub_dir_start, sub_dir_start + sub_dir_len, &full_name, tags);
}
}
}
pos += entry_size;
}
}
fn real_parse_rjmd_date(s: &str) -> String {
if s.len() >= 14 {
format!("{}:{}:{} {}:{}:{}", &s[..4], &s[4..6], &s[6..8], &s[8..10], &s[10..12], &s[12..14])
} else {
s.to_string()
}
}
fn real_rjmd_tag_name(full_name: &str) -> String {
match full_name {
"Album/Name" => "AlbumName".to_string(),
"Track/Category" => "TrackCategory".to_string(),
"Track/Comments" => "TrackComments".to_string(),
"Track/Lyrics" => "TrackLyrics".to_string(),
_ => {
let clean: String = full_name.split('/')
.filter(|s| !s.is_empty())
.map(|s| {
let cleaned: String = s.chars().filter(|&c| c.is_alphanumeric() || c == '_').collect();
ucfirst_first_char(&cleaned)
})
.collect();
clean
}
}
}
fn ucfirst_first_char(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => {
let upper: String = c.to_uppercase().collect();
upper + chars.as_str()
}
}
}
fn real_parse_id3v1(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 128 || &data[..3] != b"TAG" { return; }
let title = read_null_terminated_str(&data[3..33]);
let artist = read_null_terminated_str(&data[33..63]);
let album = read_null_terminated_str(&data[63..93]);
let year = read_null_terminated_str(&data[93..97]);
let comment = if data[125] == 0 && data[126] != 0 {
read_null_terminated_str(&data[97..125])
} else {
read_null_terminated_str(&data[97..127])
};
let genre_byte = data[127] as usize;
let mut new_tags: Vec<Tag> = Vec::new();
if !title.is_empty() && !tags.iter().any(|t| t.name == "Title") {
new_tags.push(mktag("ID3", "Title", "Title", Value::String(title)));
}
if !artist.is_empty() && !tags.iter().any(|t| t.name == "Artist") {
new_tags.push(mktag("ID3", "Artist", "Artist", Value::String(artist)));
}
if !album.is_empty() && !tags.iter().any(|t| t.name == "Album") {
new_tags.push(mktag("ID3", "Album", "Album", Value::String(album)));
}
if !year.is_empty() && !tags.iter().any(|t| t.name == "Year") {
new_tags.push(mktag("ID3", "Year", "Year", Value::String(year)));
}
if !comment.is_empty() && !tags.iter().any(|t| t.name == "Comment") {
new_tags.push(mktag("ID3", "Comment", "Comment", Value::String(comment)));
}
if let Some(genre_name) = id3v1_genre_name(genre_byte) {
if !tags.iter().any(|t| t.name == "Genre") {
new_tags.push(mktag("ID3", "Genre", "Genre", Value::String(genre_name.to_string())));
}
}
tags.extend(new_tags);
}
fn id3v1_genre_name(n: usize) -> Option<&'static str> {
match n {
0 => Some("Blues"), 1 => Some("Classic Rock"), 2 => Some("Country"),
3 => Some("Dance"), 4 => Some("Disco"), 5 => Some("Funk"),
6 => Some("Grunge"), 7 => Some("Hip-Hop"), 8 => Some("Jazz"),
9 => Some("Metal"), 10 => Some("New Age"), 11 => Some("Oldies"),
12 => Some("Other"), 13 => Some("Pop"), 14 => Some("R&B"),
15 => Some("Rap"), 16 => Some("Reggae"), 17 => Some("Rock"),
18 => Some("Techno"), 19 => Some("Industrial"), 20 => Some("Alternative"),
21 => Some("Ska"), 22 => Some("Death Metal"), 23 => Some("Pranks"),
24 => Some("Soundtrack"), 25 => Some("Euro-Techno"), 26 => Some("Ambient"),
27 => Some("Trip-Hop"), 28 => Some("Vocal"), 29 => Some("Jazz+Funk"),
30 => Some("Fusion"), 31 => Some("Trance"), 32 => Some("Classical"),
33 => Some("Instrumental"), 34 => Some("Acid"), 35 => Some("House"),
36 => Some("Game"), 37 => Some("Sound Clip"), 38 => Some("Gospel"),
39 => Some("Noise"), 40 => Some("Alt. Rock"), 41 => Some("Bass"),
42 => Some("Soul"), 43 => Some("Punk"), 44 => Some("Space"),
45 => Some("Meditative"), 46 => Some("Instrumental Pop"),
47 => Some("Instrumental Rock"), 48 => Some("Ethnic"), 49 => Some("Gothic"),
50 => Some("Darkwave"), 51 => Some("Techno-Industrial"), 52 => Some("Electronic"),
53 => Some("Pop-Folk"), 54 => Some("Eurodance"), 55 => Some("Dream"),
56 => Some("Southern Rock"), 57 => Some("Comedy"), 58 => Some("Cult"),
59 => Some("Gangsta Rap"), 60 => Some("Top 40"), 61 => Some("Christian Rap"),
62 => Some("Pop/Funk"), 63 => Some("Jungle"), 64 => Some("Native American"),
65 => Some("Cabaret"), 66 => Some("New Wave"), 67 => Some("Psychedelic"),
68 => Some("Rave"), 69 => Some("Showtunes"), 70 => Some("Trailer"),
71 => Some("Lo-Fi"), 72 => Some("Tribal"), 73 => Some("Acid Punk"),
74 => Some("Acid Jazz"), 75 => Some("Polka"), 76 => Some("Retro"),
77 => Some("Musical"), 78 => Some("Rock & Roll"), 79 => Some("Hard Rock"),
80 => Some("Folk"), 81 => Some("Folk-Rock"), 82 => Some("National Folk"),
83 => Some("Swing"), 84 => Some("Fast-Fusion"), 85 => Some("Bebop"),
86 => Some("Latin"), 87 => Some("Revival"), 88 => Some("Celtic"),
89 => Some("Bluegrass"), 90 => Some("Avantgarde"), 91 => Some("Gothic Rock"),
92 => Some("Progressive Rock"), 93 => Some("Psychedelic Rock"),
94 => Some("Symphonic Rock"), 95 => Some("Slow Rock"), 96 => Some("Big Band"),
97 => Some("Chorus"), 98 => Some("Easy Listening"), 99 => Some("Acoustic"),
100 => Some("Humour"), 101 => Some("Speech"), 102 => Some("Chanson"),
103 => Some("Opera"), 104 => Some("Chamber Music"), 105 => Some("Sonata"),
106 => Some("Symphony"), 107 => Some("Booty Bass"), 108 => Some("Primus"),
109 => Some("Porn Groove"), 110 => Some("Satire"), 111 => Some("Slow Jam"),
112 => Some("Club"), 113 => Some("Tango"), 114 => Some("Samba"),
115 => Some("Folklore"), 116 => Some("Ballad"), 117 => Some("Power Ballad"),
118 => Some("Rhythmic Soul"), 119 => Some("Freestyle"), 120 => Some("Duet"),
121 => Some("Punk Rock"), 122 => Some("Drum Solo"), 123 => Some("A Cappella"),
124 => Some("Euro-House"), 125 => Some("Dance Hall"), 126 => Some("Goa"),
127 => Some("Drum & Bass"), 128 => Some("Club-House"), 129 => Some("Hardcore"),
130 => Some("Terror"), 131 => Some("Indie"), 132 => Some("BritPop"),
133 => Some("Afro-Punk"), 134 => Some("Polsk Punk"), 135 => Some("Beat"),
136 => Some("Christian Gangsta Rap"), 137 => Some("Heavy Metal"),
138 => Some("Black Metal"), 139 => Some("Crossover"),
140 => Some("Contemporary Christian"), 141 => Some("Christian Rock"),
142 => Some("Merengue"), 143 => Some("Salsa"), 144 => Some("Thrash Metal"),
145 => Some("Anime"), 146 => Some("JPop"), 147 => Some("Synthpop"),
148 => Some("Abstract"), 149 => Some("Art Rock"), 150 => Some("Baroque"),
151 => Some("Bhangra"), 152 => Some("Big Beat"), 153 => Some("Breakbeat"),
154 => Some("Chillout"), 155 => Some("Downtempo"), 156 => Some("Dub"),
157 => Some("EBM"), 158 => Some("Eclectic"), 159 => Some("Electro"),
160 => Some("Electroclash"), 161 => Some("Emo"), 162 => Some("Experimental"),
163 => Some("Garage"), 164 => Some("Global"), 165 => Some("IDM"),
166 => Some("Illbient"), 167 => Some("Industro-Goth"), 168 => Some("Jam Band"),
169 => Some("Krautrock"), 170 => Some("Leftfield"), 171 => Some("Lounge"),
172 => Some("Math Rock"), 173 => Some("New Romantic"), 174 => Some("Nu-Breakz"),
175 => Some("Post-Punk"), 176 => Some("Post-Rock"), 177 => Some("Psytrance"),
178 => Some("Shoegaze"), 179 => Some("Space Rock"), 180 => Some("Trop Rock"),
181 => Some("World Music"), 182 => Some("Neoclassical"), 183 => Some("Audiobook"),
184 => Some("Audio Theatre"), 185 => Some("Neue Deutsche Welle"),
186 => Some("Podcast"), 187 => Some("Indie Rock"), 188 => Some("G-Funk"),
189 => Some("Dubstep"), 190 => Some("Garage Rock"), 191 => Some("Psybient"),
255 => Some("None"),
_ => None,
}
}
fn read_null_terminated_str(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
fn real_convert_bitrate(bps: f64) -> String {
flv_convert_bitrate(bps)
}
fn real_convert_duration(secs: f64) -> String {
flv_convert_duration(secs)
}
pub fn read_photo_cd(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 2056 || &data[2048..2055] != b"PCD_IPI" {
return Err(Error::InvalidData("not a PhotoCD file".into()));
}
let pcd = &data[2048..]; if pcd.len() < 1540 {
return Err(Error::InvalidData("PhotoCD data too short".into()));
}
let mut tags: Vec<Tag> = Vec::new();
let sv = pcd[7];
let sv2 = pcd[8];
if sv != 255 || sv2 != 255 {
tags.push(mktag("PhotoCD", "SpecificationVersion", "Specification Version",
Value::String(format!("{}.{}", sv, sv2))));
}
let ar = pcd[9];
let ar2 = pcd[10];
if ar != 255 || ar2 != 255 {
tags.push(mktag("PhotoCD", "AuthoringSoftwareRelease", "Authoring Software Release",
Value::String(format!("{}.{}", ar, ar2))));
}
let im1 = pcd[11];
let im2 = pcd[12];
tags.push(mktag("PhotoCD", "ImageMagnificationDescriptor", "Image Magnification Descriptor",
Value::String(format!("{}.{}", im1, im2))));
if pcd.len() >= 17 {
let ts = u32::from_be_bytes([pcd[13], pcd[14], pcd[15], pcd[16]]);
if ts != 0xffffffff {
let dt = gzip_unix_to_datetime(ts as i64);
tags.push(mktag("PhotoCD", "CreateDate", "Create Date", Value::String(dt)));
}
}
if pcd.len() >= 21 {
let ts = u32::from_be_bytes([pcd[17], pcd[18], pcd[19], pcd[20]]);
if ts != 0xffffffff {
let dt = gzip_unix_to_datetime(ts as i64);
tags.push(mktag("PhotoCD", "ModifyDate", "Modify Date", Value::String(dt)));
}
}
let medium = pcd[21];
let medium_str = match medium {
0 => "Color negative",
1 => "Color reversal",
2 => "Color hard copy",
3 => "Thermal hard copy",
4 => "Black and white negative",
5 => "Black and white reversal",
6 => "Black and white hard copy",
7 => "Internegative",
8 => "Synthetic image",
_ => "",
};
if !medium_str.is_empty() {
tags.push(mktag("PhotoCD", "ImageMedium", "Image Medium", Value::String(medium_str.into())));
}
if pcd.len() >= 42 {
let s = pcd_rtrim_str(&pcd[22..42]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "ProductType", "Product Type", Value::String(s)));
}
}
if pcd.len() >= 62 {
let s = pcd_rtrim_str(&pcd[42..62]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "ScannerVendorID", "Scanner Vendor ID", Value::String(s)));
}
}
if pcd.len() >= 78 {
let s = pcd_rtrim_str(&pcd[62..78]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "ScannerProductID", "Scanner Product ID", Value::String(s)));
}
}
if pcd.len() >= 82 {
let s = pcd_rtrim_str(&pcd[78..82]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "ScannerFirmwareVersion", "Scanner Firmware Version", Value::String(s)));
}
}
if pcd.len() >= 90 {
let s = pcd_rtrim_str(&pcd[82..90]);
tags.push(mktag("PhotoCD", "ScannerFirmwareDate", "Scanner Firmware Date", Value::String(s)));
}
if pcd.len() >= 110 {
let s = pcd_rtrim_str(&pcd[90..110]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "ScannerSerialNumber", "Scanner Serial Number", Value::String(s)));
}
}
if pcd.len() >= 112 {
let h1 = pcd[110];
let h2 = pcd[111];
let pixel_size = format!("{:02x}.{:02x}", h1, h2).trim_start_matches('0').to_string();
let pixel_size = if pixel_size.starts_with('.') {
format!("0{}", pixel_size)
} else {
pixel_size
};
tags.push(mktag("PhotoCD", "ScannerPixelSize", "Scanner Pixel Size",
Value::String(format!("{} micrometers", pixel_size))));
}
if pcd.len() >= 132 {
let s = pcd_rtrim_str(&pcd[112..132]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "ImageWorkstationMake", "Image Workstation Make", Value::String(s)));
}
}
if pcd.len() >= 133 {
let cs = pcd[132];
let cs_str = match cs {
1 => "38 characters ISO 646",
2 => "65 characters ISO 646",
3 => "95 characters ISO 646",
4 => "191 characters ISO 8850-1",
5 => "ISO 2022",
6 => "Includes characters not ISO 2375 registered",
_ => "",
};
if !cs_str.is_empty() {
tags.push(mktag("PhotoCD", "CharacterSet", "Character Set", Value::String(cs_str.into())));
}
}
if pcd.len() >= 225 {
let s = pcd_rtrim_str(&pcd[165..225]);
if !s.is_empty() {
tags.push(mktag("PhotoCD", "PhotoFinisherName", "Photo Finisher Name", Value::String(s)));
}
}
let has_sba = pcd.len() >= 228 && &pcd[225..228] == b"SBA";
if has_sba && pcd.len() >= 230 {
let r1 = pcd[228];
let r2 = pcd[229];
tags.push(mktag("PhotoCD", "SceneBalanceAlgorithmRevision", "Scene Balance Algorithm Revision",
Value::String(format!("{}.{}", r1, r2))));
let cmd = pcd[230];
let cmd_str = match cmd {
0 => "Neutral SBA On, Color SBA On",
1 => "Neutral SBA Off, Color SBA Off",
2 => "Neutral SBA On, Color SBA Off",
3 => "Neutral SBA Off, Color SBA On",
_ => "",
};
if !cmd_str.is_empty() {
tags.push(mktag("PhotoCD", "SceneBalanceAlgorithmCommand", "Scene Balance Algorithm Command",
Value::String(cmd_str.into())));
}
if pcd.len() >= 327 {
let film_id = u16::from_be_bytes([pcd[325], pcd[326]]) as u32;
let film_str = pcd_film_id_name(film_id);
tags.push(mktag("PhotoCD", "SceneBalanceAlgorithmFilmID", "Scene Balance Algorithm Film ID",
Value::String(film_str.to_string())));
}
if pcd.len() >= 332 {
let cs = pcd[331];
let cs_str = match cs {
1 => "Restrictions apply",
0xff => "Not specified",
_ => "",
};
if !cs_str.is_empty() {
tags.push(mktag("PhotoCD", "CopyrightStatus", "Copyright Status", Value::String(cs_str.into())));
}
}
}
if pcd.len() >= 1539 {
let byte = pcd[1538];
let orient_raw = byte & 0x03;
let size_raw = (byte & 0x0c) >> 2;
let class_raw = (byte & 0x60) >> 5;
let orient_str = match orient_raw {
0 => "Horizontal (normal)",
1 => "Rotate 270 CW",
2 => "Rotate 180",
3 => "Rotate 90 CW",
_ => "",
};
tags.push(mktag("PhotoCD", "Orientation", "Orientation", Value::String(orient_str.into())));
let scale = if size_raw > 0 { (size_raw * 2) as u32 } else { 1 };
let (w, h) = if orient_raw & 0x01 != 0 {
(512 * scale, 768 * scale) } else {
(768 * scale, 512 * scale) };
tags.push(mktag("PhotoCD", "ImageWidth", "Image Width", Value::String(w.to_string())));
tags.push(mktag("PhotoCD", "ImageHeight", "Image Height", Value::String(h.to_string())));
let class_str = match class_raw {
0 => "Class 1 - 35mm film; Pictoral hard copy",
1 => "Class 2 - Large format film",
2 => "Class 3 - Text and graphics, high resolution",
3 => "Class 4 - Text and graphics, high dynamic range",
_ => "",
};
if !class_str.is_empty() {
tags.push(mktag("PhotoCD", "CompressionClass", "Compression Class", Value::String(class_str.into())));
}
}
Ok(tags)
}
fn pcd_rtrim_str(bytes: &[u8]) -> String {
let null_end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let s = String::from_utf8_lossy(&bytes[..null_end]);
s.trim_end_matches(|c: char| c == ' ' || c == '\0').to_string()
}
fn pcd_film_id_name(n: u32) -> &'static str {
match n {
1 => "3M ScotchColor AT 100", 2 => "3M ScotchColor AT 200",
3 => "3M ScotchColor HR2 400", 7 => "3M Scotch HR 200 Gen 2",
9 => "3M Scotch HR 400 Gen 2", 16 => "Agfa Agfacolor XRS 400 Gen 1",
17 => "Agfa Agfacolor XRG/XRS 400", 18 => "Agfa Agfacolor XRG/XRS 200",
19 => "Agfa Agfacolor XRS 1000 Gen 2", 20 => "Agfa Agfacolor XRS 400 Gen 2",
21 => "Agfa Agfacolor XRS/XRC 100", 26 => "Fuji Reala 100 (JAPAN)",
27 => "Fuji Reala 100 Gen 1", 28 => "Fuji Reala 100 Gen 2",
29 => "Fuji SHR 400 Gen 2", 30 => "Fuji Super HG 100",
31 => "Fuji Super HG 1600 Gen 1", 32 => "Fuji Super HG 200",
33 => "Fuji Super HG 400", 34 => "Fuji Super HG 100 Gen 2",
35 => "Fuji Super HR 100 Gen 1", 36 => "Fuji Super HR 100 Gen 2",
37 => "Fuji Super HR 1600 Gen 2", 38 => "Fuji Super HR 200 Gen 1",
39 => "Fuji Super HR 200 Gen 2", 40 => "Fuji Super HR 400 Gen 1",
43 => "Fuji NSP 160S (Pro)", 45 => "Kodak Kodacolor VR 100 Gen 2",
47 => "Kodak Gold 400 Gen 3", 55 => "Kodak Ektar 100 Gen 1",
56 => "Kodak Ektar 1000 Gen 1", 57 => "Kodak Ektar 125 Gen 1",
58 => "Kodak Royal Gold 25 RZ", 60 => "Kodak Gold 1600 Gen 1",
61 => "Kodak Gold 200 Gen 2", 62 => "Kodak Gold 400 Gen 2",
65 => "Kodak Kodacolor VR 100 Gen 1", 66 => "Kodak Kodacolor VR 1000 Gen 2",
67 => "Kodak Kodacolor VR 1000 Gen 1", 68 => "Kodak Kodacolor VR 200 Gen 1",
69 => "Kodak Kodacolor VR 400 Gen 1", 70 => "Kodak Kodacolor VR 200 Gen 2",
71 => "Kodak Kodacolor VRG 100 Gen 1", 72 => "Kodak Gold 100 Gen 2",
73 => "Kodak Kodacolor VRG 200 Gen 1", 74 => "Kodak Gold 400 Gen 1",
87 => "Kodak Ektacolor Gold 160", 88 => "Kodak Ektapress 1600 Gen 1 PPC",
89 => "Kodak Ektapress Gold 100 Gen 1 PPA", 90 => "Kodak Ektapress Gold 400 PPB-3",
92 => "Kodak Ektar 25 Professional PHR", 97 => "Kodak T-Max 100 Professional",
98 => "Kodak T-Max 3200 Professional", 99 => "Kodak T-Max 400 Professional",
101 => "Kodak Vericolor 400 Prof VPH", 102 => "Kodak Vericolor III Pro",
121 => "Konika Konica Color SR-G 3200", 122 => "Konika Konica Color Super SR100",
123 => "Konika Konica Color Super SR 400", 138 => "Kodak Gold Unknown",
139 => "Kodak Unknown Neg A- Normal SBA", 143 => "Kodak Ektar 100 Gen 2",
147 => "Kodak Kodacolor CII", 148 => "Kodak Kodacolor II",
149 => "Kodak Gold Plus 200 Gen 3", 150 => "Kodak Internegative +10% Contrast",
151 => "Agfa Agfacolor Ultra 50", 152 => "Fuji NHG 400",
153 => "Agfa Agfacolor XRG 100", 154 => "Kodak Gold Plus 100 Gen 3",
155 => "Konika Konica Color Super SR200 Gen 1", 156 => "Konika Konica Color SR-G 160",
157 => "Agfa Agfacolor Optima 125", 158 => "Agfa Agfacolor Portrait 160",
162 => "Kodak Kodacolor VRG 400 Gen 1", 163 => "Kodak Gold 200 Gen 1",
164 => "Kodak Kodacolor VRG 100 Gen 2", 174 => "Kodak Internegative +20% Contrast",
175 => "Kodak Internegative +30% Contrast", 176 => "Kodak Internegative +40% Contrast",
184 => "Kodak TMax-100 D-76 CI = .40", 185 => "Kodak TMax-100 D-76 CI = .50",
186 => "Kodak TMax-100 D-76 CI = .55", 187 => "Kodak TMax-100 D-76 CI = .70",
188 => "Kodak TMax-100 D-76 CI = .80", 189 => "Kodak TMax-100 TMax CI = .40",
190 => "Kodak TMax-100 TMax CI = .50", 191 => "Kodak TMax-100 TMax CI = .55",
192 => "Kodak TMax-100 TMax CI = .70", 193 => "Kodak TMax-100 TMax CI = .80",
195 => "Kodak TMax-400 D-76 CI = .40", 196 => "Kodak TMax-400 D-76 CI = .50",
197 => "Kodak TMax-400 D-76 CI = .55", 198 => "Kodak TMax-400 D-76 CI = .70",
214 => "Kodak TMax-400 D-76 CI = .80", 215 => "Kodak TMax-400 TMax CI = .40",
216 => "Kodak TMax-400 TMax CI = .50", 217 => "Kodak TMax-400 TMax CI = .55",
218 => "Kodak TMax-400 TMax CI = .70", 219 => "Kodak TMax-400 TMax CI = .80",
224 => "3M ScotchColor ATG 400/EXL 400", 266 => "Agfa Agfacolor Optima 200",
267 => "Konika Impressa 50", 268 => "Polaroid Polaroid CP 200",
269 => "Konika Konica Color Super SR200 Gen 2", 270 => "ILFORD XP2 400",
271 => "Polaroid Polaroid Color HD2 100", 272 => "Polaroid Polaroid Color HD2 400",
273 => "Polaroid Polaroid Color HD2 200", 282 => "3M ScotchColor ATG-1 200",
284 => "Konika XG 400", 307 => "Kodak Universal Reversal B/W",
308 => "Kodak RPC Copy Film Gen 1", 312 => "Kodak Universal E6",
324 => "Kodak Gold Ultra 400 Gen 4", 328 => "Fuji Super G 100",
329 => "Fuji Super G 200", 330 => "Fuji Super G 400 Gen 2",
333 => "Kodak Universal K14", 334 => "Fuji Super G 400 Gen 1",
366 => "Kodak Vericolor HC 6329 VHC", 367 => "Kodak Vericolor HC 4329 VHC",
368 => "Kodak Vericolor L 6013 VPL", 369 => "Kodak Vericolor L 4013 VPL",
418 => "Kodak Ektacolor Gold II 400 Prof", 430 => "Kodak Royal Gold 1000",
431 => "Kodak Kodacolor VR 200 / 5093", 432 => "Kodak Gold Plus 100 Gen 4",
443 => "Kodak Royal Gold 100", 444 => "Kodak Royal Gold 400",
445 => "Kodak Universal E6 auto-balance", 446 => "Kodak Universal E6 illum. corr.",
447 => "Kodak Universal K14 auto-balance", 448 => "Kodak Universal K14 illum. corr.",
449 => "Kodak Ektar 100 Gen 3 SY", 456 => "Kodak Ektar 25",
457 => "Kodak Ektar 100 Gen 3 CX", 458 => "Kodak Ektapress Plus 100 Prof PJA-1",
459 => "Kodak Ektapress Gold II 100 Prof", 460 => "Kodak Pro 100 PRN",
461 => "Kodak Vericolor HC 100 Prof VHC-2", 462 => "Kodak Prof Color Neg 100",
463 => "Kodak Ektar 1000 Gen 2", 464 => "Kodak Ektapress Plus 1600 Pro PJC-1",
465 => "Kodak Ektapress Gold II 1600 Prof", 466 => "Kodak Super Gold 1600 GF Gen 2",
467 => "Kodak Kodacolor 100 Print Gen 4", 468 => "Kodak Super Gold 100 Gen 4",
469 => "Kodak Gold 100 Gen 4", 470 => "Kodak Gold III 100 Gen 4",
471 => "Kodak Funtime 100 FA", 472 => "Kodak Funtime 200 FB",
473 => "Kodak Kodacolor VR 200 Gen 4", 474 => "Kodak Gold Super 200 Gen 4",
475 => "Kodak Kodacolor 200 Print Gen 4", 476 => "Kodak Super Gold 200 Gen 4",
477 => "Kodak Gold 200 Gen 4", 478 => "Kodak Gold III 200 Gen 4",
479 => "Kodak Gold Ultra 400 Gen 5", 480 => "Kodak Super Gold 400 Gen 5",
481 => "Kodak Gold 400 Gen 5", 482 => "Kodak Gold III 400 Gen 5",
483 => "Kodak Kodacolor 400 Print Gen 5", 484 => "Kodak Ektapress Plus 400 Prof PJB-2",
485 => "Kodak Ektapress Gold II 400 Prof G5", 486 => "Kodak Pro 400 PPF-2",
487 => "Kodak Ektacolor Gold II 400 EGP-4", 488 => "Kodak Ektacolor Gold 400 Prof EGP-4",
489 => "Kodak Ektapress Gold II Multspd PJM", 490 => "Kodak Pro 400 MC PMC",
491 => "Kodak Vericolor 400 Prof VPH-2", 492 => "Kodak Vericolor 400 Plus Prof VPH-2",
493 => "Kodak Unknown Neg Product Code 83", 505 => "Kodak Ektacolor Pro Gold 160 GPX",
508 => "Kodak Royal Gold 200", 517 => "Kodak 4050000000",
519 => "Kodak Gold Plus 100 Gen 5", 520 => "Kodak Gold 800 Gen 1",
521 => "Kodak Gold Super 200 Gen 5", 522 => "Kodak Ektapress Plus 200 Prof",
523 => "Kodak 4050 E6 auto-balance", 524 => "Kodak 4050 E6 ilum. corr.",
525 => "Kodak 4050 K14", 526 => "Kodak 4050 K14 auto-balance",
527 => "Kodak 4050 K14 ilum. corr.", 528 => "Kodak 4050 Reversal B&W",
532 => "Kodak Advantix 200", 533 => "Kodak Advantix 400",
534 => "Kodak Advantix 100", 535 => "Kodak Ektapress Multspd Prof PJM-2",
536 => "Kodak Kodacolor VR 200 Gen 5", 537 => "Kodak Funtime 200 FB Gen 2",
538 => "Kodak Commercial 200", 539 => "Kodak Royal Gold 25 Copystand",
540 => "Kodak Kodacolor DA 100 Gen 5", 545 => "Kodak Kodacolor VR 400 Gen 2",
546 => "Kodak Gold 100 Gen 6", 547 => "Kodak Gold 200 Gen 6",
548 => "Kodak Gold 400 Gen 6", 549 => "Kodak Royal Gold 100 Gen 2",
550 => "Kodak Royal Gold 200 Gen 2", 551 => "Kodak Royal Gold 400 Gen 2",
552 => "Kodak Gold Max 800 Gen 2", 554 => "Kodak 4050 E6 high contrast",
555 => "Kodak 4050 E6 low saturation high contrast", 556 => "Kodak 4050 E6 low saturation",
557 => "Kodak Universal E-6 Low Saturation", 558 => "Kodak T-Max T400 CN",
563 => "Kodak Ektapress PJ100", 564 => "Kodak Ektapress PJ400",
565 => "Kodak Ektapress PJ800", 567 => "Kodak Portra 160NC",
568 => "Kodak Portra 160VC", 569 => "Kodak Portra 400NC",
570 => "Kodak Portra 400VC", 575 => "Kodak Advantix 100-2",
576 => "Kodak Advantix 200-2", 577 => "Kodak Advantix Black & White + 400",
578 => "Kodak Ektapress PJ800-2",
_ => "",
}
}
pub fn read_mxf(data: &[u8]) -> Result<Vec<Tag>> {
let magic = b"\x06\x0e\x2b\x34";
let start = data.windows(4).position(|w| w == magic.as_ref())
.ok_or_else(|| Error::InvalidData("not an MXF file".into()))?;
let data = &data[start..];
let mut tags: Vec<Tag> = Vec::new();
let mut registry: std::collections::HashMap<[u8;2], [u8;16]> = std::collections::HashMap::new();
let mut pos = 0;
while pos + 17 <= data.len() {
if &data[pos..pos+4] != b"\x06\x0e\x2b\x34" {
pos += 1;
continue;
}
let key = &data[pos..pos+16];
let len_byte = data[pos+16];
let (val_len, ber_size) = if len_byte < 0x80 {
(len_byte as usize, 1usize)
} else {
let n = (len_byte & 0x7f) as usize;
if pos + 17 + n > data.len() { break; }
let mut l = 0usize;
for i in 0..n { l = (l << 8) | (data[pos+17+i] as usize); }
(l, 1 + n)
};
let val_start = pos + 16 + ber_size;
if val_start + val_len > data.len() { break; }
let val = &data[val_start..val_start+val_len];
if key[4] == 0x02 && key[5] == 0x05 && key[12] == 0x01 && key[13] == 0x02 {
if val.len() >= 4 && !tags.iter().any(|t| t.name == "MXFVersion") {
let major = u16::from_be_bytes([val[0], val[1]]);
let minor = u16::from_be_bytes([val[2], val[3]]);
tags.push(mktag("MXF", "MXFVersion", "MXF Version",
Value::String(format!("{}.{}", major, minor))));
}
}
else if key[4] == 0x02 && key[5] == 0x05 && key[12] == 0x01 && key[13] == 0x05 {
if val.len() >= 8 {
let count = u32::from_be_bytes([val[0],val[1],val[2],val[3]]) as usize;
let item_size = u32::from_be_bytes([val[4],val[5],val[6],val[7]]) as usize;
if item_size >= 18 {
for i in 0..count {
let off = 8 + i * item_size;
if off + 18 > val.len() { break; }
let mut ltag = [0u8;2];
ltag.copy_from_slice(&val[off..off+2]);
let mut ul = [0u8;16];
ul.copy_from_slice(&val[off+2..off+18]);
registry.insert(ltag, ul);
}
}
}
}
else if key[4] == 0x02 && key[5] == 0x53 {
mxf_parse_local_set(val, ®istry, &mut tags);
}
pos = val_start + val_len;
}
let mut last_index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (i, t) in tags.iter().enumerate() {
last_index.insert(t.name.clone(), i);
}
let mut result = Vec::new();
for (i, t) in tags.into_iter().enumerate() {
if last_index.get(&t.name) == Some(&i) {
result.push(t);
}
}
tags = result;
Ok(tags)
}
fn mxf_parse_local_set(
data: &[u8],
registry: &std::collections::HashMap<[u8;2], [u8;16]>,
tags: &mut Vec<Tag>,
) {
let mut pos = 0;
while pos + 4 <= data.len() {
let ltag = [data[pos], data[pos+1]];
let llen = u16::from_be_bytes([data[pos+2], data[pos+3]]) as usize;
pos += 4;
if pos + llen > data.len() { break; }
let val = &data[pos..pos+llen];
if let Some(ul) = registry.get(<ag) {
let ul_hex: String = ul.iter().map(|b| format!("{:02x}", b)).collect();
if let Some((name, value)) = mxf_decode_tag(&ul_hex, val) {
tags.push(mktag("MXF", &name, &name, Value::String(value)));
}
}
pos += llen;
}
}
fn mxf_decode_tag(ul: &str, val: &[u8]) -> Option<(String, String)> {
match ul {
"060e2b34010101020702011002040000" => Some(("ContainerLastModifyDate".into(), mxf_decode_timestamp(val))),
"060e2b34010101020702011002030000" => Some(("ModifyDate".into(), mxf_decode_timestamp(val))),
"060e2b34010101020702011001030000" => Some(("CreateDate".into(), mxf_decode_timestamp(val))),
"060e2b34010101020702011002050000" => Some(("PackageLastModifyDate".into(), mxf_decode_timestamp(val))),
"060e2b34010101020301020105000000" => Some(("SDKVersion".into(), mxf_decode_version_short(val))),
"060e2b3401010102052007010a000000" => Some(("ToolkitVersion".into(), mxf_decode_product_version(val))),
"060e2b34010101020520070102010000" => Some(("ApplicationSupplierName".into(), mxf_decode_utf16(val))),
"060e2b34010101020520070103010000" => Some(("ApplicationName".into(), mxf_decode_utf16(val))),
"060e2b34010101020520070105010000" => Some(("ApplicationVersionString".into(), mxf_decode_utf16(val))),
"060e2b34010101020520070106010000" => Some(("ApplicationPlatform".into(), mxf_decode_utf16(val))),
"060e2b34010101020107010201000000" => Some(("TrackName".into(), mxf_decode_utf16(val))),
"060e2b34010101020104010300000000" => {
if val.len() >= 4 {
let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]);
Some(("TrackNumber".into(), n.to_string()))
} else { None }
},
"060e2b34010101020107010100000000" => {
if val.len() >= 4 {
let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]);
Some(("TrackID".into(), n.to_string()))
} else { None }
},
"060e2b34010101020530040500000000" => {
if val.len() >= 8 {
let num = i32::from_be_bytes([val[0],val[1],val[2],val[3]]);
let den = i32::from_be_bytes([val[4],val[5],val[6],val[7]]);
let rate = if den != 0 && den != 1 {
let r = num as f64 / den as f64;
if r == r.floor() { format!("{}", r as i64) } else { format!("{:.6}", r).trim_end_matches('0').to_string() }
} else { num.to_string() };
Some(("EditRate".into(), rate))
} else { None }
},
"060e2b34010101020404010102060000" => {
if val.len() >= 2 {
let n = u16::from_be_bytes([val[0],val[1]]);
Some(("RoundedTimecodeTimebase".into(), n.to_string()))
} else { None }
},
"060e2b34010101010404010105000000" => {
if !val.is_empty() {
Some(("DropFrame".into(), if val[0] != 0 {"True"} else {"False"}.into()))
} else { None }
},
"060e2b34010101020702010301050000" => {
if val.len() >= 8 {
let n = i64::from_be_bytes([val[0],val[1],val[2],val[3],val[4],val[5],val[6],val[7]]);
Some(("StartTimecode".into(), format!("{} s", n)))
} else { None }
},
"060e2b34010101020601010401020000" => {
Some(("ComponentDataDefinition".into(), mxf_decode_component_def(val)))
},
"060e2b34010101020702010301040000" => {
if val.len() >= 8 {
let n = i64::from_be_bytes([val[0],val[1],val[2],val[3],val[4],val[5],val[6],val[7]]);
Some(("Origin".into(), format!("{} s", n)))
} else { None }
},
"060e2b34010101020702010301030000" |
"060e2b34010101020702010302000000" => {
if val.len() >= 8 {
let n = i64::from_be_bytes([val[0],val[1],val[2],val[3],val[4],val[5],val[6],val[7]]);
if n > 1000000000 { return None; } Some(("Duration".into(), format!("{} s", n)))
} else { None }
},
"060e2b34010101010406010200000000" => {
if val.len() >= 8 {
let n = i64::from_be_bytes([val[0],val[1],val[2],val[3],val[4],val[5],val[6],val[7]]);
if n > 1000000000 { return None; }
Some(("EssenceLength".into(), format!("{} s", n)))
} else { None }
},
"060e2b34010101010406010100000000" => {
if val.len() >= 8 {
let num = i32::from_be_bytes([val[0],val[1],val[2],val[3]]);
let den = i32::from_be_bytes([val[4],val[5],val[6],val[7]]);
let rate = if den != 0 && den != 1 {
let r = num as f64 / den as f64;
if r == r.floor() { format!("{}", r as i64) } else { format!("{:.6}", r) }
} else { num.to_string() };
Some(("SampleRate".into(), rate))
} else { None }
},
"060e2b34010101050402010104000000" => {
if val.len() >= 4 { let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]); Some(("ChannelCount".into(), n.to_string())) }
else if !val.is_empty() { Some(("ChannelCount".into(), val[0].to_string())) }
else { None }
},
"060e2b34010101050402030101010000" => {
if val.len() >= 8 {
let num = i32::from_be_bytes([val[0],val[1],val[2],val[3]]);
let den = i32::from_be_bytes([val[4],val[5],val[6],val[7]]);
let rate = if den != 0 && den != 1 {
let r = num as f64 / den as f64;
if r == r.floor() { format!("{}", r as i64) } else { format!("{:.6}", r) }
} else { num.to_string() };
Some(("AudioSampleRate".into(), rate))
} else { None }
},
"060e2b34010101050402030201000000" => {
if val.len() >= 2 { let n = u16::from_be_bytes([val[0],val[1]]); Some(("BlockAlign".into(), n.to_string())) } else { None }
},
"060e2b34010101050402030305000000" => {
if val.len() >= 4 { let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]); Some(("AverageBytesPerSecond".into(), n.to_string())) } else { None }
},
"060e2b34010101040402030104000000" => {
if !val.is_empty() {
Some(("LockedIndicator".into(), if val[0] != 0 {"True"} else {"False"}.into()))
} else { None }
},
"060e2b34010101040402030304000000" => {
if val.len() >= 4 { let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]); Some(("BitsPerAudioSample".into(), n.to_string())) } else { None }
},
"060e2b34010101050601010305000000" => {
if val.len() >= 4 { let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]); Some(("LinkedTrackID".into(), n.to_string())) } else { None }
},
"060e2b34010101040103040400000000" => {
if val.len() >= 4 { let n = u32::from_be_bytes([val[0],val[1],val[2],val[3]]); Some(("EssenceStreamID".into(), n.to_string())) }
else if val.len() >= 2 { let n = u16::from_be_bytes([val[0],val[1]]); Some(("EssenceStreamID".into(), n.to_string())) }
else { None }
},
_ => None,
}
}
fn mxf_decode_timestamp(val: &[u8]) -> String {
if val.len() < 8 { return String::new(); }
let year = u16::from_be_bytes([val[0], val[1]]);
let month = val[2]; let day = val[3];
let hour = val[4]; let min = val[5]; let sec = val[6];
let msec = (val[7] as u32) * 4;
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}.{:03}", year, month, day, hour, min, sec, msec)
}
fn mxf_decode_version_short(val: &[u8]) -> String {
if val.len() < 2 { return String::new(); }
format!("{}.{}", val[0], val[1])
}
fn mxf_decode_product_version(val: &[u8]) -> String {
if val.len() < 10 { return String::new(); }
let major = u16::from_be_bytes([val[0],val[1]]);
let minor = u16::from_be_bytes([val[2],val[3]]);
let patch = u16::from_be_bytes([val[4],val[5]]);
let build = u16::from_be_bytes([val[6],val[7]]);
let rel_type = u16::from_be_bytes([val[8],val[9]]);
let rel_str = match rel_type {
0 => "unknown".to_string(),
1 => "released".to_string(),
2 => "debug".to_string(),
3 => "patched".to_string(),
4 => "beta".to_string(),
5 => "private build".to_string(),
_ => format!("unknown {}", rel_type),
};
format!("{}.{}.{}.{} {}", major, minor, patch, build, rel_str)
}
fn mxf_decode_utf16(val: &[u8]) -> String {
let chars: Vec<u16> = val.chunks(2)
.filter(|c| c.len() == 2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&chars).trim_end_matches('\0').to_string()
}
fn mxf_decode_component_def(val: &[u8]) -> String {
if val.len() < 16 { return String::new(); }
let ul: String = val[..16].iter().map(|b| format!("{:02x}", b)).collect();
match ul.as_str() {
"060e2b34040101020d01030102060200" => "Sound Essence Track".to_string(),
"060e2b34040101010103020100000000" => "Picture Essence Track".to_string(),
"060e2b34040101010103020200000000" => "Sound Essence Track".to_string(),
"060e2b34040101010103020300000000" => "Data Essence Track".to_string(),
"060e2b34040101010103020400000000" => "Descriptive Metadata Track".to_string(),
_ => ul,
}
}
pub fn read_iiq(data: &[u8]) -> Result<Vec<Tag>> {
let mut tags = crate::formats::tiff::read_tiff(data).unwrap_or_default();
{
let mut seen_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut strip_offsets_count = 0;
let mut strip_bytes_count = 0;
let mut subfile_type_removed = false;
let mut remove_indices: std::collections::HashSet<usize> = std::collections::HashSet::new();
let keep_both: std::collections::HashSet<&str> = [
"SubfileType", "StripOffsets", "StripByteCounts",
].iter().cloned().collect();
for (i, t) in tags.iter().enumerate() {
if t.name == "ExifByteOrder" {
remove_indices.insert(i);
continue;
}
if t.name == "SubfileType" && !subfile_type_removed {
let raw_v = if let Value::String(ref v) = t.raw_value { v.as_str() } else { "" };
if raw_v == "0" || t.print_value == "0" {
remove_indices.insert(i);
subfile_type_removed = true;
continue;
}
}
if t.name == "StripOffsets" {
strip_offsets_count += 1;
if strip_offsets_count == 1 { remove_indices.insert(i); continue; }
}
if t.name == "StripByteCounts" {
strip_bytes_count += 1;
if strip_bytes_count == 1 { remove_indices.insert(i); continue; }
}
if (t.name == "ImageWidth" || t.name == "ImageHeight") && t.print_value == "1" {
remove_indices.insert(i);
continue;
}
if !keep_both.contains(t.name.as_str()) {
if seen_names.contains(&t.name) {
remove_indices.insert(i);
} else {
seen_names.insert(t.name.clone());
}
}
}
let mut new_tags = Vec::new();
for (i, t) in tags.into_iter().enumerate() {
if !remove_indices.contains(&i) {
new_tags.push(t);
}
}
tags = new_tags;
}
if data.len() < 20 {
return Ok(tags);
}
let is_le = &data[8..12] == b"IIII";
let is_be = &data[8..12] == b"MMMM";
if !is_le && !is_be {
return Ok(tags);
}
let phaseone_start = 8usize;
let ifd_offset_in_block = iiq_read_u32(data, phaseone_start + 8, is_le) as usize;
let abs_ifd_start = phaseone_start + ifd_offset_in_block;
if abs_ifd_start + 8 > data.len() {
return Ok(tags);
}
let num_entries = iiq_read_u32(data, abs_ifd_start, is_le) as usize;
if num_entries > 300 || abs_ifd_start + 8 + num_entries * 16 > data.len() {
return Ok(tags);
}
let entry_start = abs_ifd_start + 8;
let mut phaseone_tags: Vec<Tag> = Vec::new();
for i in 0..num_entries {
let off = entry_start + i * 16;
let tag_id = iiq_read_u32(data, off, is_le);
let _fmt_size = iiq_read_u32(data, off + 4, is_le);
let size = iiq_read_u32(data, off + 8, is_le) as usize;
let val_or_ptr = iiq_read_u32(data, off + 12, is_le) as usize;
let raw: &[u8] = if size <= 4 {
let end = (off + 12 + size).min(data.len());
&data[off + 12..end]
} else {
let abs_ptr = phaseone_start + val_or_ptr;
if abs_ptr + size > data.len() { continue; }
&data[abs_ptr..abs_ptr + size]
};
iiq_decode_tag(tag_id, raw, is_le, size, data, phaseone_start, &mut phaseone_tags);
}
iiq_parse_sensor_calibration(data, phaseone_start, is_le, entry_start, num_entries, &mut phaseone_tags);
{
let _existing: std::collections::HashSet<String> = tags.iter().map(|t| t.name.clone()).collect();
let phaseone_names: std::collections::HashSet<String> = phaseone_tags.iter().map(|t| t.name.clone()).collect();
let phaseone_overrides: std::collections::HashSet<&str> = [
"ShutterSpeedValue", "ApertureValue",
].iter().cloned().collect();
tags.retain(|t| !phaseone_overrides.contains(t.name.as_str()) || !phaseone_names.contains(&t.name));
let existing2: std::collections::HashSet<String> = tags.iter().map(|t| t.name.clone()).collect();
for t in phaseone_tags {
if !existing2.contains(&t.name) {
tags.push(t);
}
}
}
{
let wb_val = tags.iter().find(|t| t.name == "WB_RGBLevels")
.map(|t| t.print_value.clone());
if let Some(s) = wb_val {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() >= 3 {
if let Ok(r) = parts[0].parse::<f64>() {
tags.push(mktag("Composite", "RedBalance", "Red Balance",
Value::String(format!("{:.5}", r))));
}
if let Ok(b) = parts[2].parse::<f64>() {
tags.push(mktag("Composite", "BlueBalance", "Blue Balance",
Value::String(format!("{:.5}", b))));
}
}
}
}
Ok(tags)
}
fn iiq_fmt_f64(v: f64) -> String {
let _s = format!("{:.15e}", v);
let formatted = format!("{:.14}", v);
if formatted.contains('.') {
let stripped = formatted.trim_end_matches('0').trim_end_matches('.');
stripped.to_string()
} else {
formatted
}
}
fn iiq_read_u32(data: &[u8], off: usize, is_le: bool) -> u32 {
if off + 4 > data.len() { return 0; }
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]])
}
}
fn iiq_read_f32(data: &[u8], off: usize, is_le: bool) -> f32 {
if off + 4 > data.len() { return 0.0; }
let bytes = [data[off], data[off+1], data[off+2], data[off+3]];
if is_le { f32::from_le_bytes(bytes) } else { f32::from_be_bytes(bytes) }
}
fn iiq_read_str(raw: &[u8]) -> String {
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
String::from_utf8_lossy(&raw[..end]).trim().to_string()
}
fn iiq_decode_tag(
tag_id: u32, raw: &[u8], is_le: bool, size: usize,
full_data: &[u8], phaseone_start: usize,
tags: &mut Vec<Tag>,
) {
let push = |tags: &mut Vec<Tag>, name: &str, desc: &str, val: String| {
tags.push(mktag("MakerNotes", name, desc, Value::String(val)));
};
match tag_id {
0x010f => {
let display = format!("(Binary data {} bytes, use -b option to extract)", size);
push(tags, "RawData", "Raw Data", display);
}
0x0100 => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) & 0x03 } else { 0 };
let s = match v {
0 => "Horizontal (normal)".to_string(),
1 => "Rotate 90 CW".to_string(),
2 => "Rotate 270 CW".to_string(),
3 => "Rotate 180".to_string(),
_ => v.to_string(),
};
push(tags, "CameraOrientation", "Camera Orientation", s);
}
0x0102 => {
push(tags, "SerialNumber", "Serial Number", iiq_read_str(raw));
}
0x0105 => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "ISO", "ISO", v.to_string());
}
0x0106 => {
if raw.len() >= 36 {
let vals: Vec<f32> = (0..9).map(|i| iiq_read_f32(raw, i*4, is_le)).collect();
let s: Vec<String> = vals.iter().map(|v| format!("{:.3}", v)).collect();
push(tags, "ColorMatrix1", "Color Matrix 1", s.join(" "));
}
}
0x0107 => {
if raw.len() >= 12 {
let r = iiq_read_f32(raw, 0, is_le) as f64;
let g = iiq_read_f32(raw, 4, is_le) as f64;
let b = iiq_read_f32(raw, 8, is_le) as f64;
let s = if g != 0.0 {
format!("{} {} {}", iiq_fmt_f64(r/g), 1.0f64, iiq_fmt_f64(b/g))
} else {
format!("{} {} {}", iiq_fmt_f64(r), iiq_fmt_f64(g), iiq_fmt_f64(b))
};
push(tags, "WB_RGBLevels", "WB RGB Levels", s);
}
}
0x0108 => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "SensorWidth", "Sensor Width", v.to_string());
}
0x0109 => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "SensorHeight", "Sensor Height", v.to_string());
}
0x010a => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "SensorLeftMargin", "Sensor Left Margin", v.to_string());
}
0x010b => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "SensorTopMargin", "Sensor Top Margin", v.to_string());
}
0x010c => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "ImageWidth", "Image Width", v.to_string());
}
0x010d => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "ImageHeight", "Image Height", v.to_string());
}
0x010e => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
let s = match v {
0 => "Uncompressed".to_string(),
1 => "RAW 1".to_string(),
2 => "RAW 2".to_string(),
3 => "IIQ L".to_string(),
5 => "IIQ S".to_string(),
6 => "IIQ Sv2".to_string(),
8 => "IIQ L16".to_string(),
_ => v.to_string(),
};
push(tags, "RawFormat", "Raw Format", s);
}
0x0113 => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "ImageNumber", "Image Number", v.to_string());
}
0x0203 => {
push(tags, "Software", "Software", iiq_read_str(raw));
}
0x0204 => {
push(tags, "System", "System", iiq_read_str(raw));
}
0x0210 => {
let v = if raw.len() >= 4 { iiq_read_f32(raw, 0, is_le) } else { 0.0 };
push(tags, "SensorTemperature", "Sensor Temperature", format!("{:.2} C", v));
}
0x0211 => {
let v = if raw.len() >= 4 { iiq_read_f32(raw, 0, is_le) } else { 0.0 };
push(tags, "SensorTemperature2", "Sensor Temperature 2", format!("{:.2} C", v));
}
0x021d => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "BlackLevel", "Black Level", v.to_string());
}
0x0222 => {
let v = if raw.len() >= 4 { iiq_read_u32(raw, 0, is_le) } else { 0 };
push(tags, "SplitColumn", "Split Column", v.to_string());
}
0x0223 => {
let count = raw.len() / 2;
if count > 0 {
let vals: Vec<String> = (0..count).map(|i| {
let v = if is_le {
u16::from_le_bytes([raw[i*2], raw[i*2+1]])
} else {
u16::from_be_bytes([raw[i*2], raw[i*2+1]])
};
v.to_string()
}).collect();
let s = vals.join(" ");
let display = format!("(Binary data {} bytes, use -b option to extract)", s.len());
push(tags, "BlackLevelData", "Black Level Data", display);
} else {
push(tags, "BlackLevelData", "Black Level Data",
format!("(Binary data {} bytes, use -b option to extract)", raw.len()));
}
}
0x0226 => {
if raw.len() >= 36 {
let vals: Vec<f32> = (0..9).map(|i| iiq_read_f32(raw, i*4, is_le)).collect();
let s: Vec<String> = vals.iter().map(|v| format!("{:.3}", v)).collect();
push(tags, "ColorMatrix2", "Color Matrix 2", s.join(" "));
}
}
0x0301 => {
push(tags, "FirmwareVersions", "Firmware Versions", iiq_read_str(raw));
}
0x0400 => {
let v = if raw.len() >= 4 { iiq_read_f32(raw, 0, is_le) } else { 0.0 };
let exposure = if v.abs() < 100.0 { 2.0f32.powf(-v) } else { 0.0 };
let s = iiq_format_exposure_time(exposure);
push(tags, "ShutterSpeedValue", "Shutter Speed Value", s);
}
0x0401 => {
let v = if raw.len() >= 4 { iiq_read_f32(raw, 0, is_le) } else { 0.0 };
let aperture = 2.0f32.powf(v / 2.0);
push(tags, "ApertureValue", "Aperture Value", format!("{:.1}", aperture));
}
0x0403 => {
let v = if raw.len() >= 4 { iiq_read_f32(raw, 0, is_le) } else { 0.0 };
push(tags, "FocalLength", "Focal Length", format!("{:.1} mm", v));
}
0x0412 => {
push(tags, "LensModel", "Lens Model", iiq_read_str(raw));
}
_ => {}
}
let _ = (full_data, phaseone_start); }
fn iiq_format_exposure_time(t: f32) -> String {
if t <= 0.0 { return "0".to_string(); }
if t >= 1.0 {
let rounded = t.round() as u32;
if (t - rounded as f32).abs() < 0.05 {
return rounded.to_string();
}
return format!("{:.1}", t);
}
let n = (1.0 / t).round() as u32;
format!("1/{}", n)
}
fn iiq_parse_sensor_calibration(
data: &[u8], phaseone_start: usize, is_le: bool,
entry_start: usize, num_entries: usize,
tags: &mut Vec<Tag>,
) {
for i in 0..num_entries {
let off = entry_start + i * 16;
let tag_id = iiq_read_u32(data, off, is_le);
if tag_id != 0x0110 { continue; }
let size = iiq_read_u32(data, off + 8, is_le) as usize;
let val_or_ptr = iiq_read_u32(data, off + 12, is_le) as usize;
if size <= 4 { return; }
let abs_ptr = phaseone_start + val_or_ptr;
if abs_ptr + size > data.len() { return; }
let sub = &data[abs_ptr..abs_ptr + size];
if sub.len() < 12 { return; }
let sub_is_le = &sub[0..4] == b"IIII";
let sub_is_be = &sub[0..4] == b"MMMM";
if !sub_is_le && !sub_is_be { return; }
let sub_ifd_off = iiq_read_u32(sub, 8, sub_is_le) as usize;
if sub_ifd_off + 8 > sub.len() { return; }
let num_sub = iiq_read_u32(sub, sub_ifd_off, sub_is_le) as usize;
if num_sub > 300 { return; }
let sub_entry_start = sub_ifd_off + 8;
if sub_entry_start + num_sub * 12 > sub.len() { return; }
for j in 0..num_sub {
let eoff = sub_entry_start + j * 12;
let etag = iiq_read_u32(sub, eoff, sub_is_le);
let esize = iiq_read_u32(sub, eoff + 4, sub_is_le) as usize;
let _eval_ptr = iiq_read_u32(sub, eoff + 8, sub_is_le) as usize;
if etag == 0x0400 {
let display = format!("(Binary data {} bytes, use -b option to extract)", esize);
tags.push(mktag("MakerNotes", "SensorDefects", "Sensor Defects", Value::String(display)));
break;
}
}
return;
}
}