A library to aid in using colors

facelessuser, updated 🕥 2023-03-21 14:02:38

Donate via PayPal Discord Build Coverage Status PyPI Version PyPI Downloads PyPI - Python Version License



ColorAide is a pure Python, object oriented approach to colors.


from coloraide import Color c = Color("red") c.to_string() 'rgb(255 0 0)' c.convert('hsl').to_string() 'hsl(0 100% 50%)' c.set("lch.chroma", 30).to_string() 'rgb(173.81 114.29 97.218)' Color("blue").mix("yellow", space="lch").to_string() 'rgb(255 65.751 107.47)' ```

ColorAide particularly has a focus on the following:

  • Accurate colors.

  • Proper round tripping (where reasonable).

  • Be generally easy to pick up for the average user.

  • Support modern CSS color spaces and syntax.

  • Make accessible many new and old non-CSS color spaces.

  • Provide a number of useful utilities such as interpolation, color distancing, blending, gamut mapping, etc.

  • Provide a plugin API to extend supported color spaces and more.

  • Allow users to configure defaults to their liking.

With ColorAide, you can specify a color, convert it to other color spaces, mix it with other colors, output it in different CSS formats, and much more!






CSS Powerless interpolation

opened on 2023-03-21 03:03:42 by facelessuser

We should consider the possibility of adding the CSS approach to powerless channels when interpolating. In general, the concept is pretty straightforward. When interpolating, if lightness creates a white or black color, chroma/saturation becomes powerless during the interpolation. It's kind of like how hue is generally powerless and gets undefined when chroma/saturation is 0 (or near zero). This would apply to Lab colors as well with lightness causing a and b to become powerless. The big issue is with colors like CAM16 JMh and HCT. Neither of which have easy to predict achromatic responses and could cause complications in how things get sanely calculated for them.

Continuous Linear interpolator

opened on 2023-03-21 02:55:53 by facelessuser

Currently, linear mode for interpolation is very much like CSS. Two colors are evaluated at a time, and undefined values are evaluated only between those two colors. This is okay, and helps us provide a CSS like experience, but consider the following examples.

We want to take a list of colors and set all the lightness to undefined except the start and end colors which will have 0 and 1 respectively for lightness. The second case does this for alpha. The results are not great

Screenshot 2023-03-20 at 8 51 35 PM

But in our spline interpolations, we had to create a new undefined handler as 4 colors must be used at one time for the splines, so just evaluating interpolation with the context of just two colors doesn't work (unless you only have two colors).

So taking the same undefined handling that we use in splines and applying it to linear piecewise interpolation, we are able to have these same two examples give us a nice intuitive response. In both the lightness case and the alpha case, the colors in between that do not have those values defined have that component interpolated continuously throughout all the colors between.

Screenshot 2023-03-20 at 8 51 51 PM

It's a far more practical approach to undefined values. We already use it in splines, so we'd just create a linear version using this approach. Those who want to use a more CSS-faithful approach can use the traditional piecewise approach.

HCT speed up translation back to CAM16

opened on 2023-03-17 13:56:36 by facelessuser

While not the most pressing issue, we should work on a shortcut to help HCT get back to CAM16 faster.

Currently, the whole thing uses bisecting to figure out the best J to use that satisfies the current L*Ch which is actually what HCT is. You can't get back to CAM16 without the right J, and J isn't used in the calculation for L* since it's from a completely different color space. Bisecting alone isn't nearly efficient enough.

The whole color space is weird in that since. It tries to take the best of CAM16 and Lab and magnifies the worst parts of CAM16, the slow conversion.

Interpolation should just use the specified space as output space if output_space is not specified

opened on 2023-03-16 20:43:53 by facelessuser

Originally we used the starting color as the output space if output_space is not specified. In retrospect, this is a bit too "magical". It is also a more expensive default as you have to convert in the species space and then convert back. If you want to keep in the same space, specify it.

This will be in 2.0 as it will be a breaking change.

CAM16/HCT conversion issue

opened on 2023-03-13 13:43:40 by facelessuser

So, I've realized a couple of things after running with CAM16 and HCT for a bit. We need to make some adjustments. When working through CAM16 and the derived HCT, I had not realized how important the hue actually is with achromatic colors.

With most of the LCh-like color spaces, when converting to an achromatic color, the actual hue value doesn't matter that much simply because chroma is so small. When chroma is small, like very small, the impact hue has in the calculation is so minimal, you can get away with ignoring it. But with CAM16 (and HCT which is derived from it) the chroma at times can be quite large. In the SDR region, it can get up to 2.x, maybe bigger. This value is significant enough that the hue actually has some impact.

We calculated the achromatic response in CAM16 and HCT but that response exists mainly in the 209.5x hue range. In other hue ranges, the colors are not quite achromatic, and the colors below that line, aren't necessarily achromatic as well.

For instance, consider this color in HCT, we cannot reproduce it.


hct = Color('#80807f').convert('hct') hct color(--hct none 1.805 53.557 / 1) hct.convert('srgb').to_string(hex=True)



So, what does this mean? It means we need to restrict the achromatic response to the given colors with a hue of 209.5x, give or take. It means that we need to restrict the achromatic none replacement as close to that response line as possible, and we should not assume colors below that response line are achromatic.

This has slightly bigger implications. Right now, we replace none with zero in almost every color space, but moving forward, that will not be the case. CAM16 JMh and HCT will both need to replace none with something close to the actual hue needed. I already have this roughly working locally:


hct = Color('#80807f').convert('hct') hct color(--hct 181.65 1.805 53.557 / 1) hct.convert('srgb').to_string(hex=True) '#80807f' ```

