/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 | | } |