From 7d5f6a8b7366e12409b86419d81485932cc3a70b Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:23:52 +0100 Subject: [PATCH] Improve color conversions (#2659) --- crates/typst/src/geom/color.rs | 127 ++++++++++++++++++++------------- tests/ref/compiler/color.png | Bin 1253 -> 1253 bytes 2 files changed, 79 insertions(+), 48 deletions(-) diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs index 3c86a3063..ffb8c965c 100644 --- a/crates/typst/src/geom/color.rs +++ b/crates/typst/src/geom/color.rs @@ -2,9 +2,10 @@ use std::str::FromStr; use ecow::EcoVec; use once_cell::sync::Lazy; +use palette::convert::FromColorUnclamped; use palette::encoding::{self, Linear}; use palette::{ - Darken, Desaturate, FromColor, Lighten, OklabHue, RgbHue, Saturate, ShiftHue, + Darken, Desaturate, FromColor, Lighten, Okhsva, OklabHue, RgbHue, Saturate, ShiftHue, }; use super::*; @@ -1251,13 +1252,16 @@ impl Color { pub fn to_luma(self) -> Self { Self::Luma(match self { Self::Luma(c) => c, - Self::Oklab(c) => Luma::from_color(c), - Self::Oklch(c) => Luma::from_color(c), - Self::Rgba(c) => Luma::from_color(c), - Self::LinearRgb(c) => Luma::from_color(c), - Self::Cmyk(c) => Luma::from_color(c.to_rgba()), - Self::Hsl(c) => Luma::from_color(c), - Self::Hsv(c) => Luma::from_color(c), + // Perform sRGB gamut mapping by converting to Okhsv first. + // This yields better results than clamping. + Self::Oklab(c) => Luma::from_color_unclamped(Okhsva::from_color(c)), + Self::Oklch(c) => Luma::from_color_unclamped(Okhsva::from_color(c)), + // No clamping necessary because these color spaces are all within sRGB, same as [`Luma`]. + Self::Rgba(c) => Luma::from_color_unclamped(c), + Self::LinearRgb(c) => Luma::from_color_unclamped(c), + Self::Cmyk(c) => Luma::from_color_unclamped(c.to_rgba()), + Self::Hsl(c) => Luma::from_color_unclamped(c), + Self::Hsv(c) => Luma::from_color_unclamped(c), }) } @@ -1265,7 +1269,9 @@ impl Color { Self::Oklab(match self { Self::Luma(c) => Oklab::from_color(c), Self::Oklab(c) => c, - Self::Oklch(c) => Oklab::from_color(c), + // No clamping is necessary for this conversion because the + // lightness property is the same for both Oklab and Oklch. + Self::Oklch(c) => Oklab::from_color_unclamped(c), Self::Rgba(c) => Oklab::from_color(c), Self::LinearRgb(c) => Oklab::from_color(c), Self::Cmyk(c) => Oklab::from_color(c.to_rgba()), @@ -1277,7 +1283,9 @@ impl Color { pub fn to_oklch(self) -> Self { Self::Oklch(match self { Self::Luma(c) => Oklch::from_color(c), - Self::Oklab(c) => Oklch::from_color(c), + // No clamping is necessary for this conversion because the + // lightness property is the same for both Oklab and Oklch. + Self::Oklab(c) => Oklch::from_color_unclamped(c), Self::Oklch(c) => c, Self::Rgba(c) => Oklch::from_color(c), Self::LinearRgb(c) => Oklch::from_color(c), @@ -1287,67 +1295,90 @@ impl Color { }) } - pub fn to_linear_rgb(self) -> Self { - Self::LinearRgb(match self { - Self::Luma(c) => LinearRgba::from_color(c), - Self::Oklab(c) => LinearRgba::from_color(c), - Self::Oklch(c) => LinearRgba::from_color(c), - Self::Rgba(c) => LinearRgba::from_color(c), - Self::LinearRgb(c) => c, - Self::Cmyk(c) => LinearRgba::from_color(c.to_rgba()), - Self::Hsl(c) => LinearRgba::from_color(Rgba::from_color(c)), - Self::Hsv(c) => LinearRgba::from_color(Rgba::from_color(c)), + pub fn to_rgba(self) -> Self { + Self::Rgba(match self { + // No clamping necessary because Luma is within sRGB, same as [`Rgba`]. + Self::Luma(c) => Rgba::from_color_unclamped(c), + // Perform sRGB gamut mapping by converting to Okhsv first. + // This yields better results than clamping. + Self::Oklab(c) => Rgba::from_color_unclamped(Okhsva::from_color(c)), + Self::Oklch(c) => Rgba::from_color_unclamped(Okhsva::from_color(c)), + // No clamping necessary because these color spaces are all within sRGB, same as [`Rgba`]. + Self::Rgba(c) => c, + Self::LinearRgb(c) => Rgba::from_linear(c), + Self::Cmyk(c) => Rgba::from_color_unclamped(c.to_rgba()), + Self::Hsl(c) => Rgba::from_color_unclamped(c), + Self::Hsv(c) => Rgba::from_color_unclamped(c), }) } - pub fn to_rgba(self) -> Self { - Self::Rgba(match self { - Self::Luma(c) => Rgba::from_color(c), - Self::Oklab(c) => Rgba::from_color(c), - Self::Oklch(c) => Rgba::from_color(c), - Self::Rgba(c) => c, - Self::LinearRgb(c) => Rgba::from_linear(c), - Self::Cmyk(c) => c.to_rgba(), - Self::Hsl(c) => Rgba::from_color(c), - Self::Hsv(c) => Rgba::from_color(c), + pub fn to_linear_rgb(self) -> Self { + Self::LinearRgb(match self { + // No clamping necessary because Luma is within sRGB, same as $to. + Self::Luma(c) => LinearRgba::from_color_unclamped(c), + // Perform sRGB gamut mapping by converting to Okhsv first. + // This yields better results than clamping. + Self::Oklab(c) => LinearRgba::from_color_unclamped(Okhsva::from_color(c)), + Self::Oklch(c) => LinearRgba::from_color_unclamped(Okhsva::from_color(c)), + // No clamping necessary because these color spaces are all within sRGB, same as $to. + Self::Rgba(c) => LinearRgba::from_color_unclamped(c), + Self::LinearRgb(c) => c, + Self::Cmyk(c) => LinearRgba::from_color_unclamped(c.to_rgba()), + Self::Hsl(c) => Rgba::from_color_unclamped(c).into_linear(), + Self::Hsv(c) => Rgba::from_color_unclamped(c).into_linear(), }) } pub fn to_cmyk(self) -> Self { Self::Cmyk(match self { Self::Luma(c) => Cmyk::from_luma(c), - Self::Oklab(c) => Cmyk::from_rgba(Rgba::from_color(c)), - Self::Oklch(c) => Cmyk::from_rgba(Rgba::from_color(c)), + // Perform sRGB gamut mapping by converting to Okhsv first. + // This yields better results than clamping. + Self::Oklab(c) => { + Cmyk::from_rgba(Rgba::from_color_unclamped(Okhsva::from_color(c))) + } + Self::Oklch(c) => { + Cmyk::from_rgba(Rgba::from_color_unclamped(Okhsva::from_color(c))) + } Self::Rgba(c) => Cmyk::from_rgba(c), Self::LinearRgb(c) => Cmyk::from_rgba(Rgba::from_linear(c)), Self::Cmyk(c) => c, - Self::Hsl(c) => Cmyk::from_rgba(Rgba::from_color(c)), - Self::Hsv(c) => Cmyk::from_rgba(Rgba::from_color(c)), + // No clamping necessary because these color spaces are all within sRGB, same as [`Rgba`]. + Self::Hsl(c) => Cmyk::from_rgba(Rgba::from_color_unclamped(c)), + Self::Hsv(c) => Cmyk::from_rgba(Rgba::from_color_unclamped(c)), }) } pub fn to_hsl(self) -> Self { Self::Hsl(match self { - Self::Luma(c) => Hsl::from_color(c), - Self::Oklab(c) => Hsl::from_color(c), - Self::Oklch(c) => Hsl::from_color(c), - Self::Rgba(c) => Hsl::from_color(c), - Self::LinearRgb(c) => Hsl::from_color(Rgba::from_linear(c)), - Self::Cmyk(c) => Hsl::from_color(c.to_rgba()), + // No clamping necessary because Luma is within sRGB, same as [`Hsl`]. + Self::Luma(c) => Hsl::from_color_unclamped(c), + // Perform sRGB gamut mapping by converting to Okhsv first. + // This yields better results than clamping. + Self::Oklab(c) => Hsl::from_color_unclamped(Okhsva::from_color(c)), + Self::Oklch(c) => Hsl::from_color_unclamped(Okhsva::from_color(c)), + // No clamping necessary because these color spaces are all within sRGB, same as [`Hsl`]. + Self::Rgba(c) => Hsl::from_color_unclamped(c), + Self::LinearRgb(c) => Hsl::from_color_unclamped(Rgba::from_linear(c)), + Self::Cmyk(c) => Hsl::from_color_unclamped(c.to_rgba()), Self::Hsl(c) => c, - Self::Hsv(c) => Hsl::from_color(c), + Self::Hsv(c) => Hsl::from_color_unclamped(c), }) } pub fn to_hsv(self) -> Self { Self::Hsv(match self { - Self::Luma(c) => Hsv::from_color(c), - Self::Oklab(c) => Hsv::from_color(c), - Self::Oklch(c) => Hsv::from_color(c), - Self::Rgba(c) => Hsv::from_color(c), - Self::LinearRgb(c) => Hsv::from_color(Rgba::from_linear(c)), - Self::Cmyk(c) => Hsv::from_color(c.to_rgba()), - Self::Hsl(c) => Hsv::from_color(c), + // No clamping necessary because Luma is within sRGB, same as [`Hsv`]. + Self::Luma(c) => Hsv::from_color_unclamped(c), + // Perform sRGB gamut mapping by converting to Okhsv first. + // This yields better results than clamping. + Self::Oklab(c) => Hsv::from_color_unclamped(Okhsva::from_color(c)), + Self::Oklch(c) => Hsv::from_color_unclamped(Okhsva::from_color(c)), + // No clamping necessary because these color spaces are all within sRGB, same as [`Hsv`]. + Self::Rgba(c) => Hsv::from_color_unclamped(c), + Self::LinearRgb(c) => Hsv::from_color_unclamped(Rgba::from_linear(c)), + Self::Cmyk(c) => Hsv::from_color_unclamped(c.to_rgba()), + Self::Hsl(c) => Hsv::from_color_unclamped(c), Self::Hsv(c) => c, }) } diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png index 078a0b6212c5e2344d2f9bb6176a118fe19df435..faf6a9b2b1e2fa286c49ea2e1f00ba63cc425d75 100644 GIT binary patch delta 102 zcmV-s0Ga>g3FQfpXeYUm0NQpcl6=R2qbYx*DQ20ObBv6DfPkZYDTHjmbAW&Vu(JTB z;E|7A1Pz<=O_A^y1I2}9lL`U00j0CS0VV+h!}K7t>H_Bh6fz1c2rmWb^#A|>07*qo IM6N<$f=3f7Qvd(} delta 102 zcmV-s0Ga>g3FQfpXeZisDw2H1fTJmYqba$K0A`t)bBv6DfPkZYDTHjmbAW&Vue1Q7 z;gOGB1P5&~@saQs14RX4lL`U00j9IT0VV+hgY+n~>H_Bh6y*mS*wcqSU;qFB07*qo IM6N<$f*+qM761SM