Skip to main content

hayro_interpret/color/
mod.rs

1//! PDF colors and color spaces.
2
3mod cal_gray;
4mod cal_rgb;
5mod device_cmyk;
6mod device_gray;
7mod device_n;
8mod device_rgb;
9mod icc;
10mod indexed;
11mod lab;
12mod pattern;
13mod separation;
14
15use self::cal_gray::CalGray;
16use self::cal_rgb::CalRgb;
17use self::device_cmyk::DeviceCmyk;
18use self::device_gray::DeviceGray;
19use self::device_n::DeviceN;
20use self::device_rgb::DeviceRgb;
21use self::icc::ICCProfile;
22use self::indexed::Indexed;
23use self::lab::Lab;
24use self::pattern::Pattern;
25use self::separation::Separation;
26use crate::cache::{Cache, CacheKey};
27use hayro_syntax::object::Dict;
28use hayro_syntax::object::Name;
29use hayro_syntax::object::Object;
30use hayro_syntax::object::Stream;
31use hayro_syntax::object::dict::keys::*;
32use smallvec::{SmallVec, smallvec};
33use std::ops::Deref;
34use std::sync::{Arc, OnceLock};
35
36/// A storage for the components of colors.
37pub type ColorComponents = SmallVec<[f32; 4]>;
38
39#[derive(Clone)]
40pub(super) struct U8Lookup<T>(Arc<OnceLock<Option<Box<[T; 256]>>>>);
41
42impl<T> Default for U8Lookup<T> {
43    fn default() -> Self {
44        Self(Arc::new(OnceLock::new()))
45    }
46}
47
48impl<T> std::fmt::Debug for U8Lookup<T> {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.write_str("U8Lookup")
51    }
52}
53
54impl<T> U8Lookup<T> {
55    pub(super) fn get_or_init_with(
56        &self,
57        init: impl FnOnce(&[u8; 256]) -> Option<Box<[T; 256]>>,
58    ) -> Option<&[T; 256]> {
59        self.0
60            .get_or_init(|| {
61                let input: [u8; 256] = core::array::from_fn(|index| index as u8);
62                init(&input)
63            })
64            .as_deref()
65    }
66}
67
68impl U8Lookup<[u8; 3]> {
69    pub(super) fn get_or_init(
70        &self,
71        convert: impl FnOnce(&[u8], &mut [u8]) -> Option<()>,
72    ) -> Option<&[[u8; 3]; 256]> {
73        self.get_or_init_with(|input| {
74            let mut output = Box::new([[0; 3]; 256]);
75            convert(input, output.as_flattened_mut())?;
76            Some(output)
77        })
78    }
79}
80
81/// An RGB color with an alpha channel.
82#[derive(Debug, Copy, Clone)]
83pub struct AlphaColor {
84    components: [f32; 4],
85}
86
87impl AlphaColor {
88    /// A black color.
89    pub const BLACK: Self = Self::new([0., 0., 0., 1.]);
90
91    /// A transparent color.
92    pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]);
93
94    /// A white color.
95    pub const WHITE: Self = Self::new([1., 1., 1., 1.]);
96
97    /// Create a new color from the given components.
98    pub const fn new(components: [f32; 4]) -> Self {
99        Self { components }
100    }
101
102    /// Create a new color from RGB8 values.
103    pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
104        let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.];
105        Self::new(components)
106    }
107
108    /// Return the color as premulitplied RGBF32.
109    pub fn premultiplied(&self) -> [f32; 4] {
110        [
111            self.components[0] * self.components[3],
112            self.components[1] * self.components[3],
113            self.components[2] * self.components[3],
114            self.components[3],
115        ]
116    }
117
118    /// Create a new color from RGBA8 values.
119    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
120        let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)];
121        Self::new(components)
122    }
123
124    /// Return the color as RGBA8.
125    pub fn to_rgba8(&self) -> [u8; 4] {
126        [
127            (self.components[0] * 255.0 + 0.5) as u8,
128            (self.components[1] * 255.0 + 0.5) as u8,
129            (self.components[2] * 255.0 + 0.5) as u8,
130            (self.components[3] * 255.0 + 0.5) as u8,
131        ]
132    }
133
134    /// Return the components of the color as RGBF32.
135    pub fn components(&self) -> [f32; 4] {
136        self.components
137    }
138}
139
140const fn u8_to_f32(x: u8) -> f32 {
141    x as f32 * (1.0 / 255.0)
142}
143
144#[derive(Debug, Clone)]
145pub(crate) enum ColorSpaceType {
146    DeviceCmyk(DeviceCmyk),
147    DeviceGray(DeviceGray),
148    DeviceRgb(DeviceRgb),
149    Pattern(Pattern),
150    Indexed(Indexed),
151    ICCBased(ICCProfile),
152    CalGray(CalGray),
153    CalRgb(CalRgb),
154    Lab(Lab),
155    Separation(Separation),
156    DeviceN(DeviceN),
157}
158
159impl ColorSpaceType {
160    fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
161        Self::new_inner(object, cache)
162    }
163
164    fn new_inner(object: Object<'_>, cache: &Cache) -> Option<Self> {
165        if let Object::Name(name) = object {
166            return Self::new_from_name(&name);
167        } else if let Object::Array(color_array) = object {
168            let mut iter = color_array.flex_iter();
169            let name = iter.next::<Name<'_>>()?;
170
171            match name.deref() {
172                ICC_BASED => {
173                    let icc_stream = iter.next::<Stream<'_>>()?;
174                    let dict = icc_stream.dict();
175                    let num_components = dict.get::<usize>(N)?;
176
177                    return cache.get_or_insert_with(icc_stream.cache_key(), || {
178                        if let Some(decoded) = icc_stream.decoded().ok().as_ref() {
179                            ICCProfile::new(decoded, num_components)
180                                .map(|icc| {
181                                    // TODO: For SVG and PNG we can assume that the output color space is
182                                    // sRGB. If we ever implement PDF-to-PDF, we probably want to
183                                    // let the user pass the native color type and don't make this optimization
184                                    // if it's not sRGB.
185                                    if icc.is_srgb() {
186                                        Self::DeviceRgb(DeviceRgb)
187                                    } else {
188                                        Self::ICCBased(icc)
189                                    }
190                                })
191                                .or_else(|| {
192                                    dict.get::<Object<'_>>(ALTERNATE)
193                                        .and_then(|o| Self::new(o, cache))
194                                })
195                                .or_else(|| match dict.get::<u8>(N) {
196                                    Some(1) => Some(Self::DeviceGray(DeviceGray)),
197                                    Some(3) => Some(Self::DeviceRgb(DeviceRgb)),
198                                    Some(4) => Some(Self::DeviceCmyk(DeviceCmyk)),
199                                    _ => None,
200                                })
201                        } else {
202                            None
203                        }
204                    });
205                }
206                CALCMYK => return Some(Self::DeviceCmyk(DeviceCmyk)),
207                CALGRAY => {
208                    let cal_dict = iter.next::<Dict<'_>>()?;
209                    return Some(Self::CalGray(CalGray::new(&cal_dict)?));
210                }
211                CALRGB => {
212                    let cal_dict = iter.next::<Dict<'_>>()?;
213                    return Some(Self::CalRgb(CalRgb::new(&cal_dict)?));
214                }
215                DEVICE_RGB | RGB => return Some(Self::DeviceRgb(DeviceRgb)),
216                DEVICE_GRAY | G => return Some(Self::DeviceGray(DeviceGray)),
217                DEVICE_CMYK | CMYK => return Some(Self::DeviceCmyk(DeviceCmyk)),
218                LAB => {
219                    let lab_dict = iter.next::<Dict<'_>>()?;
220                    return Some(Self::Lab(Lab::new(&lab_dict)?));
221                }
222                INDEXED | I => {
223                    return Some(Self::Indexed(Indexed::new(&color_array, cache)?));
224                }
225                SEPARATION => {
226                    return Some(Self::Separation(Separation::new(&color_array, cache)?));
227                }
228                DEVICE_N => {
229                    return Some(Self::DeviceN(DeviceN::new(&color_array, cache)?));
230                }
231                PATTERN => {
232                    let _ = iter.next::<Name<'_>>();
233                    let cs = iter
234                        .next::<Object<'_>>()
235                        .and_then(|o| ColorSpace::new(o, cache))
236                        .unwrap_or(ColorSpace::device_rgb());
237                    return Some(Self::Pattern(Pattern::new(cs)));
238                }
239                _ => {
240                    warn!("unsupported color space: {}", name.as_str());
241                    return None;
242                }
243            }
244        }
245
246        None
247    }
248
249    fn new_from_name(name: &Name<'_>) -> Option<Self> {
250        match name.deref() {
251            DEVICE_RGB | RGB => Some(Self::DeviceRgb(DeviceRgb)),
252            DEVICE_GRAY | G => Some(Self::DeviceGray(DeviceGray)),
253            DEVICE_CMYK | CMYK => Some(Self::DeviceCmyk(DeviceCmyk)),
254            CALCMYK => Some(Self::DeviceCmyk(DeviceCmyk)),
255            PATTERN => Some(Self::Pattern(Pattern::new(ColorSpace::device_rgb()))),
256            _ => None,
257        }
258    }
259}
260
261/// A PDF color space.
262#[derive(Debug, Clone)]
263pub struct ColorSpace(Arc<ColorSpaceType>);
264
265impl ColorSpace {
266    /// Create a new color space from the given object.
267    pub(crate) fn new(object: Object<'_>, cache: &Cache) -> Option<Self> {
268        Some(Self(Arc::new(ColorSpaceType::new(object, cache)?)))
269    }
270
271    /// Create a new color space from the name.
272    pub(crate) fn new_from_name(name: &Name<'_>) -> Option<Self> {
273        ColorSpaceType::new_from_name(name).map(|c| Self(Arc::new(c)))
274    }
275
276    /// Return the device gray color space.
277    pub(crate) fn device_gray() -> Self {
278        Self(Arc::new(ColorSpaceType::DeviceGray(DeviceGray)))
279    }
280
281    /// Return the device RGB color space.
282    pub(crate) fn device_rgb() -> Self {
283        Self(Arc::new(ColorSpaceType::DeviceRgb(DeviceRgb)))
284    }
285
286    /// Return the device CMYK color space.
287    pub(crate) fn device_cmyk() -> Self {
288        Self(Arc::new(ColorSpaceType::DeviceCmyk(DeviceCmyk)))
289    }
290
291    /// Return the pattern color space.
292    pub(crate) fn pattern() -> Self {
293        Self(Arc::new(ColorSpaceType::Pattern(Pattern::new(
294            Self::device_gray(),
295        ))))
296    }
297
298    pub(crate) fn pattern_cs(&self) -> Option<Self> {
299        match self.0.as_ref() {
300            ColorSpaceType::Pattern(pattern) => Some(pattern.color_space()),
301            _ => None,
302        }
303    }
304
305    /// Return `true` if the current color space is the pattern color space.
306    pub(crate) fn is_pattern(&self) -> bool {
307        matches!(self.0.as_ref(), ColorSpaceType::Pattern(_))
308    }
309
310    /// Return `true` if the current color space is an indexed color space.
311    pub(crate) fn is_indexed(&self) -> bool {
312        matches!(self.0.as_ref(), ColorSpaceType::Indexed(_))
313    }
314
315    pub(crate) fn indexed_hival(&self) -> Option<u8> {
316        match self.0.as_ref() {
317            ColorSpaceType::Indexed(indexed) => Some(indexed.hival()),
318            ColorSpaceType::Pattern(pattern) => pattern.color_space().indexed_hival(),
319            _ => None,
320        }
321    }
322
323    /// Get the default decode array for the color space.
324    pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
325        match self.0.as_ref() {
326            ColorSpaceType::DeviceCmyk(_) => {
327                smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)]
328            }
329            ColorSpaceType::DeviceGray(_) => smallvec![(0.0, 1.0)],
330            ColorSpaceType::DeviceRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
331            ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.number_components()],
332            ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
333            ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
334            ColorSpaceType::Lab(l) => smallvec![
335                (0.0, 100.0),
336                (l.range[0], l.range[1]),
337                (l.range[2], l.range[3]),
338            ],
339            ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0_f32.powf(n) - 1.0)],
340            ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
341            ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize],
342            // Not a valid image color space.
343            ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)],
344        }
345    }
346
347    pub(crate) fn inverted_default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> {
348        self.default_decode_arr(n)
349            .iter()
350            .map(|(min, max)| (*max, *min))
351            .collect()
352    }
353
354    pub(crate) fn component_ranges(&self) -> SmallVec<[(f32, f32); 4]> {
355        match self.0.as_ref() {
356            ColorSpaceType::DeviceCmyk(_) => smallvec![(0.0, 1.0); 4],
357            ColorSpaceType::DeviceGray(_) => smallvec![(0.0, 1.0)],
358            ColorSpaceType::DeviceRgb(_) => smallvec![(0.0, 1.0); 3],
359            ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.number_components()],
360            ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)],
361            ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0); 3],
362            ColorSpaceType::Lab(_) => {
363                smallvec![(0.0, 100.0), (-128.0, 127.0), (-128.0, 127.0)]
364            }
365            ColorSpaceType::Indexed(i) => smallvec![(0.0, i.hival() as f32)],
366            ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)],
367            ColorSpaceType::Pattern(pattern) => pattern.color_space().component_ranges(),
368            ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize],
369        }
370    }
371
372    pub(crate) fn convert_values(&self, input: &[f32], output: &mut [u8]) -> Option<()> {
373        let converted = self.encode_values(input);
374        self.convert(&converted, output)
375    }
376
377    pub(crate) fn encode_values(&self, input: &[f32]) -> SmallVec<[u8; 4]> {
378        if let Some(hival) = self.indexed_hival() {
379            return input
380                .iter()
381                .map(|value| (*value + 0.5).clamp(0.0, hival as f32) as u8)
382                .collect();
383        }
384
385        let ranges = match self.0.as_ref() {
386            ColorSpaceType::ICCBased(icc) if icc.is_lab() => {
387                smallvec![(0.0, 100.0), (-128.0, 127.0), (-128.0, 127.0)]
388            }
389            _ => self.component_ranges(),
390        };
391        encode_components(input, &ranges)
392    }
393
394    /// Get the initial color of the color space.
395    pub(crate) fn initial_color(&self) -> ColorComponents {
396        match self.0.as_ref() {
397            ColorSpaceType::DeviceCmyk(_) => smallvec![0.0, 0.0, 0.0, 1.0],
398            ColorSpaceType::DeviceGray(_) => smallvec![0.0],
399            ColorSpaceType::DeviceRgb(_) => smallvec![0.0, 0.0, 0.0],
400            ColorSpaceType::ICCBased(icc) => match icc.number_components() {
401                1 => smallvec![0.0],
402                3 => smallvec![0.0, 0.0, 0.0],
403                4 => smallvec![0.0, 0.0, 0.0, 1.0],
404                _ => unreachable!(),
405            },
406            ColorSpaceType::CalGray(_) => smallvec![0.0],
407            ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0],
408            ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0],
409            ColorSpaceType::Indexed(_) => smallvec![0.0],
410            ColorSpaceType::Separation(_) => smallvec![1.0],
411            ColorSpaceType::Pattern(pattern) => pattern.initial_color(),
412            ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components as usize],
413        }
414    }
415
416    /// Get the number of components of the color space.
417    pub(crate) fn num_components(&self) -> u8 {
418        match self.0.as_ref() {
419            ColorSpaceType::DeviceCmyk(_) => 4,
420            ColorSpaceType::DeviceGray(_) => 1,
421            ColorSpaceType::DeviceRgb(_) => 3,
422            ColorSpaceType::ICCBased(icc) => icc.number_components() as u8,
423            ColorSpaceType::CalGray(_) => 1,
424            ColorSpaceType::CalRgb(_) => 3,
425            ColorSpaceType::Lab(_) => 3,
426            ColorSpaceType::Indexed(_) => 1,
427            ColorSpaceType::Separation(_) => 1,
428            ColorSpaceType::Pattern(pattern) => pattern.num_components(),
429            ColorSpaceType::DeviceN(d) => d.num_components,
430        }
431    }
432
433    /// Turn the given component values and opacity into an RGBA color.
434    #[inline]
435    pub fn to_rgba(&self, c: &[f32], opacity: f32) -> AlphaColor {
436        let alpha = f32_to_u8(opacity);
437
438        match self.0.as_ref() {
439            ColorSpaceType::DeviceGray(_) => {
440                let gray = c.first().copied().map(f32_to_u8).unwrap_or(0);
441                AlphaColor::from_rgba8(gray, gray, gray, alpha)
442            }
443            ColorSpaceType::DeviceRgb(_) => AlphaColor::from_rgba8(
444                c.first().copied().map(f32_to_u8).unwrap_or(0),
445                c.get(1).copied().map(f32_to_u8).unwrap_or(0),
446                c.get(2).copied().map(f32_to_u8).unwrap_or(0),
447                alpha,
448            ),
449            ColorSpaceType::DeviceCmyk(device_cmyk) if c.len() == 4 => {
450                let input = [
451                    f32_to_u8(c[0]),
452                    f32_to_u8(c[1]),
453                    f32_to_u8(c[2]),
454                    f32_to_u8(c[3]),
455                ];
456                let mut output = [0; 3];
457
458                if device_cmyk.convert(&input, &mut output).is_some() {
459                    AlphaColor::from_rgba8(output[0], output[1], output[2], alpha)
460                } else {
461                    AlphaColor::BLACK
462                }
463            }
464            _ => self.to_alpha_color(c, opacity).unwrap_or(AlphaColor::BLACK),
465        }
466    }
467
468    fn to_alpha_color(&self, input: &[f32], mut opacity: f32) -> Option<AlphaColor> {
469        let mut output = [0; 3];
470        self.convert_values(input, &mut output)?;
471
472        // For separation color spaces:
473        // "The special colourant name None shall not produce any visible output.
474        // Painting operations in a Separation space with this colourant name
475        // shall have no effect on the current page."
476        if self.is_none() {
477            opacity = 0.0;
478        }
479
480        Some(AlphaColor::from_rgba8(
481            output[0],
482            output[1],
483            output[2],
484            (opacity * 255.0 + 0.5) as u8,
485        ))
486    }
487}
488
489impl ToRgb for ColorSpace {
490    fn convert(&self, input: &[u8], output: &mut [u8]) -> Option<()> {
491        match self.0.as_ref() {
492            ColorSpaceType::DeviceCmyk(i) => i.convert(input, output),
493            ColorSpaceType::DeviceGray(i) => i.convert(input, output),
494            ColorSpaceType::DeviceRgb(i) => i.convert(input, output),
495            ColorSpaceType::Pattern(i) => i.convert(input, output),
496            ColorSpaceType::Indexed(i) => i.convert(input, output),
497            ColorSpaceType::ICCBased(i) => i.convert(input, output),
498            ColorSpaceType::CalGray(i) => i.convert(input, output),
499            ColorSpaceType::CalRgb(i) => i.convert(input, output),
500            ColorSpaceType::Lab(i) => i.convert(input, output),
501            ColorSpaceType::Separation(i) => i.convert(input, output),
502            ColorSpaceType::DeviceN(i) => i.convert(input, output),
503        }
504    }
505
506    fn convert_in_place(&self, input: &mut [u8]) -> Option<()> {
507        match self.0.as_ref() {
508            ColorSpaceType::DeviceCmyk(i) => i.convert_in_place(input),
509            ColorSpaceType::DeviceGray(i) => i.convert_in_place(input),
510            ColorSpaceType::DeviceRgb(i) => i.convert_in_place(input),
511            ColorSpaceType::Pattern(i) => i.convert_in_place(input),
512            ColorSpaceType::Indexed(i) => i.convert_in_place(input),
513            ColorSpaceType::ICCBased(i) => i.convert_in_place(input),
514            ColorSpaceType::CalGray(i) => i.convert_in_place(input),
515            ColorSpaceType::CalRgb(i) => i.convert_in_place(input),
516            ColorSpaceType::Lab(i) => i.convert_in_place(input),
517            ColorSpaceType::Separation(i) => i.convert_in_place(input),
518            ColorSpaceType::DeviceN(i) => i.convert_in_place(input),
519        }
520    }
521
522    fn is_none(&self) -> bool {
523        match self.0.as_ref() {
524            ColorSpaceType::Separation(s) => s.is_none(),
525            ColorSpaceType::DeviceN(d) => d.is_none(),
526            _ => false,
527        }
528    }
529}
530
531impl ToLuma for ColorSpace {
532    fn to_luma(&self, input: &mut [u8]) -> Option<()> {
533        match self.0.as_ref() {
534            ColorSpaceType::DeviceGray(i) => i.to_luma(input),
535            ColorSpaceType::Pattern(i) => i.to_luma(input),
536            ColorSpaceType::Indexed(i) => i.to_luma(input),
537            ColorSpaceType::ICCBased(i) => i.to_luma(input),
538            ColorSpaceType::CalGray(i) => i.to_luma(input),
539            ColorSpaceType::DeviceCmyk(_)
540            | ColorSpaceType::DeviceRgb(_)
541            | ColorSpaceType::CalRgb(_)
542            | ColorSpaceType::Lab(_)
543            | ColorSpaceType::Separation(_)
544            | ColorSpaceType::DeviceN(_) => None,
545        }
546    }
547}
548
549#[inline(always)]
550fn f32_to_u8(val: f32) -> u8 {
551    (val * 255.0 + 0.5) as u8
552}
553
554#[derive(Debug, Clone)]
555/// A color.
556pub struct Color {
557    color_space: ColorSpace,
558    components: ColorComponents,
559    opacity: f32,
560}
561
562impl Color {
563    pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self {
564        Self {
565            color_space,
566            components,
567            opacity,
568        }
569    }
570
571    /// Return the color as an RGBA color.
572    #[inline]
573    pub fn to_rgba(&self) -> AlphaColor {
574        self.color_space.to_rgba(&self.components, self.opacity)
575    }
576
577    /// Create a color from RGBA.
578    #[inline]
579    pub fn from_rgba(rgba: AlphaColor) -> Self {
580        let c = rgba.components();
581        Self {
582            color_space: ColorSpace::device_rgb(),
583            components: smallvec![c[0], c[1], c[2]],
584            opacity: c[3],
585        }
586    }
587}
588
589pub(crate) trait ToRgb {
590    fn convert(&self, input: &[u8], output: &mut [u8]) -> Option<()>;
591    fn convert_in_place(&self, _input: &mut [u8]) -> Option<()> {
592        None
593    }
594    fn is_none(&self) -> bool {
595        false
596    }
597}
598
599pub(crate) trait ToLuma {
600    fn to_luma(&self, input: &mut [u8]) -> Option<()>;
601}
602
603#[inline]
604fn encode_components(input: &[f32], ranges: &[(f32, f32)]) -> SmallVec<[u8; 4]> {
605    input
606        .iter()
607        .enumerate()
608        .map(|(index, value)| {
609            let (min, max) = ranges[index % ranges.len()];
610            (((*value - min) / (max - min)) * 255.0 + 0.5) as u8
611        })
612        .collect()
613}