Coverage Report

Created: 2026-08-31 07:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/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
use crate::err::CmsError;
31
use crate::interceptors::{FromCmykaInterceptor, ToCmykaInterceptor};
32
use crate::trc::GammaLutInterpolate;
33
use crate::{ColorProfile, DataColorSpace, LutWarehouse, RenderingIntent, Vector3f, Xyzd};
34
use num_traits::AsPrimitive;
35
use std::sync::Arc;
36
37
/// Transformation executor itself
38
pub trait TransformExecutor<V: Copy + Default> {
39
    /// Count of samples always must match.
40
    /// If there is N samples of *Cmyk* source then N samples of *Rgb* is expected as an output.
41
    fn transform(&self, src: &[V], dst: &mut [V]) -> Result<(), CmsError>;
42
}
43
44
/// Helper for intermediate transformation stages
45
pub trait Stage {
46
    fn transform(&self, src: &[f32], dst: &mut [f32]) -> Result<(), CmsError>;
47
}
48
49
/// Helper for intermediate transformation stages
50
pub trait InPlaceStage {
51
    fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError>;
52
}
53
54
pub trait InPlaceTransformExecutor<V: Copy + Default> {
55
    fn transform(&self, in_out: &mut [V]) -> Result<(), CmsError>;
56
}
57
58
/// Barycentric interpolation weights size.
59
///
60
/// Bigger weights increases precision.
61
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
62
pub enum BarycentricWeightScale {
63
    #[default]
64
    /// Low scale weights is enough for common case.
65
    ///
66
    /// However, it might crush dark zones and gradients.
67
    /// Weights increasing costs 5% performance.
68
    Low,
69
    #[cfg(feature = "options")]
70
    High,
71
}
72
73
/// Declares additional transformation options
74
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
75
pub struct TransformOptions {
76
    pub rendering_intent: RenderingIntent,
77
    /// If set it will try to use Transfer Characteristics from CICP
78
    /// on transform. This might be more precise and faster.
79
    pub allow_use_cicp_transfer: bool,
80
    /// Prefers fixed point where implemented as default.
81
    /// Most of the applications actually do not need floating point.
82
    ///
83
    /// Do not change it if you're not sure that extreme precision is required,
84
    /// in most cases it is a simple way to spend energy to warming up environment
85
    /// a little.
86
    ///
87
    /// Q2.13 for RGB->XYZ->RGB is used.
88
    /// LUT interpolation use Q0.15.
89
    pub prefer_fixed_point: bool,
90
    /// Interpolation method for 3D LUT
91
    ///
92
    /// This parameter has no effect on LAB/XYZ interpolation and scene linear RGB.
93
    ///
94
    /// Technically, it should be assumed to perform cube dividing interpolation:
95
    /// - Source colorspace is gamma-encoded (discards scene linear RGB and XYZ).
96
    /// - Colorspace is uniform.
97
    /// - Colorspace has linear scaling (discards LAB).
98
    /// - Interpolation doesn't shift hues (discards LAB).
99
    ///
100
    /// For LAB, XYZ and scene linear RGB `trilinear/quadlinear` always in force.
101
    pub interpolation_method: InterpolationMethod,
102
    /// Barycentric weights scale.
103
    ///
104
    /// This value controls LUT weights precision.
105
    pub barycentric_weight_scale: BarycentricWeightScale,
106
    /// For floating points transform, it will try to detect gamma function on *Matrix Shaper* profiles.
107
    /// If gamma function is found, then it will be used instead of LUT table.
108
    /// This allows to work with excellent precision with extended range,
109
    /// at a cost of execution time.
110
    #[cfg(feature = "extended_range")]
111
    pub allow_extended_range_rgb_xyz: bool,
112
    // pub black_point_compensation: bool,
113
}
114
115
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
116
/// Defines the interpolation method.
117
///
118
/// All methods produce very close results that almost not possible to separate without
119
/// some automation tools.
120
///
121
/// This implementation chooses the fastest method as default.
122
pub enum InterpolationMethod {
123
    /// General Tetrahedron interpolation.
124
    /// This is used in lcms2 and others CMS.
125
    #[cfg(feature = "options")]
126
    Tetrahedral,
127
    /// Divides cube into a pyramids and interpolate then in the pyramid.
128
    #[cfg(feature = "options")]
129
    Pyramid,
130
    /// Interpolation by dividing cube into prisms.
131
    #[cfg(feature = "options")]
132
    Prism,
133
    /// Trilinear/Quadlinear interpolation
134
    #[default]
135
    Linear,
136
}
137
138
impl Default for TransformOptions {
139
0
    fn default() -> Self {
140
0
        Self {
141
0
            rendering_intent: RenderingIntent::default(),
142
0
            allow_use_cicp_transfer: true,
143
0
            prefer_fixed_point: true,
144
0
            interpolation_method: InterpolationMethod::default(),
145
0
            barycentric_weight_scale: BarycentricWeightScale::default(),
146
0
            #[cfg(feature = "extended_range")]
147
0
            allow_extended_range_rgb_xyz: false,
148
0
            // black_point_compensation: false,
149
0
        }
150
0
    }
151
}
152
153
pub type Transform8BitExecutor = dyn TransformExecutor<u8> + Send + Sync;
154
pub type Transform16BitExecutor = dyn TransformExecutor<u16> + Send + Sync;
155
pub type TransformF32Executor = dyn TransformExecutor<f32> + Send + Sync;
156
pub type TransformF64Executor = dyn TransformExecutor<f64> + Send + Sync;
157
158
/// Layout declares a data layout.
159
/// For RGB it shows also the channel order.
160
/// To handle different data bit-depth appropriate executor must be used.
161
/// Cmyk8 uses the same layout as Rgba8.
162
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
163
pub enum Layout {
164
    Rgb = 0,
165
    Rgba = 1,
166
    Cmyka = 16,
167
    Gray = 2,
168
    GrayAlpha = 3,
169
    Inks5 = 4,
170
    Inks6 = 5,
171
    Inks7 = 6,
172
    Inks8 = 7,
173
    Inks9 = 8,
174
    Inks10 = 9,
175
    Inks11 = 10,
176
    Inks12 = 11,
177
    Inks13 = 12,
178
    Inks14 = 13,
179
    Inks15 = 14,
180
}
181
182
impl Layout {
183
    /// Returns Red channel index
184
    #[inline(always)]
185
0
    pub const fn r_i(self) -> usize {
186
0
        match self {
187
0
            Layout::Rgb => 0,
188
0
            Layout::Rgba => 0,
189
0
            Layout::Gray => unimplemented!(),
190
0
            Layout::GrayAlpha => unimplemented!(),
191
0
            _ => unimplemented!(),
192
        }
193
0
    }
194
195
    /// Returns Green channel index
196
    #[inline(always)]
197
0
    pub const fn g_i(self) -> usize {
198
0
        match self {
199
0
            Layout::Rgb => 1,
200
0
            Layout::Rgba => 1,
201
0
            Layout::Gray => unimplemented!(),
202
0
            Layout::GrayAlpha => unimplemented!(),
203
0
            _ => unimplemented!(),
204
        }
205
0
    }
206
207
    /// Returns Blue channel index
208
    #[inline(always)]
209
0
    pub const fn b_i(self) -> usize {
210
0
        match self {
211
0
            Layout::Rgb => 2,
212
0
            Layout::Rgba => 2,
213
0
            Layout::Gray => unimplemented!(),
214
0
            Layout::GrayAlpha => unimplemented!(),
215
0
            _ => unimplemented!(),
216
        }
217
0
    }
218
219
    #[inline(always)]
220
0
    pub const fn a_i(self) -> usize {
221
0
        match self {
222
0
            Layout::Rgb => unimplemented!(),
223
0
            Layout::Rgba => 3,
224
0
            Layout::Gray => unimplemented!(),
225
0
            Layout::GrayAlpha => 1,
226
0
            _ => unimplemented!(),
227
        }
228
0
    }
229
230
    #[inline(always)]
231
0
    pub const fn has_alpha(self) -> bool {
232
0
        match self {
233
0
            Layout::Rgb => false,
234
0
            Layout::Rgba => true,
235
0
            Layout::Gray => false,
236
0
            Layout::GrayAlpha => true,
237
0
            _ => false,
238
        }
239
0
    }
240
241
    #[inline]
242
0
    pub const fn channels(self) -> usize {
243
0
        match self {
244
0
            Layout::Rgb => 3,
245
0
            Layout::Rgba => 4,
246
0
            Layout::Gray => 1,
247
0
            Layout::GrayAlpha => 2,
248
0
            Layout::Cmyka => 5,
249
0
            Layout::Inks5 => 5,
250
0
            Layout::Inks6 => 6,
251
0
            Layout::Inks7 => 7,
252
0
            Layout::Inks8 => 8,
253
0
            Layout::Inks9 => 9,
254
0
            Layout::Inks10 => 10,
255
0
            Layout::Inks11 => 11,
256
0
            Layout::Inks12 => 12,
257
0
            Layout::Inks13 => 13,
258
0
            Layout::Inks14 => 14,
259
0
            Layout::Inks15 => 15,
260
        }
261
0
    }
262
263
    #[cfg(feature = "any_to_any")]
264
    pub(crate) fn from_inks(inks: usize) -> Self {
265
        match inks {
266
            1 => Layout::Gray,
267
            2 => Layout::GrayAlpha,
268
            3 => Layout::Rgb,
269
            4 => Layout::Rgba,
270
            5 => Layout::Inks5,
271
            6 => Layout::Inks6,
272
            7 => Layout::Inks7,
273
            8 => Layout::Inks8,
274
            9 => Layout::Inks9,
275
            10 => Layout::Inks10,
276
            11 => Layout::Inks11,
277
            12 => Layout::Inks12,
278
            13 => Layout::Inks13,
279
            14 => Layout::Inks14,
280
            15 => Layout::Inks15,
281
            _ => unreachable!("Impossible amount of inks"),
282
        }
283
    }
284
}
285
286
impl From<u8> for Layout {
287
0
    fn from(value: u8) -> Self {
288
0
        match value {
289
0
            0 => Layout::Rgb,
290
0
            1 => Layout::Rgba,
291
0
            2 => Layout::Gray,
292
0
            3 => Layout::GrayAlpha,
293
0
            _ => unimplemented!(),
294
        }
295
0
    }
296
}
297
298
impl Layout {
299
    #[inline(always)]
300
0
    pub const fn resolve(value: u8) -> Self {
301
0
        match value {
302
0
            0 => Layout::Rgb,
303
0
            1 => Layout::Rgba,
304
0
            2 => Layout::Gray,
305
0
            3 => Layout::GrayAlpha,
306
0
            4 => Layout::Inks5,
307
0
            5 => Layout::Inks6,
308
0
            6 => Layout::Inks7,
309
0
            7 => Layout::Inks8,
310
0
            8 => Layout::Inks9,
311
0
            9 => Layout::Inks10,
312
0
            10 => Layout::Inks11,
313
0
            11 => Layout::Inks12,
314
0
            12 => Layout::Inks13,
315
0
            13 => Layout::Inks14,
316
0
            14 => Layout::Inks15,
317
0
            _ => unimplemented!(),
318
        }
319
0
    }
320
}
321
322
#[doc(hidden)]
323
pub trait PointeeSizeExpressible {
324
    fn _as_usize(self) -> usize;
325
    const FINITE: bool;
326
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize;
327
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize;
328
    const IS_U8: bool;
329
    const IS_U16: bool;
330
}
331
332
impl PointeeSizeExpressible for u8 {
333
    #[inline(always)]
334
0
    fn _as_usize(self) -> usize {
335
0
        self as usize
336
0
    }
337
338
    const FINITE: bool = true;
339
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
340
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
341
    const IS_U8: bool = true;
342
    const IS_U16: bool = false;
343
}
344
345
impl PointeeSizeExpressible for u16 {
346
    #[inline(always)]
347
0
    fn _as_usize(self) -> usize {
348
0
        self as usize
349
0
    }
350
351
    const FINITE: bool = true;
352
353
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 1;
354
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1;
355
356
    const IS_U8: bool = false;
357
    const IS_U16: bool = true;
358
}
359
360
impl PointeeSizeExpressible for f32 {
361
    #[inline(always)]
362
0
    fn _as_usize(self) -> usize {
363
        const MAX_14_BIT: f32 = ((1 << 14u32) - 1) as f32;
364
0
        ((self * MAX_14_BIT).max(0f32).min(MAX_14_BIT) as u16) as usize
365
0
    }
366
367
    const FINITE: bool = false;
368
369
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 32768;
370
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 14u32;
371
    const IS_U8: bool = false;
372
    const IS_U16: bool = false;
373
}
374
375
impl PointeeSizeExpressible for f64 {
376
    #[inline(always)]
377
0
    fn _as_usize(self) -> usize {
378
        const MAX_16_BIT: f64 = ((1 << 16u32) - 1) as f64;
379
0
        ((self * MAX_16_BIT).max(0.).min(MAX_16_BIT) as u16) as usize
380
0
    }
381
382
    const FINITE: bool = false;
383
384
    const NOT_FINITE_GAMMA_TABLE_SIZE: usize = 65536;
385
    const NOT_FINITE_LINEAR_TABLE_SIZE: usize = 1 << 16;
386
    const IS_U8: bool = false;
387
    const IS_U16: bool = false;
388
}
389
390
impl ColorProfile {
391
    /// Checks if profile is valid *Matrix Shaper* profile
392
0
    pub fn is_matrix_shaper(&self) -> bool {
393
0
        self.color_space == DataColorSpace::Rgb
394
0
            && self.red_colorant != Xyzd::default()
395
0
            && self.green_colorant != Xyzd::default()
396
0
            && self.blue_colorant != Xyzd::default()
397
0
            && self.red_trc.is_some()
398
0
            && self.green_trc.is_some()
399
0
            && self.blue_trc.is_some()
400
0
    }
401
402
    /// Creates transform between source and destination profile
403
    /// Use for 16 bit-depth data bit-depth only.
404
0
    pub fn create_transform_16bit(
405
0
        &self,
406
0
        src_layout: Layout,
407
0
        dst_pr: &ColorProfile,
408
0
        dst_layout: Layout,
409
0
        options: TransformOptions,
410
0
    ) -> Result<Arc<Transform16BitExecutor>, CmsError> {
411
0
        let mut core_src_layout = src_layout;
412
0
        if src_layout == Layout::Cmyka {
413
0
            core_src_layout = Layout::Rgba;
414
0
        }
415
0
        let mut core_dst_layout = dst_layout;
416
0
        if dst_layout == Layout::Cmyka {
417
0
            core_dst_layout = Layout::Rgba;
418
0
        }
419
0
        let handle = self.create_transform_nbit::<u16, 16, 65536, 65536>(
420
0
            core_src_layout,
421
0
            dst_pr,
422
0
            core_dst_layout,
423
0
            options,
424
0
        )?;
425
0
        if core_src_layout == Layout::Cmyka {
426
0
            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
427
0
        } else if core_dst_layout == Layout::Cmyka {
428
0
            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
429
0
        }
430
0
        Ok(handle)
431
0
    }
432
433
    /// Creates transform between source and destination profile
434
    /// Use for 16 bit-depth data bit-depth only.
435
    ///
436
    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
437
    #[cfg(feature = "in_place")]
438
    pub fn create_in_place_transform_16bit(
439
        &self,
440
        layout: Layout,
441
        dst_pr: &ColorProfile,
442
        options: TransformOptions,
443
    ) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
444
        self.create_transform_in_place_nbit::<u16, 16, 65536, 65536>(layout, dst_pr, options)
445
    }
