ogeon / palette Goto Github PK
View Code? Open in Web Editor NEWA Rust library for linear color calculations and conversion
License: Apache License 2.0
A Rust library for linear color calculations and conversion
License: Apache License 2.0
The operation traits, such as Mix
and Hue
, are starting to pile up, so I thought it would be useful to create a sort of traits
or prelude
or ops
module for easy glob inclusion. They can still also be available from the crate root.
seems like can only make from radians
Hey I just started working on a similar library repo.
I noticed your cie -> lab conversion does follow wikipedia or http://www.brucelindbloom.com/index.html?ColorCalculator.html which I used as reference. What is your source for the conversions formulas?
link to the conversion I implemented for reference.
A custom color can't have transparency at the moment. A part of the problem is that it's not possible to implement From
for Alpha<MyColor, T>
. A possible solution is to add a #[palette_alpha]
attribute that marks the alpha component in the custom color struct and extend the derive code to use this when implementing From
and Into
, to preserve the transparency.
Should partial equality be customized for each color to be within a tolerance level? Or maybe have a separate ColorEqual trait.
It will be better than the current macro which cannot take hues into account and it will definitely help in the tests.
I am not sure about its use case other than the tests. Another issue is selecting the right tolerance level.
For colors with known ranges like rgb, hsl etc., should we clamp the values? A possible issue is with the f32 -> u8 conversion panicking if not clamped.
...something which is more flexible than From
and Into
, that allows user defined conversion functions. As sketched in #6:
That would look like this (pseudocode):
let my_rgb = Rgb::new(0.5, 0.3, 0.7); let my_foo: FooColor = my_rgb.convert::<FooColor>(|channels|{ let a = some_calc(channels[0], channels[2]); let b = some_cals(channels[1], channels[2]); FooColor::new(a,b) });Then you can also offer official standard profiles and just do s.th. like
let my_cmyk = my_rgb.convert::<Cmyk>(Cmyk::Profiles::ISOUncoded_v2);
This looks like an exciting library, and I'd love to see documentation!
(Hoping you'll see this issue as an encouragement rather than a criticism.)
The implementations of Add
, Sub
, Mul
and Div
for some of the colors have been changed to output Self::Output
(basically ColorType<<T as Add<T>>::Output>
instead of just ColorType<T>
). Not that it matter too much which one is used, but consistency is important and outputting T
is an unnecessary restriction.
It's not a straight forward task, but it could help a lot. It would most importantly make it possible for third party types to easily implement conversion without having to understand a weird macro.
This makes it possible to make use of the type system to differ between them and linear RGB, and lowers the number of constructors on Rgb
.
Have a common rgb struct
struct RgbColor<RgbVariant, WP, T> {
r: T,
g: T,
b: T,
_color_space: PhantomData<RGBVariant>,
_white_point: PhantomData<WP>,
}
then we can define the individual colors as
const Rgb = RgbColor<LinearSrgbSpace, D65, T>
const Srgb = RgbColor<SrgbSpace, D65, T>
const AdobeRgb = RgbColor<AdobeSpace, D65, T>
conversions will be
impl<RgbVariant, WP,T: Float> From<RgbColor<RgbVariant, WP,T>> for Xyz<T> {
fn from(rgb: Rgb<T>) -> Xyz<T> {
let conv_matrix = RgbVariant::get_matrix();
let wp_xyz = WP::get_xyz();
Xyz {
.... calculations
}
}
}
impl<RgbVariant, WP,T: Float> From<Xyz<T>> for RgbColor<RgbVariant, WP,T> {
fn from(xyz: Rgb<T>) -> RgbColor<RgbVariant, WP,T> {
let conv_matrix = RgbVariant::get_matrix();
let wp_xyz = WP::get_xyz();
Rgb {
.... calculations
}
}
}
Hi!
Would it be interesting to add support for spectrum to XYZ conversions?
The spectrum can be represented as a [f32; XXX]
(XXX depending on the sampling of the spectrum). Where the f32
values are the intensities (in arbitrary units), for each wavelength from ~400 to ~800 nm. Then the XYZ coordinates are a matter of simple matrix multiplication, given the XYZ weights.
The weights are available on different websites (http://www.cvrl.org/cie.htm)
There is also analytic aproximations (http://jcgt.org/published/0002/02/01/paper.pdf)
I've already started a POC, so I might submit a pull request (the code is not that clean yet though).
Tell me what you think about this.
Do we use a trait based solution for the Rgb variants, we have atleast 3 variants so far Adobe Rgb, sRgb and linear sRgb (maybe use linear sRgb as the default Rgb variant).
Opening this issue to keep track of it. We need to implement other changes before we come back to this.
How to handle color macro with rgb variant and white point changes?
Currently
enum Color<T:Float> {
Lab(Lab<T>),
Rgb(Rgb<T>),
Hsl(Hsl<T>),
Xyz(Xyz<T>),
.. etc
}
With proposed changes it becomes
enum Color<RGB: RgbVariant, WP:WhitePoint, T:Float> {
Lab(Lab<WP, T>),
Rgb(Rgb<RGB, WP,T>),
Hsl(Hsl<V, WP, T>),
Xyz(Xyz<T>),
.. etc
}
So for example for Color::Xyz, we will still have to specify the RgbType and WP even though we do not use it. Is there any alternative?
Makes it easier to format it as #abc123
.
Continuing discussion from #48.
Lab values are currently scaled, L by 100 and a and b by 128. Even though a and b are unbound, typical ranges are -128 to 128.
By my limited research so far, I have not seen scaled lab values used ( unlike rgb which is usually assumed in the range 0-1 ). For example Conversion formula's, Color Difference formula's and most research papers assume unscaled lab values (I am happy to be corrected on this).
Right now using the lab spaces implies unnecessary scaling when used practically. So I am proposing that the Lab values should be changed to be unscaled. Same for Lch values.
If the decision is to still scale the value, we should provide a new_from_unscaled and into_unscaled function, to get values in and out.
Having the alpha channel being affected by operations as arithmetics isn't always what you want, so it would be nice if it can be separated from the color types in a good, convenient and intuitive enough way. This is also useful for when the alpha channel is insignificant (always 1.0), to save some space. I have been thinking about an Alpha<C>
wrapper type along these lines:
struct Alpha<C> {
pub color: C,
pub alpha: f32,
}
impl<C: Mix> Mix for Alpha<C> {
//...
}
//etc.
and maybe even make it dereference as the color type C
. I don't know if this will end up being too inconvenient or not, and better solutions are always welcome. I'll see if I can figure something else out before making a test implementation.
Define three macros
macro_rules! flt {
($x: expr) => {
T::from($x).unwrap()
};
}
macro_rules! f_0 {
() => (T::zero());
}
macro_rules! f_1 {
() => (T::one());
}
The the equation become easier to read
x: T::from(X_N).unwrap() * f_inv((T::one() / T::from(116.0).unwrap()) *
(lab.l * T::from(100.0).unwrap() + T::from(16.0).unwrap()) +
(T::one() / T::from(500.0).unwrap()) * lab.a * T::from(128.0).unwrap()),
y: T::from(Y_N).unwrap() * f_inv((T::one() / T::from(116.0).unwrap()) *
(lab.l * T::from(100.0).unwrap() + T::from(16.0).unwrap())),
z: T::from(Z_N).unwrap() * f_inv((T::one() / T::from(116.0).unwrap()) *
(lab.l * T::from(100.0).unwrap() + T::from(16.0).unwrap()) -
(T::one() / T::from(200.0).unwrap()) * lab.b * T::from(128.0).unwrap()),
x: rgb.red * T::from(0.4124).unwrap() + rgb.green * T::from(0.3576).unwrap() + rgb.blue * T::from(0.1805).unwrap(),
y: rgb.red * T::from(0.2126).unwrap() + rgb.green * T::from(0.7152).unwrap() + rgb.blue * T::from(0.0722).unwrap(),
z: rgb.red * T::from(0.0193).unwrap() + rgb.green * T::from(0.1192).unwrap() + rgb.blue * T::from(0.9505).unwrap(),
becomes
x: flt!(X_N) * f_inv((f_1!() / flt!(116.0)) *
(lab.l * flt!(100.0) + flt!(16.0)) +
(f_1!() / flt!(500.0)) * lab.a * flt!(128.0)),
y: flt!(Y_N) * f_inv((f_1!() / flt!(116.0)) *
(lab.l * flt!(100.0) + flt!(16.0))),
z: flt!(Z_N) * f_inv((f_1!() / flt!(116.0)) *
(lab.l * flt!(100.0) + flt!(16.0)) -
(f_1!() / flt!(200.0)) * lab.b * flt!(128.0)),
x: rgb.red * flt!(0.4124) + rgb.green * flt!(0.3576) + rgb.blue * flt!(0.1805),
y: rgb.red * flt!(0.2126) + rgb.green * flt!(0.7152) + rgb.blue * flt!(0.0722),
z: rgb.red * flt!(0.0193) + rgb.green * flt!(0.1192) + rgb.blue * flt!(0.9505),
A collection of named color constants (i.e. the named CSS and HTML colors) may be good to have. It should be possible to choose what kind of gamma correction they are assumed have when converting them to a linear RGB value.
Being able to do some basic math with colors is often useful, so it should be supported. This is just element wise arithmetics, so it's more naive than blending and can usually be applied to any color space.
Just like you have named colors, it would be nice to have a collection of predefined gradients.
For reference:
http://matplotlib.org/xkcd/examples/color/colormaps_reference.html
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/_cm.py
I see that Palette has had a lot of work done to it over the last year since the last published version, some of it not being backwards compatible.
Are there any estimates for how long until the next published version? Or perhaps publish a beta version?
I've used the master branch in my own projects without any serious issue, however crates with git deps cannot be published, so I'd be forced to work with the old published version of Palette or instruct users to use a Cargo [replace]
section.
Some color conversions depends on a white point, and Palette is currently making some assumptions in this area. Avoiding these assumptions would of course allow a lot more freedom, so it would be nice to find a good way to customize which white point to use when converting. Even better if it was encoded into the color types, themselves, when relevant.
Floating point numbers are crazy and should not be trusted. They should therefore not be compared in the examples (for aesthetic reasons) and any unit test that compares them should make use of the approx
crate.
It's unwieldy to implement, hides things unnecessarily, and I suspect that it's not as useful as I initially thought it would be. It feels like it does more harm than good.
I may be reading things wrong in the specs, but I believe that the version of HSL that CSS supports is in fact HSL in sRGB space, not linear HSL.
It'd be nice if we had an option to store this type of HSL without hackishly converting an Hsl
to an Rgb
and then copying the components from the Rgb
into an Srgb
. Again, if I'm wrong and this is actually linear Hsl
, feel free to ignore this use case.
For XYZ, both http://www.easyrgb.com/ and http://www.brucelindbloom.com/index.html?ColorCalculator.html show z as 1.088830 for srgb(1,1,1).
The is_valid() method for xyz assumes a range of [0-1].
For Lab, both http://www.easyrgb.com/ and http://www.brucelindbloom.com/index.html?ColorCalculator.html have values above 1 and also negative. For example, srgb(1,0.5,1) -> lab(72.087 ,65.181, -42.228)
The is_valid() method for lab assumes a range of [0-1].
Not sure what the correct range is. Opening this issue for tracking this.
I was trying the Hsl type and it seems inconsistent with the rest of the library.
All the other colors take floats for new, but hsl needs Hsl::new((240.0).into(), 0.0, 0.0)
You input the hue as 240, but you cannot use hsl.hue, you need to use hsl.hue.to_positive_degrees()
. I have been looking at this code for past few weeks and I made this mistake. This is going to be a very common issue. There is no compile error, but the code will not work as expected.
I don't see the need for hsl + 0.1
. I would rather this be a compile time error, because this is most likely a typo. The values are not on the same scale and does not make sense.
What do you think of removing the huetype and storing the hues as 0-360 floats? I understand that HueType avoids some issues in the code, but maybe we can mitigate it by using more test cases?
I was reading through the CSS level 4 color module to see what's coming up and came across a new color model: HWB. It was a total love at first sight. It works allowing you to select a hue and then mix in whiteness and blackness.
Support for it in this library would be great.
It should check for #[repr(C)]
and that all non-zero sized properties are of the same type. There is no straight forward way to detect zero sized types, so an attribute for marking zero sized properties is probably necessary. There should also be asserts that checks the sizes (some are already in place).
Since most color space conversions are through Xyz space, we can have a common conversion trait
trait XyzConversion {
fn to_xyz();
fn from_xyz();
}
and then we can auto imply all conversions
impl<T: XyzConversion,U: XzyConversion> From<T> for U {
...
}
instead of manually specifying hsl -> lab, rgb -> lab etc. and the corresponding inverses.
Numbers within [0, 360) degrees seems to be more common when working with hues, but the trigonometric functions in Rust gives negative angles instead of angles above 180 degrees. The big question is which one to adopt for our hues. Note that this is only relevant when printing a hue type or when converting it to a "regular" number (e.g. in impl Into<f32>
).
There should be support for the common blending functions, like add, subtract, screen, overlay, etc. This may be restricted to RGB, unless it's intuitive in other spaces as well.
Need to add tests for conversions to make sure there are no regressions. For color enum, alpha and individual colors.
I had an RGB pixel which i wanted to shift the hue by 180 , so I initially converted the color to LCH and then did a hue shift on it , and converted it back to RGB . The converted RGB values are way out of bounds .
let dominant_color: self::rgb::RGB<u8> = viewer.get_prominent_color( width, height); let lch_color: Lch = Rgb::from(Srgb::new(dominant_color.r as f32,dominant_color.g as f32,dominant_color.b as f32)).into(); let new_color: Rgb = lch_color.shift_hue(180.0.into()).into();
Dominant Color : RGB { r: 175, g: 180, b: 171 }
lch_color : Lch { l: 70.13459, chroma: 3.5053592, hue: LabHue(130.76137) }
hueshifted : Rgb { red: 232155.52, green: 217398.52, blue: 245035.61 }
Being able to take a slice of a gradient is quite useful for various reasons. Adding support for some kind of padding may also be nice.
Something that has been growing in my mind for a few days is the idea of changing the way colors are encoded into pixel friendly formats. The current procedure may look like this:
let linear = Rgb::new(1.0, 0.3, 0.6);
let srgb = Srgb::from_linear(linear);
let pixel: [u8; 3] = srgb.to_pixel();
or, using a shortcut:
let linear = Rgb::new(1.0, 0.3, 0.6);
let pixel: [u8; 3] = Srgb::linear_to_pixel(linear);
There's also the case where a simple custom gamma exponent can be used:
let linear = Rgb::new(1.0, 0.3, 0.6);
let pixel: [u8; 3] = GammaRgb::linear_to_pixel(linear, 2.2);
This is fine and all, but it's not really easy to use in a general way:
GammaRgb
has a variable gamma exponent, which makes it very different from other encodings, and impossible to fully express as a simple type.This problem is tightly connected to #31, where a general RGB type is proposed. The way GammaRgb
is structures interferes with that proposal, since it has an additional gamma
component, besides the usual red
, green
and blue
. I'm therefore proposing that we redesign the way pixel encoding works to make it more friendly towards the idea of a more generalized RGB type. I would also like to extend this to other RGB related color spaces, such as HSV, HSL, Luma and HWB. These can all be seen as input types and they are, often enough, defined in terms of already encoded RGB.
The initial proposal is to keep linear and encoded types separate. The purpose of this library is to strongly encourage a linear work flow and this can easily be achieved by only implementing operations on the linear variant. There was a proposal, by @sidred, to have marker type parameters, such as Rgb<Linear, Srgb, ...>
and Rgb<Gamma, Srgb, ...>
, but recent problems with such marker parameters, as well as the additional complexity of extra type parameters, makes that method less attractive.
The separation of linear and encoded colors, using two different structs, will come with a bit of duplication, but the duplication will mainly be restricted to construction and fields. The linear variants will implement the operation traits, while the encoded variants will be focused on packing and converting data. Here is an example of how it may look when combined with #31:
pub trait RgbEncoding {
///Encode a color component.
fn encode<T: Float>(value: T) -> T;
///Decode a color component.
fn decode<T: Float>(value: T) -> T;
}
pub trait RgbPrimaries {
//get red, green, blue primaries, etc.
}
///Marker type, which both defines a set of primaries and an encoding
pub struct Srgb;
impl RgbEncoding for Srgb {
//...
}
impl RgbPrimaries for Srgb {
//...
}
struct LinearRgb<Prim: RgbPrimaries = Srgb, ...> {
//..
primaries: PhantomType<Prim>,
}
struct Rgb<Enc: RgbEncoding = Srgb, ...> {
//..
primaries: PhantomType<Enc>,
}
Encodings and primaries are often strongly connected to each other, which is why the same type can be used for both. The problem with this is that it's hard to decouple them, but that's hopefully rare enough to make this more good than bad. The only problem left is the GammaRgb
. Its variable gamma component makes it incompatible with the above pattern, and having floating point values as type parameters is nothing but a (bad?) dream at this stage. We can, however, make more assumptions and add more escape hatches. I'm therefore proposing the addition of the probably most common gamma exponent, 2.2, as an encoding type, and the extension of the above RgbEncoding
that makes it possible for an encoding to map to any set of primaries:
pub trait RgbEncoding {
type Primaries: RgbPrimaries;
///Encode a color component.
fn encode<T: Float>(value: T) -> T;
///Decode a color component.
fn decode<T: Float>(value: T) -> T;
}
///Gamma exponent 2.2, with any primaries.
struct Gamma22<P: RgbPrimaries>;
impl<P: RgbPrimaries> RgbEncoding for Primaries<P>
type Primaries = P;
//...
}
impl RgbEncoding for Srgb {
type Primaries = Srgb;
//...
}
fn main() {
let encoded = Rgb::<Gamma22<Srgb>>::new(...);
let linear = encoded.to_linear(); //Gives LinearRgb<Srgb>
}
The preferred primaries can then be fetched from the associated Primaries
type when converting from encoded to linear. Now, what about those other gamma values? That's where we need escape hatches. One such escape hatch could be to keep the old GammaRgb
type around. It may be a bit unwieldy, but it works. An other is to just have a straight-to-pixel conversion, like the *::linear_to_pixel
function in the current design. More suggestions are welcome here.
The last question is what encoded types are allowed to do. My proposal is "basically nothing", since I'm still strongly in favor of keeping them strictly separated from linear types. This is to minimize any risk of mistaking non-linear values for linear and forcing the user to make an explicit conversion between them. The minimal set of functions I can think of at the moment is:
impl<E: RgbEncoding, ...> Rgb<E, ...> {
fn new(red: T, green: T, blue: T) -> Rgb<E, ...> {...}
fn new_u8(red: u8, green: u8, blue: u8) -> Rgb<E, ...> {...}
fn from_pixel<P: RgbPixel<T>>(pixel: P) -> Rgb<E, ...> {...}
fn to_pixel<P: RgbPixel<T>>(&self) -> P {...}
fn from_linear<C: Into<LinearRgb<E::Primaries, ...>>(color: C) -> Rgb<E, ...> {...}
fn to_linear<C: From<LinearRgb<E::Primaries, ...>>(&self) -> C {...}
//Useful addition
fn linear_to_pixel<C: Into<LinearRgb<E::Primaries, ...>, P: RgbPixel<T>>(color: C) -> P {
Rgb<E, ...>::from_linear(color).to_pixel()
}
}
These can, of course, be tweaked and changed. Some of these functions would be nice as a general encoding trait, but that's quite complex and may be better discussed at a later stage.
Something this proposal hasn't touched, so far, is what to do with the alpha
component. I think it would be best to make use of the Alpha
type, but I haven't figured out exactly how, so that question remains open.
An other open question is type aliases for common RGB types, such as AdobeRgb. One idea is to not alias sRGB, and only implement new
for Rgb<Srgb, D65, T>
, similar to how it's done with only D65
today. An other is to alias all of them and not pick a default.
The hue types can be improved in various ways. See #45 (comment) for a short list.
fn normalize_angle_division(mut deg: f32) -> f32 {
let c360 = 360.0_f32;
deg - ((deg / c360).floor() * c360)
}
Faster than using mod and as fast as while. No slowdown for large numbers. Putting it here so I don't forget.
I'm currently using palette
in a downstream crate where I'd like to allow the user to serialize and deserialize custom Theme
s - a set of styling defaults including some color types from palette. I'm hoping to avoid writing custom implementations of the serde traits in favour of adding the necessary derive
s to palette itself where others may also find this useful. Perhaps this could be added as an opt-in feature similarly to cgmath, petgraph and others do?
The Munsell color system may be an interesting addition from a more artistic point of view. RIT provides some data files that may be used to convert from Munsell to xyY, but I haven't found any clear terms of use for those, but the colors themselves are not copyrighted and it looks like it's under a section of free-to-use stuff. That will need more investigation, since we must consider possible commercial use of this library.
Embedding a Munsell database into the library may be quite a bit of bloat, so this should probably be included as an opt-in feature.
This would replace the old from_pixel
and into_pixel
methods on Rgb
and it should be implemented on all color types, except for Color
where it doesn't make sense. They should take/output tuples with the color components in the same order as in new
.
The reason for adding them is that tuples are convenient when destructuring and also a somewhat common storage format. From<(...)>
and Into<(...)>
could also be implemented.
There should be no need for the users to add palette_derive
to Cargo.toml
. Look at how Diesel does it.
I can be useful for some applications to have colors that are based on other numerical types than just f32
. Most probably for the sake of higher precision or lower memory usage. It would therefore be good to support more than just f32
, and preferably in a generic way. We can still keep f32
as the default.
It would be cool to be able to convert to an from CMYK :)
Add some sort of support for ICC profiles. Could be statically embedded, fully dynamic or both. This will make things like accurate CMYK possible, as discussed in #6.
It's common and useful. We will probably need it sooner or later, anyway.
This is real confusing:
extern crate palette;
use palette::*;
fn main() {
let foo = Lab {l: 1., a: -1., b: 0.};
assert!(foo.is_valid());
assert!(Rgb::from(foo).is_valid());
}
enolan at Behemoth in ~/mystuff/code/palettebug
$ cargo run
Running `target/debug/palettebug`
thread '<main>' panicked at 'assertion failed: Rgb::from(foo).is_valid()', main.rs:7
Process didn't exit successfully: `target/debug/palettebug` (exit code: 101)
I assumed it was a bug in the conversion routine, but it's just that Lab* is wider than sRGB. Returning an option type, assert!
ing when the input is outside the target color space, or clamping the output value would all be preferable to the current situation.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.