Palette 0.6.0

12 Jul 2021 — Permalink

It’s time for another release of Palette! :tada:

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 gradients 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.

New Conversion Traits

Starting with the largest breaking change, the new traits for converting between color spaces have finally been implemented. After thinking a lot about the pros and cons, the decision was made to move away from From and Into in favor of traits specifically made for converting colors.

The new traits are

  • FromColor and IntoColor for converting with automatic clamping.
    This is always lossy, including in the FromColor<Self> for Self case.
  • TryFromColor and TryIntoColor that returns a Result instead of clamping.
  • FromColorUnclamped and IntoColorUnclamped for converting without any clamping at all.

Here’s a small example of FromColor and IntoColor from the README:

use palette::{FromColor, IntoColor, Hsl, Lch, Srgb};

let my_rgb = Srgb::new(0.8, 0.3, 0.3);

let mut my_lch = Lch::from_color(my_rgb);
my_lch.hue += 180.0;

let mut my_hsl: Hsl = my_lch.into_color();
my_hsl.lightness *= 0.6;

let my_new_rgb = Srgb::from_color(my_hsl);

…and a short migration guide:

  • If you were using From or Into or the old FromColor or IntoColor:
    • You can retain the old behavior with FromColorUnclamped or IntoColorUnclamped,
    • or consider using the new FromColor or IntoColor to get automatic clamping.
  • If you were using the temporary ConvertFrom or ConvertInto
    • convert_{from,into} corresponds to FromColor and IntoColor,
    • convert_unclamped_{from,into} corresponds to FromColorUnclamped and IntoColorUnclamped,
    • try_convert_{from,into} corresponds to TryFromColor and TryIntoColor.

There may also be differences in type inference and the number of required conversion steps. Other than that, you should be able to do the same things a before. Feel free to open a discussion thread or a bug report if you have questions or if it doesn’t work as expected.

Other Breaking Changes

The Limited trait is now named Clamp to better reflect what it does. It’s implemented for every color space type and is a requirement for the blanket implementation of FromColor.

The Component trait has been reworked and there are now two traits: Component and FloatComponent. Component is implemented by all kinds of color component types while FloatComponent is for components that aren’t integers.

The conversion between different component types is also different. The traits FromComponent and IntoComponent have been added to allow more flexible and optimized conversions.

New Color Spaces

A handful of new color spaces have been added:

  • Oklab and Oklch - Oklab is similar to CIE L*a*b* but tries to improve on some of its shortcomings. See Björn Ottosson’s page on it for details.
  • Luv, Lchuv and Hsluv - HSLuv is described as “a human-friendly alternative to HSL”. HSLuv is based on CIE 1976 L*u*v*, so Luv and Lchuv were also added in the process.

These color spaces are made to be as perceptually uniform as possible, meaning the numbers and how they are changed aligns well with how we perceive the resulting colors. For example, two colors with the same numerical lightness should have the same perceived lightness.

RGB, HSV, HSL and HWB Improvements

Rgb with u8 components implements FromStr for parsing hexadecimal strings, such as #a1f, #421ee7, c0ffee, etc. The # is optional and the length can be either 3 or 6 in this initial implementation.

Rgb and Rgba with u8 components can now also be converted to and from u32. A new helper struct, called Packed, has been added to represent RGB colors in u32 format. It supports multiple different channel orderings, and is meant to be used as a temporary representation when reading and writing colors represented as u32:

use palette::{Packed, Pixel, Srgba};
use palette::rgb::channels::Argb;

let raw = &[0x7F0080u32, 0x60BBCC];

// Cast the slice to `&[Packed<Argb>]` without copying
let colors = Packed::<Argb>::from_raw_slice(raw);

let first_color: Srgba<u8> = colors[0].into();

Hsv, Hsl and Hwb have been made “encoding aware”, meaning they can represent non-linear RGB colors. They were previously only linear, due to an incorrect assumption, resulting in some situations where they could not give the same result as in other graphics software. At least not without workarounds. This has been fixed in 0.6.0 and it’s now possible to choose their encoding.

Gradient Improvements

