use crate::error::{Error, Result};
use crate::metadata::{ExifReader, XmpReader};
use crate::metadata::exif::ByteOrderMark;
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
pub fn read_riff(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 12 || !data.starts_with(b"RIFF") {
return Err(Error::InvalidData("not a RIFF file".into()));
}
let _file_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
let form_type = &data[8..12];
let mut tags = Vec::new();
match form_type {
b"WEBP" => read_webp_chunks(data, 12, &mut tags)?,
b"AVI " => read_avi_chunks(data, 12, &mut tags)?,
b"WAVE" => read_wav_chunks(data, 12, &mut tags)?,
_ => {
return Err(Error::InvalidData(format!(
"unknown RIFF type: {}",
String::from_utf8_lossy(form_type)
)));
}
}
Ok(tags)
}
fn read_webp_chunks(data: &[u8], start: usize, tags: &mut Vec<Tag>) -> Result<()> {
let mut pos = start;
while pos + 8 <= data.len() {
let chunk_id = &data[pos..pos + 4];
let chunk_size = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]) as usize;
let chunk_data_start = pos + 8;
let chunk_data_end = chunk_data_start + chunk_size;
if chunk_data_end > data.len() {
break;
}
let chunk_data = &data[chunk_data_start..chunk_data_end];
match chunk_id {
b"VP8 " => {
if chunk_data.len() >= 10 {
let frame_tag = u32::from_le_bytes([
chunk_data[0], chunk_data[1], chunk_data[2], 0,
]);
let is_keyframe = (frame_tag & 1) == 0;
if is_keyframe && chunk_data.len() >= 10 {
if chunk_data[3] == 0x9D && chunk_data[4] == 0x01 && chunk_data[5] == 0x2A {
let version = (chunk_data[0] >> 1) & 0x07;
let version_str = match version {
0 => "0 (bicubic reconstruction, normal loop)",
1 => "1 (bilinear reconstruction, simple loop)",
2 => "2 (bilinear reconstruction, no loop)",
3 => "3 (no reconstruction, no loop)",
v => return {
let width = u16::from_le_bytes([chunk_data[6], chunk_data[7]]) & 0x3FFF;
let height = u16::from_le_bytes([chunk_data[8], chunk_data[9]]) & 0x3FFF;
let hscale = (u16::from_le_bytes([chunk_data[6], chunk_data[7]]) >> 14) & 0x3;
let vscale = (u16::from_le_bytes([chunk_data[8], chunk_data[9]]) >> 14) & 0x3;
tags.push(mk_webp("VP8Version", "VP8 Version", Value::String(format!("{}", v))));
tags.push(mk_webp("ImageWidth", "Image Width", Value::U16(width)));
tags.push(mk_webp("HorizontalScale", "Horizontal Scale", Value::U16(hscale)));
tags.push(mk_webp("ImageHeight", "Image Height", Value::U16(height)));
tags.push(mk_webp("VerticalScale", "Vertical Scale", Value::U16(vscale)));
Ok(())
},
};
let width = u16::from_le_bytes([chunk_data[6], chunk_data[7]]) & 0x3FFF;
let height = u16::from_le_bytes([chunk_data[8], chunk_data[9]]) & 0x3FFF;
let hscale = (u16::from_le_bytes([chunk_data[6], chunk_data[7]]) >> 14) & 0x3;
let vscale = (u16::from_le_bytes([chunk_data[8], chunk_data[9]]) >> 14) & 0x3;
tags.push(mk_webp("VP8Version", "VP8 Version", Value::String(version_str.into())));
tags.push(mk_webp("ImageWidth", "Image Width", Value::U16(width)));
tags.push(mk_webp("HorizontalScale", "Horizontal Scale", Value::U16(hscale)));
tags.push(mk_webp("ImageHeight", "Image Height", Value::U16(height)));
tags.push(mk_webp("VerticalScale", "Vertical Scale", Value::U16(vscale)));
}
}
}
}
b"VP8L" => {
if chunk_data.len() >= 5 && chunk_data[0] == 0x2F {
let bits = u32::from_le_bytes([chunk_data[1], chunk_data[2], chunk_data[3], chunk_data[4]]);
let width = (bits & 0x3FFF) + 1;
let height = ((bits >> 14) & 0x3FFF) + 1;
let alpha = (bits >> 28) & 1;
tags.push(mk_webp("ImageWidth", "Image Width", Value::U32(width)));
tags.push(mk_webp("ImageHeight", "Image Height", Value::U32(height)));
tags.push(mk_webp("AlphaIsUsed", "Alpha Is Used", Value::String(if alpha != 0 { "Yes".into() } else { "No".into() })));
}
}
b"VP8X" => {
if chunk_data.len() >= 10 {
let flags = u32::from_le_bytes([chunk_data[0], chunk_data[1], chunk_data[2], chunk_data[3]]);
let width = (chunk_data[4] as u32)
| ((chunk_data[5] as u32) << 8)
| ((chunk_data[6] as u32) << 16);
let height = (chunk_data[7] as u32)
| ((chunk_data[8] as u32) << 8)
| ((chunk_data[9] as u32) << 16);
let mut flag_parts = Vec::new();
if flags & (1 << 1) != 0 { flag_parts.push("Animation"); }
if flags & (1 << 2) != 0 { flag_parts.push("XMP"); }
if flags & (1 << 3) != 0 { flag_parts.push("EXIF"); }
if flags & (1 << 4) != 0 { flag_parts.push("Alpha"); }
if flags & (1 << 5) != 0 { flag_parts.push("ICC Profile"); }
if !flag_parts.is_empty() {
tags.push(mk_webp("WebP_Flags", "WebP Flags", Value::String(flag_parts.join(", "))));
}
tags.push(mk_webp("ImageWidth", "Image Width", Value::U32(width + 1)));
tags.push(mk_webp("ImageHeight", "Image Height", Value::U32(height + 1)));
}
}
b"ALPH" => {
if !chunk_data.is_empty() {
let byte0 = chunk_data[0];
let preprocessing = byte0 & 0x03;
let filtering = (byte0 >> 2) & 0x03;
let compression = (byte0 >> 4) & 0x03;
let preprocessing_str = match preprocessing {
0 => "none",
1 => "Level Reduction",
_ => "Unknown",
};
let filtering_str = match filtering {
0 => "none",
1 => "Horizontal",
2 => "Vertical",
3 => "Gradient",
_ => "Unknown",
};
let compression_str = match compression {
0 => "none",
1 => "Lossless",
_ => "Unknown",
};
tags.push(mk_webp("AlphaPreprocessing", "Alpha Preprocessing", Value::String(preprocessing_str.into())));
tags.push(mk_webp("AlphaFiltering", "Alpha Filtering", Value::String(filtering_str.into())));
tags.push(mk_webp("AlphaCompression", "Alpha Compression", Value::String(compression_str.into())));
}
}
b"EXIF" => {
let exif_data = if chunk_data.len() > 6 && chunk_data.starts_with(b"Exif\0\0") {
&chunk_data[6..]
} else {
chunk_data
};
if let Ok(exif_tags) = ExifReader::read(exif_data) {
tags.extend(exif_tags);
}
}
b"XMP " => {
if let Ok(xmp_tags) = XmpReader::read(chunk_data) {
tags.extend(xmp_tags);
}
}
b"ICCP" => {
tags.push(mk_webp(
"ICC_Profile",
"ICC Profile",
Value::Binary(chunk_data.to_vec()),
));
}
b"ANIM" => {
if chunk_data.len() >= 6 {
let bg_color = u32::from_le_bytes([
chunk_data[0], chunk_data[1], chunk_data[2], chunk_data[3],
]);
let loop_count = u16::from_le_bytes([chunk_data[4], chunk_data[5]]);
tags.push(mk_webp(
"BackgroundColor",
"Background Color",
Value::U32(bg_color),
));
tags.push(mk_webp(
"AnimationLoopCount",
"Animation Loop Count",
Value::U16(loop_count),
));
}
}
_ => {}
}
pos = chunk_data_end + (chunk_size & 1);
}
Ok(())
}
fn read_avi_chunks(data: &[u8], start: usize, tags: &mut Vec<Tag>) -> Result<()> {
let mut state = AviState::new();
read_riff_chunks(data, start, data.len(), tags, "AVI", &mut state)
}
fn read_wav_chunks(data: &[u8], start: usize, tags: &mut Vec<Tag>) -> Result<()> {
let mut state = AviState::new();
state.file_size = data.len() as u64;
read_riff_chunks(data, start, data.len(), tags, "WAV", &mut state)
}
struct AviState {
current_stream_type: Option<String>,
data_len: u64,
file_size: u64,
avg_bytes_per_sec: u32,
us_per_frame: u32,
total_frames: u32,
video_frame_count: u32,
video_frame_rate: Option<f64>,
depth: usize,
stream_count_seen: u32,
}
impl AviState {
fn new() -> Self {
AviState {
current_stream_type: None,
data_len: 0,
file_size: 0,
avg_bytes_per_sec: 0,
us_per_frame: 0,
total_frames: 0,
video_frame_count: 0,
video_frame_rate: None,
depth: 0,
stream_count_seen: 0,
}
}
}
fn read_riff_chunks(
data: &[u8],
start: usize,
end: usize,
tags: &mut Vec<Tag>,
family: &str,
state: &mut AviState,
) -> Result<()> {
let mut pos = start;
while pos + 8 <= end {
let chunk_id = &data[pos..pos + 4];
let chunk_size = u32::from_le_bytes([
data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
]) as usize;
let chunk_data_start = pos + 8;
let chunk_data_end = (chunk_data_start + chunk_size).min(end);
if chunk_data_start > end {
break;
}
match chunk_id {
b"LIST" => {
if chunk_data_end >= chunk_data_start + 4 {
let list_type = &data[chunk_data_start..chunk_data_start + 4];
match list_type {
b"INFO" => {
read_info_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family)?;
}
b"INF0" => {
read_info_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family)?;
}
b"hdrl" => {
state.depth += 1;
read_riff_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family, state)?;
state.depth -= 1;
}
b"strl" => {
state.depth += 1;
read_riff_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family, state)?;
state.depth -= 1;
}
b"odml" => {
state.depth += 1;
read_riff_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family, state)?;
state.depth -= 1;
}
b"exif" => {
read_exif_list_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family)?;
}
b"adtl" => {
state.depth += 1;
read_riff_chunks(data, chunk_data_start + 4, chunk_data_end, tags, family, state)?;
state.depth -= 1;
}
b"hydt" | b"pntx" => {
read_pentax_avi_chunks(data, chunk_data_start + 4, chunk_data_end, tags)?;
}
_ => {}
}
}
}
b"avih" => {
if chunk_size >= 40 {
let cd = &data[chunk_data_start..chunk_data_end];
let us_per_frame = u32::from_le_bytes([cd[0], cd[1], cd[2], cd[3]]);
let max_data_rate = u32::from_le_bytes([cd[4], cd[5], cd[6], cd[7]]);
let total_frames = u32::from_le_bytes([cd[16], cd[17], cd[18], cd[19]]);
let stream_count = u32::from_le_bytes([cd[24], cd[25], cd[26], cd[27]]);
let width = u32::from_le_bytes([cd[32], cd[33], cd[34], cd[35]]);
let height = u32::from_le_bytes([cd[36], cd[37], cd[38], cd[39]]);
state.us_per_frame = us_per_frame;
state.total_frames = total_frames;
if us_per_frame > 0 {
let fps = 1_000_000.0_f64 / us_per_frame as f64;
let fps_rounded = (fps * 1000.0 + 0.5).floor() / 1000.0;
tags.push(mk_riff(family, "FrameRate", "Frame Rate", Value::String(format!("{}", fps_rounded))));
}
let kbps = max_data_rate as f64 / 1000.0;
let max_data_rate_str = format_sig4(kbps, "kB/s");
tags.push(mk_riff(family, "MaxDataRate", "Max Data Rate", Value::String(max_data_rate_str)));
tags.push(mk_riff(family, "FrameCount", "Frame Count", Value::U32(total_frames)));
tags.push(mk_riff(family, "StreamCount", "Stream Count", Value::U32(stream_count)));
tags.push(mk_riff(family, "ImageWidth", "Image Width", Value::U32(width)));
tags.push(mk_riff(family, "ImageHeight", "Image Height", Value::U32(height)));
}
}
b"strh" => {
if chunk_size >= 4 {
let cd = &data[chunk_data_start..chunk_data_end];
let fcc_type = String::from_utf8_lossy(&cd[0..4]).to_string();
state.current_stream_type = Some(fcc_type.clone());
state.stream_count_seen += 1;
let is_first_stream = state.stream_count_seen == 1;
if is_first_stream {
let stream_type_str = match fcc_type.as_str() {
"auds" => "Audio",
"mids" => "MIDI",
"txts" => "Text",
"vids" => "Video",
"iavs" => "Interleaved Audio+Video",
_ => &fcc_type,
};
tags.push(mk_riff(family, "StreamType", "Stream Type", Value::String(stream_type_str.to_string())));
}
if chunk_size >= 8 {
let fcc_handler = String::from_utf8_lossy(&cd[4..8]).trim_end_matches('\0').to_string();
if fcc_type == "vids" {
tags.push(mk_riff(family, "VideoCodec", "Video Codec", Value::String(fcc_handler)));
} else if fcc_type == "auds" {
tags.push(mk_riff(family, "AudioCodec", "Audio Codec", Value::String(fcc_handler)));
}
}
if chunk_size >= 28 {
let scale = u32::from_le_bytes([cd[20], cd[21], cd[22], cd[23]]);
let rate = u32::from_le_bytes([cd[24], cd[25], cd[26], cd[27]]);
if fcc_type == "auds" && scale > 0 {
let audio_rate = rate as f64 / scale as f64;
let audio_rate_rounded = (audio_rate * 100.0 + 0.5).floor() / 100.0;
tags.push(mk_riff(family, "AudioSampleRate", "Audio Sample Rate", Value::String(format!("{}", audio_rate_rounded))));
} else if fcc_type == "vids" && scale > 0 {
let vfr = rate as f64 / scale as f64;
let vfr_rounded = (vfr * 1000.0 + 0.5).floor() / 1000.0;
state.video_frame_rate = Some(vfr);
tags.push(mk_riff(family, "VideoFrameRate", "Video Frame Rate", Value::String(format!("{}", vfr_rounded))));
}
}
if chunk_size >= 36 {
let length = u32::from_le_bytes([cd[32], cd[33], cd[34], cd[35]]);
if fcc_type == "auds" {
tags.push(mk_riff(family, "AudioSampleCount", "Audio Sample Count", Value::U32(length)));
} else if fcc_type == "vids" {
state.video_frame_count = length;
tags.push(mk_riff(family, "VideoFrameCount", "Video Frame Count", Value::U32(length)));
}
}
if chunk_size >= 48 && is_first_stream {
let quality = u32::from_le_bytes([cd[40], cd[41], cd[42], cd[43]]);
let sample_size = u32::from_le_bytes([cd[44], cd[45], cd[46], cd[47]]);
let quality_str = if quality == 0xFFFFFFFF {
"Default".to_string()
} else {
format!("{}", quality)
};
tags.push(mk_riff(family, "Quality", "Quality", Value::String(quality_str)));
let sample_size_str = if sample_size == 0 {
"Variable".to_string()
} else if sample_size == 1 {
"1 byte".to_string()
} else {
format!("{} bytes", sample_size)
};
tags.push(mk_riff(family, "SampleSize", "Sample Size", Value::String(sample_size_str)));
}
}
}
b"strf" => {
match state.current_stream_type.as_deref() {
Some("auds") => {
parse_wave_format(data, chunk_data_start, chunk_data_end, tags, family);
}
Some("vids") => {
parse_bitmapinfoheader(data, chunk_data_start, chunk_data_end, tags, family);
}
_ => {}
}
}
b"strd" => {
if chunk_data_end >= chunk_data_start + 4 {
let tag = &data[chunk_data_start..chunk_data_start + 4];
if tag == b"AVIF" && chunk_data_end >= chunk_data_start + 8 {
if let Ok(exif_tags) = ExifReader::read(&data[chunk_data_start + 8..chunk_data_end]) {
tags.extend(exif_tags);
}
}
}
}
b"dmlh" => {
if chunk_size >= 4 {
let cd = &data[chunk_data_start..chunk_data_end];
let total_frame_count = u32::from_le_bytes([cd[0], cd[1], cd[2], cd[3]]);
tags.push(mk_riff(family, "TotalFrameCount", "Total Frame Count", Value::U32(total_frame_count)));
}
}
b"fmt " => {
parse_wave_format(data, chunk_data_start, chunk_data_end, tags, family);
if chunk_size >= 12 {
let cd = &data[chunk_data_start..chunk_data_end];
state.avg_bytes_per_sec = u32::from_le_bytes([cd[8], cd[9], cd[10], cd[11]]);
}
}
b"data" => {
state.data_len += chunk_size as u64;
}
b"IDIT" => {
let s = String::from_utf8_lossy(&data[chunk_data_start..chunk_data_end])
.trim_end_matches('\0')
.to_string();
if !s.is_empty() {
let converted = convert_riff_date(&s);
tags.push(mk_riff(family, "DateTimeOriginal", "Date/Time Original", Value::String(converted)));
}
}
b"EXIF" => {
let exif_data = &data[chunk_data_start..chunk_data_end];
let exif_data = if exif_data.starts_with(b"Exif\0\0") {
&exif_data[6..]
} else {
exif_data
};
if let Ok(exif_tags) = ExifReader::read(exif_data) {
tags.extend(exif_tags);
}
}
b"_PMX" | b"XMP " => {
if let Ok(xmp_tags) = XmpReader::read(&data[chunk_data_start..chunk_data_end]) {
tags.extend(xmp_tags);
}
}
b"bext" => {
parse_bext(data, chunk_data_start, chunk_data_end, chunk_size, tags, family);
}
_ => {}
}
pos = chunk_data_end + (chunk_size & 1);
}
if state.depth > 0 {
return Ok(());
}
if family == "WAV" && state.avg_bytes_per_sec > 0 {
let effective_len = if state.data_len > 0 { state.data_len } else { state.file_size };
if effective_len > 0 {
let duration = effective_len as f64 / state.avg_bytes_per_sec as f64;
tags.push(mk_riff(family, "Duration", "Duration", Value::String(format_duration(duration))));
}
}
if family == "AVI" && state.us_per_frame > 0 {
let fps = 1_000_000.0_f64 / state.us_per_frame as f64;
let dur = if let (Some(vfr), vc) = (state.video_frame_rate, state.video_frame_count) {
if vc > 0 && vfr > 0.0 {
let dur1 = state.total_frames as f64 / fps;
let dur2 = vc as f64 / vfr;
let rat = dur1 / dur2;
if rat > 1.9 && rat < 3.1 { dur2 } else { dur1 }
} else if state.total_frames > 0 {
state.total_frames as f64 / fps
} else {
0.0
}
} else if state.total_frames > 0 {
state.total_frames as f64 / fps
} else {
0.0
};
if dur > 0.0 {
tags.push(mk_riff(family, "Duration", "Duration", Value::String(format_duration(dur))));
}
}
Ok(())
}
fn parse_wave_format(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>, family: &str) {
let chunk_size = end - start;
if chunk_size < 14 {
return;
}
let cd = &data[start..end];
let format_tag = u16::from_le_bytes([cd[0], cd[1]]);
let channels = u16::from_le_bytes([cd[2], cd[3]]);
let sample_rate = u32::from_le_bytes([cd[4], cd[5], cd[6], cd[7]]);
let avg_bytes = u32::from_le_bytes([cd[8], cd[9], cd[10], cd[11]]);
let encoding = audio_encoding_name(format_tag);
tags.push(mk_riff(family, "Encoding", "Encoding", Value::String(encoding.into())));
tags.push(mk_riff(family, "NumChannels", "Num Channels", Value::U16(channels)));
tags.push(mk_riff(family, "SampleRate", "Sample Rate", Value::U32(sample_rate)));
tags.push(mk_riff(family, "AvgBytesPerSec", "Avg Bytes Per Sec", Value::U32(avg_bytes)));
if chunk_size >= 16 {
let bits_per_sample = u16::from_le_bytes([cd[14], cd[15]]);
tags.push(mk_riff(family, "BitsPerSample", "Bits Per Sample", Value::U16(bits_per_sample)));
}
}
fn parse_bitmapinfoheader(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>, family: &str) {
let chunk_size = end - start;
if chunk_size < 40 {
return;
}
let cd = &data[start..end];
let bmp_size = u32::from_le_bytes([cd[0], cd[1], cd[2], cd[3]]);
let bmp_version = match bmp_size {
40 => "Windows V3",
68 => "AVI BMP structure?",
108 => "Windows V4",
124 => "Windows V5",
_ => "Unknown",
};
tags.push(mk_riff(family, "BMPVersion", "BMP Version", Value::String(bmp_version.into())));
let planes = u16::from_le_bytes([cd[12], cd[13]]);
tags.push(mk_riff(family, "Planes", "Planes", Value::U16(planes)));
let bit_depth = u16::from_le_bytes([cd[14], cd[15]]);
tags.push(mk_riff(family, "BitDepth", "Bit Depth", Value::U16(bit_depth)));
let compression_raw = u32::from_le_bytes([cd[16], cd[17], cd[18], cd[19]]);
let compression_str = if compression_raw > 256 {
let bytes = [cd[16], cd[17], cd[18], cd[19]];
String::from_utf8_lossy(&bytes).to_uppercase()
} else {
match compression_raw {
0 => "None".into(),
1 => "8-Bit RLE".into(),
2 => "4-Bit RLE".into(),
3 => "Bitfields".into(),
4 => "JPEG".into(),
5 => "PNG".into(),
_ => format!("{}", compression_raw).into(),
}
};
tags.push(mk_riff(family, "Compression", "Compression", Value::String(compression_str.to_string())));
let image_length = u32::from_le_bytes([cd[20], cd[21], cd[22], cd[23]]);
tags.push(mk_riff(family, "ImageLength", "Image Length", Value::U32(image_length)));
let ppm_x = u32::from_le_bytes([cd[24], cd[25], cd[26], cd[27]]);
tags.push(mk_riff(family, "PixelsPerMeterX", "Pixels Per Meter X", Value::U32(ppm_x)));
let ppm_y = u32::from_le_bytes([cd[28], cd[29], cd[30], cd[31]]);
tags.push(mk_riff(family, "PixelsPerMeterY", "Pixels Per Meter Y", Value::U32(ppm_y)));
let num_colors = u32::from_le_bytes([cd[32], cd[33], cd[34], cd[35]]);
let num_colors_str = if num_colors == 0 {
"Use BitDepth".to_string()
} else {
format!("{}", num_colors)
};
tags.push(mk_riff(family, "NumColors", "Num Colors", Value::String(num_colors_str)));
let num_important = u32::from_le_bytes([cd[36], cd[37], cd[38], cd[39]]);
let num_important_str = if num_important == 0 {
"All".to_string()
} else {
format!("{}", num_important)
};
tags.push(mk_riff(family, "NumImportantColors", "Num Important Colors", Value::String(num_important_str)));
}
fn parse_bext(data: &[u8], start: usize, end: usize, chunk_size: usize, tags: &mut Vec<Tag>, family: &str) {
if chunk_size < 256 {
return;
}
let cd = &data[start..end];
let description = String::from_utf8_lossy(&cd[..256.min(cd.len())])
.trim_end_matches('\0')
.to_string();
if !description.is_empty() {
tags.push(mk_riff(family, "Description", "Description", Value::String(description)));
}
if cd.len() >= 288 {
let originator = String::from_utf8_lossy(&cd[256..288])
.trim_end_matches('\0')
.to_string();
if !originator.is_empty() {
tags.push(mk_riff(family, "Originator", "Originator", Value::String(originator)));
}
}
if cd.len() >= 320 {
let orig_ref = String::from_utf8_lossy(&cd[288..320])
.trim_end_matches('\0')
.to_string();
if !orig_ref.is_empty() {
tags.push(mk_riff(family, "OriginatorReference", "Originator Reference", Value::String(orig_ref)));
}
}
if cd.len() >= 338 {
let dt_str = String::from_utf8_lossy(&cd[320..338])
.trim_end_matches('\0')
.to_string();
if !dt_str.is_empty() {
let converted = dt_str.replace('-', ":");
let converted = if converted.len() >= 10 {
format!("{} {}", &converted[..10], &converted[10..].trim())
} else {
converted
};
tags.push(mk_riff(family, "DateTimeOriginal", "Date/Time Original", Value::String(converted.trim().to_string())));
}
}
}
fn read_pentax_avi_chunks(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>) -> Result<()> {
let mut pos = start;
while pos + 8 <= end {
let chunk_id = &data[pos..pos + 4];
let chunk_size = u32::from_le_bytes([
data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
]) as usize;
let data_start = pos + 8;
let data_end = (data_start + chunk_size).min(end);
if data_start > end {
break;
}
match chunk_id {
b"hymn" | b"mknt" => {
let mn_data = &data[data_start..data_end];
if mn_data.len() >= 12 && mn_data.starts_with(b"PENTAX \0") {
let bo = if mn_data[8] == b'M' && mn_data[9] == b'M' {
ByteOrderMark::BigEndian
} else {
ByteOrderMark::LittleEndian
};
let mn_tags = crate::metadata::makernotes::parse_makernotes(
mn_data, 0, mn_data.len(), "PENTAX", "", bo,
);
tags.extend(mn_tags);
}
}
_ => {}
}
pos = data_end + (chunk_size & 1);
}
Ok(())
}
fn read_exif_list_chunks(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>, family: &str) -> Result<()> {
let mut pos = start;
while pos + 8 <= end {
let chunk_id = std::str::from_utf8(&data[pos..pos + 4]).unwrap_or("????");
let chunk_size = u32::from_le_bytes([
data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
]) as usize;
pos += 8;
if pos + chunk_size > end {
break;
}
let raw_bytes = &data[pos..pos + chunk_size];
let value = String::from_utf8_lossy(raw_bytes)
.trim_end_matches('\0')
.to_string();
if !value.is_empty() {
match chunk_id {
"ever" => tags.push(mk_riff(family, "ExifVersion", "Exif Version", Value::String(value))),
"erel" => tags.push(mk_riff(family, "RelatedImageFile", "Related Image File", Value::String(value))),
"etim" => tags.push(mk_riff(family, "TimeCreated", "Time Created", Value::String(value))),
"ecor" => tags.push(mk_riff(family, "Make", "Make", Value::String(value))),
"emdl" => tags.push(mk_riff(family, "Model", "Camera Model Name", Value::String(value))),
"emnt" => tags.push(mk_riff(family, "MakerNotes", "Maker Notes", Value::Binary(raw_bytes.to_vec()))),
"eucm" => tags.push(mk_riff(family, "UserComment", "User Comment", Value::String(value))),
_ => {}
}
}
pos += chunk_size + (chunk_size & 1);
}
Ok(())
}
fn read_info_chunks(
data: &[u8],
start: usize,
end: usize,
tags: &mut Vec<Tag>,
family: &str,
) -> Result<()> {
let mut pos = start;
while pos + 8 <= end {
let chunk_id = std::str::from_utf8(&data[pos..pos + 4]).unwrap_or("????");
let chunk_size = u32::from_le_bytes([
data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
]) as usize;
pos += 8;
if pos + chunk_size > end {
break;
}
let value = String::from_utf8_lossy(&data[pos..pos + chunk_size])
.trim_end_matches('\0')
.to_string();
if !value.is_empty() {
let (name, description) = info_chunk_name(chunk_id);
let value = if chunk_id == "ICRD" {
value.replace('-', ":")
} else {
value
};
tags.push(mk_riff(family, name, description, Value::String(value)));
}
pos += chunk_size + (chunk_size & 1);
}
Ok(())
}
fn info_chunk_name(id: &str) -> (&str, &str) {
match id {
"IARL" => ("ArchivalLocation", "Archival Location"),
"IART" => ("Artist", "Artist"),
"ICMS" => ("Commissioned", "Commissioned"),
"ICMT" => ("Comment", "Comment"),
"ICOP" => ("Copyright", "Copyright"),
"ICRD" => ("DateCreated", "Date Created"),
"ICRP" => ("Cropped", "Cropped"),
"IDIM" => ("Dimensions", "Dimensions"),
"IDPI" => ("DotsPerInch", "Dots Per Inch"),
"IENG" => ("Engineer", "Engineer"),
"IGNR" => ("Genre", "Genre"),
"IKEY" => ("Keywords", "Keywords"),
"ILGT" => ("Lightness", "Lightness"),
"IMED" => ("Medium", "Medium"),
"INAM" => ("Title", "Title"),
"ITRK" => ("TrackNumber", "Track Number"),
"IPLT" => ("NumColors", "Num Colors"),
"IPRD" => ("Product", "Product"),
"ISBJ" => ("Subject", "Subject"),
"ISFT" => ("Software", "Software"),
"ISHP" => ("Sharpness", "Sharpness"),
"ISRC" => ("Source", "Source"),
"ISRF" => ("SourceForm", "Source Form"),
"ITCH" => ("Technician", "Technician"),
"ISGN" => ("SecondaryGenre", "Secondary Genre"),
"IWRI" => ("WrittenBy", "Written By"),
"IPRO" => ("ProducedBy", "Produced By"),
"ICNM" => ("Cinematographer", "Cinematographer"),
"IPDS" => ("ProductionDesigner", "Production Designer"),
"IEDT" => ("EditedBy", "Edited By"),
"ICDS" => ("CostumeDesigner", "Costume Designer"),
"IMUS" => ("MusicBy", "Music By"),
"ISTD" => ("ProductionStudio", "Production Studio"),
"IDST" => ("DistributedBy", "Distributed By"),
"ICNT" => ("Country", "Country"),
"ILNG" => ("Language", "Language"),
"IRTD" => ("Rating", "Rating"),
"ISTR" => ("Starring", "Starring"),
"TITL" => ("Title", "Title"),
"DIRC" => ("Directory", "Directory"),
"YEAR" => ("Year", "Year"),
"GENR" => ("Genre", "Genre"),
"COMM" => ("Comments", "Comments"),
"LANG" => ("Language", "Language"),
"AGES" => ("Rated", "Rated"),
"STAR" => ("Starring", "Starring"),
"CODE" => ("EncodedBy", "Encoded By"),
"PRT1" => ("Part", "Part"),
"PRT2" => ("NumberOfParts", "Number Of Parts"),
"IDIT" => ("DateTimeOriginal", "Date/Time Original"),
"ISMP" => ("TimeCode", "Time Code"),
"DISP" => ("SoundSchemeTitle", "Sound Scheme Title"),
"TLEN" => ("Length", "Length"),
"TRCK" => ("TrackNumber", "Track Number"),
"TURL" => ("URL", "URL"),
"TVER" => ("Version", "Version"),
"LOCA" => ("Location", "Location"),
"TORG" => ("Organization", "Organization"),
"TAPE" => ("TapeName", "Tape Name"),
"CMNT" => ("Comment", "Comment"),
"RATE" => ("Rate", "Rate"),
"IENC" => ("EncodedBy", "Encoded By"),
"IRIP" => ("RippedBy", "Ripped By"),
_ => (id, id),
}
}
fn audio_encoding_name(format_tag: u16) -> &'static str {
match format_tag {
0x0001 => "Microsoft PCM",
0x0002 => "Microsoft ADPCM",
0x0003 => "Microsoft IEEE float",
0x0004 => "Compaq VSELP",
0x0005 => "IBM CVSD",
0x0006 => "Microsoft a-Law",
0x0007 => "Microsoft u-Law",
0x0008 => "Microsoft DTS",
0x0009 => "DRM",
0x000a => "WMA 9 Speech",
0x000b => "Microsoft Windows Media RT Voice",
0x0010 => "OKI-ADPCM",
0x0011 => "Intel IMA/DVI-ADPCM",
0x0050 => "Microsoft MPEG",
0x0055 => "MP3",
0x00ff => "AAC",
0x0161 => "Windows Media Audio V2 V7 V8 V9 / DivX audio (WMA) / Alex AC3 Audio",
0x0162 => "Windows Media Audio Professional V9",
0x0163 => "Windows Media Audio Lossless V9",
0xfffe => "Extensible",
0xffff => "Development",
_ => "Unknown",
}
}
fn convert_riff_date(val: &str) -> String {
let months = [
"jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec",
];
let parts: Vec<&str> = val.split_whitespace().collect();
if parts.len() >= 5 {
let month_lower = parts[1].to_lowercase();
if let Some(mon_idx) = months.iter().position(|&m| m == month_lower) {
if let (Ok(day), Ok(year)) = (parts[2].parse::<u32>(), parts[4].parse::<u32>()) {
return format!("{:04}:{:02}:{:02} {}", year, mon_idx + 1, day, parts[3]);
}
}
}
if let Some(cap) = parse_casio_date(val) {
return cap;
}
let normalized = val.replace('/', ":").replace('-', ":");
if let Some(colon_pos) = normalized.find(' ') {
let date_part = &normalized[..colon_pos];
let time_part = normalized[colon_pos + 1..].trim();
if date_part.len() == 10 {
return format!("{} {}", date_part, time_part);
}
}
val.to_string()
}
fn parse_casio_date(val: &str) -> Option<String> {
let bytes = val.as_bytes();
let len = bytes.len();
if len < 4 || !bytes[0..4].iter().all(|b| b.is_ascii_digit()) {
return None;
}
let year: u32 = val[0..4].parse().ok()?;
let mut pos = 4;
if pos >= len || bytes[pos] != b'/' { return None; }
pos += 1;
while pos < len && bytes[pos] == b' ' { pos += 1; }
let month_start = pos;
while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
if pos == month_start { return None; }
let month: u32 = val[month_start..pos].parse().ok()?;
if pos >= len || bytes[pos] != b'/' { return None; }
pos += 1;
while pos < len && bytes[pos] == b' ' { pos += 1; }
let day_start = pos;
while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
if pos == day_start { return None; }
let day: u32 = val[day_start..pos].parse().ok()?;
if pos < len && bytes[pos] == b'/' { pos += 1; }
while pos < len && bytes[pos] == b' ' { pos += 1; }
if pos >= len { return None; }
let hh_start = pos;
while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
if pos == hh_start { return None; }
let hh: u32 = val[hh_start..pos].parse().ok()?;
if pos >= len || bytes[pos] != b':' { return None; }
pos += 1;
while pos < len && bytes[pos] == b' ' { pos += 1; }
let mm_start = pos;
while pos < len && bytes[pos].is_ascii_digit() { pos += 1; }
if pos == mm_start { return None; }
let mm: u32 = val[mm_start..pos].parse().ok()?;
while pos < len && bytes[pos] == b' ' { pos += 1; }
let pm = pos < len && (bytes[pos] == b'P' || bytes[pos] == b'p');
let hh_final = if pm { hh + 12 } else { hh };
Some(format!("{:04}:{:02}:{:02} {:02}:{:02}:00", year, month, day, hh_final, mm))
}
fn format_duration(seconds: f64) -> String {
if seconds < 60.0 {
let rounded = (seconds * 100.0 + 0.5).floor() / 100.0;
format!("{:.2} s", rounded)
} else {
let hours = (seconds / 3600.0).floor() as u64;
let remaining = seconds - hours as f64 * 3600.0;
let minutes = (remaining / 60.0).floor() as u64;
let secs = remaining - minutes as f64 * 60.0;
if hours > 0 {
format!("{}:{:02}:{:05.2}", hours, minutes, secs)
} else {
format!("{}:{:05.2}", minutes, secs)
}
}
}
fn format_sig4(val: f64, unit: &str) -> String {
if val == 0.0 {
return format!("0 {}", unit);
}
let magnitude = val.abs().log10().floor() as i32;
let decimals = if magnitude >= 3 { 0 } else { (3 - magnitude).max(0) as usize };
let s = format!("{:.prec$}", val, prec = decimals);
let s = if s.contains('.') {
s.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
s
};
format!("{} {}", s, unit)
}
fn mk_webp(name: &str, description: &str, value: Value) -> Tag {
let print_value = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: "RIFF".into(),
family1: "WebP".into(),
family2: "Image".into(),
},
raw_value: value,
print_value,
priority: 0,
}
}
fn mk_riff(family: &str, name: &str, description: &str, value: Value) -> Tag {
let print_value = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: "RIFF".into(),
family1: family.into(),
family2: if family == "AVI" { "Video".into() } else { "Audio".into() },
},
raw_value: value,
print_value,
priority: 0,
}
}