use crate::conversions::*;
use crate::err::CmsError;
use crate::interceptors::{FromCmykaInterceptor, ToCmykaInterceptor};
use crate::trc::GammaLutInterpolate;
use crate::{ColorProfile, DataColorSpace, LutWarehouse, RenderingIntent, Vector3f, Xyzd};
use num_traits::AsPrimitive;
use std::sync::Arc;
pub trait TransformExecutor<V: Copy + Default> {
fn transform(&self, src: &[V], dst: &mut [V]) -> Result<(), CmsError>;
}
pub trait Stage {
fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError>;
}
pub trait InPlaceStage {
fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError>;
}
pub trait InPlaceTransformExecutor<V: Copy + Default> {
fn transform(&self, in_out: &mut [V]) -> Result<(), CmsError>;
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
pub enum BarycentricWeightScale {
#[default]
Low,
#[cfg(feature = "options")]
High,
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct TransformOptions {
pub rendering_intent: RenderingIntent,
pub allow_use_cicp_transfer: bool,
pub prefer_fixed_point: bool,
pub interpolation_method: InterpolationMethod,
pub barycentric_weight_scale: BarycentricWeightScale,
#[cfg(feature = "extended_range")]
pub allow_extended_range_rgb_xyz: bool,
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
pub enum InterpolationMethod {
#[cfg(feature = "options")]
Tetrahedral,
#[cfg(feature = "options")]
Pyramid,
#[cfg(feature = "options")]
Prism,
#[default]
Linear,
}
impl Default for TransformOptions {
fn default() -> Self {
Self {
rendering_intent: RenderingIntent::default(),
allow_use_cicp_transfer: true,
prefer_fixed_point: true,
interpolation_method: InterpolationMethod::default(),
barycentric_weight_scale: BarycentricWeightScale::default(),
#[cfg(feature = "extended_range")]
allow_extended_range_rgb_xyz: false,
}
}
}
pub type Transform8BitExecutor = dyn TransformExecutor<u8> + Send + Sync;
pub type Transform16BitExecutor = dyn TransformExecutor<u16> + Send + Sync;
pub type TransformF32Executor = dyn TransformExecutor<f32> + Send + Sync;
pub type TransformF64Executor = dyn TransformExecutor<f64> + Send + Sync;
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Layout {
Rgb = 0,
Rgba = 1,
Cmyka = 16,
Gray = 2,
GrayAlpha = 3,
Inks5 = 4,
Inks6 = 5,
Inks7 = 6,
Inks8 = 7,
Inks9 = 8,
Inks10 = 9,
Inks11 = 10,
Inks12 = 11,
Inks13 = 12,
Inks14 = 13,
Inks15 = 14,
}
impl Layout {
#[inline(always)]
pub const fn r_i(self) -> usize {
match self {
Layout::Rgb => 0,
Layout::Rgba => 0,
Layout::Gray => unimplemented!(),
Layout::GrayAlpha => unimplemented!(),
_ => unimplemented!(),
}
}
#[inline(always)]
pub const fn g_i(self) -> usize {
match self {
Layout::Rgb => 1,
Layout::Rgba => 1,
Layout::Gray => unimplemented!(),
Layout::GrayAlpha => unimplemented!(),
_ => unimplemented!(),
}
}
#[inline(always)]
pub const fn b_i(self) -> usize {
match self {
Layout::Rgb => 2,
Layout::Rgba => 2,
Layout::Gray => unimplemented!(),
Layout::GrayAlpha => unimplemented!(),
_ => unimplemented!(),
}
}
#[inline(always)]
pub const fn a_i(self) -> usize {
match self {
Layout::Rgb => unimplemented!(),
Layout::Rgba => 3,
Layout::Gray => unimplemented!(),
Layout::GrayAlpha => 1,
_ => unimplemented!(),
}
}
#[inline(always)]
pub const fn has_alpha(self) -> bool {
match self {
Layout::Rgb => false,
Layout::Rgba => true,
Layout::Gray => false,
Layout::GrayAlpha => true,
_ => false,
}
}
#[inline]
pub const fn channels(self) -> usize {
match self {
Layout::Rgb => 3,
Layout::Rgba => 4,
Layout::Gray => 1,
Layout::GrayAlpha => 2,
Layout::Cmyka => 5,
Layout::Inks5 => 5,
Layout::Inks6 => 6,
Layout::Inks7 => 7,
Layout::Inks8 => 8,
Layout::Inks9 => 9,
Layout::Inks10 => 10,
Layout::Inks11 => 11,
Layout::Inks12 => 12,
Layout::Inks13 => 13,
Layout::Inks14 => 14,
Layout::Inks15 => 15,
}
}
#[cfg(feature = "any_to_any")]
pub(crate) fn from_inks(inks: usize) -> Self {
match inks {
1 => Layout::Gray,
2 => Layout::GrayAlpha,
3 => Layout::Rgb,
4 => Layout::Rgba,
5 => Layout::Inks5,
6 => Layout::Inks6,
7 => Layout::Inks7,
8 => Layout::Inks8,
9 => Layout::Inks9,
10 => Layout::Inks10,
11 => Layout::Inks11,
12 => Layout::Inks12,
13 => Layout::Inks13,
14 => Layout::Inks14,
15 => Layout::Inks15,
_ => unreachable!("Impossible amount of inks"),
}
}
}
impl From<u8> for Layout {
fn from(value: u8) -> Self {
match value {
0 => Layout::Rgb,
1 => Layout::Rgba,
2 => Layout::Gray,
3 => Layout::GrayAlpha,
_ => unimplemented!(),
}
}
}
impl Layout {
#[inline(always)]
pub const fn resolve(value: u8) -> Self {
match value {
0 => Layout::Rgb,
1 => Layout::Rgba,
2 => Layout::Gray,
3 => Layout::GrayAlpha,
4 => Layout::Inks5,
5 => Layout::Inks6,
6 => Layout::Inks7,
7 => Layout::Inks8,
8 => Layout::Inks9,
9 => Layout::Inks10,
10 => Layout::Inks11,
11 => Layout::Inks12,
12 => Layout::Inks13,
13 => Layout::Inks14,
14 => Layout::Inks15,
_ => unimplemented!(),
}
}
}
#[doc(hidden)]
pub trait PointeeSizeExpressible {
fn _as_usize(self) -> usize;
const FINITE: bool;
const NOT_FINITE_GAMMA_TABLE_SIZE: usize;
const NOT_FINITE_LINEAR_TABLE_SIZE: usize;
const IS_U8: bool;
const IS_U16: bool;
}
impl PointeeSizeExpressible for u8 {
#[inline(always)]
fn _as_usize(self) -> usize {
self as usize
}
const FINITE: bool = true;
const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
const IS_U8: bool = true;
const IS_U16: bool = false;
}
impl PointeeSizeExpressible for u16 {
#[inline(always)]
fn _as_usize(self) -> usize {
self as usize
}
const FINITE: bool = true;
const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
const IS_U8: bool = false;
const IS_U16: bool = true;
}
impl PointeeSizeExpressible for f32 {
#[inline(always)]
fn _as_usize(self) -> usize {
const MAX_14_BIT: f32 = ((1 << 14u32) - 1) as f32;
((self * MAX_14_BIT).max(0f32).min(MAX_14_BIT) as u16) as usize
}
const FINITE: bool = false;
const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 32768;
const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 14u32;
const IS_U8: bool = false;
const IS_U16: bool = false;
}
impl PointeeSizeExpressible for f64 {
#[inline(always)]
fn _as_usize(self) -> usize {
const MAX_16_BIT: f64 = ((1 << 16u32) - 1) as f64;
((self * MAX_16_BIT).max(0.).min(MAX_16_BIT) as u16) as usize
}
const FINITE: bool = false;
const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 65536;
const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 16;
const IS_U8: bool = false;
const IS_U16: bool = false;
}
impl ColorProfile {
pub fn is_matrix_shaper(&self) -> bool {
self.color_space == DataColorSpace::Rgb
&& self.red_colorant != Xyzd::default()
&& self.green_colorant != Xyzd::default()
&& self.blue_colorant != Xyzd::default()
&& self.red_trc.is_some()
&& self.green_trc.is_some()
&& self.blue_trc.is_some()
}
pub fn create_transform_16bit(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<Transform16BitExecutor>, CmsError> {
let mut core_src_layout = src_layout;
if src_layout == Layout::Cmyka {
core_src_layout = Layout::Rgba;
}
let mut core_dst_layout = dst_layout;
if dst_layout == Layout::Cmyka {
core_dst_layout = Layout::Rgba;
}
let handle = self.create_transform_nbit::<u16, 16, 65536, 65536>(
core_src_layout,
dst_pr,
core_dst_layout,
options,
)?;
if core_src_layout == Layout::Cmyka {
return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
} else if core_dst_layout == Layout::Cmyka {
return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
}
Ok(handle)
}
#[cfg(feature = "in_place")]
pub fn create_in_place_transform_16bit(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
self.create_transform_in_place_nbit::<u16, 16, 65536, 65536>(layout, dst_pr, options)
}
pub fn create_transform_12bit(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<Transform16BitExecutor>, CmsError> {
let mut core_src_layout = src_layout;
if src_layout == Layout::Cmyka {
core_src_layout = Layout::Rgba;
}
let mut core_dst_layout = dst_layout;
if dst_layout == Layout::Cmyka {
core_dst_layout = Layout::Rgba;
}
let handle = self.create_transform_nbit::<u16, 12, 65536, 16384>(
core_src_layout,
dst_pr,
core_dst_layout,
options,
)?;
if core_src_layout == Layout::Cmyka {
return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
} else if core_dst_layout == Layout::Cmyka {
return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
}
Ok(handle)
}
#[cfg(feature = "in_place")]
pub fn create_in_place_transform_12bit(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
self.create_transform_in_place_nbit::<u16, 12, 65536, 16384>(layout, dst_pr, options)
}
pub fn create_transform_10bit(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<Transform16BitExecutor>, CmsError> {
let mut core_src_layout = src_layout;
if src_layout == Layout::Cmyka {
core_src_layout = Layout::Rgba;
}
let mut core_dst_layout = dst_layout;
if dst_layout == Layout::Cmyka {
core_dst_layout = Layout::Rgba;
}
let handle = self.create_transform_nbit::<u16, 10, 65536, 8192>(
core_src_layout,
dst_pr,
core_dst_layout,
options,
)?;
if core_src_layout == Layout::Cmyka {
return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
} else if core_dst_layout == Layout::Cmyka {
return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
}
Ok(handle)
}
#[cfg(feature = "in_place")]
pub fn create_in_place_transform_10bit(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
self.create_transform_in_place_nbit::<u16, 10, 65536, 8192>(layout, dst_pr, options)
}
pub fn create_transform_f32(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<TransformF32Executor>, CmsError> {
let mut core_src_layout = src_layout;
if src_layout == Layout::Cmyka {
core_src_layout = Layout::Rgba;
}
let mut core_dst_layout = dst_layout;
if dst_layout == Layout::Cmyka {
core_dst_layout = Layout::Rgba;
}
let handle = self.create_transform_nbit::<f32, 1, 65536, 32768>(
core_src_layout,
dst_pr,
core_dst_layout,
options,
)?;
if core_src_layout == Layout::Cmyka {
return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
} else if core_dst_layout == Layout::Cmyka {
return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
}
Ok(handle)
}
#[cfg(feature = "in_place")]
pub fn create_in_place_transform_f32(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<f32> + Send + Sync>, CmsError> {
self.create_transform_in_place_nbit::<f32, 1, 65536, 32768>(layout, dst_pr, options)
}
pub fn create_transform_f64(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<TransformF64Executor>, CmsError> {
let mut core_src_layout = src_layout;
if src_layout == Layout::Cmyka {
core_src_layout = Layout::Rgba;
}
let mut core_dst_layout = dst_layout;
if dst_layout == Layout::Cmyka {
core_dst_layout = Layout::Rgba;
}
let handle = self.create_transform_nbit::<f64, 1, 65536, 65536>(
core_src_layout,
dst_pr,
core_dst_layout,
options,
)?;
if core_src_layout == Layout::Cmyka {
return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
} else if core_dst_layout == Layout::Cmyka {
return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
}
Ok(handle)
}
#[cfg(feature = "in_place")]
pub fn create_in_place_transform_f64(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<f64> + Send + Sync>, CmsError> {
self.create_transform_in_place_nbit::<f64, 1, 65536, 65536>(layout, dst_pr, options)
}
#[cfg(feature = "in_place")]
fn create_transform_in_place_nbit<
T: Copy
+ Default
+ AsPrimitive<usize>
+ PointeeSizeExpressible
+ Send
+ Sync
+ AsPrimitive<f32>
+ RgbXyzFactory<T>
+ RgbXyzFactoryOpt<T>
+ GammaLutInterpolate,
const BIT_DEPTH: usize,
const LINEAR_CAP: usize,
const GAMMA_CAP: usize,
>(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<T> + Send + Sync>, CmsError>
where
f32: AsPrimitive<T>,
u32: AsPrimitive<T>,
{
let is_rgb_transform = self.color_space == DataColorSpace::Rgb
&& dst_pr.pcs == DataColorSpace::Xyz
&& dst_pr.color_space == DataColorSpace::Rgb
&& self.pcs == DataColorSpace::Xyz
&& self.is_matrix_shaper()
&& dst_pr.is_matrix_shaper();
let is_gray_transform = (self.color_space == DataColorSpace::Gray
&& self.gray_trc.is_some())
&& (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
&& self.pcs == DataColorSpace::Xyz
&& dst_pr.pcs == DataColorSpace::Xyz;
if is_rgb_transform {
let transform = self.transform_matrix(dst_pr);
if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.red_trc,
options.allow_use_cicp_transfer,
)?;
let profile_transform = TransformMatrixShaperOptimized {
linear,
gamma,
adaptation_matrix: transform.to_f32(),
};
return T::make_in_place_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
layout,
profile_transform,
options,
);
}
let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.red_trc,
options.allow_use_cicp_transfer,
)?;
let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.green_trc,
options.allow_use_cicp_transfer,
)?;
let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.blue_trc,
options.allow_use_cicp_transfer,
)?;
let profile_transform = TransformMatrixShaper {
r_linear: lin_r,
g_linear: lin_g,
b_linear: lin_b,
r_gamma: gamma_r,
g_gamma: gamma_g,
b_gamma: gamma_b,
adaptation_matrix: transform.to_f32(),
};
return T::make_in_place_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
layout,
profile_transform,
options,
);
}
if is_gray_transform {
let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.gray_trc,
options.allow_use_cicp_transfer,
)?;
use crate::conversions::make_gray_to_gray_in_place;
return make_gray_to_gray_in_place::<T, LINEAR_CAP>(
layout,
&gray_linear,
&gray_gamma,
BIT_DEPTH,
GAMMA_CAP,
);
}
Err(CmsError::UnsupportedProfileConnection)
}
fn create_transform_nbit<
T: Copy
+ Default
+ AsPrimitive<usize>
+ PointeeSizeExpressible
+ Send
+ Sync
+ AsPrimitive<f32>
+ RgbXyzFactory<T>
+ RgbXyzFactoryOpt<T>
+ GammaLutInterpolate,
const BIT_DEPTH: usize,
const LINEAR_CAP: usize,
const GAMMA_CAP: usize,
>(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<dyn TransformExecutor<T> + Send + Sync>, CmsError>
where
f32: AsPrimitive<T>,
u32: AsPrimitive<T>,
(): LutBarycentricReduction<T, u8>,
(): LutBarycentricReduction<T, u16>,
{
if self.color_space == DataColorSpace::Rgb
&& dst_pr.pcs == DataColorSpace::Xyz
&& dst_pr.color_space == DataColorSpace::Rgb
&& self.pcs == DataColorSpace::Xyz
&& self.is_matrix_shaper()
&& dst_pr.is_matrix_shaper()
{
if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
return Err(CmsError::InvalidLayout);
}
if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
return Err(CmsError::InvalidLayout);
}
#[cfg(feature = "lut")]
if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
src_layout, self, dst_layout, dst_pr, options,
);
}
let transform = self.transform_matrix(dst_pr);
#[cfg(feature = "extended_range")]
if !T::FINITE && options.allow_extended_range_rgb_xyz {
if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
use crate::conversions::{
TransformShaperFloatInOut, make_rgb_xyz_rgb_transform_float_in_out,
};
use std::marker::PhantomData;
let p = TransformShaperFloatInOut {
linear_evaluator,
gamma_evaluator,
adaptation_matrix: transform.to_f32(),
phantom_data: PhantomData,
};
return make_rgb_xyz_rgb_transform_float_in_out::<T>(
src_layout, dst_layout, p, BIT_DEPTH,
);
}
let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
use crate::conversions::{
TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float,
};
use std::marker::PhantomData;
let p = TransformShaperRgbFloat {
r_linear: lin_r,
g_linear: lin_g,
b_linear: lin_b,
gamma_evaluator,
adaptation_matrix: transform.to_f32(),
phantom_data: PhantomData,
};
return make_rgb_xyz_rgb_transform_float::<T, LINEAR_CAP>(
src_layout, dst_layout, p, BIT_DEPTH,
);
}
}
if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.red_trc,
options.allow_use_cicp_transfer,
)?;
let profile_transform = TransformMatrixShaperOptimized {
linear,
gamma,
adaptation_matrix: transform.to_f32(),
};
return T::make_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
src_layout,
dst_layout,
profile_transform,
options,
);
}
let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.red_trc,
options.allow_use_cicp_transfer,
)?;
let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.green_trc,
options.allow_use_cicp_transfer,
)?;
let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.blue_trc,
options.allow_use_cicp_transfer,
)?;
let profile_transform = TransformMatrixShaper {
r_linear: lin_r,
g_linear: lin_g,
b_linear: lin_b,
r_gamma: gamma_r,
g_gamma: gamma_g,
b_gamma: gamma_b,
adaptation_matrix: transform.to_f32(),
};
T::make_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
src_layout,
dst_layout,
profile_transform,
options,
)
} else if (self.color_space == DataColorSpace::Gray && self.gray_trc.is_some())
&& (dst_pr.color_space == DataColorSpace::Rgb
|| (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some()))
&& self.pcs == DataColorSpace::Xyz
&& dst_pr.pcs == DataColorSpace::Xyz
{
if src_layout != Layout::GrayAlpha && src_layout != Layout::Gray {
return Err(CmsError::InvalidLayout);
}
#[cfg(feature = "lut")]
if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
src_layout, self, dst_layout, dst_pr, options,
);
}
let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
if dst_pr.color_space == DataColorSpace::Gray {
#[cfg(feature = "extended_range")]
if !T::FINITE && options.allow_extended_range_rgb_xyz {
if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
use crate::conversions::make_gray_to_one_trc_extended;
return make_gray_to_one_trc_extended::<T>(
src_layout,
dst_layout,
linear_evaluator,
gamma_evaluator,
BIT_DEPTH,
);
}
}
}
let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.gray_trc,
options.allow_use_cicp_transfer,
)?;
make_gray_to_x::<T, LINEAR_CAP>(
src_layout,
dst_layout,
&gray_linear,
&gray_gamma,
BIT_DEPTH,
GAMMA_CAP,
)
} else {
#[allow(clippy::collapsible_if)]
if dst_pr.are_all_trc_the_same() {
#[cfg(feature = "extended_range")]
if !T::FINITE && options.allow_extended_range_rgb_xyz {
if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
if let Some(linear_evaluator) =
self.try_extended_linearizing_evaluator()
{
use crate::conversions::make_gray_to_one_trc_extended;
return make_gray_to_one_trc_extended::<T>(
src_layout,
dst_layout,
linear_evaluator,
gamma_evaluator,
BIT_DEPTH,
);
}
}
}
let rgb_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.red_trc,
options.allow_use_cicp_transfer,
)?;
make_gray_to_x::<T, LINEAR_CAP>(
src_layout,
dst_layout,
&gray_linear,
&rgb_gamma,
BIT_DEPTH,
GAMMA_CAP,
)
} else {
#[cfg(feature = "extended_range")]
if !T::FINITE && options.allow_extended_range_rgb_xyz {
if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
if let Some(linear_evaluator) =
self.try_extended_linearizing_evaluator()
{
use crate::conversions::make_gray_to_rgb_extended;
return make_gray_to_rgb_extended::<T>(
src_layout,
dst_layout,
linear_evaluator,
gamma_evaluator,
BIT_DEPTH,
);
}
}
}
let red_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.red_trc,
options.allow_use_cicp_transfer,
)?;
let green_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.green_trc,
options.allow_use_cicp_transfer,
)?;
let blue_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.blue_trc,
options.allow_use_cicp_transfer,
)?;
let mut gray_linear2 = Box::new([0f32; 65536]);
for (dst, src) in gray_linear2.iter_mut().zip(gray_linear.iter()) {
*dst = *src;
}
make_gray_to_unfused::<T, LINEAR_CAP>(
src_layout,
dst_layout,
gray_linear2,
red_gamma,
green_gamma,
blue_gamma,
BIT_DEPTH,
GAMMA_CAP,
)
}
}
} else if self.color_space == DataColorSpace::Rgb
&& (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
&& dst_pr.pcs == DataColorSpace::Xyz
&& self.pcs == DataColorSpace::Xyz
{
if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
return Err(CmsError::InvalidLayout);
}
if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha {
return Err(CmsError::InvalidLayout);
}
#[cfg(feature = "lut")]
if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
src_layout, self, dst_layout, dst_pr, options,
);
}
let transform = self.transform_matrix(dst_pr).to_f32();
let vector = Vector3f {
v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]],
};
#[cfg(feature = "extended_range")]
if !T::FINITE && options.allow_extended_range_rgb_xyz {
if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
use crate::conversions::make_rgb_to_gray_extended;
return make_rgb_to_gray_extended::<T>(
src_layout,
dst_layout,
linear_evaluator,
gamma_evaluator,
vector,
BIT_DEPTH,
);
}
}
}
let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
options.allow_use_cicp_transfer,
)?;
let gray_linear = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
&dst_pr.gray_trc,
options.allow_use_cicp_transfer,
)?;
let trc_box = ToneReproductionRgbToGray::<T, LINEAR_CAP> {
r_linear: lin_r,
g_linear: lin_g,
b_linear: lin_b,
gray_gamma: gray_linear,
};
make_rgb_to_gray::<T, LINEAR_CAP>(
src_layout, dst_layout, trc_box, vector, GAMMA_CAP, BIT_DEPTH,
)
} else if (self.color_space.is_three_channels()
|| self.color_space == DataColorSpace::Cmyk
|| self.color_space == DataColorSpace::Color4)
&& (dst_pr.color_space.is_three_channels()
|| dst_pr.color_space == DataColorSpace::Cmyk
|| dst_pr.color_space == DataColorSpace::Color4)
&& (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab)
&& (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab)
{
#[cfg(feature = "lut")]
{
if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
return Err(CmsError::InvalidLayout);
}
if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
return Err(CmsError::InvalidLayout);
}
make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
src_layout, self, dst_layout, dst_pr, options,
)
}
#[cfg(not(feature = "lut"))]
{
Err(CmsError::UnsupportedProfileConnection)
}
} else {
#[cfg(feature = "lut")]
{
make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
src_layout, self, dst_layout, dst_pr, options,
)
}
#[cfg(not(feature = "lut"))]
{
Err(CmsError::UnsupportedProfileConnection)
}
}
}
pub fn create_transform_8bit(
&self,
src_layout: Layout,
dst_pr: &ColorProfile,
dst_layout: Layout,
options: TransformOptions,
) -> Result<Arc<Transform8BitExecutor>, CmsError> {
self.create_transform_nbit::<u8, 8, 256, 4096>(src_layout, dst_pr, dst_layout, options)
}
#[cfg(feature = "in_place")]
pub fn create_in_place_transform_8bit(
&self,
layout: Layout,
dst_pr: &ColorProfile,
options: TransformOptions,
) -> Result<Arc<dyn InPlaceTransformExecutor<u8> + Send + Sync>, CmsError> {
self.create_transform_in_place_nbit::<u8, 8, 256, 4096>(layout, dst_pr, options)
}
#[allow(unused)]
pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
match intent {
RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(),
RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(),
}
}
#[allow(unused)]
pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
match intent {
RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(),
RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(),
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
use rand::RngExt;
#[test]
fn test_transform_rgb8() {
let mut srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..255);
let transform = bt2020_profile
.create_transform_8bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256 * 3];
let mut dst = vec![random_point_x; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
let transform = bt2020_profile
.create_transform_8bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions {
..TransformOptions::default()
},
)
.unwrap();
transform.transform(&src, &mut dst).unwrap();
srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric;
let transform = bt2020_profile
.create_transform_8bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions {
..TransformOptions::default()
},
)
.unwrap();
transform.transform(&src, &mut dst).unwrap();
srgb_profile.rendering_intent = RenderingIntent::Saturation;
let transform = bt2020_profile
.create_transform_8bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions {
..TransformOptions::default()
},
)
.unwrap();
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_rgba8() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..255);
let transform = bt2020_profile
.create_transform_8bit(
Layout::Rgba,
&srgb_profile,
Layout::Rgba,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256 * 4];
let mut dst = vec![random_point_x; 256 * 256 * 4];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_gray_to_rgb8() {
let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32);
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..255);
let transform = gray_profile
.create_transform_8bit(
Layout::Gray,
&bt2020_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256];
let mut dst = vec![random_point_x; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_gray_to_rgba8() {
let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..255);
let transform = srgb_profile
.create_transform_8bit(
Layout::Gray,
&bt2020_profile,
Layout::Rgba,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256];
let mut dst = vec![random_point_x; 256 * 256 * 4];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_gray_to_gray_alpha8() {
let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..255);
let transform = srgb_profile
.create_transform_8bit(
Layout::Gray,
&bt2020_profile,
Layout::GrayAlpha,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256];
let mut dst = vec![random_point_x; 256 * 256 * 2];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_rgb10() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..((1 << 10) - 1));
let transform = bt2020_profile
.create_transform_10bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256 * 3];
let mut dst = vec![random_point_x; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_rgb12() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..((1 << 12) - 1));
let transform = bt2020_profile
.create_transform_12bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256 * 3];
let mut dst = vec![random_point_x; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_rgb16() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16;
let transform = bt2020_profile
.create_transform_16bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let src = vec![random_point_x; 256 * 256 * 3];
let mut dst = vec![random_point_x; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
}
#[test]
fn test_transform_round_trip_rgb8() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let transform = srgb_profile
.create_transform_8bit(
Layout::Rgb,
&bt2020_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let mut src = vec![0u8; 256 * 256 * 3];
for dst in src.chunks_exact_mut(3) {
dst[0] = 175;
dst[1] = 75;
dst[2] = 13;
}
let mut dst = vec![0u8; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
let transform_inverse = bt2020_profile
.create_transform_8bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
transform_inverse.transform(&dst, &mut src).unwrap();
for src in src.chunks_exact_mut(3) {
let diff0 = (src[0] as i32 - 175).abs();
let diff1 = (src[1] as i32 - 75).abs();
let diff2 = (src[2] as i32 - 13).abs();
assert!(
diff0 < 3,
"On channel 0 difference should be less than 3, but it was {diff0}"
);
assert!(
diff1 < 3,
"On channel 1 difference should be less than 3, but it was {diff1}"
);
assert!(
diff2 < 3,
"On channel 2 difference should be less than 3, but it was {diff2}"
);
}
}
#[test]
fn test_transform_round_trip_rgb10() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let transform = srgb_profile
.create_transform_10bit(
Layout::Rgb,
&bt2020_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let mut src = vec![0u16; 256 * 256 * 3];
for dst in src.chunks_exact_mut(3) {
dst[0] = 175;
dst[1] = 256;
dst[2] = 512;
}
let mut dst = vec![0u16; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
let transform_inverse = bt2020_profile
.create_transform_10bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
transform_inverse.transform(&dst, &mut src).unwrap();
for src in src.chunks_exact_mut(3) {
let diff0 = (src[0] as i32 - 175).abs();
let diff1 = (src[1] as i32 - 256).abs();
let diff2 = (src[2] as i32 - 512).abs();
assert!(
diff0 < 15,
"On channel 0 difference should be less than 15, but it was {diff0}"
);
assert!(
diff1 < 15,
"On channel 1 difference should be less than 15, but it was {diff1}"
);
assert!(
diff2 < 15,
"On channel 2 difference should be less than 15, but it was {diff2}"
);
}
}
#[test]
fn test_transform_round_trip_rgb12() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let transform = srgb_profile
.create_transform_12bit(
Layout::Rgb,
&bt2020_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let mut src = vec![0u16; 256 * 256 * 3];
for dst in src.chunks_exact_mut(3) {
dst[0] = 1750;
dst[1] = 2560;
dst[2] = 3143;
}
let mut dst = vec![0u16; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
let transform_inverse = bt2020_profile
.create_transform_12bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
transform_inverse.transform(&dst, &mut src).unwrap();
for src in src.chunks_exact_mut(3) {
let diff0 = (src[0] as i32 - 1750).abs();
let diff1 = (src[1] as i32 - 2560).abs();
let diff2 = (src[2] as i32 - 3143).abs();
assert!(
diff0 < 25,
"On channel 0 difference should be less than 25, but it was {diff0}"
);
assert!(
diff1 < 25,
"On channel 1 difference should be less than 25, but it was {diff1}"
);
assert!(
diff2 < 25,
"On channel 2 difference should be less than 25, but it was {diff2}"
);
}
}
#[test]
fn test_transform_round_trip_rgb16() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let transform = srgb_profile
.create_transform_16bit(
Layout::Rgb,
&bt2020_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
let mut src = vec![0u16; 256 * 256 * 3];
for dst in src.chunks_exact_mut(3) {
dst[0] = 1760;
dst[1] = 2560;
dst[2] = 5120;
}
let mut dst = vec![0u16; 256 * 256 * 3];
transform.transform(&src, &mut dst).unwrap();
let transform_inverse = bt2020_profile
.create_transform_16bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions::default(),
)
.unwrap();
transform_inverse.transform(&dst, &mut src).unwrap();
for src in src.chunks_exact_mut(3) {
let diff0 = (src[0] as i32 - 1760).abs();
let diff1 = (src[1] as i32 - 2560).abs();
let diff2 = (src[2] as i32 - 5120).abs();
assert!(
diff0 < 35,
"On channel 0 difference should be less than 35, but it was {diff0}"
);
assert!(
diff1 < 35,
"On channel 1 difference should be less than 35, but it was {diff1}"
);
assert!(
diff2 < 35,
"On channel 2 difference should be less than 35, but it was {diff2}"
);
}
}
#[test]
#[cfg(feature = "extended_range")]
fn test_transform_rgb_to_gray_extended() {
let srgb = ColorProfile::new_srgb();
let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0);
gray_profile.color_space = DataColorSpace::Gray;
gray_profile.gray_trc = srgb.red_trc.clone();
let mut test_profile = vec![0.; 4];
test_profile[2] = 1.;
let mut dst = vec![0.; 1];
let mut inverse = vec![0.; 4];
let cvt0 = srgb
.create_transform_f32(
Layout::Rgba,
&gray_profile,
Layout::Gray,
TransformOptions {
allow_extended_range_rgb_xyz: true,
..Default::default()
},
)
.unwrap();
cvt0.transform(&test_profile, &mut dst).unwrap();
assert!((dst[0] - 0.273046) < 1e-4);
let cvt_inverse = gray_profile
.create_transform_f32(
Layout::Gray,
&srgb,
Layout::Rgba,
TransformOptions {
allow_extended_range_rgb_xyz: false,
..Default::default()
},
)
.unwrap();
cvt_inverse.transform(&dst, &mut inverse).unwrap();
assert!((inverse[0] - 0.273002833) < 1e-4);
let cvt1 = srgb
.create_transform_f32(
Layout::Rgba,
&gray_profile,
Layout::Gray,
TransformOptions {
allow_extended_range_rgb_xyz: false,
..Default::default()
},
)
.unwrap();
cvt1.transform(&test_profile, &mut dst).unwrap();
assert!((dst[0] - 0.27307168) < 1e-5);
inverse.fill(0.);
let cvt_inverse = gray_profile
.create_transform_f32(
Layout::Gray,
&srgb,
Layout::Rgba,
TransformOptions {
allow_extended_range_rgb_xyz: true,
..Default::default()
},
)
.unwrap();
cvt_inverse.transform(&dst, &mut inverse).unwrap();
assert!((inverse[0] - 0.273002833) < 1e-4);
}
#[test]
fn test_transform_rgb8_pixel_independence() {
let srgb_profile = ColorProfile::new_srgb();
let bt2020_profile = ColorProfile::new_bt2020();
let transform = bt2020_profile
.create_transform_8bit(
Layout::Rgb,
&srgb_profile,
Layout::Rgb,
TransformOptions {
prefer_fixed_point: true,
..Default::default()
},
)
.unwrap();
let src: Vec<u8> = vec![
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, ];
let mut dst = vec![0u8; 12];
transform.transform(&src, &mut dst).unwrap();
let mut single_pixel_results = Vec::new();
for pixel in src.chunks(3) {
let mut single_dst = vec![0u8; 3];
transform.transform(pixel, &mut single_dst).unwrap();
single_pixel_results.extend(single_dst);
}
for (i, (batch, single)) in dst.iter().zip(single_pixel_results.iter()).enumerate() {
assert_eq!(
batch,
single,
"Mismatch at byte {} (pixel {}, channel {}): batch={}, single={}",
i,
i / 3,
i % 3,
batch,
single
);
}
}
}