summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfrozolotl <44589151+frozolotl@users.noreply.github.com>2023-11-13 16:23:52 +0100
committerGitHub <noreply@github.com>2023-11-13 16:23:52 +0100
commit7d5f6a8b7366e12409b86419d81485932cc3a70b (patch)
tree63e07ea700b2b674f10cb48c698b4de40da97d01
parentc427ca9e4ea7d184fd4ff4b78f52c746e1ee9dc0 (diff)
Improve color conversions (#2659)
-rw-r--r--crates/typst/src/geom/color.rs125
-rw-r--r--tests/ref/compiler/color.pngbin1253 -> 1253 bytes
2 files changed, 78 insertions, 47 deletions
diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs
index 3c86a306..ffb8c965 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 {
- Self::Luma(c) => Rgba::from_color(c),
- Self::Oklab(c) => Rgba::from_color(c),
- Self::Oklch(c) => Rgba::from_color(c),
+ // 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) => c.to_rgba(),
- Self::Hsl(c) => Rgba::from_color(c),
- Self::Hsv(c) => Rgba::from_color(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_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 078a0b62..faf6a9b2 100644
--- a/tests/ref/compiler/color.png
+++ b/tests/ref/compiler/color.png
Binary files differ