use alloc::sync::Arc;
use alloc::vec::Vec;
use std::sync::OnceLock;
pub use crate::encode::extras::{
AdobeColorTransform, AdobeInfo, DensityUnits, JfifInfo, MpfImageType, MpfImageTypeExt,
SegmentType,
};
pub use crate::detect::{
Confidence, DqtTable, EncoderFamily, JpegProbe, QualityEstimate, QualityScale,
};
#[derive(Clone)]
pub struct PreserveConfig {
pub jfif: bool,
pub exif: bool,
pub xmp: bool,
pub icc: IccPreserve,
pub iptc: bool,
pub adobe: bool,
pub com: bool,
pub app_unknown: bool,
pub mpf_gainmaps: bool,
pub mpf_thumbnails: bool,
pub mpf_multiframe: bool,
pub mpf_depth: bool,
pub mpf_filter: Option<Arc<dyn Fn(usize, MpfImageType, u32) -> bool + Send + Sync>>,
}
impl core::fmt::Debug for PreserveConfig {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PreserveConfig")
.field("jfif", &self.jfif)
.field("exif", &self.exif)
.field("xmp", &self.xmp)
.field("icc", &self.icc)
.field("iptc", &self.iptc)
.field("adobe", &self.adobe)
.field("com", &self.com)
.field("app_unknown", &self.app_unknown)
.field("mpf_gainmaps", &self.mpf_gainmaps)
.field("mpf_thumbnails", &self.mpf_thumbnails)
.field("mpf_multiframe", &self.mpf_multiframe)
.field("mpf_depth", &self.mpf_depth)
.field("mpf_filter", &self.mpf_filter.is_some())
.finish()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IccPreserve {
#[default]
All,
DropStandard,
None,
}
impl Default for PreserveConfig {
fn default() -> Self {
Self {
jfif: true,
exif: true,
xmp: true,
icc: IccPreserve::All,
iptc: true,
adobe: true,
com: true,
app_unknown: false,
mpf_gainmaps: true,
mpf_thumbnails: false,
mpf_multiframe: false,
mpf_depth: false,
mpf_filter: None,
}
}
}
impl PreserveConfig {
#[must_use]
pub fn none() -> Self {
Self {
jfif: false,
exif: false,
xmp: false,
icc: IccPreserve::None,
iptc: false,
adobe: false,
com: false,
app_unknown: false,
mpf_gainmaps: false,
mpf_thumbnails: false,
mpf_multiframe: false,
mpf_depth: false,
mpf_filter: None,
}
}
#[must_use]
pub fn all() -> Self {
Self {
jfif: true,
exif: true,
xmp: true,
icc: IccPreserve::All,
iptc: true,
adobe: true,
com: true,
app_unknown: true,
mpf_gainmaps: true,
mpf_thumbnails: true,
mpf_multiframe: true,
mpf_depth: true,
mpf_filter: None,
}
}
#[must_use]
pub fn jfif(mut self, keep: bool) -> Self {
self.jfif = keep;
self
}
#[must_use]
pub fn exif(mut self, keep: bool) -> Self {
self.exif = keep;
self
}
#[must_use]
pub fn xmp(mut self, keep: bool) -> Self {
self.xmp = keep;
self
}
#[must_use]
pub fn icc(mut self, mode: IccPreserve) -> Self {
self.icc = mode;
self
}
#[must_use]
pub fn iptc(mut self, keep: bool) -> Self {
self.iptc = keep;
self
}
#[must_use]
pub fn adobe(mut self, keep: bool) -> Self {
self.adobe = keep;
self
}
#[must_use]
pub fn com(mut self, keep: bool) -> Self {
self.com = keep;
self
}
#[must_use]
pub fn app_unknown(mut self, keep: bool) -> Self {
self.app_unknown = keep;
self
}
#[must_use]
pub fn mpf_gainmaps(mut self, keep: bool) -> Self {
self.mpf_gainmaps = keep;
self
}
#[must_use]
pub fn mpf_thumbnails(mut self, keep: bool) -> Self {
self.mpf_thumbnails = keep;
self
}
#[must_use]
pub fn mpf_multiframe(mut self, keep: bool) -> Self {
self.mpf_multiframe = keep;
self
}
#[must_use]
pub fn mpf_depth(mut self, keep: bool) -> Self {
self.mpf_depth = keep;
self
}
#[must_use]
pub fn mpf_filter<F>(mut self, f: F) -> Self
where
F: Fn(usize, MpfImageType, u32) -> bool + Send + Sync + 'static,
{
self.mpf_filter = Some(Arc::new(f));
self
}
pub(crate) fn preserves_any_metadata(&self) -> bool {
self.jfif
|| self.exif
|| self.xmp
|| self.icc != IccPreserve::None
|| self.iptc
|| self.adobe
|| self.com
|| self.app_unknown
}
pub(crate) fn preserves_any_mpf(&self) -> bool {
self.mpf_gainmaps
|| self.mpf_thumbnails
|| self.mpf_multiframe
|| self.mpf_depth
|| self.mpf_filter.is_some()
}
}
#[derive(Clone, Debug)]
pub struct PreservedSegment {
pub marker: u8,
pub data: Vec<u8>,
pub segment_type: SegmentType,
}
#[derive(Clone, Debug)]
pub struct PreservedMpfImage {
pub mpf_index: usize,
pub image_type: MpfImageType,
pub data: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct MpfDirectory {
pub version: [u8; 4],
pub images: Vec<MpfEntry>,
}
#[derive(Clone, Debug)]
pub struct MpfEntry {
pub image_type: MpfImageType,
pub offset: u32,
pub size: u32,
pub dependent_image1: Option<u16>,
pub dependent_image2: Option<u16>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StandardProfile {
SrgbIec61966,
DisplayP3,
}
pub struct DecodedExtras {
pub(crate) segments: Vec<PreservedSegment>,
pub(crate) secondary_images: Vec<PreservedMpfImage>,
pub(crate) probe: Option<JpegProbe>,
xmp_cache: OnceLock<Option<String>>,
icc_cache: OnceLock<Option<Vec<u8>>>,
mpf_cache: OnceLock<Option<MpfDirectory>>,
}
impl core::fmt::Debug for DecodedExtras {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DecodedExtras")
.field("segments", &self.segments.len())
.field("secondary_images", &self.secondary_images.len())
.field("has_probe", &self.probe.is_some())
.finish()
}
}
impl Clone for DecodedExtras {
fn clone(&self) -> Self {
Self {
segments: self.segments.clone(),
secondary_images: self.secondary_images.clone(),
probe: self.probe.clone(),
xmp_cache: OnceLock::new(),
icc_cache: OnceLock::new(),
mpf_cache: OnceLock::new(),
}
}
}
impl DecodedExtras {
pub(crate) fn new() -> Self {
Self {
segments: Vec::new(),
secondary_images: Vec::new(),
probe: None,
xmp_cache: OnceLock::new(),
icc_cache: OnceLock::new(),
mpf_cache: OnceLock::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments.is_empty() && self.secondary_images.is_empty()
}
#[must_use]
pub fn segments(&self) -> &[PreservedSegment] {
&self.segments
}
pub fn segments_by_type(&self, typ: SegmentType) -> impl Iterator<Item = &PreservedSegment> {
self.segments.iter().filter(move |s| s.segment_type == typ)
}
pub(crate) fn remove_segments_by_type(&mut self, typ: SegmentType) {
self.segments.retain(|s| s.segment_type != typ);
}
pub fn segments_by_marker(&self, marker: u8) -> impl Iterator<Item = &PreservedSegment> {
self.segments.iter().filter(move |s| s.marker == marker)
}
#[must_use]
pub fn jfif(&self) -> Option<JfifInfo> {
self.segments_by_type(SegmentType::Jfif)
.next()
.and_then(|seg| parse_jfif(&seg.data))
}
#[must_use]
pub fn exif(&self) -> Option<&[u8]> {
self.segments_by_type(SegmentType::Exif)
.next()
.map(|seg| seg.data.as_slice())
}
#[must_use]
pub fn xmp(&self) -> Option<&str> {
self.xmp_cache
.get_or_init(|| self.reassemble_xmp())
.as_deref()
}
#[must_use]
pub fn icc_profile(&self) -> Option<&[u8]> {
self.icc_cache
.get_or_init(|| self.reassemble_icc())
.as_deref()
}
#[must_use]
pub fn icc_is_standard(&self) -> Option<StandardProfile> {
let icc = self.icc_profile()?;
detect_standard_profile(icc)
}
#[must_use]
pub fn iptc(&self) -> Option<&[u8]> {
self.segments_by_type(SegmentType::Iptc)
.next()
.map(|seg| seg.data.as_slice())
}
#[must_use]
pub fn adobe(&self) -> Option<AdobeInfo> {
self.segments_by_type(SegmentType::Adobe)
.next()
.and_then(|seg| parse_adobe(&seg.data))
}
pub fn comments(&self) -> impl Iterator<Item = &str> {
self.segments_by_type(SegmentType::Comment)
.filter_map(|seg| core::str::from_utf8(&seg.data).ok())
}
#[must_use]
pub fn probe(&self) -> Option<&JpegProbe> {
self.probe.as_ref()
}
#[must_use]
pub fn quality_estimate(&self) -> Option<&QualityEstimate> {
self.probe.as_ref().map(|p| &p.quality)
}
#[must_use]
pub fn encoder(&self) -> Option<EncoderFamily> {
self.probe.as_ref().map(|p| p.encoder)
}
#[must_use]
pub fn luminance_qt(&self) -> Option<&[u16; 64]> {
self.probe
.as_ref()?
.dqt_tables
.iter()
.find(|t| t.index == 0)
.map(|t| &t.values)
}
#[must_use]
pub fn chrominance_qt(&self) -> Option<&[u16; 64]> {
self.probe
.as_ref()?
.dqt_tables
.iter()
.find(|t| t.index == 1)
.map(|t| &t.values)
}
#[must_use]
pub fn dqt_tables(&self) -> Option<&[DqtTable]> {
self.probe.as_ref().map(|p| p.dqt_tables.as_slice())
}
#[must_use]
pub fn mpf(&self) -> Option<&MpfDirectory> {
self.mpf_cache.get_or_init(|| self.parse_mpf()).as_ref()
}
#[must_use]
pub fn secondary_images(&self) -> &[PreservedMpfImage] {
&self.secondary_images
}
#[must_use]
pub fn secondary_image(&self, mpf_index: usize) -> Option<&[u8]> {
self.secondary_images
.iter()
.find(|img| img.mpf_index == mpf_index)
.map(|img| img.data.as_slice())
}
#[must_use]
pub fn gainmap(&self) -> Option<&[u8]> {
self.secondary_images
.iter()
.find(|img| img.image_type.is_gainmap())
.map(|img| img.data.as_slice())
}
#[must_use]
pub fn depth_map(&self) -> Option<&[u8]> {
self.secondary_images
.iter()
.find(|img| img.image_type.is_depth())
.map(|img| img.data.as_slice())
}
#[must_use]
pub fn to_encoder_segments(&self) -> crate::encode::extras::EncoderSegments {
use crate::encode::extras::EncoderSegments;
let mut segments = EncoderSegments::new();
for seg in &self.segments {
match seg.segment_type {
SegmentType::Jfif
| SegmentType::Exif
| SegmentType::Xmp
| SegmentType::XmpExtended
| SegmentType::Icc
| SegmentType::Iptc
| SegmentType::Adobe
| SegmentType::Comment => {
segments.add_mut(seg.marker, seg.data.clone(), seg.segment_type);
}
SegmentType::Mpf => {
}
SegmentType::Unknown => {
}
}
}
for img in &self.secondary_images {
segments.add_mpf_image_mut(img.data.clone(), img.image_type);
}
segments
}
#[must_use]
pub fn to_encoder_segments_filtered<F>(
&self,
filter: F,
) -> crate::encode::extras::EncoderSegments
where
F: Fn(&PreservedSegment) -> bool,
{
use crate::encode::extras::EncoderSegments;
let mut segments = EncoderSegments::new();
for seg in &self.segments {
if seg.segment_type != SegmentType::Mpf && filter(seg) {
segments.add_mut(seg.marker, seg.data.clone(), seg.segment_type);
}
}
for img in &self.secondary_images {
segments.add_mpf_image_mut(img.data.clone(), img.image_type);
}
segments
}
#[must_use]
pub fn to_raw_segments(&self) -> Vec<(u8, Vec<u8>)> {
self.segments
.iter()
.filter(|seg| seg.segment_type != SegmentType::Mpf)
.map(|seg| (seg.marker, seg.data.clone()))
.collect()
}
fn reassemble_xmp(&self) -> Option<String> {
let primary = self
.segments_by_type(SegmentType::Xmp)
.next()
.map(|seg| &seg.data)?;
const XMP_NS: &[u8] = b"http://ns.adobe.com/xap/1.0/\0";
let primary_xmp = if primary.starts_with(XMP_NS) {
&primary[XMP_NS.len()..]
} else {
primary
};
let primary_str = core::str::from_utf8(primary_xmp).ok()?;
let extended: Vec<_> = self.segments_by_type(SegmentType::XmpExtended).collect();
if extended.is_empty() {
return Some(primary_str.to_string());
}
const EXT_NS: &[u8] = b"http://ns.adobe.com/xmp/extension/\0";
let mut chunks: Vec<(u32, &[u8])> = Vec::new();
for seg in extended {
if seg.data.len() < EXT_NS.len() + 40 {
continue;
}
if !seg.data.starts_with(EXT_NS) {
continue;
}
let offset_start = EXT_NS.len() + 32 + 4; if seg.data.len() < offset_start + 4 {
continue;
}
let offset = u32::from_be_bytes([
seg.data[offset_start],
seg.data[offset_start + 1],
seg.data[offset_start + 2],
seg.data[offset_start + 3],
]);
let data = &seg.data[offset_start + 4..];
chunks.push((offset, data));
}
if chunks.is_empty() {
return Some(primary_str.to_string());
}
chunks.sort_by_key(|(off, _)| *off);
let extended_data: Vec<u8> = chunks
.into_iter()
.flat_map(|(_, data)| data)
.copied()
.collect();
let extended_str = core::str::from_utf8(&extended_data).ok()?;
Some(format!("{}{}", primary_str, extended_str))
}
fn reassemble_icc(&self) -> Option<Vec<u8>> {
let icc_segments: Vec<_> = self.segments_by_type(SegmentType::Icc).collect();
if icc_segments.is_empty() {
return None;
}
const ICC_SIG: &[u8] = b"ICC_PROFILE\0";
let mut chunks: Vec<(u8, &[u8])> = Vec::new();
for seg in icc_segments {
if seg.data.len() < ICC_SIG.len() + 2 {
continue;
}
if !seg.data.starts_with(ICC_SIG) {
continue;
}
let chunk_num = seg.data[ICC_SIG.len()];
let data = &seg.data[ICC_SIG.len() + 2..];
chunks.push((chunk_num, data));
}
if chunks.is_empty() {
return None;
}
chunks.sort_by_key(|(num, _)| *num);
let total_size: usize = chunks.iter().map(|(_, data)| data.len()).sum();
if total_size > crate::foundation::alloc::MAX_ICC_PROFILE_SIZE {
return None;
}
let profile: Vec<u8> = chunks
.into_iter()
.flat_map(|(_, data)| data)
.copied()
.collect();
Some(profile)
}
fn parse_mpf(&self) -> Option<MpfDirectory> {
let mpf_seg = self.segments_by_type(SegmentType::Mpf).next()?;
parse_mpf_directory(&mpf_seg.data)
}
pub(crate) fn add_segment(&mut self, marker: u8, data: Vec<u8>, segment_type: SegmentType) {
self.segments.push(PreservedSegment {
marker,
data,
segment_type,
});
}
pub(crate) fn add_secondary_image(
&mut self,
mpf_index: usize,
image_type: MpfImageType,
data: Vec<u8>,
) {
self.secondary_images.push(PreservedMpfImage {
mpf_index,
image_type,
data,
});
}
}
fn parse_jfif(data: &[u8]) -> Option<JfifInfo> {
const JFIF_SIG: &[u8] = b"JFIF\0";
if data.len() < JFIF_SIG.len() + 7 {
return None;
}
if !data.starts_with(JFIF_SIG) {
return None;
}
let offset = JFIF_SIG.len();
let version_major = data[offset];
let version_minor = data[offset + 1];
let units_byte = data[offset + 2];
let x_density = u16::from_be_bytes([data[offset + 3], data[offset + 4]]);
let y_density = u16::from_be_bytes([data[offset + 5], data[offset + 6]]);
let density_units = match units_byte {
0 => DensityUnits::None,
1 => DensityUnits::PixelsPerInch,
2 => DensityUnits::PixelsPerCm,
_ => DensityUnits::None,
};
Some(JfifInfo {
version_major,
version_minor,
density_units,
x_density,
y_density,
})
}
fn parse_adobe(data: &[u8]) -> Option<AdobeInfo> {
const ADOBE_SIG: &[u8] = b"Adobe";
if data.len() < ADOBE_SIG.len() + 7 {
return None;
}
if !data.starts_with(ADOBE_SIG) {
return None;
}
let offset = ADOBE_SIG.len();
let version = u16::from_be_bytes([data[offset], data[offset + 1]]);
let color_transform_byte = data[offset + 6];
let color_transform = match color_transform_byte {
0 => AdobeColorTransform::Unknown,
1 => AdobeColorTransform::YCbCr,
2 => AdobeColorTransform::Ycck,
_ => AdobeColorTransform::Unknown,
};
Some(AdobeInfo {
version,
color_transform,
})
}
pub(crate) fn parse_mpf_directory(data: &[u8]) -> Option<MpfDirectory> {
const MPF_SIG: &[u8] = b"MPF\0";
if data.len() < MPF_SIG.len() + 8 {
return None;
}
if !data.starts_with(MPF_SIG) {
return None;
}
let offset = MPF_SIG.len();
let is_little_endian = &data[offset..offset + 2] == b"II";
let read_u16 = |pos: usize| -> u16 {
if is_little_endian {
u16::from_le_bytes([data[pos], data[pos + 1]])
} else {
u16::from_be_bytes([data[pos], data[pos + 1]])
}
};
let read_u32 = |pos: usize| -> u32 {
if is_little_endian {
u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
} else {
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
}
};
let ifd_offset = read_u32(offset + 4) as usize;
let ifd_pos = offset + ifd_offset;
if data.len() < ifd_pos + 2 {
return None;
}
let version = [0u8; 4];
let num_entries = read_u16(ifd_pos) as usize;
let mut mp_entry_offset = None;
let mut mp_entry_count = 0u32;
for i in 0..num_entries {
let entry_pos = ifd_pos + 2 + i * 12;
if data.len() < entry_pos + 12 {
break;
}
let tag = read_u16(entry_pos);
if tag == 0xB002 {
mp_entry_count = read_u32(entry_pos + 4);
mp_entry_offset = Some(read_u32(entry_pos + 8) as usize + offset);
break;
}
}
if mp_entry_offset.is_none() {
let tag_bytes = if is_little_endian {
[0x02, 0xB0] } else {
[0xB0, 0x02] };
let scan_start = ifd_pos + 2;
let scan_end = (scan_start + 256).min(data.len().saturating_sub(12));
for pos in scan_start..scan_end {
if data.len() >= pos + 12 && data[pos..pos + 2] == tag_bytes {
let type_val = read_u16(pos + 2);
if type_val == 7 {
mp_entry_count = read_u32(pos + 4);
mp_entry_offset = Some(read_u32(pos + 8) as usize + offset);
break;
}
}
}
}
let mp_entry_offset = mp_entry_offset?;
let num_images = (mp_entry_count / 16) as usize;
let mut images = Vec::with_capacity(num_images);
for i in 0..num_images {
let entry_pos = mp_entry_offset + i * 16;
if data.len() < entry_pos + 16 {
break;
}
let attr = read_u32(entry_pos);
let image_type = MpfImageType::from_type_code(attr & 0x00FFFFFF);
let size = read_u32(entry_pos + 4);
let entry_offset = read_u32(entry_pos + 8);
let dep1 = read_u16(entry_pos + 12);
let dep2 = read_u16(entry_pos + 14);
images.push(MpfEntry {
image_type,
offset: entry_offset,
size,
dependent_image1: if dep1 != 0 { Some(dep1) } else { None },
dependent_image2: if dep2 != 0 { Some(dep2) } else { None },
});
}
Some(MpfDirectory { version, images })
}
fn detect_standard_profile(icc: &[u8]) -> Option<StandardProfile> {
if icc.windows(4).any(|w| w == b"sRGB") {
return Some(StandardProfile::SrgbIec61966);
}
if icc.windows(10).any(|w| w == b"Display P3") {
return Some(StandardProfile::DisplayP3);
}
None
}
pub(crate) fn detect_segment_type(marker: u8, data: &[u8]) -> SegmentType {
match marker {
0xE0 => {
if data.starts_with(b"JFIF\0") {
SegmentType::Jfif
} else {
SegmentType::Unknown
}
}
0xE1 => {
if data.starts_with(b"Exif\0\0") {
SegmentType::Exif
} else if data.starts_with(b"http://ns.adobe.com/xap/1.0/\0") {
SegmentType::Xmp
} else if data.starts_with(b"http://ns.adobe.com/xmp/extension/\0") {
SegmentType::XmpExtended
} else {
SegmentType::Unknown
}
}
0xE2 => {
if data.starts_with(b"ICC_PROFILE\0") {
SegmentType::Icc
} else if data.starts_with(b"MPF\0") {
SegmentType::Mpf
} else {
SegmentType::Unknown
}
}
0xED => {
if data.starts_with(b"Photoshop 3.0\0") {
SegmentType::Iptc
} else {
SegmentType::Unknown
}
}
0xEE => {
if data.starts_with(b"Adobe") {
SegmentType::Adobe
} else {
SegmentType::Unknown
}
}
0xFE => SegmentType::Comment,
_ => SegmentType::Unknown,
}
}
pub(crate) fn should_preserve_segment(config: &PreserveConfig, segment_type: SegmentType) -> bool {
match segment_type {
SegmentType::Jfif => config.jfif,
SegmentType::Exif => config.exif,
SegmentType::Xmp | SegmentType::XmpExtended => config.xmp,
SegmentType::Icc => config.icc != IccPreserve::None,
SegmentType::Mpf => config.preserves_any_mpf(), SegmentType::Iptc => config.iptc,
SegmentType::Adobe => config.adobe,
SegmentType::Comment => config.com,
SegmentType::Unknown => config.app_unknown,
}
}
pub(crate) fn should_preserve_mpf_image(
config: &PreserveConfig,
index: usize,
image_type: MpfImageType,
size: u32,
) -> bool {
if let Some(ref filter) = config.mpf_filter {
return filter(index, image_type, size);
}
match image_type {
MpfImageType::Undefined => config.mpf_gainmaps,
MpfImageType::LargeThumbnailVga | MpfImageType::LargeThumbnailFullHd => {
config.mpf_thumbnails
}
MpfImageType::Panorama | MpfImageType::MultiAngle => config.mpf_multiframe,
MpfImageType::Disparity => config.mpf_depth,
MpfImageType::BaselinePrimary => false, MpfImageType::Other(_) | _ => config.app_unknown,
}
}