use alloc::string::String;
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SegmentType {
Jfif,
Exif,
Xmp,
XmpExtended,
Icc,
Mpf,
Iptc,
Adobe,
Comment,
Unknown,
}
pub use ultrahdr_core::MpImageType as MpfImageType;
pub trait MpfImageTypeExt {
fn to_type_code(self) -> u32;
fn is_gainmap(&self) -> bool;
fn is_thumbnail(&self) -> bool;
fn is_depth(&self) -> bool;
fn is_multiframe(&self) -> bool;
}
impl MpfImageTypeExt for MpfImageType {
fn to_type_code(self) -> u32 {
self.type_code()
}
fn is_gainmap(&self) -> bool {
matches!(self, Self::Undefined)
}
fn is_thumbnail(&self) -> bool {
matches!(self, Self::LargeThumbnailVga | Self::LargeThumbnailFullHd)
}
fn is_depth(&self) -> bool {
matches!(self, Self::Disparity)
}
fn is_multiframe(&self) -> bool {
matches!(self, Self::Panorama | Self::MultiAngle)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum DensityUnits {
#[default]
None = 0,
PixelsPerInch = 1,
PixelsPerCm = 2,
}
#[derive(Clone, Debug, Default)]
pub struct JfifInfo {
pub version_major: u8,
pub version_minor: u8,
pub density_units: DensityUnits,
pub x_density: u16,
pub y_density: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum AdobeColorTransform {
#[default]
Unknown = 0,
YCbCr = 1,
Ycck = 2,
}
#[derive(Clone, Debug, Default)]
pub struct AdobeInfo {
pub version: u16,
pub color_transform: AdobeColorTransform,
}
const MAX_STANDARD_XMP_BYTES: usize = 65502;
const MAX_ICC_CHUNK_BYTES: usize = 65519;
const XMP_NAMESPACE: &[u8] = b"http://ns.adobe.com/xap/1.0/\0";
const XMP_EXTENDED_NAMESPACE: &[u8] = b"http://ns.adobe.com/xmp/extension/\0";
const ICC_SIGNATURE: &[u8] = b"ICC_PROFILE\0";
const EXIF_SIGNATURE: &[u8] = b"Exif\0\0";
const JFIF_SIGNATURE: &[u8] = b"JFIF\0";
const IPTC_SIGNATURE: &[u8] = b"Photoshop 3.0\0";
const ADOBE_SIGNATURE: &[u8] = b"Adobe";
const MPF_SIGNATURE: &[u8] = b"MPF\0";
#[derive(Clone, Debug)]
pub struct EncoderSegment {
pub marker: u8,
pub data: Vec<u8>,
pub segment_type: SegmentType,
}
#[derive(Clone, Debug)]
pub struct MpfImage {
pub image_type: MpfImageType,
pub data: Vec<u8>,
}
#[derive(Clone, Debug, Default)]
pub struct EncoderSegments {
segments: Vec<EncoderSegment>,
mpf_images: Vec<MpfImage>,
}
impl EncoderSegments {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get(&self, typ: SegmentType) -> Option<&[u8]> {
self.segments
.iter()
.find(|s| s.segment_type == typ)
.map(|s| s.data.as_slice())
}
#[must_use]
pub fn get_all(&self, typ: SegmentType) -> Vec<&[u8]> {
self.segments
.iter()
.filter(|s| s.segment_type == typ)
.map(|s| s.data.as_slice())
.collect()
}
#[must_use]
pub fn has(&self, typ: SegmentType) -> bool {
self.segments.iter().any(|s| s.segment_type == typ)
}
#[must_use]
pub fn segments(&self) -> &[EncoderSegment] {
&self.segments
}
pub fn segments_of_type(&self, typ: SegmentType) -> impl Iterator<Item = &EncoderSegment> {
self.segments.iter().filter(move |s| s.segment_type == typ)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments.is_empty() && self.mpf_images.is_empty()
}
#[must_use]
pub fn add(mut self, marker: u8, data: Vec<u8>, typ: SegmentType) -> Self {
self.segments.push(EncoderSegment {
marker,
data,
segment_type: typ,
});
self
}
pub fn add_mut(&mut self, marker: u8, data: Vec<u8>, typ: SegmentType) -> &mut Self {
self.segments.push(EncoderSegment {
marker,
data,
segment_type: typ,
});
self
}
#[must_use]
pub fn add_raw(mut self, marker: u8, data: Vec<u8>) -> Self {
let typ = detect_segment_type(marker, &data);
self.segments.push(EncoderSegment {
marker,
data,
segment_type: typ,
});
self
}
pub fn add_raw_mut(&mut self, marker: u8, data: Vec<u8>) -> &mut Self {
let typ = detect_segment_type(marker, &data);
self.segments.push(EncoderSegment {
marker,
data,
segment_type: typ,
});
self
}
#[must_use]
pub fn remove(mut self, typ: SegmentType) -> Self {
self.segments.retain(|s| s.segment_type != typ);
self
}
pub fn remove_mut(&mut self, typ: SegmentType) -> &mut Self {
self.segments.retain(|s| s.segment_type != typ);
self
}
#[must_use]
pub fn remove_where<F: Fn(&EncoderSegment) -> bool>(mut self, f: F) -> Self {
self.segments.retain(|s| !f(s));
self
}
#[must_use]
pub fn replace(self, marker: u8, data: Vec<u8>, typ: SegmentType) -> Self {
self.remove(typ).add(marker, data, typ)
}
#[must_use]
pub fn set_exif(self, data: Vec<u8>) -> Self {
let mut full_data = Vec::with_capacity(EXIF_SIGNATURE.len() + data.len());
full_data.extend_from_slice(EXIF_SIGNATURE);
full_data.extend_from_slice(&data);
self.replace(0xE1, full_data, SegmentType::Exif)
}
pub fn set_exif_mut(&mut self, data: Vec<u8>) -> &mut Self {
self.remove_mut(SegmentType::Exif);
let mut full_data = Vec::with_capacity(EXIF_SIGNATURE.len() + data.len());
full_data.extend_from_slice(EXIF_SIGNATURE);
full_data.extend_from_slice(&data);
self.add_mut(0xE1, full_data, SegmentType::Exif)
}
#[must_use]
pub fn set_xmp(mut self, xmp: &str) -> Self {
self.segments.retain(|s| {
s.segment_type != SegmentType::Xmp && s.segment_type != SegmentType::XmpExtended
});
let xmp_bytes = xmp.as_bytes();
if xmp_bytes.len() <= MAX_STANDARD_XMP_BYTES {
let mut data = Vec::with_capacity(XMP_NAMESPACE.len() + xmp_bytes.len());
data.extend_from_slice(XMP_NAMESPACE);
data.extend_from_slice(xmp_bytes);
self.segments.push(EncoderSegment {
marker: 0xE1,
data,
segment_type: SegmentType::Xmp,
});
} else {
let truncated = &xmp_bytes[..MAX_STANDARD_XMP_BYTES];
let mut data = Vec::with_capacity(XMP_NAMESPACE.len() + truncated.len());
data.extend_from_slice(XMP_NAMESPACE);
data.extend_from_slice(truncated);
self.segments.push(EncoderSegment {
marker: 0xE1,
data,
segment_type: SegmentType::Xmp,
});
}
self
}
#[must_use]
pub fn modify_xmp<F: FnOnce(&str) -> String>(mut self, f: F) -> Self {
if let Some(idx) = self
.segments
.iter()
.position(|s| s.segment_type == SegmentType::Xmp)
{
let seg = &self.segments[idx];
if seg.data.len() > XMP_NAMESPACE.len() {
let xmp_start = XMP_NAMESPACE.len();
if let Ok(xmp_str) = core::str::from_utf8(&seg.data[xmp_start..]) {
let new_xmp = f(xmp_str);
self.segments.remove(idx);
return self.set_xmp(&new_xmp);
}
}
}
self
}
#[must_use]
pub fn set_icc(mut self, profile: Vec<u8>) -> Self {
self.segments.retain(|s| s.segment_type != SegmentType::Icc);
if profile.is_empty() {
return self;
}
let num_chunks = (profile.len() + MAX_ICC_CHUNK_BYTES - 1) / MAX_ICC_CHUNK_BYTES;
let mut offset = 0;
for chunk_num in 0..num_chunks {
let chunk_size = (profile.len() - offset).min(MAX_ICC_CHUNK_BYTES);
let mut data = Vec::with_capacity(ICC_SIGNATURE.len() + 2 + chunk_size);
data.extend_from_slice(ICC_SIGNATURE);
data.push((chunk_num + 1) as u8);
data.push(num_chunks as u8);
data.extend_from_slice(&profile[offset..offset + chunk_size]);
self.segments.push(EncoderSegment {
marker: 0xE2,
data,
segment_type: SegmentType::Icc,
});
offset += chunk_size;
}
self
}
#[must_use]
pub fn remove_icc(self) -> Self {
self.remove(SegmentType::Icc)
}
#[must_use]
pub fn set_iptc(self, data: Vec<u8>) -> Self {
let mut full_data = Vec::with_capacity(IPTC_SIGNATURE.len() + data.len());
full_data.extend_from_slice(IPTC_SIGNATURE);
full_data.extend_from_slice(&data);
self.replace(0xED, full_data, SegmentType::Iptc)
}
#[must_use]
pub fn set_jfif(self, info: JfifInfo) -> Self {
let mut data = Vec::with_capacity(16);
data.extend_from_slice(JFIF_SIGNATURE);
data.push(info.version_major);
data.push(info.version_minor);
data.push(info.density_units as u8);
data.push((info.x_density >> 8) as u8);
data.push(info.x_density as u8);
data.push((info.y_density >> 8) as u8);
data.push(info.y_density as u8);
data.push(0); data.push(0); self.replace(0xE0, data, SegmentType::Jfif)
}
#[must_use]
pub fn set_printer_dpi(self, dpi: u16) -> Self {
self.set_jfif(JfifInfo {
version_major: 1,
version_minor: 2,
density_units: DensityUnits::PixelsPerInch,
x_density: dpi,
y_density: dpi,
})
}
#[must_use]
pub fn add_comment(mut self, comment: &str) -> Self {
self.segments.push(EncoderSegment {
marker: 0xFE,
data: comment.as_bytes().to_vec(),
segment_type: SegmentType::Comment,
});
self
}
#[must_use]
pub fn add_mpf_image(mut self, data: Vec<u8>, typ: MpfImageType) -> Self {
self.mpf_images.push(MpfImage {
image_type: typ,
data,
});
self
}
pub fn add_mpf_image_mut(&mut self, data: Vec<u8>, typ: MpfImageType) -> &mut Self {
self.mpf_images.push(MpfImage {
image_type: typ,
data,
});
self
}
#[must_use]
pub fn add_gainmap(self, jpeg_data: Vec<u8>) -> Self {
self.add_mpf_image(jpeg_data, MpfImageType::Undefined)
}
#[must_use]
pub fn add_depth_map(self, jpeg_data: Vec<u8>) -> Self {
self.add_mpf_image(jpeg_data, MpfImageType::Disparity)
}
#[must_use]
pub fn mpf_images(&self) -> &[MpfImage] {
&self.mpf_images
}
#[must_use]
pub fn has_mpf_images(&self) -> bool {
!self.mpf_images.is_empty()
}
#[must_use]
pub fn clear_mpf_images(mut self) -> Self {
self.mpf_images.clear();
self
}
#[must_use]
pub fn remove_mpf_images(mut self, typ: MpfImageType) -> Self {
self.mpf_images.retain(|img| img.image_type != typ);
self
}
#[must_use]
pub fn merge(mut self, other: &EncoderSegments) -> Self {
for seg in &other.segments {
self.segments.push(seg.clone());
}
for img in &other.mpf_images {
self.mpf_images.push(img.clone());
}
self
}
#[must_use]
pub fn clear_segments(mut self) -> Self {
self.segments.clear();
self
}
#[must_use]
pub fn clear(mut self) -> Self {
self.segments.clear();
self.mpf_images.clear();
self
}
#[must_use]
pub fn retain(mut self, types: &[SegmentType]) -> Self {
self.segments.retain(|s| types.contains(&s.segment_type));
self
}
}
fn detect_segment_type(marker: u8, data: &[u8]) -> SegmentType {
match marker {
0xE0 if data.starts_with(JFIF_SIGNATURE) => SegmentType::Jfif,
0xE1 if data.starts_with(EXIF_SIGNATURE) => SegmentType::Exif,
0xE1 if data.starts_with(XMP_NAMESPACE) => SegmentType::Xmp,
0xE1 if data.starts_with(XMP_EXTENDED_NAMESPACE) => SegmentType::XmpExtended,
0xE2 if data.starts_with(ICC_SIGNATURE) => SegmentType::Icc,
0xE2 if data.starts_with(MPF_SIGNATURE) => SegmentType::Mpf,
0xED if data.starts_with(IPTC_SIGNATURE) => SegmentType::Iptc,
0xEE if data.starts_with(ADOBE_SIGNATURE) => SegmentType::Adobe,
0xFE => SegmentType::Comment,
_ => SegmentType::Unknown,
}
}
pub(crate) fn write_segment(output: &mut Vec<u8>, marker: u8, data: &[u8]) {
output.push(0xFF);
output.push(marker);
let length = (data.len() + 2) as u16;
output.push((length >> 8) as u8);
output.push(length as u8);
output.extend_from_slice(data);
}
pub(crate) fn write_encoder_segments(output: &mut Vec<u8>, segments: &EncoderSegments) {
for seg in segments.segments_of_type(SegmentType::Jfif) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Exif) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Xmp) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::XmpExtended) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Icc) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Iptc) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Adobe) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Comment) {
write_segment(output, seg.marker, &seg.data);
}
for seg in segments.segments_of_type(SegmentType::Unknown) {
write_segment(output, seg.marker, &seg.data);
}
}
pub(crate) fn generate_mpf_directory(
num_images: usize,
primary_size: u32,
image_sizes: &[(u32, MpfImageType)],
mpf_insert_offset: usize,
) -> Vec<u8> {
let total_images = 1 + num_images;
let mut data = Vec::with_capacity(128);
data.extend_from_slice(MPF_SIGNATURE);
data.extend_from_slice(b"II"); data.extend_from_slice(&0x002Au16.to_le_bytes()); data.extend_from_slice(&0x00000008u32.to_le_bytes());
let num_entries: u16 = 3; data.extend_from_slice(&num_entries.to_le_bytes());
let ifd_size = 2 + 12 * num_entries as usize + 4; let mp_entry_offset = 8 + ifd_size;
write_mpf_ifd_entry(&mut data, 0xB000, 7, 4, 0x30303130);
write_mpf_ifd_entry(&mut data, 0xB001, 4, 1, total_images as u32);
let mp_entry_size = total_images * 16;
write_mpf_ifd_entry(
&mut data,
0xB002,
7,
mp_entry_size as u32,
mp_entry_offset as u32,
);
data.extend_from_slice(&0u32.to_le_bytes());
write_mp_entry(&mut data, 0x030000, primary_size, 0, 0, 0);
let tiff_header_pos = mpf_insert_offset + 8;
let mut current_offset = (primary_size as usize).saturating_sub(tiff_header_pos) as u32;
for (size, typ) in image_sizes {
let type_code = typ.to_type_code();
write_mp_entry(&mut data, type_code, *size, current_offset, 0, 0);
current_offset += size;
}
data
}
fn write_mpf_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());
}
fn write_mp_entry(
buf: &mut Vec<u8>,
type_code: u32,
size: u32,
offset: u32,
dep_image1: u16,
dep_image2: u16,
) {
buf.extend_from_slice(&type_code.to_le_bytes());
buf.extend_from_slice(&size.to_le_bytes());
buf.extend_from_slice(&offset.to_le_bytes());
buf.extend_from_slice(&dep_image1.to_le_bytes());
buf.extend_from_slice(&dep_image2.to_le_bytes());
}
pub(crate) fn inject_encoder_segments(jpeg: Vec<u8>, segments: &EncoderSegments) -> Vec<u8> {
if segments.is_empty() {
return jpeg;
}
let mut segment_bytes = Vec::new();
write_encoder_segments(&mut segment_bytes, segments);
let has_mpf = segments.has_mpf_images();
let mpf_images = segments.mpf_images();
if has_mpf {
let image_sizes: Vec<(u32, MpfImageType)> = mpf_images
.iter()
.map(|img| (img.data.len() as u32, img.image_type))
.collect();
let mpf_insert_offset = 2 + segment_bytes.len();
let mpf_data_temp =
generate_mpf_directory(mpf_images.len(), 0, &image_sizes, mpf_insert_offset);
let mpf_segment_size = 2 + 2 + mpf_data_temp.len();
let primary_size = jpeg.len() + segment_bytes.len() + mpf_segment_size;
let mpf_data = generate_mpf_directory(
mpf_images.len(),
primary_size as u32,
&image_sizes,
mpf_insert_offset,
);
let total_size = primary_size + mpf_images.iter().map(|i| i.data.len()).sum::<usize>();
let mut result = Vec::with_capacity(total_size);
result.extend_from_slice(&jpeg[..2]);
result.extend_from_slice(&segment_bytes);
write_segment(&mut result, 0xE2, &mpf_data);
result.extend_from_slice(&jpeg[2..]);
for img in mpf_images {
result.extend_from_slice(&img.data);
}
result
} else {
let mut result = Vec::with_capacity(jpeg.len() + segment_bytes.len());
result.extend_from_slice(&jpeg[..2]);
result.extend_from_slice(&segment_bytes);
result.extend_from_slice(&jpeg[2..]);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encoder_segments_new() {
let segments = EncoderSegments::new();
assert!(segments.is_empty());
assert!(segments.segments().is_empty());
assert!(segments.mpf_images().is_empty());
}
#[test]
fn test_encoder_segments_add() {
let segments = EncoderSegments::new().add(0xE1, b"test".to_vec(), SegmentType::Unknown);
assert!(!segments.is_empty());
assert_eq!(segments.segments().len(), 1);
assert!(segments.has(SegmentType::Unknown));
}
#[test]
fn test_set_exif() {
let segments = EncoderSegments::new().set_exif(b"TIFF data".to_vec());
assert!(segments.has(SegmentType::Exif));
let exif_data = segments.get(SegmentType::Exif).unwrap();
assert!(exif_data.starts_with(EXIF_SIGNATURE));
}
#[test]
fn test_set_xmp() {
let xmp = "<?xml version=\"1.0\"?><x:xmpmeta>test</x:xmpmeta>";
let segments = EncoderSegments::new().set_xmp(xmp);
assert!(segments.has(SegmentType::Xmp));
let xmp_data = segments.get(SegmentType::Xmp).unwrap();
assert!(xmp_data.starts_with(XMP_NAMESPACE));
}
#[test]
fn test_set_icc_small() {
let profile = vec![0u8; 1000];
let segments = EncoderSegments::new().set_icc(profile);
assert!(segments.has(SegmentType::Icc));
assert_eq!(segments.get_all(SegmentType::Icc).len(), 1);
}
#[test]
fn test_set_icc_large() {
let profile = vec![0u8; 100_000];
let segments = EncoderSegments::new().set_icc(profile);
assert!(segments.has(SegmentType::Icc));
let chunks = segments.get_all(SegmentType::Icc);
assert!(chunks.len() > 1);
}
#[test]
fn test_add_gainmap() {
let gainmap = b"fake jpeg data".to_vec();
let segments = EncoderSegments::new().add_gainmap(gainmap);
assert!(segments.has_mpf_images());
assert_eq!(segments.mpf_images().len(), 1);
assert_eq!(segments.mpf_images()[0].image_type, MpfImageType::Undefined);
}
#[test]
fn test_remove() {
let segments = EncoderSegments::new()
.set_exif(b"exif".to_vec())
.set_xmp("xmp")
.remove(SegmentType::Exif);
assert!(!segments.has(SegmentType::Exif));
assert!(segments.has(SegmentType::Xmp));
}
#[test]
fn test_replace() {
let segments = EncoderSegments::new()
.set_exif(b"old exif".to_vec())
.set_exif(b"new exif".to_vec());
assert_eq!(segments.get_all(SegmentType::Exif).len(), 1);
}
#[test]
fn test_add_comment() {
let segments = EncoderSegments::new()
.add_comment("Comment 1")
.add_comment("Comment 2");
let comments = segments.get_all(SegmentType::Comment);
assert_eq!(comments.len(), 2);
}
#[test]
fn test_mpf_directory_generation() {
let image_sizes = vec![(5000, MpfImageType::Undefined)];
let mpf_data = generate_mpf_directory(1, 10000, &image_sizes, 100);
assert!(mpf_data.starts_with(MPF_SIGNATURE));
assert_eq!(&mpf_data[4..6], b"II"); }
#[test]
fn test_mpf_secondary_offset_is_relative_to_tiff_header() {
let mpf_insert_offset = 200;
let primary_size = 5000u32;
let secondary_size = 1000u32;
let image_sizes = vec![(secondary_size, MpfImageType::Undefined)];
let mpf_data = generate_mpf_directory(1, primary_size, &image_sizes, mpf_insert_offset);
let secondary_offset_bytes = &mpf_data[78..82];
let secondary_offset = u32::from_le_bytes(secondary_offset_bytes.try_into().unwrap());
let tiff_header_pos = mpf_insert_offset + 8;
let expected_offset = primary_size as usize - tiff_header_pos;
assert_eq!(
secondary_offset, expected_offset as u32,
"Secondary image offset should be {} (relative to TIFF header at {}), but got {}",
expected_offset, tiff_header_pos, secondary_offset
);
assert_ne!(
secondary_offset, primary_size,
"Secondary offset should NOT be the absolute file position ({})",
primary_size
);
}
#[test]
fn test_segment_type_detection() {
assert_eq!(detect_segment_type(0xE0, JFIF_SIGNATURE), SegmentType::Jfif);
assert_eq!(detect_segment_type(0xE1, EXIF_SIGNATURE), SegmentType::Exif);
assert_eq!(detect_segment_type(0xE1, XMP_NAMESPACE), SegmentType::Xmp);
assert_eq!(detect_segment_type(0xE2, ICC_SIGNATURE), SegmentType::Icc);
assert_eq!(detect_segment_type(0xFE, b"comment"), SegmentType::Comment);
assert_eq!(detect_segment_type(0xE5, b"unknown"), SegmentType::Unknown);
}
#[test]
fn test_set_printer_dpi() {
let segments = EncoderSegments::new().set_printer_dpi(300);
assert!(segments.has(SegmentType::Jfif));
let jfif_data = segments.get(SegmentType::Jfif).unwrap();
assert!(jfif_data.starts_with(JFIF_SIGNATURE));
assert_eq!(jfif_data[5], 1); assert_eq!(jfif_data[6], 2); assert_eq!(jfif_data[7], 1); assert_eq!(jfif_data[8], 0x01);
assert_eq!(jfif_data[9], 0x2C);
assert_eq!(jfif_data[10], 0x01);
assert_eq!(jfif_data[11], 0x2C);
}
}