The Gradient type has gotten a few fixes and improvements. The internal storage type has been made generic, which will allow the default Vec to be replaced with other types. An array, for example:

use palette::{Gradient, LinSrgb};

let gradient = Gradient::from([
    (0.0, LinSrgb::new(0.00, 0.05, 0.20)), // A pair of position and color.
    (0.2, LinSrgb::new(0.70, 0.10, 0.20)),
    (1.0, LinSrgb::new(0.95, 0.90, 0.30)),
]);

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

An illustration of the gradient with the continuous form above a row of discrete color swatches.

Making the Gradient storage generic has also made it possible to add a few pre-defined gradients in the gradient::named module. They can be used as convenient alternatives to designing custom gradients.

The Take iterator for gradients has also gotten a couple of improvements. It has been changed to always include the end of the range, instead of stopping shortly before. It does also implement the DoubleEndedIterator trait that allows it to be reversed.

Color Difference and Color Contrast

Lab and Lch implement methods for calculating their CIEDE2000 color difference. This is available via the ColorDifference trait and can give better results than the simpler option of using the Euclidean distance between two colors.

There is also a new trait, called RelativeContrast, for calculating the WCAG 2.1 contrast ratio between two colors. This is a method some accessibility tools and guidelines use for determining if there’s enough color contrast between background and foreground elements.

use std::str::FromStr;
use palette::{Srgb, RelativeContrast};

// The rustdoc "DARK" theme background and text colors
let background: Srgb<f32> = Srgb::from_str("#353535")?.into_format();
let foreground = Srgb::from_str("#ddd")?.into_format();

assert!(background.has_enhanced_contrast_text(&foreground));

Integration With Other Crates

Every color space type implements Zeroable and Pod from the bytemuck crate, behind the "bytemuck" cargo feature (disabled by default). It allows more ways of casting between different data formats.

Every color space type can also be randomly generated, using rand crate. This is available behind the "random" cargo feature (disabled by default). It’s however implemented in a way that tries to compensate for biases in some the color spaces, as opposed to just randomizing each component independently. This means that random values in HSV, HSL and HWB will be uniformly distributed in RGB, for example.

This image shows the difference. The colums are for RGB, HSV, HSL and HWB respectively. The first row has the components randomized independency. The second row is randomized with bias compensation.

A comparison of different randomization techniques.

Benchmarking and Optimizations

Some parts of the library, including matrix math, some color conversion, and component type conversion have been optimized. This was initially meant to lay a foundation for benchmarking and to pick some low hanging fruit. Some of the measurements showed between 10% and 50% reduction in computation time (even more in some cases), so there should hopefully be a noticeable improvement.

I’m a bit careful with my claims, since the actual result may be affected by other factors than what were measured. Our micro benchmarks looked very promising but your mileage may vary.

Other Notable Fixes and Improvements

The Shade (for lightening and darkening) and Saturate traits have been improved to be in line with what you may find in other graphics software. Their new form supports both fixed and relative changes:

use approx::assert_relative_eq;
use palette::{Hsl, Shade};

let color = Hsl::new(0.0, 1.0, 0.5);
// Moves the lightness value 50% of the distance towards the maximum
assert_relative_eq!(color.lighten(0.5).lightness, 0.75);

let color = Hsl::new(0.0, 1.0, 0.4);
// Adds 0.2 to the lightness value
assert_relative_eq!(color.lighten_fixed(0.2).lightness, 0.6);

All color space types implement PartialEq and Eq for component types that support them.

All color space types have functions to get minimum and maximum values of their components.

A WithAlpha trait has been added to make it easier to add or set transparency values.

use palette::{Srgb, WithAlpha};

let color = Srgb::new(255u8, 0, 255);

// This results in an `Alpha<Srgb<u8>, f32>`
let transparent = color.with_alpha(0.3f32);
assert_eq!(transparent.alpha, 0.3);

// This changes the transparency to 0.8
let transparent = transparent.with_alpha(0.8);
assert_eq!(transparent.alpha, 0.8);

Wrapping Up

I want to give a big thank you to everyone who contributed and spearheaded many of these changes.

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

Thank you for reading and I hope you will enjoy this batch of changes!