Palette 0.7.0

10 Apr 2023 — Permalink

It’s been a while, but here’s another release of Palette!

For those who aren’t familiar with it, Palette is a Rust library for working with colors, including color conversion and color management. Its features range from common tasks, like color blending or converting HSL to RGB, to more advanced topics. It leverages Rust’s type system, by encoding color space information as type parameters, to be able to prevent mistakes and to keep it open to extension and customization.

OkHsv, OkHsl And OkHwb

A set of new color spaces have been added, once again based on the work of Björn Ottosson. These are similar to the RGB based HSV, HSL and HWB, but with much better perceptual uniformity, which makes their visual properties correspond more to their numerical and mathematical properties and vice versa. This may, for example, give more predictable results in applications like color pickers and generative art.

Faster sRGB

The sRGB code has been optimized in a couple of places. First, conversion between non-linear u8 sRGB and linear f32 and f64 sRGB has been made much faster, thanks to fast-srgb8. This does not only mean that reading and writing image buffers is faster, but also that conversion can be done with one function call instead of two. {from,into}_linear and {from,into}_encoding will now accept a type parameter:

use palette::{Srgb, LinSrgb};
// Before the change, `into_format` was always necessary for converting to `f32`
let linear: LinSrgb<f32> = Srgb::new(96u8, 127, 0).into_format().into_linear();

// After the change, it's both enough and also faster to call `into_linear` directly
let linear: LinSrgb<f32> = Srgb::new(96u8, 127, 0).into_linear();

When into_linear is called on Srgb<u8>, it will use a lookup table to convert the components directly to float values. The reason why calling into_format().into_linear() is slower is that it bypasses this lookup table and uses the old float-to-float code. This is hard to prevent, from a library point of view, but it will hopefully help that the better option is also simpler. A non-scientific micro-benchmark showed an 85% to 97% reduction in conversion time, but the actual speed-up will vary from application to application.

The second speed-up is in the conversion to and from XYZ. This uses a pair of matrices that can be quite expensive to construct. A change to the RgbSpace trait makes it possible to optionally provide pre-computed matrices and speed things up a lot. The above mentioned non-scientific micro-benchmark also showed an 80% to 90% reduction in conversion time in this case, but the actual speed-up will again vary from application to application.

Goodbye Gradient

The Gradient struct and its surrounding features have been removed in Palette 0.7.0. The reason behind this decision is to make the scope more narrow and also that it wasn’t a particularly good implementation. It could only produce linear gradients, for example. With that in mind and in order to make the library easier to maintain, the decision was made to remove it entirely, despite its popularity.

It’s not entirely fair to remove a feature without providing an alternative, though. You may want to have a look at enterpolation, which has the same set of features and much more. Here’s what one of the classic examples may look like when using the enterpolation crate:

use enterpolation::{linear::Linear, Curve};
use palette::LinSrgb;

// May also be enough with `Linear::`, depending on type inference
let gradient = ConstEquidistantLinear::<f32, _, 3>::equidistant_unchecked([
    LinSrgb::new(0.00, 0.05, 0.20),
    LinSrgb::new(0.70, 0.10, 0.20),
    LinSrgb::new(0.95, 0.90, 0.30),
]);

let taken_colors = gradient.take(10).collect::<Vec<_>>();

One caveat for now is that color spaces with hues will not work out of the box. They need a small adapter to make the gradient use the Mix trait instead of arithmetic operators:

use enterpolation::{linear::Linear, Curve, Merge};
use palette::{LinSrgb, Lch, Mix};

#[derive(Clone, Copy, Debug)]
struct Adapter<T>(T);

impl<T: Mix> Merge<T::Scalar> for Adapter<T> {
    fn merge(self, to: Self, factor: T::Scalar) -> Self {
        Adapter(self.0.mix(to.0, factor))
    }
}

// May also be enough with `Linear::`, depending on type inference
let gradient = ConstEquidistantLinear::<f32, _, 3>::equidistant_unchecked([
    Adapter(Lch::from_color(LinSrgb::new(0.00, 0.05, 0.20))),
    Adapter(Lch::from_color(LinSrgb::new(0.70, 0.10, 0.20))),
    Adapter(Lch::from_color(LinSrgb::new(0.95, 0.90, 0.30))),
]);