446
447
    /// Creates transform between source and destination profile
448
    /// Use for 12 bit-depth data bit-depth only.
449
0
    pub fn create_transform_12bit(
450
0
        &self,
451
0
        src_layout: Layout,
452
0
        dst_pr: &ColorProfile,
453
0
        dst_layout: Layout,
454
0
        options: TransformOptions,
455
0
    ) -> Result<Arc<Transform16BitExecutor>, CmsError> {
456
0
        let mut core_src_layout = src_layout;
457
0
        if src_layout == Layout::Cmyka {
458
0
            core_src_layout = Layout::Rgba;
459
0
        }
460
0
        let mut core_dst_layout = dst_layout;
461
0
        if dst_layout == Layout::Cmyka {
462
0
            core_dst_layout = Layout::Rgba;
463
0
        }
464
0
        let handle = self.create_transform_nbit::<u16, 12, 65536, 16384>(
465
0
            core_src_layout,
466
0
            dst_pr,
467
0
            core_dst_layout,
468
0
            options,
469
0
        )?;
470
0
        if core_src_layout == Layout::Cmyka {
471
0
            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
472
0
        } else if core_dst_layout == Layout::Cmyka {
473
0
            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
474
0
        }
475
0
        Ok(handle)
476
0
    }
477
478
    /// Creates transform between source and destination profile
