use crate::core::metadata::ImageMetadata;
use little_exif::exif_tag::ExifTag;
use little_exif::filetype::FileExtension;
use little_exif::ifd::ExifTagGroup;
use little_exif::metadata::Metadata;
use little_exif::rational::{iR64, uR64};
#[derive(Debug)]
pub enum ExifError {
Serialization(String),
Container(String),
}
impl std::fmt::Display for ExifError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExifError::Serialization(msg) => write!(f, "EXIF serialization error: {}", msg),
ExifError::Container(msg) => write!(f, "Container manipulation error: {}", msg),
}
}
}
impl std::error::Error for ExifError {}
#[cfg(feature = "container-embed")]
impl From<img_parts::Error> for ExifError {
fn from(e: img_parts::Error) -> Self {
ExifError::Container(e.to_string())
}
}
impl From<std::io::Error> for ExifError {
fn from(e: std::io::Error) -> Self {
ExifError::Container(e.to_string())
}
}
pub struct ExifBuilder<'a> {
metadata: &'a ImageMetadata,
}
impl<'a> ExifBuilder<'a> {
pub fn new(metadata: &'a ImageMetadata) -> Self {
Self { metadata }
}
pub fn build(&self) -> Metadata {
let mut exif = Metadata::new();
if !self.metadata.camera.make.is_empty() {
exif.set_tag(ExifTag::Make(self.metadata.camera.make.clone()));
}
if !self.metadata.camera.model.is_empty() {
exif.set_tag(ExifTag::Model(self.metadata.camera.model.clone()));
}
if let Some(ref lens_make) = self.metadata.camera.lens_make {
exif.set_tag(ExifTag::LensMake(lens_make.clone()));
}
if let Some(ref lens_model) = self.metadata.camera.lens_model {
exif.set_tag(ExifTag::LensModel(lens_model.clone()));
}
if let Some(ref serial) = self.metadata.camera.serial_number {
exif.set_tag(ExifTag::SerialNumber(serial.clone()));
}
if let Some(iso) = self.metadata.exif.iso {
exif.set_tag(ExifTag::ISO(vec![iso as u16]));
}
if let Some(r) = self.metadata.exif.exposure_time {
exif.set_tag(ExifTag::ExposureTime(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(r) = self.metadata.exif.f_number {
exif.set_tag(ExifTag::FNumber(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(r) = self.metadata.exif.focal_length {
exif.set_tag(ExifTag::FocalLength(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(fl_35mm) = self.metadata.exif.focal_length_35mm {
exif.set_tag(ExifTag::FocalLengthIn35mmFormat(vec![fl_35mm]));
}
if let Some(program) = self.metadata.exif.exposure_program {
exif.set_tag(ExifTag::ExposureProgram(vec![program]));
}
if let Some(metering) = self.metadata.exif.metering_mode {
exif.set_tag(ExifTag::MeteringMode(vec![metering]));
}
if let Some(flash) = self.metadata.exif.flash {
exif.set_tag(ExifTag::Flash(vec![flash]));
}
if let Some(r) = self.metadata.exif.exposure_compensation {
exif.set_tag(ExifTag::ExposureCompensation(vec![iR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(r) = self.metadata.exif.max_aperture {
exif.set_tag(ExifTag::MaxApertureValue(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(r) = self.metadata.exif.brightness_value {
exif.set_tag(ExifTag::BrightnessValue(vec![iR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(ref dt) = self.metadata.datetime.datetime_original {
exif.set_tag(ExifTag::DateTimeOriginal(dt.clone()));
}
if let Some(ref dt) = self.metadata.datetime.create_date {
exif.set_tag(ExifTag::CreateDate(dt.clone()));
}
if let Some(ref dt) = self.metadata.datetime.modify_date {
exif.set_tag(ExifTag::ModifyDate(dt.clone()));
}
if let Some(ref offset) = self.metadata.datetime.offset_time {
exif.set_tag(ExifTag::OffsetTime(offset.clone()));
}
if let Some(ref subsec) = self.metadata.datetime.subsec_time {
exif.set_tag(ExifTag::SubSecTime(subsec.clone()));
}
self.build_gps(&mut exif);
if let Some(orient) = self.metadata.image.orientation {
exif.set_tag(ExifTag::Orientation(vec![orient]));
}
exif
}
fn build_gps(&self, exif: &mut Metadata) {
let gps = &self.metadata.gps;
if let Some(lat) = gps.latitude {
let lat_vec: Vec<uR64> = lat
.iter()
.map(|r| uR64 {
nominator: r.numerator,
denominator: r.denominator,
})
.collect();
exif.set_tag(ExifTag::GPSLatitude(lat_vec));
}
if let Some(lat_ref) = gps.latitude_ref {
exif.set_tag(ExifTag::GPSLatitudeRef(lat_ref.to_string()));
}
if let Some(lon) = gps.longitude {
let lon_vec: Vec<uR64> = lon
.iter()
.map(|r| uR64 {
nominator: r.numerator,
denominator: r.denominator,
})
.collect();
exif.set_tag(ExifTag::GPSLongitude(lon_vec));
}
if let Some(lon_ref) = gps.longitude_ref {
exif.set_tag(ExifTag::GPSLongitudeRef(lon_ref.to_string()));
}
if let Some(r) = gps.altitude {
exif.set_tag(ExifTag::GPSAltitude(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(alt_ref) = gps.altitude_ref {
exif.set_tag(ExifTag::GPSAltitudeRef(vec![alt_ref]));
}
if let Some(timestamp) = gps.timestamp {
let ts_vec: Vec<uR64> = timestamp
.iter()
.map(|r| uR64 {
nominator: r.numerator,
denominator: r.denominator,
})
.collect();
exif.set_tag(ExifTag::GPSTimeStamp(ts_vec));
}
if let Some(ref datestamp) = gps.datestamp {
exif.set_tag(ExifTag::GPSDateStamp(datestamp.clone()));
}
if let Some(r) = gps.speed {
exif.set_tag(ExifTag::GPSSpeed(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
if let Some(r) = gps.img_direction {
exif.set_tag(ExifTag::GPSImgDirection(vec![uR64 {
nominator: r.numerator,
denominator: r.denominator,
}]));
}
}
pub fn build_bytes(&self) -> Result<Vec<u8>, ExifError> {
let exif = self.build();
let jpeg_app1 = exif
.as_u8_vec(FileExtension::JPEG)
.map_err(|e| ExifError::Serialization(e.to_string()))?;
const APP1_WRAPPER_LEN: usize = 2 + 2 + 6; if jpeg_app1.len() <= APP1_WRAPPER_LEN {
return Err(ExifError::Serialization(
"EXIF data too short after APP1 header".into(),
));
}
Ok(jpeg_app1[APP1_WRAPPER_LEN..].to_vec())
}
#[cfg(feature = "container-embed")]
pub fn append_to_jpeg(&self, jpeg_data: Vec<u8>) -> Result<Vec<u8>, ExifError> {
use img_parts::jpeg::Jpeg;
use img_parts::{Bytes, ImageEXIF};
use std::io::Cursor;
let tiff_bytes = self.build_bytes()?;
let mut jpeg = Jpeg::from_bytes(Bytes::from(jpeg_data))?;
jpeg.set_exif(Some(Bytes::from(tiff_bytes)));
let mut output = Cursor::new(Vec::new());
jpeg.encoder().write_to(&mut output)?;
Ok(output.into_inner())
}
#[cfg_attr(not(feature = "avif"), allow(dead_code))]
pub fn append_to_avif(&self, mut avif_data: Vec<u8>) -> Result<Vec<u8>, ExifError> {
let exif = self.build();
exif.write_to_vec(&mut avif_data, FileExtension::HEIF)
.map_err(|e| ExifError::Container(format!("AVIF EXIF embedding failed: {e}")))?;
Ok(avif_data)
}
#[cfg_attr(not(feature = "jxl-encode"), allow(dead_code))]
pub fn append_to_jxl(&self, mut jxl_data: Vec<u8>) -> Result<Vec<u8>, ExifError> {
let exif = self.build();
exif.write_to_vec(&mut jxl_data, FileExtension::JXL)
.map_err(|e| ExifError::Container(format!("JXL EXIF embedding failed: {e}")))?;
Ok(jxl_data)
}
}
pub struct ExifParser;
impl ExifParser {
pub fn parse_from_bytes(file_data: &[u8], file_type: FileExtension) -> ImageMetadata {
let exif = match Metadata::new_from_vec(&file_data.to_vec(), file_type) {
Ok(m) => m,
Err(_) => return ImageMetadata::default(),
};
Self::parse_metadata(&exif)
}
pub fn parse_metadata(exif: &Metadata) -> ImageMetadata {
use crate::core::metadata::*;
let mut md = ImageMetadata::default();
if let Some(ExifTag::Make(s)) = exif.get_tag_by_hex(0x010f, None).next() {
md.camera.make = s.trim_end_matches('\0').to_string();
}
if let Some(ExifTag::Model(s)) = exif.get_tag_by_hex(0x0110, None).next() {
md.camera.model = s.trim_end_matches('\0').to_string();
}
if let Some(ExifTag::LensMake(s)) = exif.get_tag_by_hex(0xa433, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.camera.lens_make = Some(v);
}
}
if let Some(ExifTag::LensModel(s)) = exif.get_tag_by_hex(0xa434, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.camera.lens_model = Some(v);
}
}
if let Some(ExifTag::SerialNumber(s)) = exif.get_tag_by_hex(0xa431, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.camera.serial_number = Some(v);
}
}
if let Some(ExifTag::ISO(v)) = exif.get_tag_by_hex(0x8827, None).next()
&& let Some(&iso) = v.first()
{
md.exif.iso = Some(iso as u32);
}
if let Some(ExifTag::ExposureTime(v)) = exif.get_tag_by_hex(0x829a, None).next()
&& let Some(r) = v.first()
{
md.exif.exposure_time = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::FNumber(v)) = exif.get_tag_by_hex(0x829d, None).next()
&& let Some(r) = v.first()
{
md.exif.f_number = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::FocalLength(v)) = exif.get_tag_by_hex(0x920a, None).next()
&& let Some(r) = v.first()
{
md.exif.focal_length = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::FocalLengthIn35mmFormat(v)) = exif.get_tag_by_hex(0xa405, None).next()
&& let Some(&fl) = v.first()
{
md.exif.focal_length_35mm = Some(fl);
}
if let Some(ExifTag::ExposureProgram(v)) = exif.get_tag_by_hex(0x8822, None).next()
&& let Some(&ep) = v.first()
{
md.exif.exposure_program = Some(ep);
}
if let Some(ExifTag::MeteringMode(v)) = exif.get_tag_by_hex(0x9207, None).next()
&& let Some(&mm) = v.first()
{
md.exif.metering_mode = Some(mm);
}
if let Some(ExifTag::Flash(v)) = exif.get_tag_by_hex(0x9209, None).next()
&& let Some(&fl) = v.first()
{
md.exif.flash = Some(fl);
}
if let Some(ExifTag::ExposureCompensation(v)) = exif.get_tag_by_hex(0x9204, None).next()
&& let Some(r) = v.first()
{
md.exif.exposure_compensation = Some(SRational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::MaxApertureValue(v)) = exif.get_tag_by_hex(0x9205, None).next()
&& let Some(r) = v.first()
{
md.exif.max_aperture = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::BrightnessValue(v)) = exif.get_tag_by_hex(0x9203, None).next()
&& let Some(r) = v.first()
{
md.exif.brightness_value = Some(SRational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::DateTimeOriginal(s)) = exif.get_tag_by_hex(0x9003, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.datetime.datetime_original = Some(v);
}
}
if let Some(ExifTag::CreateDate(s)) = exif.get_tag_by_hex(0x9004, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.datetime.create_date = Some(v);
}
}
if let Some(ExifTag::ModifyDate(s)) = exif.get_tag_by_hex(0x0132, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.datetime.modify_date = Some(v);
}
}
if let Some(ExifTag::OffsetTime(s)) = exif.get_tag_by_hex(0x9010, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.datetime.offset_time = Some(v);
}
}
if let Some(ExifTag::SubSecTime(s)) = exif.get_tag_by_hex(0x9290, None).next() {
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.datetime.subsec_time = Some(v);
}
}
if let Some(ExifTag::GPSLatitude(v)) =
exif.get_tag_by_hex(0x0002, Some(ExifTagGroup::GPS)).next()
&& v.len() >= 3
{
md.gps.latitude = Some([
URational::new(v[0].nominator, v[0].denominator),
URational::new(v[1].nominator, v[1].denominator),
URational::new(v[2].nominator, v[2].denominator),
]);
}
if let Some(ExifTag::GPSLatitudeRef(s)) =
exif.get_tag_by_hex(0x0001, Some(ExifTagGroup::GPS)).next()
{
md.gps.latitude_ref = s.chars().next().filter(|c| !c.is_ascii_control());
}
if let Some(ExifTag::GPSLongitude(v)) =
exif.get_tag_by_hex(0x0004, Some(ExifTagGroup::GPS)).next()
&& v.len() >= 3
{
md.gps.longitude = Some([
URational::new(v[0].nominator, v[0].denominator),
URational::new(v[1].nominator, v[1].denominator),
URational::new(v[2].nominator, v[2].denominator),
]);
}
if let Some(ExifTag::GPSLongitudeRef(s)) =
exif.get_tag_by_hex(0x0003, Some(ExifTagGroup::GPS)).next()
{
md.gps.longitude_ref = s.chars().next().filter(|c| !c.is_ascii_control());
}
if let Some(ExifTag::GPSAltitude(v)) =
exif.get_tag_by_hex(0x0006, Some(ExifTagGroup::GPS)).next()
&& let Some(r) = v.first()
{
md.gps.altitude = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::GPSAltitudeRef(v)) =
exif.get_tag_by_hex(0x0005, Some(ExifTagGroup::GPS)).next()
&& let Some(&ar) = v.first()
{
md.gps.altitude_ref = Some(ar);
}
if let Some(ExifTag::GPSTimeStamp(v)) =
exif.get_tag_by_hex(0x0007, Some(ExifTagGroup::GPS)).next()
&& v.len() >= 3
{
md.gps.timestamp = Some([
URational::new(v[0].nominator, v[0].denominator),
URational::new(v[1].nominator, v[1].denominator),
URational::new(v[2].nominator, v[2].denominator),
]);
}
if let Some(ExifTag::GPSDateStamp(s)) =
exif.get_tag_by_hex(0x001d, Some(ExifTagGroup::GPS)).next()
{
let v = s.trim_end_matches('\0').to_string();
if !v.is_empty() {
md.gps.datestamp = Some(v);
}
}
if let Some(ExifTag::GPSSpeed(v)) =
exif.get_tag_by_hex(0x000d, Some(ExifTagGroup::GPS)).next()
&& let Some(r) = v.first()
{
md.gps.speed = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::GPSImgDirection(v)) =
exif.get_tag_by_hex(0x0011, Some(ExifTagGroup::GPS)).next()
&& let Some(r) = v.first()
{
md.gps.img_direction = Some(URational::new(r.nominator, r.denominator));
}
if let Some(ExifTag::Orientation(v)) = exif.get_tag_by_hex(0x0112, None).next()
&& let Some(&o) = v.first()
{
md.image.orientation = Some(o);
}
Self::populate_extra(exif, &mut md);
md
}
fn populate_extra(exif: &Metadata, md: &mut crate::core::metadata::ImageMetadata) {
use crate::core::metadata::{MetadataKey, MetadataNamespace};
for tag in exif {
let namespace = match tag.get_group() {
ExifTagGroup::GPS => MetadataNamespace::Gps,
_ => MetadataNamespace::Exif,
};
let key = MetadataKey::new(namespace, format!("0x{:04x}", tag.as_u16()));
md.insert(key, exif_tag_value(tag));
}
}
}
fn exif_tag_value(tag: &ExifTag) -> crate::core::metadata::MetadataValue {
use crate::core::metadata::{MetadataValue, SRational, URational};
use little_exif::endian::Endian;
use little_exif::exif_tag_format::ExifTagFormat as Fmt;
let raw = tag.value_as_u8_vec(&Endian::Little);
fn collapse(mut vals: Vec<MetadataValue>) -> MetadataValue {
if vals.len() == 1 {
vals.pop().unwrap()
} else {
MetadataValue::Array(vals)
}
}
match tag.format() {
Fmt::STRING => MetadataValue::Text(
String::from_utf8_lossy(&raw)
.trim_end_matches('\0')
.to_string(),
),
Fmt::UNDEF => MetadataValue::Bytes(raw),
Fmt::INT8U => collapse(raw.iter().map(|&b| MetadataValue::U64(b as u64)).collect()),
Fmt::INT8S => collapse(
raw.iter()
.map(|&b| MetadataValue::I64(b as i8 as i64))
.collect(),
),
Fmt::INT16U => collapse(
raw.chunks_exact(2)
.map(|c| MetadataValue::U64(u16::from_le_bytes([c[0], c[1]]) as u64))
.collect(),
),
Fmt::INT16S => collapse(
raw.chunks_exact(2)
.map(|c| MetadataValue::I64(i16::from_le_bytes([c[0], c[1]]) as i64))
.collect(),
),
Fmt::INT32U => collapse(
raw.chunks_exact(4)
.map(|c| MetadataValue::U64(u32::from_le_bytes([c[0], c[1], c[2], c[3]]) as u64))
.collect(),
),
Fmt::INT32S => collapse(
raw.chunks_exact(4)
.map(|c| MetadataValue::I64(i32::from_le_bytes([c[0], c[1], c[2], c[3]]) as i64))
.collect(),
),
Fmt::FLOAT => collapse(
raw.chunks_exact(4)
.map(|c| MetadataValue::F64(f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64))
.collect(),
),
Fmt::DOUBLE => collapse(
raw.chunks_exact(8)
.map(|c| {
MetadataValue::F64(f64::from_le_bytes([
c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7],
]))
})
.collect(),
),
Fmt::RATIONAL64U => collapse(
raw.chunks_exact(8)
.map(|c| {
let num = u32::from_le_bytes([c[0], c[1], c[2], c[3]]);
let den = u32::from_le_bytes([c[4], c[5], c[6], c[7]]);
MetadataValue::URational(URational::new(num, den))
})
.collect(),
),
Fmt::RATIONAL64S => collapse(
raw.chunks_exact(8)
.map(|c| {
let num = i32::from_le_bytes([c[0], c[1], c[2], c[3]]);
let den = i32::from_le_bytes([c[4], c[5], c[6], c[7]]);
MetadataValue::SRational(SRational::new(num, den))
})
.collect(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::metadata::*;
fn sample_metadata() -> ImageMetadata {
ImageMetadata {
camera: CameraInfo {
make: "SONY".into(),
model: "ILCE-6700".into(),
lens_model: Some("E 18-135mm F3.5-5.6 OSS".into()),
..Default::default()
},
exif: ExifInfo {
iso: Some(800),
exposure_time: Some(URational::new(1, 250)),
f_number: Some(URational::new(56, 10)),
focal_length: Some(URational::new(35, 1)),
focal_length_35mm: Some(52),
exposure_program: Some(3), metering_mode: Some(5), ..Default::default()
},
datetime: DateTimeInfo {
datetime_original: Some("2025:12:01 14:30:00".into()),
..Default::default()
},
gps: GpsInfo {
latitude: Some([
URational::new(40, 1),
URational::new(44, 1),
URational::new(0, 1),
]),
latitude_ref: Some('N'),
longitude: Some([
URational::new(73, 1),
URational::new(59, 1),
URational::new(0, 1),
]),
longitude_ref: Some('W'),
..Default::default()
},
..Default::default()
}
}
#[test]
fn test_exif_builder_creates_metadata() {
let md = sample_metadata();
let builder = ExifBuilder::new(&md);
let exif = builder.build();
let _ = exif;
}
#[test]
fn test_exif_builder_produces_bytes() {
let md = sample_metadata();
let builder = ExifBuilder::new(&md);
let bytes = builder.build_bytes().expect("Should build EXIF bytes");
assert!(!bytes.is_empty(), "EXIF bytes should not be empty");
}
#[test]
fn test_empty_metadata_no_panic() {
let md = ImageMetadata::default();
let builder = ExifBuilder::new(&md);
let exif = builder.build();
let _ = exif;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let md = ImageMetadata::default();
let builder = ExifBuilder::new(&md);
builder.build_bytes()
}));
let _ = result;
}
#[test]
fn test_exif_builder_with_partial_data() {
let md = ImageMetadata {
camera: CameraInfo {
make: "Apple".into(),
model: "iPhone 17 Pro Max".into(),
..Default::default()
},
exif: ExifInfo {
iso: Some(100),
..Default::default()
},
..Default::default()
};
let builder = ExifBuilder::new(&md);
let bytes = builder.build_bytes().expect("Should build EXIF bytes");
assert!(!bytes.is_empty());
}
}