summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-pdf/src/color.rs13
-rw-r--r--crates/typst-pdf/src/postscript/oklab.ps8
-rw-r--r--crates/typst/src/geom/color.rs41
-rw-r--r--tests/ref/compiler/color.pngbin1253 -> 1344 bytes
-rw-r--r--tests/typ/compiler/color.typ5
5 files changed, 32 insertions, 35 deletions
diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs
index 4c2f6183..a9ad0100 100644
--- a/crates/typst-pdf/src/color.rs
+++ b/crates/typst-pdf/src/color.rs
@@ -240,10 +240,10 @@ fn minify(source: &str) -> String {
/// Ensures that the values are in the range [0.0, 1.0].
///
/// # Why?
-/// - Oklab: The a and b components are in the range [-0.4, 0.4] and the PDF
+/// - Oklab: The a and b components are in the range [-0.5, 0.5] and the PDF
/// specifies (and some readers enforce) that all color values be in the range
/// [0.0, 1.0]. This means that the PostScript function and the encoded color
-/// must be offset by 0.4.
+/// must be offset by 0.5.
/// - HSV/HSL: The hue component is in the range [0.0, 360.0] and the PDF format
/// specifies that it must be in the range [0.0, 1.0]. This means that the
/// PostScript function and the encoded color must be divided by 360.0.
@@ -256,8 +256,13 @@ impl ColorEncode for ColorSpace {
fn encode(&self, color: Color) -> [f32; 4] {
match self {
ColorSpace::Oklab => {
- let [l, a, b, alpha] = color.to_oklab().to_vec4();
- [l, (a + 0.4).clamp(0.0, 1.0), (b + 0.4).clamp(0.0, 1.0), alpha]
+ let [l, c, h, alpha] = color.to_oklch().to_vec4();
+ // Clamp on Oklch's chroma, not Oklab's a\* and b\* as to not distort hue.
+ let c = c.clamp(0.0, 0.5);
+ // Convert cylindrical coordinates back to rectangular ones.
+ let a = c * h.to_radians().cos();
+ let b = c * h.to_radians().sin();
+ [l, a + 0.5, b + 0.5, alpha]
}
ColorSpace::Hsl => {
let [h, s, l, _] = color.to_hsl().to_vec4();
diff --git a/crates/typst-pdf/src/postscript/oklab.ps b/crates/typst-pdf/src/postscript/oklab.ps
index 4d6e9ad5..e766bbd8 100644
--- a/crates/typst-pdf/src/postscript/oklab.ps
+++ b/crates/typst-pdf/src/postscript/oklab.ps
@@ -3,12 +3,12 @@
% /!\ WARNING: The A and B components **MUST** be encoded
% in the range [0, 1] before calling this function.
% This is because the function assumes that the
- % A and B components are offset by a factor of 0.4
+ % A and B components are offset by a factor of 0.5
% in order to meet the range requirements of the
% PDF specification.
- exch 0.4 sub
- exch 0.4 sub
+ exch 0.5 sub
+ exch 0.5 sub
% Load L a and b into the stack
2 index
@@ -75,4 +75,4 @@
% Discard f1, f2, and f3 by rolling the stack and popping
6 3 roll pop pop pop
-} \ No newline at end of file
+}
diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs
index 3801dbab..379aca73 100644
--- a/crates/typst/src/geom/color.rs
+++ b/crates/typst/src/geom/color.rs
@@ -296,10 +296,10 @@ impl Color {
/// A linear Oklab color is represented internally by an array of four
/// components:
/// - lightness ([`ratio`]($ratio))
- /// - a ([`float`]($float) in the range `[-0.4..0.4]`
- /// or [`ratio`]($ratio) in the range `[-100%..100%]`)
- /// - b ([`float`]($float) in the range `[-0.4..0.4]`
- /// or [`ratio`]($ratio) in the range `[-100%..100%]`)
+ /// - a ([`float`]($float) or [`ratio`]($ratio).
+ /// Ratios are relative to `{0.4}`; meaning `{50%}` is equal to `{0.2}`)
+ /// - b ([`float`]($float) or [`ratio`]($ratio).
+ /// Ratios are relative to `{0.4}`; meaning `{50%}` is equal to `{0.2}`)
/// - alpha ([`ratio`]($ratio))
///
/// These components are also available using the
@@ -341,12 +341,7 @@ impl Color {
let ChromaComponent(b) = args.expect("B component")?;
let RatioComponent(alpha) =
args.eat()?.unwrap_or(RatioComponent(Ratio::one()));
- Self::Oklab(Oklab::new(
- l.get() as f32,
- a.get() as f32,
- b.get() as f32,
- alpha.get() as f32,
- ))
+ Self::Oklab(Oklab::new(l.get() as f32, a, b, alpha.get() as f32))
})
}
@@ -360,8 +355,8 @@ impl Color {
/// A linear Oklch color is represented internally by an array of four
/// components:
/// - lightness ([`ratio`]($ratio))
- /// - chroma ([`float`]($float) in the range `[0.0..0.4]`
- /// or [`ratio`]($ratio) in the range `[0%..100%]`)
+ /// - chroma ([`float`]($float) or [`ratio`]($ratio).
+ /// Ratios are relative to `{0.4}`; meaning `{50%}` is equal to `{0.2}`)
/// - hue ([`angle`]($angle))
/// - alpha ([`ratio`]($ratio))
///
@@ -406,7 +401,7 @@ impl Color {
args.eat()?.unwrap_or(RatioComponent(Ratio::one()));
Self::Oklch(Oklch::new(
l.get() as f32,
- c.get() as f32,
+ c,
OklabHue::from_degrees(h.to_deg() as f32),
alpha.get() as f32,
))
@@ -1764,23 +1759,15 @@ cast! {
},
}
-/// A component that must either be a value between:
-/// - -100% and 100%, in which case it is relative to 0.4.
-/// - -0.4 and 0.4, in which case it is taken literally.
-pub struct ChromaComponent(Ratio);
+/// A component that must either be:
+/// - a ratio, in which case it is relative to 0.4.
+/// - a float, in which case it is taken literally.
+pub struct ChromaComponent(f32);
cast! {
ChromaComponent,
- v: Ratio => if (-1.0 ..= 1.0).contains(&v.get()) {
- Self(v * 0.4)
- } else {
- bail!("ratio must be between -100% and 100%");
- },
- v: f64 => if (-0.4 ..= 0.4).contains(&v) {
- Self(Ratio::new(v))
- } else {
- bail!("ratio must be between -0.4 and 0.4");
- },
+ v: Ratio => Self((v.get() * 0.4) as f32),
+ v: f64 => Self(v as f32),
}
/// An integer or ratio component.
diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png
index faf6a9b2..a585bf30 100644
--- a/tests/ref/compiler/color.png
+++ b/tests/ref/compiler/color.png
Binary files differ
diff --git a/tests/typ/compiler/color.typ b/tests/typ/compiler/color.typ
index 86dd1542..ec1f9902 100644
--- a/tests/typ/compiler/color.typ
+++ b/tests/typ/compiler/color.typ
@@ -32,6 +32,11 @@
#box(square(size: 9pt, fill: color.hsv(col)))
---
+// Colors outside the sRGB gamut.
+#box(square(size: 9pt, fill: oklab(90%, -0.2, -0.1)))
+#box(square(size: 9pt, fill: oklch(50%, 0.5, 0deg)))
+
+---
// Test hue rotation
#let col = rgb(50%, 64%, 16%)