Coverage Report

Created: 2026-01-09 07:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.7.11/src/transform.rs
Line
Count
Source
1
/*
2
 * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved.
3
 * //
4
 * // Redistribution and use in source and binary forms, with or without modification,
5
 * // are permitted provided that the following conditions are met:
6
 * //
7
 * // 1.  Redistributions of source code must retain the above copyright notice, this
8
 * // list of conditions and the following disclaimer.
9
 * //
10
 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11
 * // this list of conditions and the following disclaimer in the documentation
12
 * // and/or other materials provided with the distribution.
13
 * //
14
 * // 3.  Neither the name of the copyright holder nor the names of its
15
 * // contributors may be used to endorse or promote products derived from
16
 * // this software without specific prior written permission.
17
 * //
18
 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29
use crate::conversions::{
30
    LutBarycentricReduction, RgbXyzFactory, RgbXyzFactoryOpt, ToneReproductionRgbToGray,
31
    TransformMatrixShaper, make_gray_to_unfused, make_gray_to_x, make_lut_transform,
32
    make_rgb_to_gray,
33
};
34
use crate::err::CmsError;
35
use crate::trc::GammaLutInterpolate;
36
use crate::{ColorProfile, DataColorSpace, LutWarehouse, RenderingIntent, Vector3f, Xyzd};
37
use num_traits::AsPrimitive;
38
use std::marker::PhantomData;
39
40
/// Transformation executor itself
41
pub trait TransformExecutor<V: Copy + Default> {
42
    /// Count of samples always must match.
43
    /// If there is N samples of *Cmyk* source then N samples of *Rgb* is expected as an output.
44
    fn transform(&self, src: &[V], dst: &mut [V]) -> Result<(), CmsError>;
45
}
46
47
/// Helper for intermediate transformation stages
48
pub trait Stage {
49
    fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError>;
50
}
51
52
/// Helper for intermediate transformation stages
53
pub trait InPlaceStage {
54
    fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError>;
55
}
56
57
/// Barycentric interpolation weights size.
58
///
59
/// Bigger weights increases precision.
60
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
61
pub enum BarycentricWeightScale {
62
    #[default]
63
    /// Low scale weights is enough for common case.
64
    ///
65
    /// However, it might crush dark zones and gradients.
66
    /// Weights increasing costs 5% performance.
67
    Low,
68
    #[cfg(feature = "options")]
69
    High,
70
}
71
72
/// Declares additional transformation options
73
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
74
pub struct TransformOptions {
75
    pub rendering_intent: RenderingIntent,
76
    /// If set it will try to use Transfer Characteristics from CICP
77
    /// on transform. This might be more precise and faster.
78
    pub allow_use_cicp_transfer: bool,
79
    /// Prefers fixed point where implemented as default.
80
    /// Most of the applications actually do not need floating point.
81
    ///
82
    /// Do not change it if you're not sure that extreme precision is required,
83
    /// in most cases it is a simple way to spend energy to warming up environment
84
    /// a little.
85
    ///
86
    /// Q2.13 for RGB->XYZ->RGB is used.
87
    /// LUT interpolation use Q0.15.
88
    pub prefer_fixed_point: bool,
89
    /// Interpolation method for 3D LUT
90
    ///
91
    /// This parameter has no effect on LAB/XYZ interpolation and scene linear RGB.
92
    ///
93
    /// Technically, it should be assumed to perform cube dividing interpolation:
94
    /// - Source colorspace is gamma-encoded (discards scene linear RGB and XYZ).
95
    /// - Colorspace is uniform.
96
    /// - Colorspace has linear scaling (discards LAB).
97
    /// - Interpolation doesn't shift hues (discards LAB).
98
    ///
99
    /// For LAB, XYZ and scene linear RGB `trilinear/quadlinear` always in force.
100
    pub interpolation_method: InterpolationMethod,
101
    /// Barycentric weights scale.
102
    ///
103
    /// This value controls LUT weights precision.
104
    pub barycentric_weight_scale: BarycentricWeightScale,
105
    /// For floating points transform, it will try to detect gamma function on *Matrix Shaper* profiles.
106
    /// If gamma function is found, then it will be used instead of LUT table.
107
    /// This allows to work with excellent precision with extended range,
108
    /// at a cost of execution time.
109
    pub allow_extended_range_rgb_xyz: bool,
110
    // pub black_point_compensation: bool,
111
}
112
113
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
114
/// Defines the interpolation method.
115
///
116
/// All methods produce very close results that almost not possible to separate without
117
/// some automation tools.
118
///
119
/// This implementation chooses the fastest method as default.
120
pub enum InterpolationMethod {
121
    /// General Tetrahedron interpolation.
122
    /// This is used in lcms2 and others CMS.
123
    #[cfg(feature = "options")]
124
    Tetrahedral,
125
    /// Divides cube into a pyramids and interpolate then in the pyramid.
126
    #[cfg(feature = "options")]
127
    Pyramid,
128
    /// Interpolation by dividing cube into prisms.
129
    #[cfg(feature = "options")]
130
    Prism,
131
    /// Trilinear/Quadlinear interpolation
132
    #[default]
133
    Linear,
134
}
135
136
impl Default for TransformOptions {
137
0
    fn default() -> Self {
138
0
        Self {
139
0
            rendering_intent: RenderingIntent::default(),
140
0
            allow_use_cicp_transfer: true,
141
0
            prefer_fixed_point: true,
142
0
            interpolation_method: InterpolationMethod::default(),
143
0
            barycentric_weight_scale: BarycentricWeightScale::default(),
144
0
            allow_extended_range_rgb_xyz: false,
145
0
            // black_point_compensation: false,
146
0
        }
147
0
    }
148
}
149
150
pub type Transform8BitExecutor = dyn TransformExecutor<u8> + Send + Sync;
151
pub type Transform16BitExecutor = dyn TransformExecutor<u16> + Send + Sync;
152
pub type TransformF32BitExecutor = dyn TransformExecutor<f32> + Send + Sync;
153
pub type TransformF64BitExecutor = dyn TransformExecutor<f64> + Send + Sync;
154
155
/// Layout declares a data layout.
156
/// For RGB it shows also the channel order.
157
/// To handle different data bit-depth appropriate executor must be used.
158
/// Cmyk8 uses the same layout as Rgba8.
159
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
160
pub enum Layout {
161
    Rgb = 0,
162
    Rgba = 1,
163
    Gray = 2,
164
    GrayAlpha = 3,
165
    Inks5 = 4,
166
    Inks6 = 5,
167
    Inks7 = 6,
168
    Inks8 = 7,
169
    Inks9 = 8,
170
    Inks10 = 9,
171
    Inks11 = 10,
172
    Inks12 = 11,
173
    Inks13 = 12,
174
    Inks14 = 13,
175
    Inks15 = 14,
176
}
177
178
impl Layout {
179
    /// Returns Red channel index
180
    #[inline(always)]
181
0
    pub const fn r_i(self) -> usize {
182
0
        match self {
183
0
            Layout::Rgb => 0,
184
0
            Layout::Rgba => 0,
185
0
            Layout::Gray => unimplemented!(),
186
0
            Layout::GrayAlpha => unimplemented!(),
187
0
            _ => unimplemented!(),
188
        }
189
0
    }
190
191
    /// Returns Green channel index
192
    #[inline(always)]
193
0
    pub const fn g_i(self) -> usize {
194
0
        match self {
195
0
            Layout::Rgb => 1,
196
0
            Layout::Rgba => 1,
197
0
            Layout::Gray => unimplemented!(),
198
0
            Layout::GrayAlpha => unimplemented!(),
199
0
            _ => unimplemented!(),
200
        }
201
0
    }
202
203
    /// Returns Blue channel index
204
    #[inline(always)]
205
0
    pub const fn b_i(self) -> usize {
206
0
        match self {
207
0
            Layout::Rgb => 2,
208
0
            Layout::Rgba => 2,
209
0
            Layout::Gray => unimplemented!(),
210
0
            Layout::GrayAlpha => unimplemented!(),
211
0
            _ => unimplemented!(),
212
        }
213
0
    }
214
215
    #[inline(always)]
216
0
    pub const fn a_i(self) -> usize {
217
0
        match self {
218
0
            Layout::Rgb => unimplemented!(),
219
0
            Layout::Rgba => 3,
220
0
            Layout::Gray => unimplemented!(),
221
0
            Layout::GrayAlpha => 1,
222
0
            _ => unimplemented!(),
223
        }
224
0
    }
225
226
    #[inline(always)]
227
0
    pub const fn has_alpha(self) -> bool {
228
0
        match self {
229
0
            Layout::Rgb => false,
230
0
            Layout::Rgba => true,
231
0
            Layout::Gray => false,
232
0
            Layout::GrayAlpha => true,
233
0
            _ => false,
234
        }
235
0
    }
236
237
    #[inline]
238
0
    pub const fn channels(self) -> usize {
239
0
        match self {
240
0
            Layout::Rgb => 3,
241
0
            Layout::Rgba => 4,
242
0
            Layout::Gray => 1,
243
0
            Layout::GrayAlpha => 2,
244
0
            Layout::Inks5 => 5,
245
0
            Layout::Inks6 => 6,
246
0
            Layout::Inks7 => 7,
247
0
            Layout::Inks8 => 8,
248
0
            Layout::Inks9 => 9,
249
0
            Layout::Inks10 => 10,
250
0
            Layout::Inks11 => 11,
251
0
            Layout::Inks12 => 12,
252
0
            Layout::Inks13 => 13,
253
0
            Layout::Inks14 => 14,
254
0
            Layout::Inks15 => 15,
255
        }
256
0
    }
257
258
0
    pub(crate) fn from_inks(inks: usize) -> Self {
259
0
        match inks {
260
0
            1 => Layout::Gray,
261
0
            2 => Layout::GrayAlpha,
262
0
            3 => Layout::Rgb,
263
0
            4 => Layout::Rgba,
264
0
            5 => Layout::Inks5,
265
0
            6 => Layout::Inks6,
266
0
            7 => Layout::Inks7,
267
0
            8 => Layout::Inks8,
268
0
            9 => Layout::Inks9,
269
0
            10 => Layout::Inks10,
270
0
            11 => Layout::Inks11,
271
0
            12 => Layout::Inks12,
272
0
            13 => Layout::Inks13,
273
0
            14 => Layout::Inks14,
274
0
            15 => Layout::Inks15,
275
0
            _ => unreachable!("Impossible amount of inks"),
276
        }
277
0
    }
278
}
279
280
impl From<u8> for Layout {
281
0
    fn from(value: u8) -> Self {
282
0
        match value {
283
0
            0 => Layout::Rgb,
284
0
            1 => Layout::Rgba,
285
0
            2 => Layout::Gray,
286
0
            3 => Layout::GrayAlpha,
287
0
            _ => unimplemented!(),
288
        }
289
0
    }
290
}
291
292
impl Layout {
293
    #[inline(always)]
294
0
    pub const fn resolve(value: u8) -> Self {
295
0
        match value {
296
0
            0 => Layout::Rgb,
297
0
            1 => Layout::Rgba,
298
0
            2 => Layout::Gray,
299
0
            3 => Layout::GrayAlpha,
300
0
            4 => Layout::Inks5,
301
0
            5 => Layout::Inks6,
302
0
            6 => Layout::Inks7,
303
0
            7 => Layout::Inks8,
304
0
            8 => Layout::Inks9,
305
0
            9 => Layout::Inks10,
306
0
            10 => Layout::Inks11,
307
0
            11 => Layout::Inks12,
308
0
            12 => Layout::Inks13,
309
0
            13 => Layout::Inks14,
310
0
            14 => Layout::Inks15,
311
0
            _ => unimplemented!(),
312
        }
313
0
    }
314
}
315
316
#[doc(hidden)]
317
pub trait PointeeSizeExpressible {
318
    fn _as_usize(self) -> usize;
319
    const FINITE: bool;
320
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize;
321
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize;
322
    const IS_U8: bool;
323
    const IS_U16: bool;
324
}
325
326
impl PointeeSizeExpressible for u8 {
327
    #[inline(always)]
328
0
    fn _as_usize(self) -> usize {
329
0
        self as usize
330
0
    }
331
332
    const FINITE: bool = true;
333
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
334
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
335
    const IS_U8: bool = true;
336
    const IS_U16: bool = false;
337
}
338
339
impl PointeeSizeExpressible for u16 {
340
    #[inline(always)]
341
0
    fn _as_usize(self) -> usize {
342
0
        self as usize
343
0
    }
344
345
    const FINITE: bool = true;
346
347
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
348
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
349
350
    const IS_U8: bool = false;
351
    const IS_U16: bool = true;
352
}
353
354
impl PointeeSizeExpressible for f32 {
355
    #[inline(always)]
356
0
    fn _as_usize(self) -> usize {
357
        const MAX_14_BIT: f32 = ((1 << 14u32) - 1) as f32;
358
0
        ((self * MAX_14_BIT).max(0f32).min(MAX_14_BIT) as u16) as usize
359
0
    }
360
361
    const FINITE: bool = false;
362
363
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 32768;
364
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 14u32;
365
    const IS_U8: bool = false;
366
    const IS_U16: bool = false;
367
}
368
369
impl PointeeSizeExpressible for f64 {
370
    #[inline(always)]
371
0
    fn _as_usize(self) -> usize {
372
        const MAX_16_BIT: f64 = ((1 << 16u32) - 1) as f64;
373
0
        ((self * MAX_16_BIT).max(0.).min(MAX_16_BIT) as u16) as usize
374
0
    }
375
376
    const FINITE: bool = false;
377
378
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 65536;
379
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 16;
380
    const IS_U8: bool = false;
381
    const IS_U16: bool = false;
382
}
383
384
impl ColorProfile {
385
    /// Checks if profile is valid *Matrix Shaper* profile
386
0
    pub fn is_matrix_shaper(&self) -> bool {
387
0
        self.color_space == DataColorSpace::Rgb
388
0
            && self.red_colorant != Xyzd::default()
389
0
            && self.green_colorant != Xyzd::default()
390
0
            && self.blue_colorant != Xyzd::default()
391
0
            && self.red_trc.is_some()
392
0
            && self.green_trc.is_some()
393
0
            && self.blue_trc.is_some()
394
0
    }
395
396
    /// Creates transform between source and destination profile
397
    /// Use for 16 bit-depth data bit-depth only.
398
0
    pub fn create_transform_16bit(
399
0
        &self,
400
0
        src_layout: Layout,
401
0
        dst_pr: &ColorProfile,
402
0
        dst_layout: Layout,
403
0
        options: TransformOptions,
404
0
    ) -> Result<Box<Transform16BitExecutor>, CmsError> {
405
0
        self.create_transform_nbit::<u16, 16, 65536, 65536>(src_layout, dst_pr, dst_layout, options)
406
0
    }
407
408
    /// Creates transform between source and destination profile
409
    /// Use for 12 bit-depth data bit-depth only.
410
0
    pub fn create_transform_12bit(
411
0
        &self,
412
0
        src_layout: Layout,
413
0
        dst_pr: &ColorProfile,
414
0
        dst_layout: Layout,
415
0
        options: TransformOptions,
416
0
    ) -> Result<Box<Transform16BitExecutor>, CmsError> {
417
0
        self.create_transform_nbit::<u16, 12, 65536, 16384>(src_layout, dst_pr, dst_layout, options)
418
0
    }
419
420
    /// Creates transform between source and destination profile
421
    /// Use for 10 bit-depth data bit-depth only.
422
0
    pub fn create_transform_10bit(
423
0
        &self,
424
0
        src_layout: Layout,
425
0
        dst_pr: &ColorProfile,
426
0
        dst_layout: Layout,
427
0
        options: TransformOptions,
428
0
    ) -> Result<Box<Transform16BitExecutor>, CmsError> {
429
0
        self.create_transform_nbit::<u16, 10, 65536, 8192>(src_layout, dst_pr, dst_layout, options)
430
0
    }
431
432
    /// Creates transform between source and destination profile
433
    /// Data has to be normalized into [0, 1] range.
434
    /// ICC profiles and LUT tables do not exist in infinite precision.
435
    /// Thus, this implementation considers `f32` as 14-bit values.
436
    /// Floating point transformer works in extended mode, that means returned data might be negative
437
    /// or more than 1.
438
0
    pub fn create_transform_f32(
439
0
        &self,
440
0
        src_layout: Layout,
441
0
        dst_pr: &ColorProfile,
442
0
        dst_layout: Layout,
443
0
        options: TransformOptions,
444
0
    ) -> Result<Box<TransformF32BitExecutor>, CmsError> {
445
0
        self.create_transform_nbit::<f32, 1, 65536, 32768>(src_layout, dst_pr, dst_layout, options)
446
0
    }
447
448
    /// Creates transform between source and destination profile
449
    /// Data has to be normalized into [0, 1] range.
450
    /// ICC profiles and LUT tables do not exist in infinite precision.
451
    /// Thus, this implementation considers `f64` as 16-bit values.
452
    /// Floating point transformer works in extended mode, that means returned data might be negative
453
    /// or more than 1.
454
0
    pub fn create_transform_f64(
455
0
        &self,
456
0
        src_layout: Layout,
457
0
        dst_pr: &ColorProfile,
458
0
        dst_layout: Layout,
459
0
        options: TransformOptions,
460
0
    ) -> Result<Box<TransformF64BitExecutor>, CmsError> {
461
0
        self.create_transform_nbit::<f64, 1, 65536, 65536>(src_layout, dst_pr, dst_layout, options)
462
0
    }
463
464
0
    fn create_transform_nbit<
465
0
        T: Copy
466
0
            + Default
467
0
            + AsPrimitive<usize>
468
0
            + PointeeSizeExpressible
469
0
            + Send
470
0
            + Sync
471
0
            + AsPrimitive<f32>
472
0
            + RgbXyzFactory<T>
473
0
            + RgbXyzFactoryOpt<T>
474
0
            + GammaLutInterpolate,
475
0
        const BIT_DEPTH: usize,
476
0
        const LINEAR_CAP: usize,
477
0
        const GAMMA_CAP: usize,
478
0
    >(
479
0
        &self,
480
0
        src_layout: Layout,
481
0
        dst_pr: &ColorProfile,
482
0
        dst_layout: Layout,
483
0
        options: TransformOptions,
484
0
    ) -> Result<Box<dyn TransformExecutor<T> + Send + Sync>, CmsError>
485
0
    where
486
0
        f32: AsPrimitive<T>,
487
0
        u32: AsPrimitive<T>,
488
0
        (): LutBarycentricReduction<T, u8>,
489
0
        (): LutBarycentricReduction<T, u16>,
490
    {
491
0
        if self.color_space == DataColorSpace::Rgb
492
0
            && dst_pr.pcs == DataColorSpace::Xyz
493
0
            && dst_pr.color_space == DataColorSpace::Rgb
494
0
            && self.pcs == DataColorSpace::Xyz
495
0
            && self.is_matrix_shaper()
496
0
            && dst_pr.is_matrix_shaper()
497
        {
498
0
            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
499
0
                return Err(CmsError::InvalidLayout);
500
0
            }
501
0
            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
502
0
                return Err(CmsError::InvalidLayout);
503
0
            }
504
505
0
            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
506
0
                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
507
0
                    src_layout, self, dst_layout, dst_pr, options,
508
                );
509
0
            }
510
511
0
            let transform = self.transform_matrix(dst_pr);
512
513
0
            if !T::FINITE && options.allow_extended_range_rgb_xyz {
514
0
                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
515
0
                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
516
                        use crate::conversions::{
517
                            TransformShaperFloatInOut, make_rgb_xyz_rgb_transform_float_in_out,
518
                        };
519
0
                        let p = TransformShaperFloatInOut {
520
0
                            linear_evaluator,
521
0
                            gamma_evaluator,
522
0
                            adaptation_matrix: transform.to_f32(),
523
0
                            phantom_data: PhantomData,
524
0
                        };
525
0
                        return make_rgb_xyz_rgb_transform_float_in_out::<T>(
526
0
                            src_layout, dst_layout, p, BIT_DEPTH,
527
                        );
528
0
                    }
529
530
0
                    let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
531
0
                        options.allow_use_cicp_transfer,
532
0
                    )?;
533
0
                    let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
534
0
                        options.allow_use_cicp_transfer,
535
0
                    )?;
536
0
                    let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
537
0
                        options.allow_use_cicp_transfer,
538
0
                    )?;
539
540
                    use crate::conversions::{
541
                        TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float,
542
                    };
543
0
                    let p = TransformShaperRgbFloat {
544
0
                        r_linear: lin_r,
545
0
                        g_linear: lin_g,
546
0
                        b_linear: lin_b,
547
0
                        gamma_evaluator,
548
0
                        adaptation_matrix: transform.to_f32(),
549
0
                        phantom_data: PhantomData,
550
0
                    };
551
0
                    return make_rgb_xyz_rgb_transform_float::<T, LINEAR_CAP>(
552
0
                        src_layout, dst_layout, p, BIT_DEPTH,
553
                    );
554
0
                }
555
0
            }
556
557
0
            if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
558
0
                let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
559
0
                    options.allow_use_cicp_transfer,
560
0
                )?;
561
562
0
                let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
563
0
                    &dst_pr.red_trc,
564
0
                    options.allow_use_cicp_transfer,
565
0
                )?;
566
567
0
                let profile_transform = crate::conversions::TransformMatrixShaperOptimized {
568
0
                    linear,
569
0
                    gamma,
570
0
                    adaptation_matrix: transform.to_f32(),
571
0
                };
572
573
0
                return T::make_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
574
0
                    src_layout,
575
0
                    dst_layout,
576
0
                    profile_transform,
577
0
                    options,
578
                );
579
0
            }
580
581
0
            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
582
0
                options.allow_use_cicp_transfer,
583
0
            )?;
584
0
            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
585
0
                options.allow_use_cicp_transfer,
586
0
            )?;
587
0
            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
588
0
                options.allow_use_cicp_transfer,
589
0
            )?;
590
591
0
            let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
592
0
                &dst_pr.red_trc,
593
0
                options.allow_use_cicp_transfer,
594
0
            )?;
595
0
            let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
596
0
                &dst_pr.green_trc,
597
0
                options.allow_use_cicp_transfer,
598
0
            )?;
599
0
            let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
600
0
                &dst_pr.blue_trc,
601
0
                options.allow_use_cicp_transfer,
602
0
            )?;
603
604
0
            let profile_transform = TransformMatrixShaper {
605
0
                r_linear: lin_r,
606
0
                g_linear: lin_g,
607
0
                b_linear: lin_b,
608
0
                r_gamma: gamma_r,
609
0
                g_gamma: gamma_g,
610
0
                b_gamma: gamma_b,
611
0
                adaptation_matrix: transform.to_f32(),
612
0
            };
613
614
0
            T::make_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
615
0
                src_layout,
616
0
                dst_layout,
617
0
                profile_transform,
618
0
                options,
619
            )
620
0
        } else if (self.color_space == DataColorSpace::Gray && self.gray_trc.is_some())
621
0
            && (dst_pr.color_space == DataColorSpace::Rgb
622
0
                || (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some()))
623
0
            && self.pcs == DataColorSpace::Xyz
624
0
            && dst_pr.pcs == DataColorSpace::Xyz
625
        {
626
0
            if src_layout != Layout::GrayAlpha && src_layout != Layout::Gray {
627
0
                return Err(CmsError::InvalidLayout);
628
0
            }
629
630
0
            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
631
0
                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
632
0
                    src_layout, self, dst_layout, dst_pr, options,
633
                );
634
0
            }
635
636
0
            let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
637
638
0
            if dst_pr.color_space == DataColorSpace::Gray {
639
0
                if !T::FINITE && options.allow_extended_range_rgb_xyz {
640
0
                    if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
641
0
                        if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
642
                            // Gray -> Gray case extended range
643
                            use crate::conversions::make_gray_to_one_trc_extended;
644
0
                            return make_gray_to_one_trc_extended::<T>(
645
0
                                src_layout,
646
0
                                dst_layout,
647
0
                                linear_evaluator,
648
0
                                gamma_evaluator,
649
                                BIT_DEPTH,
650
                            );
651
0
                        }
652
0
                    }
653
0
                }
654
655
                // Gray -> Gray case
656
0
                let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
657
0
                    &dst_pr.gray_trc,
658
0
                    options.allow_use_cicp_transfer,
659
0
                )?;
660
661
0
                make_gray_to_x::<T, LINEAR_CAP>(
662
0
                    src_layout,
663
0
                    dst_layout,
664
0
                    &gray_linear,
665
0
                    &gray_gamma,
666
                    BIT_DEPTH,
667
                    GAMMA_CAP,
668
                )
669
            } else {
670
                #[allow(clippy::collapsible_if)]
671
0
                if dst_pr.are_all_trc_the_same() {
672
0
                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
673
0
                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
674
0
                            if let Some(linear_evaluator) =
675
0
                                self.try_extended_linearizing_evaluator()
676
                            {
677
                                // Gray -> RGB where all TRC is the same with extended range
678
                                use crate::conversions::make_gray_to_one_trc_extended;
679
0
                                return make_gray_to_one_trc_extended::<T>(
680
0
                                    src_layout,
681
0
                                    dst_layout,
682
0
                                    linear_evaluator,
683
0
                                    gamma_evaluator,
684
                                    BIT_DEPTH,
685
                                );
686
0
                            }
687
0
                        }
688
0
                    }
689
690
                    // Gray -> RGB where all TRC is the same
691
0
                    let rgb_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
692
0
                        &dst_pr.red_trc,
693
0
                        options.allow_use_cicp_transfer,
694
0
                    )?;
695
696
0
                    make_gray_to_x::<T, LINEAR_CAP>(
697
0
                        src_layout,
698
0
                        dst_layout,
699
0
                        &gray_linear,
700
0
                        &rgb_gamma,
701
                        BIT_DEPTH,
702
                        GAMMA_CAP,
703
                    )
704
                } else {
705
                    // Gray -> RGB where all TRC is NOT the same
706
0
                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
707
0
                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
708
0
                            if let Some(linear_evaluator) =
709
0
                                self.try_extended_linearizing_evaluator()
710
                            {
711
                                // Gray -> RGB where all TRC is NOT the same with extended range
712
713
                                use crate::conversions::make_gray_to_rgb_extended;
714
0
                                return make_gray_to_rgb_extended::<T>(
715
0
                                    src_layout,
716
0
                                    dst_layout,
717
0
                                    linear_evaluator,
718
0
                                    gamma_evaluator,
719
                                    BIT_DEPTH,
720
                                );
721
0
                            }
722
0
                        }
723
0
                    }
724
725
0
                    let red_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
726
0
                        &dst_pr.red_trc,
727
0
                        options.allow_use_cicp_transfer,
728
0
                    )?;
729
0
                    let green_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
730
0
                        &dst_pr.green_trc,
731
0
                        options.allow_use_cicp_transfer,
732
0
                    )?;
733
0
                    let blue_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
734
0
                        &dst_pr.blue_trc,
735
0
                        options.allow_use_cicp_transfer,
736
0
                    )?;
737
738
0
                    let mut gray_linear2 = Box::new([0f32; 65536]);
739
0
                    for (dst, src) in gray_linear2.iter_mut().zip(gray_linear.iter()) {
740
0
                        *dst = *src;
741
0
                    }
742
743
0
                    make_gray_to_unfused::<T, LINEAR_CAP>(
744
0
                        src_layout,
745
0
                        dst_layout,
746
0
                        gray_linear2,
747
0
                        red_gamma,
748
0
                        green_gamma,
749
0
                        blue_gamma,
750
                        BIT_DEPTH,
751
                        GAMMA_CAP,
752
                    )
753
                }
754
            }
755
0
        } else if self.color_space == DataColorSpace::Rgb
756
0
            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
757
0
            && dst_pr.pcs == DataColorSpace::Xyz
758
0
            && self.pcs == DataColorSpace::Xyz
759
        {
760
0
            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
761
0
                return Err(CmsError::InvalidLayout);
762
0
            }
763
0
            if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha {
764
0
                return Err(CmsError::InvalidLayout);
765
0
            }
766
767
0
            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
768
0
                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
769
0
                    src_layout, self, dst_layout, dst_pr, options,
770
                );
771
0
            }
772
773
0
            let transform = self.transform_matrix(dst_pr).to_f32();
774
775
0
            let vector = Vector3f {
776
0
                v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]],
777
0
            };
778
779
0
            if !T::FINITE && options.allow_extended_range_rgb_xyz {
780
0
                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
781
0
                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
782
                        use crate::conversions::make_rgb_to_gray_extended;
783
0
                        return make_rgb_to_gray_extended::<T>(
784
0
                            src_layout,
785
0
                            dst_layout,
786
0
                            linear_evaluator,
787
0
                            gamma_evaluator,
788
0
                            vector,
789
                            BIT_DEPTH,
790
                        );
791
0
                    }
792
0
                }
793
0
            }
794
795
0
            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
796
0
                options.allow_use_cicp_transfer,
797
0
            )?;
798
0
            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
799
0
                options.allow_use_cicp_transfer,
800
0
            )?;
801
0
            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
802
0
                options.allow_use_cicp_transfer,
803
0
            )?;
804
0
            let gray_linear = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
805
0
                &dst_pr.gray_trc,
806
0
                options.allow_use_cicp_transfer,
807
0
            )?;
808
809
0
            let trc_box = ToneReproductionRgbToGray::<T, LINEAR_CAP> {
810
0
                r_linear: lin_r,
811
0
                g_linear: lin_g,
812
0
                b_linear: lin_b,
813
0
                gray_gamma: gray_linear,
814
0
            };
815
816
0
            make_rgb_to_gray::<T, LINEAR_CAP>(
817
0
                src_layout, dst_layout, trc_box, vector, GAMMA_CAP, BIT_DEPTH,
818
            )
819
0
        } else if (self.color_space.is_three_channels()
820
0
            || self.color_space == DataColorSpace::Cmyk
821
0
            || self.color_space == DataColorSpace::Color4)
822
0
            && (dst_pr.color_space.is_three_channels()
823
0
                || dst_pr.color_space == DataColorSpace::Cmyk
824
0
                || dst_pr.color_space == DataColorSpace::Color4)
825
0
            && (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab)
826
0
            && (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab)
827
        {
828
0
            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
829
0
                return Err(CmsError::InvalidLayout);
830
0
            }
831
0
            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
832
0
                return Err(CmsError::InvalidLayout);
833
0
            }
834
0
            make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
835
0
                src_layout, self, dst_layout, dst_pr, options,
836
            )
837
        } else {
838
0
            make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
839
0
                src_layout, self, dst_layout, dst_pr, options,
840
            )
841
        }
842
0
    }
Unexecuted instantiation: <moxcms::profile::ColorProfile>::create_transform_nbit::<f64, 1, 65536, 65536>
Unexecuted instantiation: <moxcms::profile::ColorProfile>::create_transform_nbit::<f32, 1, 65536, 32768>
Unexecuted instantiation: <moxcms::profile::ColorProfile>::create_transform_nbit::<u8, 8, 256, 4096>
Unexecuted instantiation: <moxcms::profile::ColorProfile>::create_transform_nbit::<u16, 16, 65536, 65536>
Unexecuted instantiation: <moxcms::profile::ColorProfile>::create_transform_nbit::<u16, 10, 65536, 8192>
Unexecuted instantiation: <moxcms::profile::ColorProfile>::create_transform_nbit::<u16, 12, 65536, 16384>
843
844
    /// Creates transform between source and destination profile
845
    /// Only 8 bit is supported.
846
0
    pub fn create_transform_8bit(
847
0
        &self,
848
0
        src_layout: Layout,
849
0
        dst_pr: &ColorProfile,
850
0
        dst_layout: Layout,
851
0
        options: TransformOptions,
852
0
    ) -> Result<Box<Transform8BitExecutor>, CmsError> {
853
0
        self.create_transform_nbit::<u8, 8, 256, 4096>(src_layout, dst_pr, dst_layout, options)
854
0
    }
855
856
0
    pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
857
0
        match intent {
858
0
            RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
859
0
            RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(),
860
0
            RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
861
0
            RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(),
862
        }
863
0
    }
864
865
0
    pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
866
0
        match intent {
867
0
            RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
868
0
            RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(),
869
0
            RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
870
0
            RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(),
871
        }
872
0
    }
873
}
874
875
#[cfg(test)]
876
mod tests {
877
    use crate::{ColorProfile, DataColorSpace, Layout, RenderingIntent, TransformOptions};
878
    use rand::Rng;
879
880
    #[test]
881
    fn test_transform_rgb8() {
882
        let mut srgb_profile = ColorProfile::new_srgb();
883
        let bt2020_profile = ColorProfile::new_bt2020();
884
        let random_point_x = rand::rng().random_range(0..255);
885
        let transform = bt2020_profile
886
            .create_transform_8bit(
887
                Layout::Rgb,
888
                &srgb_profile,
889
                Layout::Rgb,
890
                TransformOptions::default(),
891
            )
892
            .unwrap();
893
        let src = vec![random_point_x; 256 * 256 * 3];
894
        let mut dst = vec![random_point_x; 256 * 256 * 3];
895
        transform.transform(&src, &mut dst).unwrap();
896
897
        let transform = bt2020_profile
898
            .create_transform_8bit(
899
                Layout::Rgb,
900
                &srgb_profile,
901
                Layout::Rgb,
902
                TransformOptions {
903
                    ..TransformOptions::default()
904
                },
905
            )
906
            .unwrap();
907
        transform.transform(&src, &mut dst).unwrap();
908
        srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric;
909
        let transform = bt2020_profile
910
            .create_transform_8bit(
911
                Layout::Rgb,
912
                &srgb_profile,
913
                Layout::Rgb,
914
                TransformOptions {
915
                    ..TransformOptions::default()
916
                },
917
            )
918
            .unwrap();
919
        transform.transform(&src, &mut dst).unwrap();
920
        srgb_profile.rendering_intent = RenderingIntent::Saturation;
921
        let transform = bt2020_profile
922
            .create_transform_8bit(
923
                Layout::Rgb,
924
                &srgb_profile,
925
                Layout::Rgb,
926
                TransformOptions {
927
                    ..TransformOptions::default()
928
                },
929
            )
930
            .unwrap();
931
        transform.transform(&src, &mut dst).unwrap();
932
    }
933
934
    #[test]
935
    fn test_transform_rgba8() {
936
        let srgb_profile = ColorProfile::new_srgb();
937
        let bt2020_profile = ColorProfile::new_bt2020();
938
        let random_point_x = rand::rng().random_range(0..255);
939
        let transform = bt2020_profile
940
            .create_transform_8bit(
941
                Layout::Rgba,
942
                &srgb_profile,
943
                Layout::Rgba,
944
                TransformOptions::default(),
945
            )
946
            .unwrap();
947
        let src = vec![random_point_x; 256 * 256 * 4];
948
        let mut dst = vec![random_point_x; 256 * 256 * 4];
949
        transform.transform(&src, &mut dst).unwrap();
950
    }
951
952
    #[test]
953
    fn test_transform_gray_to_rgb8() {
954
        let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32);
955
        let bt2020_profile = ColorProfile::new_bt2020();
956
        let random_point_x = rand::rng().random_range(0..255);
957
        let transform = gray_profile
958
            .create_transform_8bit(
959
                Layout::Gray,
960
                &bt2020_profile,
961
                Layout::Rgb,
962
                TransformOptions::default(),
963
            )
964
            .unwrap();
965
        let src = vec![random_point_x; 256 * 256];
966
        let mut dst = vec![random_point_x; 256 * 256 * 3];
967
        transform.transform(&src, &mut dst).unwrap();
968
    }
969
970
    #[test]
971
    fn test_transform_gray_to_rgba8() {
972
        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
973
        let bt2020_profile = ColorProfile::new_bt2020();
974
        let random_point_x = rand::rng().random_range(0..255);
975
        let transform = srgb_profile
976
            .create_transform_8bit(
977
                Layout::Gray,
978
                &bt2020_profile,
979
                Layout::Rgba,
980
                TransformOptions::default(),
981
            )
982
            .unwrap();
983
        let src = vec![random_point_x; 256 * 256];
984
        let mut dst = vec![random_point_x; 256 * 256 * 4];
985
        transform.transform(&src, &mut dst).unwrap();
986
    }
987
988
    #[test]
989
    fn test_transform_gray_to_gray_alpha8() {
990
        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
991
        let bt2020_profile = ColorProfile::new_bt2020();
992
        let random_point_x = rand::rng().random_range(0..255);
993
        let transform = srgb_profile
994
            .create_transform_8bit(
995
                Layout::Gray,
996
                &bt2020_profile,
997
                Layout::GrayAlpha,
998
                TransformOptions::default(),
999
            )
1000
            .unwrap();
1001
        let src = vec![random_point_x; 256 * 256];
1002
        let mut dst = vec![random_point_x; 256 * 256 * 2];
1003
        transform.transform(&src, &mut dst).unwrap();
1004
    }
1005
1006
    #[test]
1007
    fn test_transform_rgb10() {
1008
        let srgb_profile = ColorProfile::new_srgb();
1009
        let bt2020_profile = ColorProfile::new_bt2020();
1010
        let random_point_x = rand::rng().random_range(0..((1 << 10) - 1));
1011
        let transform = bt2020_profile
1012
            .create_transform_10bit(
1013
                Layout::Rgb,
1014
                &srgb_profile,
1015
                Layout::Rgb,
1016
                TransformOptions::default(),
1017
            )
1018
            .unwrap();
1019
        let src = vec![random_point_x; 256 * 256 * 3];
1020
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1021
        transform.transform(&src, &mut dst).unwrap();
1022
    }
1023
1024
    #[test]
1025
    fn test_transform_rgb12() {
1026
        let srgb_profile = ColorProfile::new_srgb();
1027
        let bt2020_profile = ColorProfile::new_bt2020();
1028
        let random_point_x = rand::rng().random_range(0..((1 << 12) - 1));
1029
        let transform = bt2020_profile
1030
            .create_transform_12bit(
1031
                Layout::Rgb,
1032
                &srgb_profile,
1033
                Layout::Rgb,
1034
                TransformOptions::default(),
1035
            )
1036
            .unwrap();
1037
        let src = vec![random_point_x; 256 * 256 * 3];
1038
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1039
        transform.transform(&src, &mut dst).unwrap();
1040
    }
1041
1042
    #[test]
1043
    fn test_transform_rgb16() {
1044
        let srgb_profile = ColorProfile::new_srgb();
1045
        let bt2020_profile = ColorProfile::new_bt2020();
1046
        let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16;
1047
        let transform = bt2020_profile
1048
            .create_transform_16bit(
1049
                Layout::Rgb,
1050
                &srgb_profile,
1051
                Layout::Rgb,
1052
                TransformOptions::default(),
1053
            )
1054
            .unwrap();
1055
        let src = vec![random_point_x; 256 * 256 * 3];
1056
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1057
        transform.transform(&src, &mut dst).unwrap();
1058
    }
1059
1060
    #[test]
1061
    fn test_transform_round_trip_rgb8() {
1062
        let srgb_profile = ColorProfile::new_srgb();
1063
        let bt2020_profile = ColorProfile::new_bt2020();
1064
        let transform = srgb_profile
1065
            .create_transform_8bit(
1066
                Layout::Rgb,
1067
                &bt2020_profile,
1068
                Layout::Rgb,
1069
                TransformOptions::default(),
1070
            )
1071
            .unwrap();
1072
        let mut src = vec![0u8; 256 * 256 * 3];
1073
        for dst in src.chunks_exact_mut(3) {
1074
            dst[0] = 175;
1075
            dst[1] = 75;
1076
            dst[2] = 13;
1077
        }
1078
        let mut dst = vec![0u8; 256 * 256 * 3];
1079
        transform.transform(&src, &mut dst).unwrap();
1080
1081
        let transform_inverse = bt2020_profile
1082
            .create_transform_8bit(
1083
                Layout::Rgb,
1084
                &srgb_profile,
1085
                Layout::Rgb,
1086
                TransformOptions::default(),
1087
            )
1088
            .unwrap();
1089
1090
        transform_inverse.transform(&dst, &mut src).unwrap();
1091
1092
        for src in src.chunks_exact_mut(3) {
1093
            let diff0 = (src[0] as i32 - 175).abs();
1094
            let diff1 = (src[1] as i32 - 75).abs();
1095
            let diff2 = (src[2] as i32 - 13).abs();
1096
            assert!(
1097
                diff0 < 3,
1098
                "On channel 0 difference should be less than 3, but it was {diff0}"
1099
            );
1100
            assert!(
1101
                diff1 < 3,
1102
                "On channel 1 difference should be less than 3, but it was {diff1}"
1103
            );
1104
            assert!(
1105
                diff2 < 3,
1106
                "On channel 2 difference should be less than 3, but it was {diff2}"
1107
            );
1108
        }
1109
    }
1110
1111
    #[test]
1112
    fn test_transform_round_trip_rgb10() {
1113
        let srgb_profile = ColorProfile::new_srgb();
1114
        let bt2020_profile = ColorProfile::new_bt2020();
1115
        let transform = srgb_profile
1116
            .create_transform_10bit(
1117
                Layout::Rgb,
1118
                &bt2020_profile,
1119
                Layout::Rgb,
1120
                TransformOptions::default(),
1121
            )
1122
            .unwrap();
1123
        let mut src = vec![0u16; 256 * 256 * 3];
1124
        for dst in src.chunks_exact_mut(3) {
1125
            dst[0] = 175;
1126
            dst[1] = 256;
1127
            dst[2] = 512;
1128
        }
1129
        let mut dst = vec![0u16; 256 * 256 * 3];
1130
        transform.transform(&src, &mut dst).unwrap();
1131
1132
        let transform_inverse = bt2020_profile
1133
            .create_transform_10bit(
1134
                Layout::Rgb,
1135
                &srgb_profile,
1136
                Layout::Rgb,
1137
                TransformOptions::default(),
1138
            )
1139
            .unwrap();
1140
1141
        transform_inverse.transform(&dst, &mut src).unwrap();
1142
1143
        for src in src.chunks_exact_mut(3) {
1144
            let diff0 = (src[0] as i32 - 175).abs();
1145
            let diff1 = (src[1] as i32 - 256).abs();
1146
            let diff2 = (src[2] as i32 - 512).abs();
1147
            assert!(
1148
                diff0 < 15,
1149
                "On channel 0 difference should be less than 15, but it was {diff0}"
1150
            );
1151
            assert!(
1152
                diff1 < 15,
1153
                "On channel 1 difference should be less than 15, but it was {diff1}"
1154
            );
1155
            assert!(
1156
                diff2 < 15,
1157
                "On channel 2 difference should be less than 15, but it was {diff2}"
1158
            );
1159
        }
1160
    }
1161
1162
    #[test]
1163
    fn test_transform_round_trip_rgb12() {
1164
        let srgb_profile = ColorProfile::new_srgb();
1165
        let bt2020_profile = ColorProfile::new_bt2020();
1166
        let transform = srgb_profile
1167
            .create_transform_12bit(
1168
                Layout::Rgb,
1169
                &bt2020_profile,
1170
                Layout::Rgb,
1171
                TransformOptions::default(),
1172
            )
1173
            .unwrap();
1174
        let mut src = vec![0u16; 256 * 256 * 3];
1175
        for dst in src.chunks_exact_mut(3) {
1176
            dst[0] = 1750;
1177
            dst[1] = 2560;
1178
            dst[2] = 3143;
1179
        }
1180
        let mut dst = vec![0u16; 256 * 256 * 3];
1181
        transform.transform(&src, &mut dst).unwrap();
1182
1183
        let transform_inverse = bt2020_profile
1184
            .create_transform_12bit(
1185
                Layout::Rgb,
1186
                &srgb_profile,
1187
                Layout::Rgb,
1188
                TransformOptions::default(),
1189
            )
1190
            .unwrap();
1191
1192
        transform_inverse.transform(&dst, &mut src).unwrap();
1193
1194
        for src in src.chunks_exact_mut(3) {
1195
            let diff0 = (src[0] as i32 - 1750).abs();
1196
            let diff1 = (src[1] as i32 - 2560).abs();
1197
            let diff2 = (src[2] as i32 - 3143).abs();
1198
            assert!(
1199
                diff0 < 25,
1200
                "On channel 0 difference should be less than 25, but it was {diff0}"
1201
            );
1202
            assert!(
1203
                diff1 < 25,
1204
                "On channel 1 difference should be less than 25, but it was {diff1}"
1205
            );
1206
            assert!(
1207
                diff2 < 25,
1208
                "On channel 2 difference should be less than 25, but it was {diff2}"
1209
            );
1210
        }
1211
    }
1212
1213
    #[test]
1214
    fn test_transform_round_trip_rgb16() {
1215
        let srgb_profile = ColorProfile::new_srgb();
1216
        let bt2020_profile = ColorProfile::new_bt2020();
1217
        let transform = srgb_profile
1218
            .create_transform_16bit(
1219
                Layout::Rgb,
1220
                &bt2020_profile,
1221
                Layout::Rgb,
1222
                TransformOptions::default(),
1223
            )
1224
            .unwrap();
1225
        let mut src = vec![0u16; 256 * 256 * 3];
1226
        for dst in src.chunks_exact_mut(3) {
1227
            dst[0] = 1760;
1228
            dst[1] = 2560;
1229
            dst[2] = 5120;
1230
        }
1231
        let mut dst = vec![0u16; 256 * 256 * 3];
1232
        transform.transform(&src, &mut dst).unwrap();
1233
1234
        let transform_inverse = bt2020_profile
1235
            .create_transform_16bit(
1236
                Layout::Rgb,
1237
                &srgb_profile,
1238
                Layout::Rgb,
1239
                TransformOptions::default(),
1240
            )
1241
            .unwrap();
1242
1243
        transform_inverse.transform(&dst, &mut src).unwrap();
1244
1245
        for src in src.chunks_exact_mut(3) {
1246
            let diff0 = (src[0] as i32 - 1760).abs();
1247
            let diff1 = (src[1] as i32 - 2560).abs();
1248
            let diff2 = (src[2] as i32 - 5120).abs();
1249
            assert!(
1250
                diff0 < 35,
1251
                "On channel 0 difference should be less than 35, but it was {diff0}"
1252
            );
1253
            assert!(
1254
                diff1 < 35,
1255
                "On channel 1 difference should be less than 35, but it was {diff1}"
1256
            );
1257
            assert!(
1258
                diff2 < 35,
1259
                "On channel 2 difference should be less than 35, but it was {diff2}"
1260
            );
1261
        }
1262
    }
1263
1264
    #[test]
1265
    fn test_transform_rgb_to_gray_extended() {
1266
        let srgb = ColorProfile::new_srgb();
1267
        let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0);
1268
        gray_profile.color_space = DataColorSpace::Gray;
1269
        gray_profile.gray_trc = srgb.red_trc.clone();
1270
        let mut test_profile = vec![0.; 4];
1271
        test_profile[2] = 1.;
1272
        let mut dst = vec![0.; 1];
1273
1274
        let mut inverse = vec![0.; 4];
1275
1276
        let cvt0 = srgb
1277
            .create_transform_f32(
1278
                Layout::Rgba,
1279
                &gray_profile,
1280
                Layout::Gray,
1281
                TransformOptions {
1282
                    allow_extended_range_rgb_xyz: true,
1283
                    ..Default::default()
1284
                },
1285
            )
1286
            .unwrap();
1287
        cvt0.transform(&test_profile, &mut dst).unwrap();
1288
        assert!((dst[0] - 0.273046) < 1e-4);
1289
1290
        let cvt_inverse = gray_profile
1291
            .create_transform_f32(
1292
                Layout::Gray,
1293
                &srgb,
1294
                Layout::Rgba,
1295
                TransformOptions {
1296
                    allow_extended_range_rgb_xyz: false,
1297
                    ..Default::default()
1298
                },
1299
            )
1300
            .unwrap();
1301
        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1302
        assert!((inverse[0] - 0.273002833) < 1e-4);
1303
1304
        let cvt1 = srgb
1305
            .create_transform_f32(
1306
                Layout::Rgba,
1307
                &gray_profile,
1308
                Layout::Gray,
1309
                TransformOptions {
1310
                    allow_extended_range_rgb_xyz: false,
1311
                    ..Default::default()
1312
                },
1313
            )
1314
            .unwrap();
1315
        cvt1.transform(&test_profile, &mut dst).unwrap();
1316
        assert!((dst[0] - 0.27307168) < 1e-5);
1317
1318
        inverse.fill(0.);
1319
1320
        let cvt_inverse = gray_profile
1321
            .create_transform_f32(
1322
                Layout::Gray,
1323
                &srgb,
1324
                Layout::Rgba,
1325
                TransformOptions {
1326
                    allow_extended_range_rgb_xyz: true,
1327
                    ..Default::default()
1328
                },
1329
            )
1330
            .unwrap();
1331
        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1332
        assert!((inverse[0] - 0.273002833) < 1e-4);
1333
    }
1334
}