479
    /// Use for 12 bit-depth data bit-depth only.
480
    ///
481
    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
482
    #[cfg(feature = "in_place")]
483
    pub fn create_in_place_transform_12bit(
484
        &self,
485
        layout: Layout,
486
        dst_pr: &ColorProfile,
487
        options: TransformOptions,
488
    ) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
489
        self.create_transform_in_place_nbit::<u16, 12, 65536, 16384>(layout, dst_pr, options)
490
    }
491
492
    /// Creates transform between source and destination profile
493
    /// Use for 10 bit-depth data bit-depth only.
494
0
    pub fn create_transform_10bit(
495
0
        &self,
496
0
        src_layout: Layout,
497
0
        dst_pr: &ColorProfile,
498
0
        dst_layout: Layout,
499
0
        options: TransformOptions,
500
0
    ) -> Result<Arc<Transform16BitExecutor>, CmsError> {
501
0
        let mut core_src_layout = src_layout;
502
0
        if src_layout == Layout::Cmyka {
503
0
            core_src_layout = Layout::Rgba;
504
0
        }
505
0
        let mut core_dst_layout = dst_layout;
506
0
        if dst_layout == Layout::Cmyka {
507
0
            core_dst_layout = Layout::Rgba;
508
0
        }
509
0
        let handle = self.create_transform_nbit::<u16, 10, 65536, 8192>(
510
0
            core_src_layout,
511
0
            dst_pr,
512
0
            core_dst_layout,
513
0
            options,
514
0
        )?;
515
0
        if core_src_layout == Layout::Cmyka {
516
0
            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
517
0
        } else if core_dst_layout == Layout::Cmyka {
518
0
            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
519
0
        }
520
0
        Ok(handle)
521
0
    }
522
523
    /// Creates transform between source and destination profile
524
    /// Use for 10 bit-depth data bit-depth only.
525
    ///
526
    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
527
    #[cfg(feature = "in_place")]
528
    pub fn create_in_place_transform_10bit(
529
        &self,
530
        layout: Layout,
531
        dst_pr: &ColorProfile,
532
        options: TransformOptions,
533
    ) -> Result<Arc<dyn InPlaceTransformExecutor<u16> + Send + Sync>, CmsError> {
534
        self.create_transform_in_place_nbit::<u16, 10, 65536, 8192>(layout, dst_pr, options)
535
    }
536
537
    /// Creates transform between source and destination profile
538
    /// Data has to be normalized into [0, 1] range.
539
    /// ICC profiles and LUT tables do not exist in infinite precision.
540
    /// Thus, this implementation considers `f32` as 14-bit values.
541
    /// Floating point transformer works in extended mode, that means returned data might be negative
542
    /// or more than 1.
543
0
    pub fn create_transform_f32(
544
0
        &self,
545
0
        src_layout: Layout,
546
0
        dst_pr: &ColorProfile,
547
0
        dst_layout: Layout,
548
0
        options: TransformOptions,
549
0
    ) -> Result<Arc<TransformF32Executor>, CmsError> {
550
0
        let mut core_src_layout = src_layout;
551
0
        if src_layout == Layout::Cmyka {
552
0
            core_src_layout = Layout::Rgba;
553
0
        }
554
0
        let mut core_dst_layout = dst_layout;
555
0
        if dst_layout == Layout::Cmyka {
556
0
            core_dst_layout = Layout::Rgba;
557
0
        }
558
0
        let handle = self.create_transform_nbit::<f32, 1, 65536, 32768>(
559
0
            core_src_layout,
560
0
            dst_pr,
561
0
            core_dst_layout,
562
0
            options,
563
0
        )?;
564
0
        if core_src_layout == Layout::Cmyka {
565
0
            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
566
0
        } else if core_dst_layout == Layout::Cmyka {
567
0
            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
568
0
        }
569
0
        Ok(handle)
570
0
    }
571
572
    /// Creates transform between source and destination profile.
573
    /// Data has to be normalized into [0, 1] range.
574
    /// ICC profiles and LUT tables do not exist in infinite precision.
575
    /// Thus, this implementation considers `f32` as 14-bit values.
576
    /// Floating point transformer works in extended mode, that means returned data might be negative
577
    /// or more than 1.
578
    ///
579
    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
580
    #[cfg(feature = "in_place")]
581
    pub fn create_in_place_transform_f32(
582
        &self,
583
        layout: Layout,
584
        dst_pr: &ColorProfile,
585
        options: TransformOptions,
586
    ) -> Result<Arc<dyn InPlaceTransformExecutor<f32> + Send + Sync>, CmsError> {
587
        self.create_transform_in_place_nbit::<f32, 1, 65536, 32768>(layout, dst_pr, options)
588
    }
589
590
    /// Creates transform between source and destination profile
591
    /// Data has to be normalized into [0, 1] range.
592
    /// ICC profiles and LUT tables do not exist in infinite precision.
593
    /// Thus, this implementation considers `f64` as 16-bit values.
594
    /// Floating point transformer works in extended mode, that means returned data might be negative
595
    /// or more than 1.
596
0
    pub fn create_transform_f64(
597
0
        &self,
598
0
        src_layout: Layout,
599
0
        dst_pr: &ColorProfile,
600
0
        dst_layout: Layout,
601
0
        options: TransformOptions,
602
0
    ) -> Result<Arc<TransformF64Executor>, CmsError> {
603
0
        let mut core_src_layout = src_layout;
604
0
        if src_layout == Layout::Cmyka {
605
0
            core_src_layout = Layout::Rgba;
606
0
        }
607
0
        let mut core_dst_layout = dst_layout;
608
0
        if dst_layout == Layout::Cmyka {
609
0
            core_dst_layout = Layout::Rgba;
610
0
        }
611
0
        let handle = self.create_transform_nbit::<f64, 1, 65536, 65536>(
612
0
            core_src_layout,
613
0
            dst_pr,
614
0
            core_dst_layout,
615
0
            options,
616
0
        )?;
617
0
        if core_src_layout == Layout::Cmyka {
618
0
            return Ok(Arc::new(FromCmykaInterceptor::install(handle, dst_layout)));
619
0
        } else if core_dst_layout == Layout::Cmyka {
620
0
            return Ok(Arc::new(ToCmykaInterceptor::install(handle, dst_layout)));
621
0
        }
622
0
        Ok(handle)
623
0
    }
624
625
    /// Creates transform between source and destination profile
626
    /// Data has to be normalized into [0, 1] range.
627
    /// ICC profiles and LUT tables do not exist in infinite precision.
628
    /// Thus, this implementation considers `f64` as 16-bit values.
629
    /// Floating point transformer works in extended mode, that means returned data might be negative
630
    /// or more than 1.
631
    ///
632
    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
633
    #[cfg(feature = "in_place")]
634
    pub fn create_in_place_transform_f64(
635
        &self,
636
        layout: Layout,
637
        dst_pr: &ColorProfile,
638
        options: TransformOptions,
639
    ) -> Result<Arc<dyn InPlaceTransformExecutor<f64> + Send + Sync>, CmsError> {
640
        self.create_transform_in_place_nbit::<f64, 1, 65536, 65536>(layout, dst_pr, options)
641
    }
642
643
    #[cfg(feature = "in_place")]
644
    fn create_transform_in_place_nbit<
645
        T: Copy
646
            + Default
647
            + AsPrimitive<usize>
648
            + PointeeSizeExpressible
649
            + Send
650
            + Sync
651
            + AsPrimitive<f32>
652
            + RgbXyzFactory<T>
653
            + RgbXyzFactoryOpt<T>
654
            + GammaLutInterpolate,
655
        const BIT_DEPTH: usize,
656
        const LINEAR_CAP: usize,
657
        const GAMMA_CAP: usize,
658
    >(
659
        &self,
660
        layout: Layout,
661
        dst_pr: &ColorProfile,
662
        options: TransformOptions,
663
    ) -> Result<Arc<dyn InPlaceTransformExecutor<T> + Send + Sync>, CmsError>
664
    where
