Skip to main content

moxcms/conversions/
gray2rgb.rs

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#[cfg(feature = "in_place")]
30use crate::InPlaceTransformExecutor;
31use crate::transform::PointeeSizeExpressible;
32use crate::{CmsError, Layout, TransformExecutor};
33use num_traits::AsPrimitive;
34use std::sync::Arc;
35
36#[derive(Clone)]
37struct TransformGray2RgbFusedExecutor<T, const SRC_LAYOUT: u8, const DEST_LAYOUT: u8> {
38    fused_gamma: Box<[T; 65536]>,
39    bit_depth: usize,
40}
41
42pub(crate) fn make_gray_to_x<
43    T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync,
44    const BUCKET: usize,
45>(
46    src_layout: Layout,
47    dst_layout: Layout,
48    gray_linear: &[f32; BUCKET],
49    gray_gamma: &[T; 65536],
50    bit_depth: usize,
51    gamma_lut: usize,
52) -> Result<Arc<dyn TransformExecutor<T> + Sync + Send>, CmsError>
53where
54    u32: AsPrimitive<T>,
55{
56    if src_layout != Layout::Gray && src_layout != Layout::GrayAlpha {
57        return Err(CmsError::UnsupportedProfileConnection);
58    }
59
60    let mut fused_gamma = Box::new([T::default(); 65536]);
61    let max_lut_size = (gamma_lut - 1) as f32;
62    for (&src, dst) in gray_linear.iter().zip(fused_gamma.iter_mut()) {
63        let possible_value = ((src * max_lut_size).round() as u32).min(max_lut_size as u32) as u16;
64        *dst = gray_gamma[possible_value as usize];
65    }
66
67    match src_layout {
68        Layout::Gray => match dst_layout {
69            Layout::Rgb => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
70                T,
71                { Layout::Gray as u8 },
72                { Layout::Rgb as u8 },
73            > {
74                fused_gamma,
75                bit_depth,
76            })),
77            Layout::Rgba => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
78                T,
79                { Layout::Gray as u8 },
80                { Layout::Rgba as u8 },
81            > {
82                fused_gamma,
83                bit_depth,
84            })),
85            Layout::Gray => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
86                T,
87                { Layout::Gray as u8 },
88                { Layout::Gray as u8 },
89            > {
90                fused_gamma,
91                bit_depth,
92            })),
93            Layout::GrayAlpha => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
94                T,
95                { Layout::Gray as u8 },
96                { Layout::GrayAlpha as u8 },
97            > {
98                fused_gamma,
99                bit_depth,
100            })),
101            _ => Err(CmsError::UnsupportedProfileConnection),
102        },
103        Layout::GrayAlpha => match dst_layout {
104            Layout::Rgb => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
105                T,
106                { Layout::GrayAlpha as u8 },
107                { Layout::Rgb as u8 },
108            > {
109                fused_gamma,
110                bit_depth,
111            })),
112            Layout::Rgba => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
113                T,
114                { Layout::GrayAlpha as u8 },
115                { Layout::Rgba as u8 },
116            > {
117                fused_gamma,
118                bit_depth,
119            })),
120            Layout::Gray => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
121                T,
122                { Layout::GrayAlpha as u8 },
123                { Layout::Gray as u8 },
124            > {
125                fused_gamma,
126                bit_depth,
127            })),
128            Layout::GrayAlpha => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
129                T,
130                { Layout::GrayAlpha as u8 },
131                { Layout::GrayAlpha as u8 },
132            > {
133                fused_gamma,
134                bit_depth,
135            })),
136            _ => Err(CmsError::UnsupportedProfileConnection),
137        },
138        _ => Err(CmsError::UnsupportedProfileConnection),
139    }
140}
141
142#[cfg(feature = "in_place")]
143pub(crate) fn make_gray_to_gray_in_place<
144    T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync,
145    const BUCKET: usize,
146>(
147    layout: Layout,
148    gray_linear: &[f32; BUCKET],
149    gray_gamma: &[T; 65536],
150    bit_depth: usize,
151    gamma_lut: usize,
152) -> Result<Arc<dyn InPlaceTransformExecutor<T> + Sync + Send>, CmsError>
153where
154    u32: AsPrimitive<T>,
155{
156    if layout != Layout::Gray && layout != Layout::GrayAlpha {
157        return Err(CmsError::UnsupportedProfileConnection);
158    }
159
160    let mut fused_gamma = Box::new([T::default(); 65536]);
161    let max_lut_size = (gamma_lut - 1) as f32;
162    for (&src, dst) in gray_linear.iter().zip(fused_gamma.iter_mut()) {
163        let possible_value = ((src * max_lut_size).round() as u32).min(max_lut_size as u32) as u16;
164        *dst = gray_gamma[possible_value as usize];
165    }
166
167    match layout {
168        Layout::Gray => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
169            T,
170            { Layout::Gray as u8 },
171            { Layout::Gray as u8 },
172        > {
173            fused_gamma,
174            bit_depth,
175        })),
176        Layout::GrayAlpha => Ok(Arc::new(TransformGray2RgbFusedExecutor::<
177            T,
178            { Layout::GrayAlpha as u8 },
179            { Layout::GrayAlpha as u8 },
180        > {
181            fused_gamma,
182            bit_depth,
183        })),
184        _ => Err(CmsError::UnsupportedProfileConnection),
185    }
186}
187
188impl<
189    T: Copy + Default + PointeeSizeExpressible + 'static,
190    const SRC_LAYOUT: u8,
191    const DST_LAYOUT: u8,
192> TransformExecutor<T> for TransformGray2RgbFusedExecutor<T, SRC_LAYOUT, DST_LAYOUT>
193where
194    u32: AsPrimitive<T>,
195{
196    fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> {
197        let src_cn = Layout::from(SRC_LAYOUT);
198        let dst_cn = Layout::from(DST_LAYOUT);
199        let src_channels = src_cn.channels();
200        let dst_channels = dst_cn.channels();
201
202        if src.len() / src_channels != dst.len() / dst_channels {
203            return Err(CmsError::LaneSizeMismatch);
204        }
205        if src.len() % src_channels != 0 {
206            return Err(CmsError::LaneMultipleOfChannels);
207        }
208        if dst.len() % dst_channels != 0 {
209            return Err(CmsError::LaneMultipleOfChannels);
210        }
211
212        let is_gray_alpha = src_cn == Layout::GrayAlpha;
213
214        let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_();
215
216        for (src, dst) in src
217            .chunks_exact(src_channels)
218            .zip(dst.chunks_exact_mut(dst_channels))
219        {
220            let g = self.fused_gamma[src[0]._as_usize()];
221            let a = if is_gray_alpha { src[1] } else { max_value };
222
223            dst[0] = g;
224            if dst_cn == Layout::GrayAlpha {
225                dst[1] = a;
226            } else if dst_cn == Layout::Rgb {
227                dst[1] = g;
228                dst[2] = g;
229            } else if dst_cn == Layout::Rgba {
230                dst[1] = g;
231                dst[2] = g;
232                dst[3] = a;
233            }
234        }
235
236        Ok(())
237    }
238}
239
240#[cfg(feature = "in_place")]
241impl<
242    T: Copy + Default + PointeeSizeExpressible + 'static,
243    const SRC_LAYOUT: u8,
244    const DST_LAYOUT: u8,
245> InPlaceTransformExecutor<T> for TransformGray2RgbFusedExecutor<T, SRC_LAYOUT, DST_LAYOUT>
246where
247    u32: AsPrimitive<T>,
248{
249    fn transform(&self, in_out: &mut [T]) -> Result<(), CmsError> {
250        assert_eq!(
251            SRC_LAYOUT, DST_LAYOUT,
252            "This is in-place transform, layout must not diverge"
253        );
254        let src_cn = Layout::from(SRC_LAYOUT);
255        let src_channels = src_cn.channels();
256
257        if in_out.len() % src_channels != 0 {
258            return Err(CmsError::LaneMultipleOfChannels);
259        }
260
261        let is_gray_alpha = src_cn == Layout::GrayAlpha;
262
263        let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_();
264
265        for dst in in_out.chunks_exact_mut(src_channels) {
266            let g = self.fused_gamma[dst[0]._as_usize()];
267            let a = if is_gray_alpha { dst[1] } else { max_value };
268
269            dst[0] = g;
270            if src_cn == Layout::GrayAlpha {
271                dst[1] = a;
272            }
273        }
274
275        Ok(())
276    }
277}
278
279#[derive(Clone)]
280struct TransformGrayToRgbExecutor<T, const SRC_LAYOUT: u8, const DEST_LAYOUT: u8> {
281    gray_linear: Box<[f32; 65536]>,
282    red_gamma: Box<[T; 65536]>,
283    green_gamma: Box<[T; 65536]>,
284    blue_gamma: Box<[T; 65536]>,
285    bit_depth: usize,
286    gamma_lut: usize,
287}
288
289#[allow(clippy::too_many_arguments)]
290pub(crate) fn make_gray_to_unfused<
291    T: Copy + Default + PointeeSizeExpressible + 'static + Send + Sync,
292    const BUCKET: usize,
293>(
294    src_layout: Layout,
295    dst_layout: Layout,
296    gray_linear: Box<[f32; 65536]>,
297    red_gamma: Box<[T; 65536]>,
298    green_gamma: Box<[T; 65536]>,
299    blue_gamma: Box<[T; 65536]>,
300    bit_depth: usize,
301    gamma_lut: usize,
302) -> Result<Arc<dyn TransformExecutor<T> + Sync + Send>, CmsError>
303where
304    u32: AsPrimitive<T>,
305{
306    if src_layout != Layout::Gray && src_layout != Layout::GrayAlpha {
307        return Err(CmsError::UnsupportedProfileConnection);
308    }
309    if dst_layout != Layout::Rgb && dst_layout != Layout::Rgba {
310        return Err(CmsError::UnsupportedProfileConnection);
311    }
312    match src_layout {
313        Layout::Gray => match dst_layout {
314            Layout::Rgb => Ok(Arc::new(TransformGrayToRgbExecutor::<
315                T,
316                { Layout::Gray as u8 },
317                { Layout::Rgb as u8 },
318            > {
319                gray_linear,
320                red_gamma,
321                green_gamma,
322                blue_gamma,
323                bit_depth,
324                gamma_lut,
325            })),
326            Layout::Rgba => Ok(Arc::new(TransformGrayToRgbExecutor::<
327                T,
328                { Layout::Gray as u8 },
329                { Layout::Rgba as u8 },
330            > {
331                gray_linear,
332                red_gamma,
333                green_gamma,
334                blue_gamma,
335                bit_depth,
336                gamma_lut,
337            })),
338            Layout::Gray => Ok(Arc::new(TransformGrayToRgbExecutor::<
339                T,
340                { Layout::Gray as u8 },
341                { Layout::Gray as u8 },
342            > {
343                gray_linear,
344                red_gamma,
345                green_gamma,
346                blue_gamma,
347                bit_depth,
348                gamma_lut,
349            })),
350            Layout::GrayAlpha => Ok(Arc::new(TransformGrayToRgbExecutor::<
351                T,
352                { Layout::Gray as u8 },
353                { Layout::GrayAlpha as u8 },
354            > {
355                gray_linear,
356                red_gamma,
357                green_gamma,
358                blue_gamma,
359                bit_depth,
360                gamma_lut,
361            })),
362            _ => Err(CmsError::UnsupportedProfileConnection),
363        },
364        Layout::GrayAlpha => match dst_layout {
365            Layout::Rgb => Ok(Arc::new(TransformGrayToRgbExecutor::<
366                T,
367                { Layout::GrayAlpha as u8 },
368                { Layout::Rgb as u8 },
369            > {
370                gray_linear,
371                red_gamma,
372                green_gamma,
373                blue_gamma,
374                bit_depth,
375                gamma_lut,
376            })),
377            Layout::Rgba => Ok(Arc::new(TransformGrayToRgbExecutor::<
378                T,
379                { Layout::GrayAlpha as u8 },
380                { Layout::Rgba as u8 },
381            > {
382                gray_linear,
383                red_gamma,
384                green_gamma,
385                blue_gamma,
386                bit_depth,
387                gamma_lut,
388            })),
389            Layout::Gray => Ok(Arc::new(TransformGrayToRgbExecutor::<
390                T,
391                { Layout::GrayAlpha as u8 },
392                { Layout::Gray as u8 },
393            > {
394                gray_linear,
395                red_gamma,
396                green_gamma,
397                blue_gamma,
398                bit_depth,
399                gamma_lut,
400            })),
401            Layout::GrayAlpha => Ok(Arc::new(TransformGrayToRgbExecutor::<
402                T,
403                { Layout::GrayAlpha as u8 },
404                { Layout::GrayAlpha as u8 },
405            > {
406                gray_linear,
407                red_gamma,
408                green_gamma,
409                blue_gamma,
410                bit_depth,
411                gamma_lut,
412            })),
413            _ => Err(CmsError::UnsupportedProfileConnection),
414        },
415        _ => Err(CmsError::UnsupportedProfileConnection),
416    }
417}
418
419impl<
420    T: Copy + Default + PointeeSizeExpressible + 'static,
421    const SRC_LAYOUT: u8,
422    const DST_LAYOUT: u8,
423> TransformExecutor<T> for TransformGrayToRgbExecutor<T, SRC_LAYOUT, DST_LAYOUT>
424where
425    u32: AsPrimitive<T>,
426{
427    fn transform(&self, src: &[T], dst: &mut [T]) -> Result<(), CmsError> {
428        let src_cn = Layout::from(SRC_LAYOUT);
429        let dst_cn = Layout::from(DST_LAYOUT);
430        let src_channels = src_cn.channels();
431        let dst_channels = dst_cn.channels();
432
433        if src.len() / src_channels != dst.len() / dst_channels {
434            return Err(CmsError::LaneSizeMismatch);
435        }
436        if src.len() % src_channels != 0 {
437            return Err(CmsError::LaneMultipleOfChannels);
438        }
439        if dst.len() % dst_channels != 0 {
440            return Err(CmsError::LaneMultipleOfChannels);
441        }
442
443        let is_gray_alpha = src_cn == Layout::GrayAlpha;
444
445        let max_value: T = ((1u32 << self.bit_depth as u32) - 1u32).as_();
446        let max_lut_size = (self.gamma_lut - 1) as f32;
447
448        for (src, dst) in src
449            .chunks_exact(src_channels)
450            .zip(dst.chunks_exact_mut(dst_channels))
451        {
452            let g = self.gray_linear[src[0]._as_usize()];
453            let a = if is_gray_alpha { src[1] } else { max_value };
454
455            let possible_value = ((g * max_lut_size).round() as u16) as usize;
456            let red_value = self.red_gamma[possible_value];
457            let green_value = self.green_gamma[possible_value];
458            let blue_value = self.blue_gamma[possible_value];
459
460            if dst_cn == Layout::Rgb {
461                dst[0] = red_value;
462                dst[1] = green_value;
463                dst[2] = blue_value;
464            } else if dst_cn == Layout::Rgba {
465                dst[0] = red_value;
466                dst[1] = green_value;
467                dst[2] = blue_value;
468                dst[3] = a;
469            } else {
470                return Err(CmsError::UnsupportedProfileConnection);
471            }
472        }
473
474        Ok(())
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::{ColorProfile, TransformOptions};
482
483    #[test]
484    fn gray_rgb_roundtrip() {
485        let gray = ColorProfile::new_gray_with_gamma(2.2);
486        let srgb = ColorProfile::new_srgb();
487
488        let src_layout = [Layout::Gray, Layout::GrayAlpha];
489        let dst_layout = [Layout::Rgb, Layout::GrayAlpha, Layout::Rgba, Layout::Gray];
490        for src in src_layout {
491            for dst in dst_layout {
492                let transform = gray
493                    .create_transform_8bit(src, &srgb, dst, TransformOptions::default())
494                    .unwrap();
495                let mut in_px = vec![0u8; src.channels()];
496                let mut out_px = vec![0u8; dst.channels()];
497                transform.transform(&mut in_px, &mut out_px).unwrap();
498            }
499        }
500    }
501}