use crate::core::error::{XmpError, XmpResult};
use crate::core::metadata::XmpMeta;
use crate::files::formats::bmff::{
copy_bytes, is_bmff, read_box, read_box_data, skip_box, FTYP_BOX, UUID_BOX, XMP_UUID,
};
use crate::files::handler::{FileHandler, XmpOptions};
use std::io::{Read, Seek, SeekFrom, Write};
#[derive(Debug, Clone, Copy, Default)]
pub struct MpeghHandler;
const HEIF_BRANDS: &[[u8; 4]] = &[
*b"mif1", *b"msf1", *b"heic", *b"heix", *b"hevc", *b"heis", *b"avif", *b"avis",
];
const BOX_TYPE_XML: &[u8; 4] = b"xml ";
const BOX_TYPE_META: &[u8; 4] = b"meta";
impl FileHandler for MpeghHandler {
fn can_handle<R: Read + Seek>(&self, reader: &mut R) -> XmpResult<bool> {
let pos = reader.stream_position()?;
if !is_bmff(reader)? {
reader.seek(SeekFrom::Start(pos))?;
return Ok(false);
}
reader.seek(SeekFrom::Start(8))?;
let mut brand = [0u8; 4];
if reader.read_exact(&mut brand).is_err() {
reader.seek(SeekFrom::Start(pos))?;
return Ok(false);
}
reader.seek(SeekFrom::Start(pos))?;
Ok(HEIF_BRANDS.contains(&brand))
}
fn read_xmp<R: Read + Seek>(
&self,
reader: &mut R,
options: &XmpOptions,
) -> XmpResult<Option<XmpMeta>> {
Self::read_xmp(reader, options)
}
fn write_xmp<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
meta: &XmpMeta,
) -> XmpResult<()> {
Self::write_xmp(reader, writer, meta)
}
fn format_name(&self) -> &'static str {
"HEIF"
}
fn extensions(&self) -> &'static [&'static str] {
&["heic", "heif", "avif"]
}
}
impl MpeghHandler {
pub fn read_xmp<R: Read + Seek>(
mut reader: R,
options: &XmpOptions,
) -> XmpResult<Option<XmpMeta>> {
let ftyp = read_box(&mut reader)?;
if ftyp.box_type != *FTYP_BOX {
return Err(XmpError::BadValue("Not a valid HEIF file".into()));
}
skip_box(&mut reader, &ftyp)?;
loop {
let box_start = reader.stream_position()?;
let box_info = match read_box(&mut reader) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
};
if box_info.box_type == *BOX_TYPE_META {
let meta_body = read_box_data(&mut reader, &box_info)?;
let xmp_result = Self::extract_xmp_from_meta(&meta_body)?;
if options.only_xmp {
return Ok(xmp_result);
}
let xmp_result_is_none = xmp_result.is_none();
let mut meta = xmp_result.unwrap_or_else(XmpMeta::new);
let mut reconciled = false;
let meta_box_start = box_start;
let meta_box_size = box_info.size;
if let Some(native) = native_reconcile::read_native_metadata(
&meta_body,
&mut reader,
meta_box_start,
meta_box_size,
)? {
native_reconcile::reconcile_to_xmp(&mut meta, &native);
reconciled = true;
}
if xmp_result_is_none && !reconciled {
return Ok(None);
}
return Ok(Some(meta));
} else {
skip_box(&mut reader, &box_info)?;
}
}
Ok(None)
}
pub fn write_xmp<R: Read + Seek, W: Write + Seek>(
mut reader: R,
mut writer: W,
meta: &XmpMeta,
) -> XmpResult<()> {
let xmp_packet = meta.serialize_packet()?;
let xmp_bytes = xmp_packet.as_bytes();
let ftyp_box = read_box(&mut reader)?;
if ftyp_box.box_type != *FTYP_BOX {
return Err(XmpError::BadValue("Not a valid HEIF file".into()));
}
reader.seek(SeekFrom::Start(0))?;
copy_bytes(&mut reader, &mut writer, ftyp_box.size)?;
skip_box(&mut reader, &ftyp_box)?;
let mut meta_written = false;
loop {
let box_start = reader.stream_position()?;
let box_info = match read_box(&mut reader) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
};
if box_info.box_type == *BOX_TYPE_META {
let meta_body = read_box_data(&mut reader, &box_info)?;
let new_meta_body = Self::update_meta_with_xmp(&meta_body, xmp_bytes)?;
Self::write_box(&mut writer, BOX_TYPE_META, &new_meta_body)?;
meta_written = true;
} else {
reader.seek(SeekFrom::Start(box_start))?;
copy_bytes(&mut reader, &mut writer, box_info.size)?;
}
}
if !meta_written {
return Err(XmpError::BadValue(
"HEIF meta box not found; cannot write XMP".into(),
));
}
Ok(())
}
fn extract_xmp_from_meta(meta_body: &[u8]) -> XmpResult<Option<XmpMeta>> {
if meta_body.len() < 4 {
return Ok(None);
}
let mut cursor = 4usize; while cursor + 8 <= meta_body.len() {
let size = u32::from_be_bytes(meta_body[cursor..cursor + 4].try_into().unwrap()) as u64;
let box_type = &meta_body[cursor + 4..cursor + 8];
let (header, content_offset) = if size == 1 {
if cursor + 16 > meta_body.len() {
break;
}
let ext =
u64::from_be_bytes(meta_body[cursor + 8..cursor + 16].try_into().unwrap());
(16u64, ext.saturating_sub(16))
} else {
(8u64, size.saturating_sub(8))
};
let end = cursor + size as usize;
if end > meta_body.len() {
break;
}
let content_start = cursor + header as usize;
if box_type == *UUID_BOX && content_offset >= 16 {
let uuid = &meta_body[content_start..content_start + 16];
if uuid == XMP_UUID {
let payload = &meta_body[content_start + 16..end];
let payload_str = std::str::from_utf8(payload).map_err(|e| {
XmpError::BadValue(format!("Invalid UTF-8 in HEIF XMP payload: {}", e))
})?;
let xmp = XmpMeta::parse(payload_str)?;
return Ok(Some(xmp));
}
} else if box_type == BOX_TYPE_XML {
let payload = &meta_body[content_start..end];
let payload_str = std::str::from_utf8(payload).map_err(|e| {
XmpError::BadValue(format!("Invalid UTF-8 in HEIF XMP payload: {}", e))
})?;
let xmp = XmpMeta::parse(payload_str)?;
return Ok(Some(xmp));
}
let next = end;
if next <= cursor {
break;
}
cursor = next;
}
Ok(None)
}
fn update_meta_with_xmp(meta_body: &[u8], xmp_bytes: &[u8]) -> XmpResult<Vec<u8>> {
if meta_body.len() < 4 {
return Err(XmpError::BadValue(
"Invalid meta box (no version/flags)".into(),
));
}
let mut out = Vec::with_capacity(meta_body.len() + xmp_bytes.len() + 32);
out.extend_from_slice(&meta_body[..4]);
let mut cursor = 4usize;
let mut replaced = false;
while cursor + 8 <= meta_body.len() {
let size = u32::from_be_bytes(meta_body[cursor..cursor + 4].try_into().unwrap()) as u64;
let box_type = &meta_body[cursor + 4..cursor + 8];
let (header, content_offset) = if size == 1 {
if cursor + 16 > meta_body.len() {
break;
}
let ext =
u64::from_be_bytes(meta_body[cursor + 8..cursor + 16].try_into().unwrap());
(16u64, ext.saturating_sub(16))
} else {
(8u64, size.saturating_sub(8))
};
let end = cursor + size as usize;
if end > meta_body.len() {
break;
}
let content_start = cursor + header as usize;
if !replaced
&& ((box_type == *UUID_BOX
&& content_offset >= 16
&& &meta_body[content_start..content_start + 16] == XMP_UUID)
|| box_type == *BOX_TYPE_XML)
{
let new_payload = if box_type == *UUID_BOX {
let mut buf = Vec::with_capacity(16 + xmp_bytes.len());
buf.extend_from_slice(XMP_UUID);
buf.extend_from_slice(xmp_bytes);
buf
} else {
xmp_bytes.to_vec()
};
let box_tag: [u8; 4] = box_type.try_into().unwrap();
Self::write_box(&mut out, &box_tag, &new_payload)?;
replaced = true;
} else {
out.extend_from_slice(&meta_body[cursor..end]);
}
let next = end;
if next <= cursor {
break;
}
cursor = next;
}
if !replaced {
let mut payload = Vec::with_capacity(16 + xmp_bytes.len());
payload.extend_from_slice(XMP_UUID);
payload.extend_from_slice(xmp_bytes);
Self::write_box(&mut out, UUID_BOX, &payload)?;
}
Ok(out)
}
fn write_box<W: Write>(writer: &mut W, box_type: &[u8; 4], payload: &[u8]) -> XmpResult<()> {
let size = 8u64 + payload.len() as u64;
if size > u32::MAX as u64 {
return Err(XmpError::BadValue("Box too large for 32-bit size".into()));
}
writer.write_all(&(size as u32).to_be_bytes())?;
writer.write_all(box_type)?;
writer.write_all(payload)?;
Ok(())
}
}
mod native_reconcile {
use super::*;
#[derive(Debug, Clone)]
pub enum NativeMetadataItem {
Exif(ExifFields),
#[allow(dead_code)] Text {
box_type: [u8; 4],
value: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct ExifFields {
pub datetime_original: Option<String>,
pub make: Option<String>,
pub model: Option<String>,
pub artist: Option<String>,
pub copyright: Option<String>,
pub software: Option<String>,
}
pub fn read_native_metadata<R: Read + Seek>(
meta_body: &[u8],
reader: &mut R,
_meta_box_start: u64,
_meta_box_size: u64,
) -> XmpResult<Option<Vec<NativeMetadataItem>>> {
if meta_body.len() < 4 {
return Ok(None);
}
let mut items = Vec::new();
let mut cursor = 4usize;
while cursor + 8 <= meta_body.len() {
let size = u32::from_be_bytes(
meta_body[cursor..cursor + 4]
.try_into()
.map_err(|_| XmpError::BadValue("Invalid box size".into()))?,
) as u64;
if size < 8 {
break;
}
let box_type: [u8; 4] = meta_body[cursor + 4..cursor + 8]
.try_into()
.map_err(|_| XmpError::BadValue("Invalid box type".into()))?;
let (header_size, content_offset) = if size == 1 {
if cursor + 16 > meta_body.len() {
break;
}
let ext = u64::from_be_bytes(
meta_body[cursor + 8..cursor + 16]
.try_into()
.map_err(|_| XmpError::BadValue("Invalid extended size".into()))?,
);
(16u64, ext.saturating_sub(16))
} else {
(8u64, size.saturating_sub(8))
};
let end = cursor + size as usize;
if end > meta_body.len() {
break;
}
let content_start = cursor + header_size as usize;
let is_xmp = if box_type == *UUID_BOX && content_offset >= 16 {
content_start + 16 <= meta_body.len()
&& &meta_body[content_start..content_start + 16] == XMP_UUID
} else {
box_type == *BOX_TYPE_XML
};
if !is_xmp {
match &box_type {
b"iinf" => {
if let Some(exif_item_id) =
parse_iinf_for_exif(&meta_body[content_start..end])?
{
if let Some(exif_data) =
find_and_read_exif(reader, meta_body, exif_item_id)?
{
if let Some(exif_fields) = parse_exif_tiff(&exif_data)? {
items.push(NativeMetadataItem::Exif(exif_fields));
}
}
}
}
_ => {
if let Some(value) =
extract_text_from_box(&box_type, &meta_body[content_start..end])?
{
items.push(NativeMetadataItem::Text { box_type, value });
}
}
}
}
let next = end;
if next <= cursor {
break;
}
cursor = next;
}
if items.is_empty() {
Ok(None)
} else {
Ok(Some(items))
}
}
fn parse_iinf_for_exif(iinf_data: &[u8]) -> XmpResult<Option<u32>> {
if iinf_data.len() < 4 {
return Ok(None);
}
let mut cursor = 0usize;
let version = iinf_data[cursor];
cursor += 4;
let entry_count = if version == 0 {
if cursor + 2 > iinf_data.len() {
return Ok(None);
}
u16::from_be_bytes([iinf_data[cursor], iinf_data[cursor + 1]]) as u32
} else {
if cursor + 4 > iinf_data.len() {
return Ok(None);
}
u32::from_be_bytes([
iinf_data[cursor],
iinf_data[cursor + 1],
iinf_data[cursor + 2],
iinf_data[cursor + 3],
])
};
cursor += if version == 0 { 2 } else { 4 };
for _ in 0..entry_count {
if cursor + 4 > iinf_data.len() {
break;
}
let infe_size = u32::from_be_bytes([
iinf_data[cursor],
iinf_data[cursor + 1],
iinf_data[cursor + 2],
iinf_data[cursor + 3],
]) as usize;
if infe_size < 8 || cursor + infe_size > iinf_data.len() {
break;
}
let infe_type = &iinf_data[cursor + 4..cursor + 8];
if infe_type == b"infe" {
let infe_content_start = cursor + 8;
if infe_content_start + 4 <= iinf_data.len() {
let item_type_start = infe_content_start + 4; if item_type_start + 4 <= iinf_data.len() {
let item_type = &iinf_data[item_type_start..item_type_start + 4];
if item_type == b"Exif" {
if infe_content_start + 4 <= iinf_data.len() {
let item_id = u32::from_be_bytes([
iinf_data[infe_content_start],
iinf_data[infe_content_start + 1],
iinf_data[infe_content_start + 2],
iinf_data[infe_content_start + 3],
]);
return Ok(Some(item_id));
}
}
}
}
}
cursor += infe_size;
}
Ok(None)
}
fn find_and_read_exif<R: Read + Seek>(
reader: &mut R,
meta_body: &[u8],
exif_item_id: u32,
) -> XmpResult<Option<Vec<u8>>> {
let mut cursor = 4usize; let mut iloc_data: Option<&[u8]> = None;
while cursor + 8 <= meta_body.len() {
let size = u32::from_be_bytes(
meta_body[cursor..cursor + 4]
.try_into()
.map_err(|_| XmpError::BadValue("Invalid box size".into()))?,
) as usize;
if size < 8 || cursor + size > meta_body.len() {
break;
}
let box_type = &meta_body[cursor + 4..cursor + 8];
if box_type == b"iloc" {
let content_start = cursor + 8;
iloc_data = Some(&meta_body[content_start..cursor + size]);
break;
}
cursor += size;
}
let iloc_data = match iloc_data {
Some(d) => d,
None => return Ok(None),
};
let exif_location = parse_iloc_for_item(iloc_data, exif_item_id)?;
let exif_location = match exif_location {
Some(loc) => loc,
None => return Ok(None),
};
let saved_pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(0))?;
let ftyp = read_box(reader)?;
skip_box(reader, &ftyp)?;
while let Ok(box_info) = read_box(reader) {
if box_info.box_type == *b"mdat" {
let mdat_data_start = box_info.data_offset;
let exif_offset = mdat_data_start + exif_location.offset;
reader.seek(SeekFrom::Start(exif_offset))?;
let mut exif_data = vec![0u8; exif_location.length as usize];
reader.read_exact(&mut exif_data)?;
reader.seek(SeekFrom::Start(saved_pos))?;
return Ok(Some(exif_data));
} else {
skip_box(reader, &box_info)?;
}
}
reader.seek(SeekFrom::Start(saved_pos))?;
Ok(None)
}
struct ItemLocation {
offset: u64,
length: u64,
}
fn parse_iloc_for_item(iloc_data: &[u8], item_id: u32) -> XmpResult<Option<ItemLocation>> {
if iloc_data.len() < 8 {
return Ok(None);
}
let mut cursor = 0usize;
let version = iloc_data[cursor];
cursor += 4;
let size_flags = iloc_data[cursor];
cursor += 1;
let offset_size = ((size_flags >> 4) & 0x0F) as usize;
let length_size = (size_flags & 0x0F) as usize;
let base_offset_size = ((iloc_data[cursor] >> 4) & 0x0F) as usize;
cursor += 1;
let index_size = if version < 2 {
0
} else {
(iloc_data[cursor] & 0x0F) as usize
};
cursor += if version < 2 { 0 } else { 1 };
let item_count = if version < 2 {
if cursor + 2 > iloc_data.len() {
return Ok(None);
}
u16::from_be_bytes([iloc_data[cursor], iloc_data[cursor + 1]]) as u32
} else {
if cursor + 4 > iloc_data.len() {
return Ok(None);
}
u32::from_be_bytes([
iloc_data[cursor],
iloc_data[cursor + 1],
iloc_data[cursor + 2],
iloc_data[cursor + 3],
])
};
cursor += if version < 2 { 2 } else { 4 };
for _ in 0..item_count {
if cursor + 2 > iloc_data.len() {
break;
}
let current_item_id = if version < 2 {
u16::from_be_bytes([iloc_data[cursor], iloc_data[cursor + 1]]) as u32
} else {
if cursor + 4 > iloc_data.len() {
break;
}
u32::from_be_bytes([
iloc_data[cursor],
iloc_data[cursor + 1],
iloc_data[cursor + 2],
iloc_data[cursor + 3],
])
};
cursor += if version < 2 { 2 } else { 4 };
if version >= 1 {
cursor += 1; }
if cursor + 2 > iloc_data.len() {
break;
}
cursor += 2;
if cursor + base_offset_size > iloc_data.len() {
break;
}
let base_offset =
read_variable_size_int(&iloc_data[cursor..cursor + base_offset_size])?;
cursor += base_offset_size;
if cursor + 2 > iloc_data.len() {
break;
}
let extent_count =
u16::from_be_bytes([iloc_data[cursor], iloc_data[cursor + 1]]) as usize;
cursor += 2;
if current_item_id == item_id && extent_count > 0 {
if cursor + index_size + offset_size + length_size > iloc_data.len() {
break;
}
cursor += index_size; let extent_offset =
read_variable_size_int(&iloc_data[cursor..cursor + offset_size])?;
cursor += offset_size;
let extent_length =
read_variable_size_int(&iloc_data[cursor..cursor + length_size])?;
return Ok(Some(ItemLocation {
offset: base_offset + extent_offset,
length: extent_length,
}));
}
for _ in 0..extent_count {
if cursor + index_size + offset_size + length_size > iloc_data.len() {
break;
}
cursor += index_size + offset_size + length_size;
}
}
Ok(None)
}
fn read_variable_size_int(data: &[u8]) -> XmpResult<u64> {
match data.len() {
1 => Ok(data[0] as u64),
2 => Ok(u16::from_be_bytes([data[0], data[1]]) as u64),
4 => Ok(u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as u64),
8 => Ok(u64::from_be_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
])),
_ => Err(XmpError::BadValue("Invalid variable-size integer".into())),
}
}
fn parse_exif_tiff(exif_data: &[u8]) -> XmpResult<Option<ExifFields>> {
if exif_data.len() < 8 {
return Ok(None);
}
let is_le = exif_data[0] == 0x49
&& exif_data[1] == 0x49
&& exif_data[2] == 0x2A
&& exif_data[3] == 0x00;
let is_be = exif_data[0] == 0x4D
&& exif_data[1] == 0x4D
&& exif_data[2] == 0x00
&& exif_data[3] == 0x2A;
if !is_le && !is_be {
return Ok(None);
}
let mut fields = ExifFields::default();
let first_ifd_offset = if is_le {
u32::from_le_bytes([exif_data[4], exif_data[5], exif_data[6], exif_data[7]]) as usize
} else {
u32::from_be_bytes([exif_data[4], exif_data[5], exif_data[6], exif_data[7]]) as usize
};
if first_ifd_offset >= exif_data.len() {
return Ok(None);
}
parse_ifd_entries(
&exif_data[first_ifd_offset..],
exif_data,
is_le,
&mut fields,
)?;
if fields.datetime_original.is_some()
|| fields.make.is_some()
|| fields.model.is_some()
|| fields.artist.is_some()
|| fields.copyright.is_some()
|| fields.software.is_some()
{
Ok(Some(fields))
} else {
Ok(None)
}
}
fn parse_ifd_entries(
ifd_data: &[u8],
full_data: &[u8],
is_le: bool,
fields: &mut ExifFields,
) -> XmpResult<()> {
if ifd_data.len() < 2 {
return Ok(());
}
let entry_count = if is_le {
u16::from_le_bytes([ifd_data[0], ifd_data[1]]) as usize
} else {
u16::from_be_bytes([ifd_data[0], ifd_data[1]]) as usize
};
let mut cursor = 2;
for _ in 0..entry_count {
if cursor + 12 > ifd_data.len() {
break;
}
let tag = if is_le {
u16::from_le_bytes([ifd_data[cursor], ifd_data[cursor + 1]])
} else {
u16::from_be_bytes([ifd_data[cursor], ifd_data[cursor + 1]])
};
let type_ = if is_le {
u16::from_le_bytes([ifd_data[cursor + 2], ifd_data[cursor + 3]])
} else {
u16::from_be_bytes([ifd_data[cursor + 2], ifd_data[cursor + 3]])
};
let count = if is_le {
u32::from_le_bytes([
ifd_data[cursor + 4],
ifd_data[cursor + 5],
ifd_data[cursor + 6],
ifd_data[cursor + 7],
])
} else {
u32::from_be_bytes([
ifd_data[cursor + 4],
ifd_data[cursor + 5],
ifd_data[cursor + 6],
ifd_data[cursor + 7],
])
};
let value_or_offset = if is_le {
u32::from_le_bytes([
ifd_data[cursor + 8],
ifd_data[cursor + 9],
ifd_data[cursor + 10],
ifd_data[cursor + 11],
])
} else {
u32::from_be_bytes([
ifd_data[cursor + 8],
ifd_data[cursor + 9],
ifd_data[cursor + 10],
ifd_data[cursor + 11],
])
};
match tag {
0x0132 => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.datetime_original = Some(val);
}
}
0x9003 => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.datetime_original = Some(val);
}
}
0x010F => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.make = Some(val);
}
}
0x0110 => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.model = Some(val);
}
}
0x013B => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.artist = Some(val);
}
}
0x8298 => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.copyright = Some(val);
}
}
0x0131 => {
if let Some(val) =
read_exif_string(full_data, type_, count, value_or_offset, is_le)?
{
fields.software = Some(val);
}
}
_ => {}
}
cursor += 12;
}
Ok(())
}
fn read_exif_string(
full_data: &[u8],
type_: u16,
count: u32,
value_or_offset: u32,
is_le: bool,
) -> XmpResult<Option<String>> {
if type_ != 2 {
return Ok(None);
}
let data = if count <= 4 {
let bytes: [u8; 4] = if is_le {
value_or_offset.to_le_bytes()
} else {
value_or_offset.to_be_bytes()
};
bytes[..count as usize].to_vec()
} else {
let offset = value_or_offset as usize;
if offset + count as usize > full_data.len() {
return Ok(None);
}
full_data[offset..offset + count as usize].to_vec()
};
let null_pos = data.iter().position(|&b| b == 0).unwrap_or(data.len());
let text = String::from_utf8_lossy(&data[..null_pos])
.trim()
.to_string();
if text.is_empty() {
Ok(None)
} else {
Ok(Some(text))
}
}
fn extract_text_from_box(_box_type: &[u8; 4], content: &[u8]) -> XmpResult<Option<String>> {
if content.is_empty() {
return Ok(None);
}
if let Ok(text) = std::str::from_utf8(content) {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Ok(Some(trimmed.to_string()));
}
}
if content.len().is_multiple_of(2) && content.len() >= 2 {
let mut u16s = Vec::with_capacity(content.len() / 2);
for chunk in content.chunks(2) {
if chunk.len() == 2 {
u16s.push(u16::from_be_bytes([chunk[0], chunk[1]]));
}
}
let text = String::from_utf16_lossy(&u16s);
let trimmed = text.trim();
if !trimmed.is_empty()
&& trimmed.chars().all(|c| {
c.is_control()
|| c.is_alphanumeric()
|| c.is_whitespace()
|| c.is_ascii_punctuation()
})
{
return Ok(Some(trimmed.to_string()));
}
}
Ok(None)
}
pub fn reconcile_to_xmp(xmp: &mut XmpMeta, native: &Vec<NativeMetadataItem>) {
use crate::core::namespace::ns;
for item in native {
match item {
NativeMetadataItem::Exif(exif_fields) => {
if let Some(datetime) = &exif_fields.datetime_original {
if xmp.get_property(ns::XMP, "CreateDate").is_none() {
let _ =
xmp.set_property(ns::XMP, "CreateDate", datetime.clone().into());
}
}
if let Some(make) = &exif_fields.make {
if xmp.get_property(ns::TIFF, "Make").is_none() {
let _ = xmp.set_property(ns::TIFF, "Make", make.clone().into());
}
}
if let Some(model) = &exif_fields.model {
if xmp.get_property(ns::TIFF, "Model").is_none() {
let _ = xmp.set_property(ns::TIFF, "Model", model.clone().into());
}
}
if let Some(artist) = &exif_fields.artist {
if xmp.get_array_size(ns::DC, "creator").unwrap_or(0) == 0 {
let _ = xmp.append_array_item(ns::DC, "creator", artist.clone().into());
}
}
if let Some(copyright) = &exif_fields.copyright {
if xmp
.get_localized_text(ns::DC, "rights", "x-default", "x-default")
.is_none()
{
let _ = xmp.set_localized_text(
ns::DC,
"rights",
"x-default",
"x-default",
copyright,
);
}
}
if let Some(software) = &exif_fields.software {
if xmp.get_property(ns::XMP, "CreatorTool").is_none() {
let _ =
xmp.set_property(ns::XMP, "CreatorTool", software.clone().into());
}
}
}
NativeMetadataItem::Text { .. } => {
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::metadata::XmpMeta;
use crate::core::namespace::ns;
use crate::types::value::XmpValue;
use std::io::Cursor;
fn make_ftyp_heic() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&20u32.to_be_bytes()); buf.extend_from_slice(FTYP_BOX); buf.extend_from_slice(b"heic"); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(b"heic"); buf
}
fn create_minimal_heif() -> Vec<u8> {
let mut buf = make_ftyp_heic();
let meta_body_size = 4u32; let meta_box_size = 8 + meta_body_size; buf.extend_from_slice(&meta_box_size.to_be_bytes()); buf.extend_from_slice(BOX_TYPE_META); buf.extend_from_slice(&0u32.to_be_bytes());
buf
}
fn create_minimal_heif_with_xmp(xmp_data: &[u8]) -> Vec<u8> {
let mut buf = make_ftyp_heic();
let mut xmp_child = Vec::new();
xmp_child.extend_from_slice(XMP_UUID);
xmp_child.extend_from_slice(xmp_data);
let mut meta_body = Vec::new();
meta_body.extend_from_slice(&0u32.to_be_bytes());
let xmp_child_size = (8 + xmp_child.len()) as u32;
meta_body.extend_from_slice(&xmp_child_size.to_be_bytes()); meta_body.extend_from_slice(UUID_BOX); meta_body.extend_from_slice(&xmp_child);
let meta_box_size = (8 + meta_body.len()) as u32;
buf.extend_from_slice(&meta_box_size.to_be_bytes()); buf.extend_from_slice(BOX_TYPE_META); buf.extend_from_slice(&meta_body);
buf
}
#[test]
fn test_can_handle_heic() {
let data = make_ftyp_heic();
let mut cursor = Cursor::new(data);
let handler = MpeghHandler;
assert!(handler.can_handle(&mut cursor).unwrap());
}
#[test]
fn test_read_xmp_no_xmp() {
let heif_data = create_minimal_heif();
let reader = Cursor::new(heif_data);
let result = MpeghHandler::read_xmp(reader, &XmpOptions::default()).unwrap();
assert!(result.is_none());
}
#[test]
fn test_invalid_heif() {
let invalid_data = vec![0x00, 0x01, 0x02, 0x03];
let reader = Cursor::new(invalid_data);
let result = MpeghHandler::read_xmp(reader, &XmpOptions::default());
assert!(result.is_err());
}
#[test]
fn test_read_xmp_with_xmp() {
let mut meta = XmpMeta::new();
meta.set_property(ns::DC, "title", XmpValue::String("Test Image".to_string()))
.unwrap();
let xmp_packet = meta.serialize_packet().unwrap();
let xmp_bytes = xmp_packet.as_bytes();
let heif_data = create_minimal_heif_with_xmp(xmp_bytes);
let reader = Cursor::new(heif_data);
let result = MpeghHandler::read_xmp(reader, &XmpOptions::default()).unwrap();
assert!(result.is_some());
let read_meta = result.unwrap();
let title_value = read_meta.get_property(ns::DC, "title");
assert!(title_value.is_some());
if let Some(XmpValue::String(title)) = title_value {
assert_eq!(title, "Test Image");
} else {
panic!("Expected string value");
}
}
#[test]
fn test_write_xmp() {
let heif_data = create_minimal_heif();
let reader = Cursor::new(heif_data);
let mut writer = Cursor::new(Vec::new());
let mut meta = XmpMeta::new();
meta.set_property(ns::DC, "title", XmpValue::String("Test Image".to_string()))
.unwrap();
MpeghHandler::write_xmp(reader, &mut writer, &meta).unwrap();
writer.set_position(0);
let result = MpeghHandler::read_xmp(writer, &XmpOptions::default()).unwrap();
assert!(result.is_some());
let read_meta = result.unwrap();
let title_value = read_meta.get_property(ns::DC, "title");
assert!(title_value.is_some());
if let Some(XmpValue::String(title)) = title_value {
assert_eq!(title, "Test Image");
} else {
panic!("Expected string value");
}
}
#[test]
fn test_update_meta_replaces_uuid() {
let mut meta = Vec::new();
meta.extend_from_slice(&0u32.to_be_bytes());
let mut child = Vec::new();
child.extend_from_slice(XMP_UUID);
child.extend_from_slice(b"old");
let size = (8 + child.len()) as u32;
meta.extend_from_slice(&size.to_be_bytes());
meta.extend_from_slice(UUID_BOX);
meta.extend_from_slice(&child);
let updated = MpeghHandler::update_meta_with_xmp(&meta, b"new").unwrap();
assert!(updated.windows(b"new".len()).any(|w| w == b"new"));
assert!(updated.windows(XMP_UUID.len()).any(|w| w == XMP_UUID));
}
}