665
        f32: AsPrimitive<T>,
666
        u32: AsPrimitive<T>,
667
    {
668
        // In-place transforms supports only matrix shaper transforms
669
        let is_rgb_transform = self.color_space == DataColorSpace::Rgb
670
            && dst_pr.pcs == DataColorSpace::Xyz
671
            && dst_pr.color_space == DataColorSpace::Rgb
672
            && self.pcs == DataColorSpace::Xyz
673
            && self.is_matrix_shaper()
674
            && dst_pr.is_matrix_shaper();
675
        let is_gray_transform = (self.color_space == DataColorSpace::Gray
676
            && self.gray_trc.is_some())
677
            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
678
            && self.pcs == DataColorSpace::Xyz
679
            && dst_pr.pcs == DataColorSpace::Xyz;
680
681
        if is_rgb_transform {
682
            let transform = self.transform_matrix(dst_pr);
683
684
            if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
685
                let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
686
                    options.allow_use_cicp_transfer,
687
                )?;
688
689
                let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
690
                    &dst_pr.red_trc,
691
                    options.allow_use_cicp_transfer,
692
                )?;
693
694
                let profile_transform = TransformMatrixShaperOptimized {
695
                    linear,
696
                    gamma,
697
                    adaptation_matrix: transform.to_f32(),
698
                };
699
700
                return T::make_in_place_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
701
                    layout,
702
                    profile_transform,
703
                    options,
704
                );
705
            }
706
707
            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
708
                options.allow_use_cicp_transfer,
709
            )?;
710
            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
711
                options.allow_use_cicp_transfer,
712
            )?;
713
            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
714
                options.allow_use_cicp_transfer,
715
            )?;
716
717
            let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
718
                &dst_pr.red_trc,
719
                options.allow_use_cicp_transfer,
720
            )?;
721
            let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
722
                &dst_pr.green_trc,
723
                options.allow_use_cicp_transfer,
724
            )?;
725
            let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
726
                &dst_pr.blue_trc,
727
                options.allow_use_cicp_transfer,
728
            )?;
729
730
            let profile_transform = TransformMatrixShaper {
731
                r_linear: lin_r,
732
                g_linear: lin_g,
733
                b_linear: lin_b,
734
                r_gamma: gamma_r,
735
                g_gamma: gamma_g,
736
                b_gamma: gamma_b,
737
                adaptation_matrix: transform.to_f32(),
738
            };
739
740
            return T::make_in_place_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
741
                layout,
742
                profile_transform,
743
                options,
744
            );
745
        }
746
747
        if is_gray_transform {
748
            // Gray -> Gray case
749
            let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
750
751
            let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
752
                &dst_pr.gray_trc,
753
                options.allow_use_cicp_transfer,
754
            )?;
755
756
            use crate::conversions::make_gray_to_gray_in_place;
757
            return make_gray_to_gray_in_place::<T, LINEAR_CAP>(
758
                layout,
759
                &gray_linear,
760
                &gray_gamma,
761
                BIT_DEPTH,
762
                GAMMA_CAP,
763
            );
764
        }
765
766
        Err(CmsError::UnsupportedProfileConnection)
767
    }
768
769
0
    fn create_transform_nbit<
770
0
        T: Copy
771
0
            + Default
772
0
            + AsPrimitive<usize>
773
0
            + PointeeSizeExpressible
774
0
            + Send
775
0
            + Sync
776
0
            + AsPrimitive<f32>
777
0
            + RgbXyzFactory<T>
778
0
            + RgbXyzFactoryOpt<T>
779
0
            + GammaLutInterpolate,
780
0
        const BIT_DEPTH: usize,
781
0
        const LINEAR_CAP: usize,
782
0
        const GAMMA_CAP: usize,
783
0
    >(
784
0
        &self,
785
0
        src_layout: Layout,
786
0
        dst_pr: &ColorProfile,
787
0
        dst_layout: Layout,
788
0
        options: TransformOptions,
789
0
    ) -> Result<Arc<dyn TransformExecutor<T> + Send + Sync>, CmsError>
790
0
    where
791
0
        f32: AsPrimitive<T>,
792
0
        u32: AsPrimitive<T>,
793
0
        (): LutBarycentricReduction<T, u8>,
794
0
        (): LutBarycentricReduction<T, u16>,
795
    {
796
0
        if self.color_space == DataColorSpace::Rgb
797
0
            && dst_pr.pcs == DataColorSpace::Xyz
798
0
            && dst_pr.color_space == DataColorSpace::Rgb
799
0
            && self.pcs == DataColorSpace::Xyz
800
0
            && self.is_matrix_shaper()
801
0
            && dst_pr.is_matrix_shaper()
802
        {
803
0
            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
804
0
                return Err(CmsError::InvalidLayout);
805
0
            }
806
0
            if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
807
0
                return Err(CmsError::InvalidLayout);
808
0
            }
809
810
            #[cfg(feature = "lut")]
811
            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
812
                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
813
                    src_layout, self, dst_layout, dst_pr, options,
814
                );
815
            }
816
817
0
            let transform = self.transform_matrix(dst_pr);
818
819
            #[cfg(feature = "extended_range")]
820
            if !T::FINITE && options.allow_extended_range_rgb_xyz {
821
                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
822
                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
823
                        use crate::conversions::{
824
                            TransformShaperFloatInOut, make_rgb_xyz_rgb_transform_float_in_out,
825
                        };
826
                        use std::marker::PhantomData;
827
                        let p = TransformShaperFloatInOut {
828
                            linear_evaluator,
829
                            gamma_evaluator,
830
                            adaptation_matrix: transform.to_f32(),
831
                            phantom_data: PhantomData,
832
                        };
833
                        return make_rgb_xyz_rgb_transform_float_in_out::<T>(
834
                            src_layout, dst_layout, p, BIT_DEPTH,
835
                        );
836
                    }
837
838
                    let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
839
                        options.allow_use_cicp_transfer,
840
                    )?;
841
                    let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
842
                        options.allow_use_cicp_transfer,
843
                    )?;
844
                    let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
845
                        options.allow_use_cicp_transfer,
846
                    )?;
847
848
                    use crate::conversions::{
849
                        TransformShaperRgbFloat, make_rgb_xyz_rgb_transform_float,
850
                    };
851
                    use std::marker::PhantomData;
852
                    let p = TransformShaperRgbFloat {
853
                        r_linear: lin_r,
854
                        g_linear: lin_g,
855
                        b_linear: lin_b,
856
                        gamma_evaluator,
857
                        adaptation_matrix: transform.to_f32(),
858
                        phantom_data: PhantomData,
859
                    };
860
                    return make_rgb_xyz_rgb_transform_float::<T, LINEAR_CAP>(
861
                        src_layout, dst_layout, p, BIT_DEPTH,
862
                    );
863
                }
864
            }
865
866
0
            if self.are_all_trc_the_same() && dst_pr.are_all_trc_the_same() {
867
0
                let linear = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
868
0
                    options.allow_use_cicp_transfer,
869
0
                )?;
870
871
0
                let gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
872
0
                    &dst_pr.red_trc,
873
0
                    options.allow_use_cicp_transfer,
874
0
                )?;
875
876
0
                let profile_transform = TransformMatrixShaperOptimized {
877
0
                    linear,
878
0
                    gamma,
879
0
                    adaptation_matrix: transform.to_f32(),
880
0
                };
881
882
0
                return T::make_optimized_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
883
0
                    src_layout,
884
0
                    dst_layout,
885
0
                    profile_transform,
886
0
                    options,
887
                );
888
0
            }
889
890
0
            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
891
0
                options.allow_use_cicp_transfer,
892
0
            )?;
893
0
            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
894
0
                options.allow_use_cicp_transfer,
895
0
            )?;
896
0
            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
897
0
                options.allow_use_cicp_transfer,
898
0
            )?;
899
900
0
            let gamma_r = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
901
0
                &dst_pr.red_trc,
902
0
                options.allow_use_cicp_transfer,
903
0
            )?;
904
0
            let gamma_g = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
905
0
                &dst_pr.green_trc,
906
0
                options.allow_use_cicp_transfer,
907
0
            )?;
908
0
            let gamma_b = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
909
0
                &dst_pr.blue_trc,
910
0
                options.allow_use_cicp_transfer,
911
0
            )?;
