#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u16)]
pub enum Orientation {
#[default]
Normal = 1,
FlipHorizontal = 2,
Rotate180 = 3,
FlipVertical = 4,
Transpose = 5,
Rotate90 = 6,
Transverse = 7,
Rotate270 = 8,
}
#[derive(Debug, Clone)]
pub enum Exif {
Raw(Vec<u8>),
Fields(ExifFields),
}
impl Exif {
#[must_use]
pub fn raw(bytes: impl Into<Vec<u8>>) -> Self {
Exif::Raw(bytes.into())
}
#[must_use]
pub fn build() -> ExifFields {
ExifFields::default()
}
#[must_use]
pub fn to_bytes(&self) -> Option<Vec<u8>> {
match self {
Exif::Raw(bytes) => Some(bytes.clone()),
Exif::Fields(fields) => fields.to_bytes(),
}
}
}
impl From<ExifFields> for Exif {
fn from(fields: ExifFields) -> Self {
Exif::Fields(fields)
}
}
#[derive(Debug, Clone, Default)]
pub struct ExifFields {
orientation: Option<Orientation>,
copyright: Option<String>,
}
impl ExifFields {
#[must_use]
pub fn orientation(mut self, orientation: Orientation) -> Self {
self.orientation = Some(orientation);
self
}
#[must_use]
pub fn copyright(mut self, copyright: impl Into<String>) -> Self {
self.copyright = Some(copyright.into());
self
}
#[must_use]
pub fn to_bytes(&self) -> Option<Vec<u8>> {
if self.orientation.is_none() && self.copyright.is_none() {
return None;
}
Some(build_exif_tiff(self.orientation, self.copyright.as_deref()))
}
}
fn build_exif_tiff(orientation: Option<Orientation>, copyright: Option<&str>) -> Vec<u8> {
let mut entry_count: u16 = 0;
if orientation.is_some() {
entry_count += 1;
}
if copyright.is_some() {
entry_count += 1;
}
if entry_count == 0 {
return Vec::new();
}
let ifd_size = 2 + 12 * entry_count as usize + 4;
let header_and_ifd = 8 + ifd_size;
let copyright_bytes = copyright.map(|s| {
let mut bytes = s.as_bytes().to_vec();
bytes.push(0); bytes
});
let copyright_len = copyright_bytes.as_ref().map(|b| b.len()).unwrap_or(0);
let copyright_inline = copyright_len <= 4;
let total_size = if copyright_inline {
header_and_ifd
} else {
header_and_ifd + copyright_len
};
let mut exif = Vec::with_capacity(total_size);
exif.extend_from_slice(b"II");
exif.extend_from_slice(&42u16.to_le_bytes());
exif.extend_from_slice(&8u32.to_le_bytes());
exif.extend_from_slice(&entry_count.to_le_bytes());
let value_offset = header_and_ifd as u32;
if let Some(orient) = orientation {
write_ifd_entry(
&mut exif,
0x0112, 3, 1, orient as u32, );
}
if let Some(ref bytes) = copyright_bytes {
let count = bytes.len() as u32;
let value_or_offset = if copyright_inline {
let mut val = [0u8; 4];
val[..bytes.len()].copy_from_slice(bytes);
u32::from_le_bytes(val)
} else {
value_offset
};
write_ifd_entry(
&mut exif,
0x8298, 2, count,
value_or_offset,
);
}
exif.extend_from_slice(&0u32.to_le_bytes());
if !copyright_inline && let Some(bytes) = copyright_bytes {
exif.extend_from_slice(&bytes);
}
exif
}
fn write_ifd_entry(buf: &mut Vec<u8>, tag: u16, type_: u16, count: u32, value: u32) {
buf.extend_from_slice(&tag.to_le_bytes());
buf.extend_from_slice(&type_.to_le_bytes());
buf.extend_from_slice(&count.to_le_bytes());
buf.extend_from_slice(&value.to_le_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_orientation_only() {
let exif = Exif::build().orientation(Orientation::Rotate90);
let bytes = exif.to_bytes().expect("should produce bytes");
assert!(bytes.len() >= 8 + 2 + 12 + 4);
assert_eq!(&bytes[0..2], b"II"); assert_eq!(&bytes[2..4], &42u16.to_le_bytes());
assert_eq!(&bytes[8..10], &1u16.to_le_bytes());
assert_eq!(&bytes[10..12], &0x0112u16.to_le_bytes()); assert_eq!(&bytes[12..14], &3u16.to_le_bytes()); assert_eq!(&bytes[14..18], &1u32.to_le_bytes()); assert_eq!(&bytes[18..20], &6u16.to_le_bytes()); }
#[test]
fn test_copyright_short() {
let exif = Exif::build().copyright("AB");
let bytes = exif.to_bytes().expect("should produce bytes");
assert_eq!(bytes.len(), 8 + 2 + 12 + 4);
assert_eq!(&bytes[10..12], &0x8298u16.to_le_bytes()); assert_eq!(&bytes[12..14], &2u16.to_le_bytes()); assert_eq!(&bytes[14..18], &3u32.to_le_bytes()); }
#[test]
fn test_copyright_long() {
let long_copyright = "Copyright 2024 Example Corp";
let exif = Exif::build().copyright(long_copyright);
let bytes = exif.to_bytes().expect("should produce bytes");
let expected_len = 8 + 2 + 12 + 4 + long_copyright.len() + 1;
assert_eq!(bytes.len(), expected_len);
let string_start = 8 + 2 + 12 + 4;
assert_eq!(
&bytes[string_start..string_start + long_copyright.len()],
long_copyright.as_bytes()
);
}
#[test]
fn test_both_fields() {
let exif = Exif::build()
.orientation(Orientation::Rotate180)
.copyright("Test");
let bytes = exif.to_bytes().expect("should produce bytes");
assert_eq!(&bytes[8..10], &2u16.to_le_bytes());
assert_eq!(&bytes[10..12], &0x0112u16.to_le_bytes());
assert_eq!(&bytes[22..24], &0x8298u16.to_le_bytes());
}
#[test]
fn test_empty_fields() {
let exif = Exif::build();
assert!(exif.to_bytes().is_none(), "empty fields should return None");
}
#[test]
fn test_raw_bytes() {
let raw = vec![1u8, 2, 3, 4, 5];
let exif = Exif::raw(raw.clone());
let bytes = exif.to_bytes().expect("should produce bytes");
assert_eq!(bytes, raw);
}
#[test]
fn test_chaining_preserves_both() {
let exif = Exif::build()
.orientation(Orientation::Rotate90)
.copyright("Test");
let bytes = exif.to_bytes().expect("should produce bytes");
assert_eq!(&bytes[8..10], &2u16.to_le_bytes());
}
}