What is this? A double announcement? Of the same library? Yes! Why not have twice the fun? No, seriously, they are more like two parts of the same release, kind of like the last two Harry Potter movies. Let’s begin with a short introduction, to get everyone up to speed with what Palette is.
Palette is a Rust crate for working with colors and color spaces. It uses the type system to prevent mistakes, like mixing incompatible colors or working with non-linear RGB. It encodes the color spaces and their meta data (such as RGB primaries and white point) into the types to help making color processing a bit more accessible to those who don’t want to dive into the rabbit hole that is colors in computing.
The reason for the double announcement is that I chose not to announce 0.3.0 when it was released. It was mostly a way to clear the table and start a bit fresher when I picked the project up again after a looong break. Version 0.4.0 extends and enhances the changes in 0.3.0 in a way that makes me want to present them together. With that said, let’s look at what’s new since 0.2.1.
White Point Awareness
The white point determines what is considered “white”. This is a bit of a mind twister, but I’m sure most people have noticed that when taking photos some will appear a bit blue or yellow tinted. A simplified explanation of that is that the white point of the camera doesn’t match the environment’s white point (the main light source). Correcting that to make it look good is called white balancing, which is basically the process of adapting to another white point.
Colors in Palette are now aware of their own white point, which is encoded into the type itself. That makes it impossible to work with colors of different white points without either white balancing (chromatic adaptation) or explicitly changing it.
extern crate palette;
use palette::Xyz;
use palette::white_point::{D65, E};
use palette::chromatic_adaptation::AdaptInto;
fn main() {
let color_a = Xyz::<D65>::with_wp(0.1, 0.2, 0.3);
let color_b = Xyz::<E>::with_wp(0.3, 0.2, 0.1);
// let result = color_a + color_b;
// ^ no implementation for `palette::Xyz + palette::Xyz<palette::white_point::E>`
let adapted_b: Xyz<D65> = color_b.adapt_into();
let result = color_a + adapted_b;
}
Some aspects of white point awareness will still need some polishing, like that with_wp
method. It’s not great.
RGB and Luma Changes
RGB may be the most important color space, since it’s so common, so this is where a lot of the work has gone. There is now only one (1) RGB type, but it’s aware of its RGB standard and encoding. There is not just one kind of RGB, but rather a bunch of different standards that covers slightly different areas of the visible light. RGB, which is often thought of as a quite straight forward color space, is ironically one of the more complex ones, thanks to this.
Thankfully sRGB has a strong position as some kind of standard RGB standard (it’s even in the name), so Palette offers Srgb
for encoded/compressed/non-linear sRGB and LinSrgb
for linear RGB with the sRGB primaries and white point. These are type aliases for Rgb<Srgb, T>
(another Srgb
, this time a representation of the standard) and Rgb<Linear<Srgb>, T>
. As far as I know, it’s now possible to compose any kind of RGB standard, as long as it follows the pattern.
Luma
, which is gray scale, has gotten the same treatment. The reason for this is that it’s a somewhat common image format and it may also be non-linearly encoded.
Here’s an example of how conversion from encoded to linear sRGB may look:
extern crate palette;
use palette::Srgb;
fn main() {
let orangeish = Srgb::new(1.0, 0.6, 0.0).into_linear();
let blueish = Srgb::new(0.0, 0.2, 1.0).into_linear();
let whatever_it_becomes = orangeish + blueish;
}
Not too bad.
Bytes, Pixels and Interoperability
The storage formats for Rgb
and Luma
have been relaxed a bit to allow integer components. This makes more sense now when there is only one RGB type. Doing this opens the door to better interoperability with other parts of an application and other crates. Let’s say you have an image buffer, where colors are stored as a u8
sequence. This buffer can be directly converted to an RGB slice without copying or iterating:
extern crate palette;
use palette::{Pixel, Srgb};
fn main() {
let raw = &mut [255u8, 128, 64, 10, 20, 30];
{
let colors = Srgb::from_raw_slice_mut(raw);
assert_eq!(colors.len(), 2);
// These changes affects the raw slice, since they are the same data
colors[0].blue = 100;
colors[1].red = 200;
}
// Notice the two values in the middle:
assert_eq!(raw, &[255, 128, 100, 200, 20, 30]);
}
The key element in the above example is the Pixel
trait. It has a number of methods for converting to and from contiguous sequences of data, such as slices and arrays. Actually just slices and arrays at the moment. Tuples are left out because they doesn’t seem to have a fixed memory layout (as in #[repr(C)]
). To compensate for that, every color type have form_components
and into_components
for converting from and into tuples.
Implementing this trait requires some care to make sure the components are all the same size and the memory layout stays the same on all platforms. This leads us to…
Deriving
With the more recent Rust versions it’s now possible to make custom implementations for #[derive(...)]
. Palette does currently offer implementations for Pixel
, FromColor
and IntoColor
. The Pixel
derive makes sure that all of the components are of the same type and counts them for you. If you want to deviate from that rule you have to explicitly opt in with additional attributes (containing the scary word “unsafe”). It may look like this:
#[macro_use]
extern crate palette;
use palette::Pixel;
#[derive(PartialEq, Debug, Pixel)]
#[repr(C)]
struct MyCmyk {
cyan: f32,
magenta: f32,
yellow: f32,
key: f32,
}
fn main() {
let buffer = [0.1, 0.2, 0.3, 0.4];
let color = MyCmyk::from_raw(&buffer);
assert_eq!(
color,
&MyCmyk {
cyan: 0.1,
magenta: 0.2,
yellow: 0.3,
key: 0.4,
}
);
}
The FromColor
and IntoColor
are a bit more special. They come with From
and Into
implementations as well, which may be seen as stepping outside the boundaries, but they are closely tied and super tedious to implement by hand. They are the actual win from using the derive at all, as the FromColor
and IntoColor
are pretty simple to implement manually. Here’s an example from the documentation:
#[macro_use]
extern crate palette;
use palette::{Srgb, Xyz};
/// A custom version of Xyz that stores integer values from 0 to 100.
#[derive(PartialEq, Debug, FromColor)]
struct Xyz100 {
x: u8,
y: u8,
z: u8,
}
// We have to at least implement conversion from Xyz if we don't
// specify anything else, using the `palette_manual_from` attribute.
impl From<Xyz> for Xyz100 {
fn from(color: Xyz) -> Self {
let scaled = color * 100.0;
Xyz100 {
x: scaled.x.max(0.0).min(100.0) as u8,
y: scaled.y.max(0.0).min(100.0) as u8,
z: scaled.z.max(0.0).min(100.0) as u8,
}
}
}
fn main() {
// Start with an sRGB color and convert it from u8 to f32,
// which is the default component type.
let rgb = Srgb::new(196u8, 238, 155).into_format();
// Convert the rgb color to our own format.
let xyz = Xyz100::from(rgb);
assert_eq!(
xyz,
Xyz100 {
x: 59,
y: 75,
z: 42,
}
);
}
It assumes XYZ as the From
implementation that everything converts through, so the conversion is Srgb
→ Xyz
→ Xyz100
. The example mentions the palette_manual_from
attribute, which is used for listing other and additional manual From
implementations, since it’s impossible to detect them automatically. More attributes are available and sometimes necessary, but I’ll just refer to the documentation for those and more examples. It’s pretty neat to be able to replace up to eight From
implementations with just one derive attribute (and a couple of meta data attributes).
Additional Changes
Other than the above changes, there have been a number of smaller changes and improvements, including:
Lab
andLch
have the specified scales, rather than0.0 - 1.0
, to avoid confusion.- The hue types are a bit easier to use and doesn’t always have to be explicitly created or converted into.
Hsv::new(120.0.into(), 0.2, 0.9)
can now be written asHsv::new(120.0, 0.2, 0.9)
. - All colors can be serialized and deserialized with
serde
(opt-in feature). Rgb
andLuma
(with and without transparency) can be formatted as hexadecimal strings.
What’s Next?
The next release will probably contain some cleaning and hopefully some simplification and more quality of life changes. Oh, and I’ll see if I can get some benchmarking going to make sure it’s not slower than necessary. There are a number of issues left that didn’t make it into the release for various reasons, that may be included in the next one. The most notable one may be the removal of the Color
type, which doesn’t really fit into the library anymore. More on that when the time comes!
Palette can be found on GitHub, where contributions are most welcome, and on crates.io.
Thank you for reading, and have fun with the new features!