912
913
0
            let profile_transform = TransformMatrixShaper {
914
0
                r_linear: lin_r,
915
0
                g_linear: lin_g,
916
0
                b_linear: lin_b,
917
0
                r_gamma: gamma_r,
918
0
                g_gamma: gamma_g,
919
0
                b_gamma: gamma_b,
920
0
                adaptation_matrix: transform.to_f32(),
921
0
            };
922
923
0
            T::make_transform::<LINEAR_CAP, GAMMA_CAP, BIT_DEPTH>(
924
0
                src_layout,
925
0
                dst_layout,
926
0
                profile_transform,
927
0
                options,
928
            )
929
0
        } else if (self.color_space == DataColorSpace::Gray && self.gray_trc.is_some())
930
0
            && (dst_pr.color_space == DataColorSpace::Rgb
931
0
                || (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some()))
932
0
            && self.pcs == DataColorSpace::Xyz
933
0
            && dst_pr.pcs == DataColorSpace::Xyz
934
        {
935
0
            if src_layout != Layout::GrayAlpha && src_layout != Layout::Gray {
936
0
                return Err(CmsError::InvalidLayout);
937
0
            }
938
939
            #[cfg(feature = "lut")]
940
            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
941
                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
942
                    src_layout, self, dst_layout, dst_pr, options,
943
                );
944
            }
945
946
0
            let gray_linear = self.build_gray_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>()?;
947
948
0
            if dst_pr.color_space == DataColorSpace::Gray {
949
                #[cfg(feature = "extended_range")]
950
                if !T::FINITE && options.allow_extended_range_rgb_xyz {
951
                    if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
952
                        if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
953
                            // Gray -> Gray case extended range
954
                            use crate::conversions::make_gray_to_one_trc_extended;
955
                            return make_gray_to_one_trc_extended::<T>(
956
                                src_layout,
957
                                dst_layout,
958
                                linear_evaluator,
959
                                gamma_evaluator,
960
                                BIT_DEPTH,
961
                            );
962
                        }
963
                    }
964
                }
965
966
                // Gray -> Gray case
967
0
                let gray_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
968
0
                    &dst_pr.gray_trc,
969
0
                    options.allow_use_cicp_transfer,
970
0
                )?;
971
972
0
                make_gray_to_x::<T, LINEAR_CAP>(
973
0
                    src_layout,
974
0
                    dst_layout,
975
0
                    &gray_linear,
976
0
                    &gray_gamma,
977
                    BIT_DEPTH,
978
                    GAMMA_CAP,
979
                )
980
            } else {
981
                #[allow(clippy::collapsible_if)]
982
0
                if dst_pr.are_all_trc_the_same() {
983
                    #[cfg(feature = "extended_range")]
984
                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
985
                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
986
                            if let Some(linear_evaluator) =
987
                                self.try_extended_linearizing_evaluator()
988
                            {
989
                                // Gray -> RGB where all TRC is the same with extended range
990
                                use crate::conversions::make_gray_to_one_trc_extended;
991
                                return make_gray_to_one_trc_extended::<T>(
992
                                    src_layout,
993
                                    dst_layout,
994
                                    linear_evaluator,
995
                                    gamma_evaluator,
996
                                    BIT_DEPTH,
997
                                );
998
                            }
999
                        }
1000
                    }
1001
1002
                    // Gray -> RGB where all TRC is the same
1003
0
                    let rgb_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1004
0
                        &dst_pr.red_trc,
1005
0
                        options.allow_use_cicp_transfer,
1006
0
                    )?;
1007
1008
0
                    make_gray_to_x::<T, LINEAR_CAP>(
1009
0
                        src_layout,
1010
0
                        dst_layout,
1011
0
                        &gray_linear,
1012
0
                        &rgb_gamma,
1013
                        BIT_DEPTH,
1014
                        GAMMA_CAP,
1015
                    )
1016
                } else {
1017
                    // Gray -> RGB where all TRC is NOT the same
1018
                    #[cfg(feature = "extended_range")]
1019
                    if !T::FINITE && options.allow_extended_range_rgb_xyz {
1020
                        if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
1021
                            if let Some(linear_evaluator) =
1022
                                self.try_extended_linearizing_evaluator()
1023
                            {
1024
                                // Gray -> RGB where all TRC is NOT the same with extended range
1025
1026
                                use crate::conversions::make_gray_to_rgb_extended;
1027
                                return make_gray_to_rgb_extended::<T>(
1028
                                    src_layout,
1029
                                    dst_layout,
1030
                                    linear_evaluator,
1031
                                    gamma_evaluator,
1032
                                    BIT_DEPTH,
1033
                                );
1034
                            }
1035
                        }
1036
                    }
1037
1038
0
                    let red_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1039
0
                        &dst_pr.red_trc,
1040
0
                        options.allow_use_cicp_transfer,
1041
0
                    )?;
1042
0
                    let green_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1043
0
                        &dst_pr.green_trc,
1044
0
                        options.allow_use_cicp_transfer,
1045
0
                    )?;
1046
0
                    let blue_gamma = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1047
0
                        &dst_pr.blue_trc,
1048
0
                        options.allow_use_cicp_transfer,
1049
0
                    )?;
1050
1051
0
                    let mut gray_linear2 = Box::new([0f32; 65536]);
1052
0
                    for (dst, src) in gray_linear2.iter_mut().zip(gray_linear.iter()) {
1053
0
                        *dst = *src;
1054
0
                    }
1055
1056
0
                    make_gray_to_unfused::<T, LINEAR_CAP>(
1057
0
                        src_layout,
1058
0
                        dst_layout,
1059
0
                        gray_linear2,
1060
0
                        red_gamma,
1061
0
                        green_gamma,
1062
0
                        blue_gamma,
1063
                        BIT_DEPTH,
1064
                        GAMMA_CAP,
1065
                    )
1066
                }
1067
            }
1068
0
        } else if self.color_space == DataColorSpace::Rgb
1069
0
            && (dst_pr.color_space == DataColorSpace::Gray && dst_pr.gray_trc.is_some())
1070
0
            && dst_pr.pcs == DataColorSpace::Xyz
1071
0
            && self.pcs == DataColorSpace::Xyz
1072
        {
1073
0
            if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
1074
0
                return Err(CmsError::InvalidLayout);
1075
0
            }
1076
0
            if dst_layout != Layout::Gray && dst_layout != Layout::GrayAlpha {
1077
0
                return Err(CmsError::InvalidLayout);
1078
0
            }
1079
1080
            #[cfg(feature = "lut")]
1081
            if self.has_device_to_pcs_lut() || dst_pr.has_pcs_to_device_lut() {
1082
                return make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
1083
                    src_layout, self, dst_layout, dst_pr, options,
1084
                );
1085
            }
1086
1087
0
            let transform = self.transform_matrix(dst_pr).to_f32();
1088
1089
0
            let vector = Vector3f {
1090
0
                v: [transform.v[1][0], transform.v[1][1], transform.v[1][2]],
1091
0
            };
1092
1093
            #[cfg(feature = "extended_range")]
1094
            if !T::FINITE && options.allow_extended_range_rgb_xyz {
1095
                if let Some(gamma_evaluator) = dst_pr.try_extended_gamma_evaluator() {
1096
                    if let Some(linear_evaluator) = self.try_extended_linearizing_evaluator() {
1097
                        use crate::conversions::make_rgb_to_gray_extended;
1098
                        return make_rgb_to_gray_extended::<T>(
1099
                            src_layout,
1100
                            dst_layout,
1101
                            linear_evaluator,
1102
                            gamma_evaluator,
1103
                            vector,
1104
                            BIT_DEPTH,
1105
                        );
1106
                    }
1107
                }
1108
            }
1109
1110
0
            let lin_r = self.build_r_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
1111
0
                options.allow_use_cicp_transfer,
1112
0
            )?;
1113
0
            let lin_g = self.build_g_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
1114
0
                options.allow_use_cicp_transfer,
1115
0
            )?;
1116
0
            let lin_b = self.build_b_linearize_table::<T, LINEAR_CAP, BIT_DEPTH>(
1117
0
                options.allow_use_cicp_transfer,
1118
0
            )?;
1119
0
            let gray_linear = dst_pr.build_gamma_table::<T, 65536, GAMMA_CAP, BIT_DEPTH>(
1120
0
                &dst_pr.gray_trc,
1121
0
                options.allow_use_cicp_transfer,
1122
0
            )?;
