#![allow(dead_code)]
#![allow(unused_imports)]
use crate::error::{Error, Result};
pub const ICC_PROFILE_SIGNATURE: &[u8; 12] = b"ICC_PROFILE\0";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum TargetColorSpace {
#[default]
Srgb,
DisplayP3,
Rec2020,
}
const XYB_PROFILE_MARKER: &[u8] = b"XYB";
pub fn extract_icc_profile(jpeg_data: &[u8]) -> Option<Vec<u8>> {
let mut chunks: Vec<(u8, Vec<u8>)> = Vec::new();
let mut i = 2;
while i + 3 < jpeg_data.len() {
if jpeg_data[i] != 0xFF {
break; }
while i + 1 < jpeg_data.len() && jpeg_data[i + 1] == 0xFF {
i += 1;
}
if i + 3 >= jpeg_data.len() {
break;
}
let marker_type = jpeg_data[i + 1];
if marker_type == 0xDA || marker_type == 0xD9 {
break;
}
if marker_type == 0x00 || marker_type == 0x01 || (0xD0..=0xD7).contains(&marker_type) {
i += 2;
continue;
}
let length = ((jpeg_data[i + 2] as usize) << 8) | (jpeg_data[i + 3] as usize);
if length < 2 || i + 2 + length > jpeg_data.len() {
break;
}
if marker_type == 0xE2 && length >= 16 && &jpeg_data[i + 4..i + 16] == ICC_PROFILE_SIGNATURE
{
let chunk_num = jpeg_data[i + 16];
let _total_chunks = jpeg_data[i + 17];
let icc_data = jpeg_data[i + 18..i + 2 + length].to_vec();
chunks.push((chunk_num, icc_data));
}
i += 2 + length;
}
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).collect();
Some(profile)
}
pub fn is_xyb_profile(icc_data: &[u8]) -> bool {
use crate::foundation::consts::XYB_ICC_PROFILE;
if icc_data == XYB_ICC_PROFILE {
return true;
}
const XYB_UTF16BE: [u8; 6] = [0, b'X', 0, b'Y', 0, b'B'];
icc_data
.windows(XYB_PROFILE_MARKER.len())
.any(|w| w == XYB_PROFILE_MARKER)
|| icc_data.windows(6).any(|w| w == XYB_UTF16BE)
}
#[cfg(feature = "moxcms")]
pub fn apply_icc_transform(
rgb_data: &[u8],
_width: usize,
_height: usize,
icc_profile: &[u8],
target: TargetColorSpace,
) -> Result<Vec<u8>> {
use moxcms::{ColorProfile, Layout};
let input_profile = ColorProfile::new_from_slice(icc_profile)
.map_err(|e| Error::icc_error(format!("moxcms: {e:?}")))?;
let output_profile = make_moxcms_target(target);
let transform = input_profile
.create_transform_8bit(
Layout::Rgb,
&output_profile,
Layout::Rgb,
moxcms_transform_opts(),
)
.map_err(|e| Error::icc_error(format!("moxcms transform: {e:?}")))?;
let mut output = vec![0u8; rgb_data.len()];
transform
.transform(rgb_data, &mut output)
.map_err(|e| Error::icc_error(format!("moxcms transform execution: {e:?}")))?;
Ok(output)
}
#[cfg(not(feature = "moxcms"))]
pub fn apply_icc_transform(
rgb_data: &[u8],
_width: usize,
_height: usize,
_icc_profile: &[u8],
_target: TargetColorSpace,
) -> Result<Vec<u8>> {
Ok(rgb_data.to_vec())
}
#[cfg(feature = "moxcms")]
pub fn apply_icc_transform_f32(
rgb_data: &[f32],
_width: usize,
_height: usize,
icc_profile: &[u8],
target: TargetColorSpace,
) -> Result<Vec<f32>> {
use moxcms::{ColorProfile, Layout};
let input_profile = ColorProfile::new_from_slice(icc_profile)
.map_err(|e| Error::icc_error(format!("moxcms: {e:?}")))?;
let output_profile = make_moxcms_target(target);
let transform = input_profile
.create_transform_f32(
Layout::Rgb,
&output_profile,
Layout::Rgb,
moxcms_transform_opts(),
)
.map_err(|e| Error::icc_error(format!("moxcms f32 transform: {e:?}")))?;
let mut output = vec![0f32; rgb_data.len()];
transform
.transform(rgb_data, &mut output)
.map_err(|e| Error::icc_error(format!("moxcms f32 transform execution: {e:?}")))?;
Ok(output)
}
#[cfg(not(feature = "moxcms"))]
pub fn apply_icc_transform_f32(
rgb_data: &[f32],
_width: usize,
_height: usize,
_icc_profile: &[u8],
_target: TargetColorSpace,
) -> Result<Vec<f32>> {
Ok(rgb_data.to_vec())
}
#[cfg(feature = "moxcms")]
fn moxcms_transform_opts() -> moxcms::TransformOptions {
use moxcms::{BarycentricWeightScale, InterpolationMethod, TransformOptions};
TransformOptions {
allow_use_cicp_transfer: false,
barycentric_weight_scale: BarycentricWeightScale::High,
interpolation_method: InterpolationMethod::Tetrahedral,
..Default::default()
}
}
#[cfg(feature = "moxcms")]
fn make_moxcms_target(target: TargetColorSpace) -> moxcms::ColorProfile {
match target {
TargetColorSpace::Srgb => moxcms::ColorProfile::new_srgb(),
TargetColorSpace::DisplayP3 => moxcms::ColorProfile::new_display_p3(),
TargetColorSpace::Rec2020 => moxcms::ColorProfile::new_bt2020(),
}
}
#[inline]
pub fn srgb_to_linear(v: f32) -> f32 {
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
pub fn srgb_to_linear_inplace(pixels: &mut [f32]) {
for v in pixels.iter_mut() {
*v = srgb_to_linear(*v);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_xyb_profile() {
let xyb_profile = b"...XYB_Per...";
assert!(is_xyb_profile(xyb_profile));
assert!(is_xyb_profile(&crate::foundation::consts::XYB_ICC_PROFILE));
let srgb = b"sRGB IEC61966-2.1";
assert!(!is_xyb_profile(srgb));
let mut jxl_srgb = vec![0u8; 128];
jxl_srgb[4..8].copy_from_slice(b"jxl ");
jxl_srgb[8..23].copy_from_slice(b"sRGB IEC61966-2");
assert!(
!is_xyb_profile(&jxl_srgb),
"jxl CMM type alone should not be detected as XYB"
);
}
#[test]
fn test_extract_icc_profile_empty() {
let no_icc = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10]; assert!(extract_icc_profile(&no_icc).is_none());
}
}