let taken_colors = gradient
  .take(10)
  .map(|Adapter(color)| color)
  .collect::<Vec<_>>();

The Big Trait Rework - Part 1

A number of traits, including TransferFn, Blend, Saturate, Shade and Hue, have been reworked to be more flexible and have less restrictions. Some inspiration has been taken from the std::ops traits in the sense that there are now also *Assign variants of most traits. For example, Saturate has been split into Saturate, SaturateAssign, Desaturate, and DesaturateAssign.

A bonus addition from this split is the implementation of *Assign trait for slices, for the sake of convenience:

 use palette::{Hsl, SaturateAssign};

let mut my_vec = vec![Hsl::new_srgb(104.0, 0.3, 0.8), Hsl::new_srgb(113.0, 0.5, 0.8)];
let mut my_array = [Hsl::new_srgb(104.0, 0.3, 0.8), Hsl::new_srgb(113.0, 0.5, 0.8)];
let mut my_slice = &mut [Hsl::new_srgb(104.0, 0.3, 0.8), Hsl::new_srgb(113.0, 0.5, 0.8)];

my_vec.saturate_assign(0.5);
my_array.saturate_assign(0.5);
my_slice.saturate_assign(0.5);

As part of this change, the Blend trait also got some long awaited love. All of the blending functions have been re-implemented in a way that’s much easier to read and maintain. They are also available for more color spaces than just RGB.

This work will continue in a later release.

Array And Integer Casting

The Pixel trait has been replaced by ArrayCast, which essentially is a marker trait that promises that a type can be cast to and from an array of arbitrary values. The ArrayCast doesn’t add any methods to the implementing type, itself. Those have instead been moved into the palette::cast module. There are also a number of From, Into, AsRef and AsMut implementations that makes life easier. Here’s an example that uses both From and palette::cast::from_array_slice_mut:

use palette::{cast, Srgb, IntoColor};

let color = Srgb::from([23u8, 198, 76]).into_linear();

let buffer = &mut [[64u8, 139, 10], [93, 18, 214]];
let color_buffer = cast::from_array_slice_mut::<Srgb<u8>>(buffer);

for destination in color_buffer {
    let linear_dst = destination.into_linear::<f32>();
    *destination = (linear_dst + color).into_encoding();
}

Casting to and from unsigned integers has also gotten a similar treatment, but the UintCast trait enables the casting this time. Integer casting is a bit more restricted for a few reason, including endianness, but works largely the same as array casting. For example, casting a color into u32 can be done with a single into call:

use palette::Srgba;

let color = Srgba::new(23u8, 198, 76, 255);
let uint_color: u32 = color.into();

A lot more can be found in the palette::cast module, including how to use the Packed wrapper to control the channel ordering.

SIMD Work In Progress

Parts of the code have been rewritten to work with SIMD types, starting with wide support. This was made possible by replacing the num_traits dependency with a custom set of traits that don’t assume that the number type is a single floating point value.

The point of all of this is not only to squeeze some more performance out of one’s CPU. It will also make it easier for someone who is already using SIMD types in their application to use Palette without having to go back and forth between SIMD and scalar types as much.

Note: Keep in mind that the current arrangement isn’t optimal and is still a bit of a proof of concept, so be prepared for some rough edges!

Other Changes

Other notable changes include:

  • Improved compilation speed when default features are turned off. (YMMV)
  • In-place temporary color space conversion for spaces that have the same memory representation, using FromColorMut and friends.
  • In-place permanent color space conversion for Vec<T> and Box<[T]>, again for spaces that have the same memory representation.
  • Relaxed trait bounds and lifted linearity restrictions, allowing use cases the were previously assumed incorrect.
  • Better color constructors, including const fn constructors.

Wrapping Up

I want to give a big thank you to everyone who contributed or took part in discussions. This crate wouldn’t be where it is without you all.

Palette can be found on GitHub, and on crates.io.

Thank you for reading!