1123
1124
0
            let trc_box = ToneReproductionRgbToGray::<T, LINEAR_CAP> {
1125
0
                r_linear: lin_r,
1126
0
                g_linear: lin_g,
1127
0
                b_linear: lin_b,
1128
0
                gray_gamma: gray_linear,
1129
0
            };
1130
1131
0
            make_rgb_to_gray::<T, LINEAR_CAP>(
1132
0
                src_layout, dst_layout, trc_box, vector, GAMMA_CAP, BIT_DEPTH,
1133
            )
1134
0
        } else if (self.color_space.is_three_channels()
1135
0
            || self.color_space == DataColorSpace::Cmyk
1136
0
            || self.color_space == DataColorSpace::Color4)
1137
0
            && (dst_pr.color_space.is_three_channels()
1138
0
                || dst_pr.color_space == DataColorSpace::Cmyk
1139
0
                || dst_pr.color_space == DataColorSpace::Color4)
1140
0
            && (dst_pr.pcs == DataColorSpace::Xyz || dst_pr.pcs == DataColorSpace::Lab)
1141
0
            && (self.pcs == DataColorSpace::Xyz || self.pcs == DataColorSpace::Lab)
1142
        {
1143
            #[cfg(feature = "lut")]
1144
            {
1145
                if src_layout == Layout::Gray || src_layout == Layout::GrayAlpha {
1146
                    return Err(CmsError::InvalidLayout);
1147
                }
1148
                if dst_layout == Layout::Gray || dst_layout == Layout::GrayAlpha {
1149
                    return Err(CmsError::InvalidLayout);
1150
                }
1151
                make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
1152
                    src_layout, self, dst_layout, dst_pr, options,
1153
                )
1154
            }
1155
            #[cfg(not(feature = "lut"))]
1156
            {
1157
0
                Err(CmsError::UnsupportedProfileConnection)
1158
            }
1159
        } else {
1160
            #[cfg(feature = "lut")]
1161
            {
1162
                make_lut_transform::<T, BIT_DEPTH, LINEAR_CAP, GAMMA_CAP>(
1163
                    src_layout, self, dst_layout, dst_pr, options,
1164
                )
1165
            }
1166
            #[cfg(not(feature = "lut"))]
1167
            {
1168
0
                Err(CmsError::UnsupportedProfileConnection)
1169
            }
1170
        }
1171
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>
1172
1173
    /// Creates transform between source and destination profile
1174
    /// Only 8 bit is supported.
1175
0
    pub fn create_transform_8bit(
1176
0
        &self,
1177
0
        src_layout: Layout,
1178
0
        dst_pr: &ColorProfile,
1179
0
        dst_layout: Layout,
1180
0
        options: TransformOptions,
1181
0
    ) -> Result<Arc<Transform8BitExecutor>, CmsError> {
1182
0
        self.create_transform_nbit::<u8, 8, 256, 4096>(src_layout, dst_pr, dst_layout, options)
1183
0
    }
1184
1185
    /// Creates transform between source and destination profile
1186
    ///
1187
    /// In place transform only the same amount of channels, and only RGBX -> RGBX, or GrayX -> GrayX.
1188
    #[cfg(feature = "in_place")]
1189
    pub fn create_in_place_transform_8bit(
1190
        &self,
1191
        layout: Layout,
1192
        dst_pr: &ColorProfile,
1193
        options: TransformOptions,
1194
    ) -> Result<Arc<dyn InPlaceTransformExecutor<u8> + Send + Sync>, CmsError> {
1195
        self.create_transform_in_place_nbit::<u8, 8, 256, 4096>(layout, dst_pr, options)
1196
    }
1197
1198
    #[allow(unused)]
1199
0
    pub(crate) fn get_device_to_pcs(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
1200
0
        match intent {
1201
0
            RenderingIntent::AbsoluteColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
1202
0
            RenderingIntent::Saturation => self.lut_a_to_b_saturation.as_ref(),
1203
0
            RenderingIntent::RelativeColorimetric => self.lut_a_to_b_colorimetric.as_ref(),
1204
0
            RenderingIntent::Perceptual => self.lut_a_to_b_perceptual.as_ref(),
1205
        }
1206
0
    }
1207
1208
    #[allow(unused)]
1209
0
    pub(crate) fn get_pcs_to_device(&self, intent: RenderingIntent) -> Option<&LutWarehouse> {
1210
0
        match intent {
1211
0
            RenderingIntent::AbsoluteColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
1212
0
            RenderingIntent::Saturation => self.lut_b_to_a_saturation.as_ref(),
1213
0
            RenderingIntent::RelativeColorimetric => self.lut_b_to_a_colorimetric.as_ref(),
1214
0
            RenderingIntent::Perceptual => self.lut_b_to_a_perceptual.as_ref(),
1215
        }
1216
0
    }