And then when we use an actual achromatic color:


hct = Color('#808080').convert('hct') hct color(--hct none 1.8977 53.585 / 1) hct.to_string() 'color(--hct 209.54 1.8977 53.585)' hct.to_string(hex=True) 'color(--hct 209.54 1.8977 53.585)' ```

Do not gamut map HSL and similar spaces when interpolating?

opened on 2023-03-10 18:18:22 by facelessuser

The people over CSS are debating dropping the gamut map requirement for certain spaces like HSL and such in order to defer gamut mapping to a later spec. Generally, HSL can round trip fine, but when dealing with interpolation, colors can give negative saturation which can make interpolation messy.

I actually don't have a problem with removing the requirement to gamut map with interpolation, but I'd at least want to tighten up the window for on some of the spaces when the lightness is very close to 1 (or 100 depending on the space's scaling). Basically, for some of the conversions, if lightness is very close 1 or 100, we count saturation as 0. This helps provide sane behavior during interpolation. It slightly affects roundtripping when very close to white, but the overall benefit that is gained when working with the colors outweighs the negative I think.

Anyways, there are couple of color spaces who's range won't quite capture resolve crazy saturation with something like oklch(1 0 0). We can adjust that range to give good results. We also had some spaces where we forgot to add the flag to force gamut mapping anyways (hsi).

Generally, we can mention that some spaces may give odd results if lightness exceeds the SDR threshold we put in place, and we can suggest that people should probably fit the inputs for best results if using a finicky interpolation space that is sensitive to SDR ranges.


1.8.2 2023-03-08 19:08:35


  • FIX: Ensure Brettel CVD approach uses Judd-Vos CMFs
  • FIX: Fix some exception messages.

1.8.1 2023-03-07 02:53:23


  • FIX: Ensure Judd-Vos correction is applied to linear RGB to LMS conversion for CVD.
  • FIX: Fix outdated API information in docs.

1.8 2023-03-05 15:00:01


  • NEW: Modern sRGB, HSL, and HWB should allow mixed percentage and numbers. HSL and HWB percentages in the hsl() and hwb() formats respectively will resolve to numbers in the range [0, 100]. These changes reflect the latest changes in the CSS Level 4 Color spec.
  • NEW: HSL and HWB can serialize to a modern syntax that does not use percentages, but the default still uses percentages.
  • NEW: Rework CSS parsing for better performance.
  • FIX: Handle some parsing corner cases that are handled by browsers, but not by ColorAide. For example, color(srgb 1-0.5.4) should parse as color(srgb 1 -0.5 0.4).
  • FIX: Ensure that COLOR_FORMAT is respected.

1.7.1 2023-02-09 02:07:40


  • FIX: Ensure CAM16 spaces mirrors positive and negative percentages for a and b components.
  • FIX: Since the CAM16 JMh model can not predict achromatic colors with negative lightness and, more importantly, negative lightness is not useful, limit the lower end of lightness in CAM16 spaces to zero.
  • FIX: When a CAM16 JMh (or HCT) color's chroma, when not discounting illuminance, has chroma drop below the actual ideal achromatic chroma threshold, just use the ideal chroma to ensure better conversion back to XYZ.
  • FIX: Jzazbz and JzCzhz model can never translate a color with a negative lightness, so just clamp negative lightness while in Jzazbz and JzCzhz.
  • FIX: Fix a math error in CAM16.
  • FIX: Fix CAM16 JMh M limit which was too low.
  • FIX: IPT was set to "bound" when it should have an unbounded gamut.
  • FIX: When both comma and none are enabled it could make undefined alpha values show up as none in legacy CSS format.
  • FIX: Sane handling of inverse lightness in DIN99o.

1.7 2023-02-06 02:02:46


  • NEW: Add support for CAM16 Jab and JMh: cam16 and cam16-jmh respectively.
  • NEW: Add support for CAM16 UCS (Jab forms): cam16-ucs, cam16-scd, and cam16-lcd.
  • NEW: Add support for the HCT color space (hct) which combines the colorfulness and hue from CAM16 JMh and the lightness from CIELab.
  • NEW: Gamut mapping classes derived from fit_lch_chroma can set DE_OPTIONS to pass ∆E parameters.
  • NEW: While rare, some cylindrical color spaces have an algorithm such that achromatic colors convert best with a very specific hue. Internally, this is now handled during conversions, but there can be reasons where knowing the hue can be useful such as plotting. Cylindrical spaces now expose a method called achromatic_hue() which will return this specific hue if needed.
  • FIX: Fix rec2100-hlg transform.
  • FIX: Some color transformation improvements.
  • FIX: Relax some achromatic detection logic for sRGB cylindrical models. Improves achromatic hue detection results when converting to and from various non-sRGB color spaces.

1.6 2023-01-28 17:21:24


  • NEW: Add rec2100-hlg color space.
  • BREAKING: rec2100pq should have been named rec2100-pq for consistency. It has been renamed to rec2100-pq and serializes with the CSS ID of --rec2100-pq. This is likely to have little impact on most users.

colors css python