1217
}
1218
1219
#[cfg(test)]
1220
mod tests {
1221
    use crate::*;
1222
    use rand::RngExt;
1223
1224
    #[test]
1225
    fn test_transform_rgb8() {
1226
        let mut srgb_profile = ColorProfile::new_srgb();
1227
        let bt2020_profile = ColorProfile::new_bt2020();
1228
        let random_point_x = rand::rng().random_range(0..255);
1229
        let transform = bt2020_profile
1230
            .create_transform_8bit(
1231
                Layout::Rgb,
1232
                &srgb_profile,
1233
                Layout::Rgb,
1234
                TransformOptions::default(),
1235
            )
1236
            .unwrap();
1237
        let src = vec![random_point_x; 256 * 256 * 3];
1238
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1239
        transform.transform(&src, &mut dst).unwrap();
1240
1241
        let transform = bt2020_profile
1242
            .create_transform_8bit(
1243
                Layout::Rgb,
1244
                &srgb_profile,
1245
                Layout::Rgb,
1246
                TransformOptions {
1247
                    ..TransformOptions::default()
1248
                },
1249
            )
1250
            .unwrap();
1251
        transform.transform(&src, &mut dst).unwrap();
1252
        srgb_profile.rendering_intent = RenderingIntent::RelativeColorimetric;
1253
        let transform = bt2020_profile
1254
            .create_transform_8bit(
1255
                Layout::Rgb,
1256
                &srgb_profile,
1257
                Layout::Rgb,
1258
                TransformOptions {
1259
                    ..TransformOptions::default()
1260
                },
1261
            )
1262
            .unwrap();
1263
        transform.transform(&src, &mut dst).unwrap();
1264
        srgb_profile.rendering_intent = RenderingIntent::Saturation;
1265
        let transform = bt2020_profile
1266
            .create_transform_8bit(
1267
                Layout::Rgb,
1268
                &srgb_profile,
1269
                Layout::Rgb,
1270
                TransformOptions {
1271
                    ..TransformOptions::default()
1272
                },
1273
            )
1274
            .unwrap();
1275
        transform.transform(&src, &mut dst).unwrap();
1276
    }
1277
1278
    #[test]
1279
    fn test_transform_rgba8() {
1280
        let srgb_profile = ColorProfile::new_srgb();
1281
        let bt2020_profile = ColorProfile::new_bt2020();
1282
        let random_point_x = rand::rng().random_range(0..255);
1283
        let transform = bt2020_profile
1284
            .create_transform_8bit(
1285
                Layout::Rgba,
1286
                &srgb_profile,
1287
                Layout::Rgba,
1288
                TransformOptions::default(),
1289
            )
1290
            .unwrap();
1291
        let src = vec![random_point_x; 256 * 256 * 4];
1292
        let mut dst = vec![random_point_x; 256 * 256 * 4];
1293
        transform.transform(&src, &mut dst).unwrap();
1294
    }
1295
1296
    #[test]
1297
    fn test_transform_gray_to_rgb8() {
1298
        let gray_profile = ColorProfile::new_gray_with_gamma(2.2f32);
1299
        let bt2020_profile = ColorProfile::new_bt2020();
1300
        let random_point_x = rand::rng().random_range(0..255);
1301
        let transform = gray_profile
1302
            .create_transform_8bit(
1303
                Layout::Gray,
1304
                &bt2020_profile,
1305
                Layout::Rgb,
1306
                TransformOptions::default(),
1307
            )
1308
            .unwrap();
1309
        let src = vec![random_point_x; 256 * 256];
1310
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1311
        transform.transform(&src, &mut dst).unwrap();
1312
    }
1313
1314
    #[test]
1315
    fn test_transform_gray_to_rgba8() {
1316
        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
1317
        let bt2020_profile = ColorProfile::new_bt2020();
1318
        let random_point_x = rand::rng().random_range(0..255);
1319
        let transform = srgb_profile
1320
            .create_transform_8bit(
1321
                Layout::Gray,
1322
                &bt2020_profile,
1323
                Layout::Rgba,
1324
                TransformOptions::default(),
1325
            )
1326
            .unwrap();
1327
        let src = vec![random_point_x; 256 * 256];
1328
        let mut dst = vec![random_point_x; 256 * 256 * 4];
1329
        transform.transform(&src, &mut dst).unwrap();
1330
    }
1331
1332
    #[test]
1333
    fn test_transform_gray_to_gray_alpha8() {
1334
        let srgb_profile = ColorProfile::new_gray_with_gamma(2.2f32);
1335
        let bt2020_profile = ColorProfile::new_bt2020();
1336
        let random_point_x = rand::rng().random_range(0..255);
1337
        let transform = srgb_profile
1338
            .create_transform_8bit(
1339
                Layout::Gray,
1340
                &bt2020_profile,
1341
                Layout::GrayAlpha,
1342
                TransformOptions::default(),
1343
            )
1344
            .unwrap();
1345
        let src = vec![random_point_x; 256 * 256];
1346
        let mut dst = vec![random_point_x; 256 * 256 * 2];
1347
        transform.transform(&src, &mut dst).unwrap();
1348
    }
1349
1350
    #[test]
1351
    fn test_transform_rgb10() {
1352
        let srgb_profile = ColorProfile::new_srgb();
1353
        let bt2020_profile = ColorProfile::new_bt2020();
1354
        let random_point_x = rand::rng().random_range(0..((1 << 10) - 1));
1355
        let transform = bt2020_profile
1356
            .create_transform_10bit(
1357
                Layout::Rgb,
1358
                &srgb_profile,
1359
                Layout::Rgb,
1360
                TransformOptions::default(),
1361
            )
1362
            .unwrap();
1363
        let src = vec![random_point_x; 256 * 256 * 3];
1364
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1365
        transform.transform(&src, &mut dst).unwrap();
1366
    }
1367
1368
    #[test]
1369
    fn test_transform_rgb12() {
1370
        let srgb_profile = ColorProfile::new_srgb();
1371
        let bt2020_profile = ColorProfile::new_bt2020();
1372
        let random_point_x = rand::rng().random_range(0..((1 << 12) - 1));
1373
        let transform = bt2020_profile
1374
            .create_transform_12bit(
1375
                Layout::Rgb,
1376
                &srgb_profile,
1377
                Layout::Rgb,
1378
                TransformOptions::default(),
1379
            )
1380
            .unwrap();
1381
        let src = vec![random_point_x; 256 * 256 * 3];
1382
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1383
        transform.transform(&src, &mut dst).unwrap();
1384
    }
1385
1386
    #[test]
1387
    fn test_transform_rgb16() {
1388
        let srgb_profile = ColorProfile::new_srgb();
1389
        let bt2020_profile = ColorProfile::new_bt2020();
1390
        let random_point_x = rand::rng().random_range(0..((1u32 << 16u32) - 1u32)) as u16;
1391
        let transform = bt2020_profile
1392
            .create_transform_16bit(
1393
                Layout::Rgb,
1394
                &srgb_profile,
1395
                Layout::Rgb,
1396
                TransformOptions::default(),
1397
            )
1398
            .unwrap();
1399
        let src = vec![random_point_x; 256 * 256 * 3];
1400
        let mut dst = vec![random_point_x; 256 * 256 * 3];
1401
        transform.transform(&src, &mut dst).unwrap();
1402
    }
1403
1404
    #[test]
1405
    fn test_transform_round_trip_rgb8() {
1406
        let srgb_profile = ColorProfile::new_srgb();
1407
        let bt2020_profile = ColorProfile::new_bt2020();
1408
        let transform = srgb_profile
1409
            .create_transform_8bit(
1410
                Layout::Rgb,
1411
                &bt2020_profile,
1412
                Layout::Rgb,
1413
                TransformOptions::default(),
1414
            )
1415
            .unwrap();
1416
        let mut src = vec![0u8; 256 * 256 * 3];
1417
        for dst in src.chunks_exact_mut(3) {
1418
            dst[0] = 175;
1419
            dst[1] = 75;
1420
            dst[2] = 13;
1421
        }
1422
        let mut dst = vec![0u8; 256 * 256 * 3];
1423
        transform.transform(&src, &mut dst).unwrap();
1424
1425
        let transform_inverse = bt2020_profile
1426
            .create_transform_8bit(
1427
                Layout::Rgb,
1428
                &srgb_profile,
1429
                Layout::Rgb,
1430
                TransformOptions::default(),
1431
            )
1432
            .unwrap();
1433
1434
        transform_inverse.transform(&dst, &mut src).unwrap();
1435
1436
        for src in src.chunks_exact_mut(3) {
1437
            let diff0 = (src[0] as i32 - 175).abs();
1438
            let diff1 = (src[1] as i32 - 75).abs();
1439
            let diff2 = (src[2] as i32 - 13).abs();
1440
            assert!(
1441
                diff0 < 3,
1442
                "On channel 0 difference should be less than 3, but it was {diff0}"
1443
            );
1444
            assert!(
1445
                diff1 < 3,
1446
                "On channel 1 difference should be less than 3, but it was {diff1}"
1447
            );
1448
            assert!(
1449
                diff2 < 3,
1450
                "On channel 2 difference should be less than 3, but it was {diff2}"
1451
            );
1452
        }
1453
    }
1454
1455
    #[test]
1456
    fn test_transform_round_trip_rgb10() {
1457
        let srgb_profile = ColorProfile::new_srgb();
1458
        let bt2020_profile = ColorProfile::new_bt2020();
1459
        let transform = srgb_profile
1460
            .create_transform_10bit(
1461
                Layout::Rgb,
1462
                &bt2020_profile,
1463
                Layout::Rgb,
1464
                TransformOptions::default(),
1465
            )
1466
            .unwrap();
1467
        let mut src = vec![0u16; 256 * 256 * 3];
1468
        for dst in src.chunks_exact_mut(3) {
1469
            dst[0] = 175;
1470
            dst[1] = 256;
1471
            dst[2] = 512;
1472
        }
1473
        let mut dst = vec![0u16; 256 * 256 * 3];
1474
        transform.transform(&src, &mut dst).unwrap();
1475
1476
        let transform_inverse = bt2020_profile
1477
            .create_transform_10bit(
1478
                Layout::Rgb,
1479
                &srgb_profile,
1480
                Layout::Rgb,
1481
                TransformOptions::default(),
1482
            )
1483
            .unwrap();
1484
1485
        transform_inverse.transform(&dst, &mut src).unwrap();
1486
1487
        for src in src.chunks_exact_mut(3) {
1488
            let diff0 = (src[0] as i32 - 175).abs();
1489
            let diff1 = (src[1] as i32 - 256).abs();
1490
            let diff2 = (src[2] as i32 - 512).abs();
1491
            assert!(
1492
                diff0 < 15,
1493
                "On channel 0 difference should be less than 15, but it was {diff0}"
1494
            );
1495
            assert!(
1496
                diff1 < 15,
1497
                "On channel 1 difference should be less than 15, but it was {diff1}"
1498
            );
1499
            assert!(
1500
                diff2 < 15,
1501
                "On channel 2 difference should be less than 15, but it was {diff2}"
1502
            );
1503
        }
1504
    }
1505
1506
    #[test]
1507
    fn test_transform_round_trip_rgb12() {
1508
        let srgb_profile = ColorProfile::new_srgb();
1509
        let bt2020_profile = ColorProfile::new_bt2020();
1510
        let transform = srgb_profile
1511
            .create_transform_12bit(
1512
                Layout::Rgb,
1513
                &bt2020_profile,
1514
                Layout::Rgb,
1515
                TransformOptions::default(),
1516
            )
1517
            .unwrap();
1518
        let mut src = vec![0u16; 256 * 256 * 3];
1519
        for dst in src.chunks_exact_mut(3) {
1520
            dst[0] = 1750;
1521
            dst[1] = 2560;
1522
            dst[2] = 3143;
1523
        }
1524
        let mut dst = vec![0u16; 256 * 256 * 3];
1525
        transform.transform(&src, &mut dst).unwrap();
1526
1527
        let transform_inverse = bt2020_profile
1528
            .create_transform_12bit(
1529
                Layout::Rgb,
1530
                &srgb_profile,
1531
                Layout::Rgb,
1532
                TransformOptions::default(),
1533
            )
1534
            .unwrap();
1535
1536
        transform_inverse.transform(&dst, &mut src).unwrap();
1537
1538
        for src in src.chunks_exact_mut(3) {
1539
            let diff0 = (src[0] as i32 - 1750).abs();
1540
            let diff1 = (src[1] as i32 - 2560).abs();
1541
            let diff2 = (src[2] as i32 - 3143).abs();
1542
            assert!(
1543
                diff0 < 25,
1544
                "On channel 0 difference should be less than 25, but it was {diff0}"
1545
            );
1546
            assert!(
1547
                diff1 < 25,
1548
                "On channel 1 difference should be less than 25, but it was {diff1}"
1549
            );
1550
            assert!(
1551
                diff2 < 25,
1552
                "On channel 2 difference should be less than 25, but it was {diff2}"
1553
            );
1554
        }
1555
    }
1556
1557
    #[test]
1558
    fn test_transform_round_trip_rgb16() {
1559
        let srgb_profile = ColorProfile::new_srgb();
1560
        let bt2020_profile = ColorProfile::new_bt2020();
1561
        let transform = srgb_profile
1562
            .create_transform_16bit(
1563
                Layout::Rgb,
1564
                &bt2020_profile,
1565
                Layout::Rgb,
1566
                TransformOptions::default(),
1567
            )
1568
            .unwrap();
1569
        let mut src = vec![0u16; 256 * 256 * 3];
1570
        for dst in src.chunks_exact_mut(3) {
1571
            dst[0] = 1760;
1572
            dst[1] = 2560;
1573
            dst[2] = 5120;
1574
        }
1575
        let mut dst = vec![0u16; 256 * 256 * 3];
1576
        transform.transform(&src, &mut dst).unwrap();
1577
1578
        let transform_inverse = bt2020_profile
1579
            .create_transform_16bit(
1580
                Layout::Rgb,
1581
                &srgb_profile,
1582
                Layout::Rgb,
1583
                TransformOptions::default(),
1584
            )
1585
            .unwrap();
1586
1587
        transform_inverse.transform(&dst, &mut src).unwrap();
1588
1589
        for src in src.chunks_exact_mut(3) {
1590
            let diff0 = (src[0] as i32 - 1760).abs();
1591
            let diff1 = (src[1] as i32 - 2560).abs();
1592
            let diff2 = (src[2] as i32 - 5120).abs();
1593
            assert!(
1594
                diff0 < 35,
1595
                "On channel 0 difference should be less than 35, but it was {diff0}"
1596
            );
1597
            assert!(
1598
                diff1 < 35,
1599
                "On channel 1 difference should be less than 35, but it was {diff1}"
1600
            );
1601
            assert!(
1602
                diff2 < 35,
1603
                "On channel 2 difference should be less than 35, but it was {diff2}"
1604
            );
1605
        }
1606
    }
1607
1608
    #[test]
1609
    #[cfg(feature = "extended_range")]
1610
    fn test_transform_rgb_to_gray_extended() {
1611
        let srgb = ColorProfile::new_srgb();
1612
        let mut gray_profile = ColorProfile::new_gray_with_gamma(1.0);
1613
        gray_profile.color_space = DataColorSpace::Gray;
1614
        gray_profile.gray_trc = srgb.red_trc.clone();
1615
        let mut test_profile = vec![0.; 4];
1616
        test_profile[2] = 1.;
1617
        let mut dst = vec![0.; 1];
1618
1619
        let mut inverse = vec![0.; 4];
1620
1621
        let cvt0 = srgb
1622
            .create_transform_f32(
1623
                Layout::Rgba,
1624
                &gray_profile,
1625
                Layout::Gray,
1626
                TransformOptions {
1627
                    allow_extended_range_rgb_xyz: true,
1628
                    ..Default::default()
1629
                },
1630
            )
1631
            .unwrap();
1632
        cvt0.transform(&test_profile, &mut dst).unwrap();
1633
        assert!((dst[0] - 0.273046) < 1e-4);
1634
1635
        let cvt_inverse = gray_profile
1636
            .create_transform_f32(
1637
                Layout::Gray,
1638
                &srgb,
1639
                Layout::Rgba,
1640
                TransformOptions {
1641
                    allow_extended_range_rgb_xyz: false,
1642
                    ..Default::default()
1643
                },
1644
            )
1645
            .unwrap();
1646
        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1647
        assert!((inverse[0] - 0.273002833) < 1e-4);
1648
1649
        let cvt1 = srgb
1650
            .create_transform_f32(
1651
                Layout::Rgba,
1652
                &gray_profile,
1653
                Layout::Gray,
1654
                TransformOptions {
1655
                    allow_extended_range_rgb_xyz: false,
1656
                    ..Default::default()
1657
                },
1658
            )
1659
            .unwrap();
1660
        cvt1.transform(&test_profile, &mut dst).unwrap();
1661
        assert!((dst[0] - 0.27307168) < 1e-5);
1662
1663
        inverse.fill(0.);
1664
1665
        let cvt_inverse = gray_profile
1666
            .create_transform_f32(
1667
                Layout::Gray,
1668
                &srgb,
1669
                Layout::Rgba,
1670
                TransformOptions {
1671
                    allow_extended_range_rgb_xyz: true,
1672
                    ..Default::default()
1673
                },
1674
            )
1675
            .unwrap();
1676
        cvt_inverse.transform(&dst, &mut inverse).unwrap();
1677
        assert!((inverse[0] - 0.273002833) < 1e-4);
1678
    }
1679
1680
    /// Test that multi-pixel RGB transforms produce consistent results for each pixel.
1681
    /// This catches bugs where SIMD implementations incorrectly share data between pixels.
1682
    /// Specifically tests the case where even/odd pixels should have independent blue channels.
1683
    #[test]
1684
    fn test_transform_rgb8_pixel_independence() {
1685
        let srgb_profile = ColorProfile::new_srgb();
1686
        let bt2020_profile = ColorProfile::new_bt2020();
1687
1688
        let transform = bt2020_profile
1689
            .create_transform_8bit(
1690
                Layout::Rgb,
1691
                &srgb_profile,
1692
                Layout::Rgb,
1693
                TransformOptions {
1694
                    prefer_fixed_point: true,
1695
                    ..Default::default()
1696
                },
1697
            )
1698
            .unwrap();
1699
1700
        // Create 4 pixels with distinct colors - specifically testing that
1701
        // even/odd pixels don't share blue channel values
1702
        // Pixel 0: Red (255, 0, 0)
1703
        // Pixel 1: Green (0, 255, 0)
1704
        // Pixel 2: Blue (0, 0, 255)
1705
        // Pixel 3: Yellow (255, 255, 0)
1706
        let src: Vec<u8> = vec![
1707
            255, 0, 0, // Pixel 0: Red
1708
            0, 255, 0, // Pixel 1: Green
1709
            0, 0, 255, // Pixel 2: Blue
1710
            255, 255, 0, // Pixel 3: Yellow
1711
        ];
1712
        let mut dst = vec![0u8; 12];
1713
        transform.transform(&src, &mut dst).unwrap();
1714
1715
        // Process same pixels individually for comparison
1716
        let mut single_pixel_results = Vec::new();
1717
        for pixel in src.chunks(3) {
1718
            let mut single_dst = vec![0u8; 3];
1719
            transform.transform(pixel, &mut single_dst).unwrap();
1720
            single_pixel_results.extend(single_dst);
1721
        }
1722
1723
        // Each pixel in batch processing should match single-pixel processing
1724
        // This catches the vr0/vr1 bug where pixel 1's blue channel would get pixel 0's value
1725
        for (i, (batch, single)) in dst.iter().zip(single_pixel_results.iter()).enumerate() {
1726
            assert_eq!(
1727
                batch,
1728
                single,
1729
                "Mismatch at byte {} (pixel {}, channel {}): batch={}, single={}",
1730
                i,
1731
                i / 3,
1732
                i % 3,
1733
                batch,
1734
                single
1735
            );
1736
        }
1737
    }
1738
}