abachrome 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/CLA.md +45 -0
- data/CODE-OF-CONDUCT.md +9 -0
- data/LICENSE +19 -0
- data/README.md +212 -6
- data/Rakefile +8 -5
- data/SECURITY.md +94 -0
- data/abachrome.gemspec +36 -0
- data/devenv.lock +8 -8
- data/lib/abachrome/abc_decimal.rb +2 -2
- data/lib/abachrome/color.rb +13 -1
- data/lib/abachrome/color_models/lms.rb +34 -0
- data/lib/abachrome/color_models/xyz.rb +28 -0
- data/lib/abachrome/color_space.rb +12 -0
- data/lib/abachrome/converter.rb +1 -1
- data/lib/abachrome/converters/lms_to_lrgb.rb +36 -0
- data/lib/abachrome/converters/lms_to_srgb.rb +23 -0
- data/lib/abachrome/converters/lms_to_xyz.rb +30 -0
- data/lib/abachrome/converters/lrgb_to_lms.rb +0 -0
- data/lib/abachrome/converters/lrgb_to_xyz.rb +29 -0
- data/lib/abachrome/converters/oklab_to_lms.rb +41 -0
- data/lib/abachrome/converters/oklab_to_lrgb.rb +39 -44
- data/lib/abachrome/converters/oklch_to_lrgb.rb +63 -26
- data/lib/abachrome/converters/oklch_to_oklab.rb +4 -4
- data/lib/abachrome/converters/oklch_to_xyz.rb +66 -0
- data/lib/abachrome/converters/xyz_to_lms.rb +30 -0
- data/lib/abachrome/converters/xyz_to_oklab.rb +38 -0
- data/lib/abachrome/parsers/css.rb +437 -0
- data/lib/abachrome/version.rb +1 -1
- data/security/assesments/2025-10-12-SECURITY_ASSESSMENT.md +53 -0
- data/security/vex.json +21 -0
- metadata +20 -1
@@ -168,4 +168,16 @@ module Abachrome
|
|
168
168
|
s.white_point = :D65
|
169
169
|
s.color_model = :oklch
|
170
170
|
end
|
171
|
+
|
172
|
+
ColorSpace.register(:xyz) do |s|
|
173
|
+
s.coordinates = %i[x y z]
|
174
|
+
s.white_point = :D65
|
175
|
+
s.color_model = :xyz
|
176
|
+
end
|
177
|
+
|
178
|
+
ColorSpace.register(:lms) do |s|
|
179
|
+
s.coordinates = %i[l m s]
|
180
|
+
s.white_point = :D65
|
181
|
+
s.color_model = :lms
|
182
|
+
end
|
171
183
|
end
|
data/lib/abachrome/converter.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class LmsToLrgb < Abachrome::Converters::Base
|
4
|
+
# Converts a color from LMS color space to linear RGB color space.
|
5
|
+
#
|
6
|
+
# This method implements the final part of the OKLAB to linear RGB transformation,
|
7
|
+
# converting LMS (Long, Medium, Short) coordinates to linear RGB coordinates
|
8
|
+
# using the standard transformation matrix. The LMS color space represents
|
9
|
+
# the response of the three types of cone cells in the human eye.
|
10
|
+
#
|
11
|
+
# @param lms_color [Abachrome::Color] The color in LMS color space
|
12
|
+
# @raise [ArgumentError] If the input color is not in LMS color space
|
13
|
+
# @return [Abachrome::Color] The resulting color in linear RGB color space with
|
14
|
+
# the same alpha as the input color
|
15
|
+
def self.convert(lms_color)
|
16
|
+
raise_unless lms_color, :lms
|
17
|
+
|
18
|
+
l, m, s = lms_color.coordinates.map { |_| AbcDecimal(_) }
|
19
|
+
|
20
|
+
r = (l * AD("4.07674166134799")) +
|
21
|
+
(m * AD("-3.307711590408193")) +
|
22
|
+
(s * AD("0.230969928729428"))
|
23
|
+
g = (l * AD("-1.2684380040921763")) +
|
24
|
+
(m * AD("2.6097574006633715")) +
|
25
|
+
(s * AD("-0.3413193963102197"))
|
26
|
+
b = (l * AD("-0.004196086541837188")) +
|
27
|
+
(m * AD("-0.7034186144594493")) +
|
28
|
+
(s * AD("1.7076147009309444"))
|
29
|
+
|
30
|
+
output_coords = [r, g, b].map { |it| [it, 0].max }
|
31
|
+
|
32
|
+
Color.new(ColorSpace.find(:lrgb), output_coords, lms_color.alpha)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class LmsToSrgb < Abachrome::Converters::Base
|
4
|
+
# Converts a color from LMS color space to sRGB color space.
|
5
|
+
#
|
6
|
+
# This method implements a two-step conversion process:
|
7
|
+
# 1. First converts from LMS to linear RGB using the standard transformation matrix
|
8
|
+
# 2. Then converts from linear RGB to sRGB by applying gamma correction
|
9
|
+
#
|
10
|
+
# @param lms_color [Abachrome::Color] The color in LMS color space
|
11
|
+
# @raise [ArgumentError] If the input color is not in LMS color space
|
12
|
+
# @return [Abachrome::Color] The resulting color in sRGB color space with
|
13
|
+
# the same alpha as the input color
|
14
|
+
def self.convert(lms_color)
|
15
|
+
# First convert LMS to linear RGB
|
16
|
+
lrgb_color = LmsToLrgb.convert(lms_color)
|
17
|
+
|
18
|
+
# Then convert linear RGB to sRGB
|
19
|
+
LrgbToSrgb.convert(lrgb_color)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class LmsToXyz < Abachrome::Converters::Base
|
4
|
+
# Converts a color from LMS color space to XYZ color space.
|
5
|
+
#
|
6
|
+
# This method implements the LMS to XYZ transformation using the standard
|
7
|
+
# transformation matrix. The LMS color space represents the response of
|
8
|
+
# the three types of cone cells in the human eye (Long, Medium, Short),
|
9
|
+
# while XYZ is the CIE 1931 color space that forms the basis for most
|
10
|
+
# other color space definitions.
|
11
|
+
#
|
12
|
+
# @param lms_color [Abachrome::Color] The color in LMS color space
|
13
|
+
# @raise [ArgumentError] If the input color is not in LMS color space
|
14
|
+
# @return [Abachrome::Color] The resulting color in XYZ color space with
|
15
|
+
# the same alpha as the input color
|
16
|
+
def self.convert(lms_color)
|
17
|
+
raise_unless lms_color, :lms
|
18
|
+
|
19
|
+
l, m, s = lms_color.coordinates.map { |_| AbcDecimal(_) }
|
20
|
+
|
21
|
+
# LMS to XYZ transformation matrix
|
22
|
+
x = (l * AD("1.86006661")) - (m * AD("1.12948190")) + (s * AD("0.21989740"))
|
23
|
+
y = (l * AD("0.36122292")) + (m * AD("0.63881308")) - (s * AD("0.00000000"))
|
24
|
+
z = (l * AD("0.00000000")) - (m * AD("0.00000000")) + (s * AD("1.08906362"))
|
25
|
+
|
26
|
+
Color.new(ColorSpace.find(:xyz), [x, y, z], lms_color.alpha)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
File without changes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class LrgbToXyz < Abachrome::Converters::Base
|
4
|
+
# Converts a color from linear RGB color space to XYZ color space.
|
5
|
+
#
|
6
|
+
# This method implements the linear RGB to XYZ transformation using the standard
|
7
|
+
# transformation matrix for the sRGB color space with D65 white point. The XYZ
|
8
|
+
# color space is the CIE 1931 color space that forms the basis for most other
|
9
|
+
# color space definitions and serves as a device-independent reference.
|
10
|
+
#
|
11
|
+
# @param lrgb_color [Abachrome::Color] The color in linear RGB color space
|
12
|
+
# @raise [ArgumentError] If the input color is not in linear RGB color space
|
13
|
+
# @return [Abachrome::Color] The resulting color in XYZ color space with
|
14
|
+
# the same alpha as the input color
|
15
|
+
def self.convert(lrgb_color)
|
16
|
+
raise_unless lrgb_color, :lrgb
|
17
|
+
|
18
|
+
r, g, b = lrgb_color.coordinates.map { |_| AbcDecimal(_) }
|
19
|
+
|
20
|
+
# Linear RGB to XYZ transformation matrix (sRGB/D65)
|
21
|
+
x = (r * AD("0.4124564")) + (g * AD("0.3575761")) + (b * AD("0.1804375"))
|
22
|
+
y = (r * AD("0.2126729")) + (g * AD("0.7151522")) + (b * AD("0.0721750"))
|
23
|
+
z = (r * AD("0.0193339")) + (g * AD("0.1191920")) + (b * AD("0.9503041"))
|
24
|
+
|
25
|
+
Color.new(ColorSpace.find(:xyz), [x, y, z], lrgb_color.alpha)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class OklabToLms < Abachrome::Converters::Base
|
4
|
+
# Converts a color from OKLAB color space to LMS color space.
|
5
|
+
#
|
6
|
+
# This method implements the first part of the OKLAB to linear RGB transformation,
|
7
|
+
# converting OKLAB coordinates to the intermediate LMS (Long, Medium, Short) color space
|
8
|
+
# which represents the response of the three types of cone cells in the human eye.
|
9
|
+
#
|
10
|
+
# @param oklab_color [Abachrome::Color] The color in OKLAB color space
|
11
|
+
# @raise [ArgumentError] If the input color is not in OKLAB color space
|
12
|
+
# @return [Abachrome::Color] The resulting color in LMS color space with
|
13
|
+
# the same alpha as the input color
|
14
|
+
def self.convert(oklab_color)
|
15
|
+
raise_unless oklab_color, :oklab
|
16
|
+
|
17
|
+
l, a, b = oklab_color.coordinates.map { |_| AbcDecimal(_) }
|
18
|
+
|
19
|
+
l_ = AbcDecimal((l ) +
|
20
|
+
(AD("0.39633779217376785678") * a) +
|
21
|
+
(AD("0.21580375806075880339") * b))
|
22
|
+
|
23
|
+
m_ = AbcDecimal((l) -
|
24
|
+
(a * AD("-0.1055613423236563494")) +
|
25
|
+
(b * AD("-0.063854174771705903402")))
|
26
|
+
|
27
|
+
s_ = AbcDecimal((l) -
|
28
|
+
(a * AD("-0.089484182094965759684")) +
|
29
|
+
(b * AD("-1.2914855378640917399")))
|
30
|
+
|
31
|
+
# Apply cubic operation to convert from L'M'S' to LMS
|
32
|
+
l_lms = AbcDecimal(l_)**3
|
33
|
+
m_lms = AbcDecimal(m_)**3
|
34
|
+
s_lms = AbcDecimal(s_)**3
|
35
|
+
|
36
|
+
Color.new(ColorSpace.find(:lms), [l_lms, m_lms, s_lms], oklab_color.alpha)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -1,36 +1,32 @@
|
|
1
1
|
# Abachrome::Converters::OklabToLrgb - OKLAB to Linear RGB color space converter
|
2
2
|
#
|
3
3
|
# This converter transforms colors from the OKLAB color space to the linear RGB (LRGB) color space
|
4
|
-
# using the standard OKLAB
|
5
|
-
#
|
6
|
-
#
|
4
|
+
# using the standard OKLAB transformation matrices. The conversion process first transforms
|
5
|
+
# OKLAB coordinates to the intermediate LMS (Long, Medium, Short) color space, then applies
|
6
|
+
# another matrix transformation to convert LMS coordinates to linear RGB coordinates.
|
7
7
|
#
|
8
8
|
# Key features:
|
9
|
-
# - Implements the official OKLAB
|
10
|
-
# - Converts OKLAB
|
11
|
-
# - Applies cubic
|
12
|
-
# - Clamps negative RGB values to zero to ensure valid linear RGB output
|
9
|
+
# - Implements the official OKLAB inverse transformation algorithm with high-precision matrices
|
10
|
+
# - Converts OKLAB coordinates through intermediate LMS color space representation
|
11
|
+
# - Applies cubic transformation for perceptual uniformity in the OKLAB space
|
13
12
|
# - Maintains alpha channel transparency values during conversion
|
14
13
|
# - Uses AbcDecimal arithmetic for precise color science calculations
|
15
14
|
# - Validates input color space to ensure proper OKLAB source data
|
16
15
|
#
|
17
|
-
# The linear RGB color space provides
|
18
|
-
#
|
19
|
-
#
|
16
|
+
# The linear RGB color space provides a linear relationship between stored numeric values and
|
17
|
+
# actual light intensity, making it essential for accurate color calculations and serving as
|
18
|
+
# an intermediate color space for many color transformations, particularly when converting
|
19
|
+
# between different color models or preparing colors for display on standard monitors.
|
20
20
|
|
21
21
|
module Abachrome
|
22
22
|
module Converters
|
23
23
|
class OklabToLrgb < Abachrome::Converters::Base
|
24
|
-
# Converts a color from OKLAB color space to linear RGB color space.
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
# 2. Applies a cubic operation to get LMS values
|
31
|
-
# 3. Transforms LMS to linear RGB
|
32
|
-
# 4. Clamps negative values to zero
|
33
|
-
#
|
24
|
+
# Converts a color from OKLAB color space to linear RGB (LRGB) color space.
|
25
|
+
#
|
26
|
+
# This method performs a two-step conversion:
|
27
|
+
# 1. OKLAB to LMS (cone response space)
|
28
|
+
# 2. LMS to LRGB (linear RGB)
|
29
|
+
#
|
34
30
|
# @param oklab_color [Abachrome::Color] The color in OKLAB color space
|
35
31
|
# @raise [ArgumentError] If the input color is not in OKLAB color space
|
36
32
|
# @return [Abachrome::Color] The resulting color in linear RGB color space with
|
@@ -38,37 +34,36 @@ module Abachrome
|
|
38
34
|
def self.convert(oklab_color)
|
39
35
|
raise_unless oklab_color, :oklab
|
40
36
|
|
41
|
-
|
37
|
+
l_ok, a_ok, b_ok = oklab_color.coordinates.map { |_| AbcDecimal(_) }
|
42
38
|
|
43
|
-
|
44
|
-
|
45
|
-
|
39
|
+
# Step 1: OKLAB to L'M'S' (cone responses, non-linear)
|
40
|
+
# These are the M_lms_prime_from_oklab matrix operations.
|
41
|
+
l_prime = AbcDecimal(l_ok + (AD("0.39633779217376785678") * a_ok) + (AD("0.21580375806075880339") * b_ok))
|
42
|
+
m_prime = AbcDecimal(l_ok - (a_ok * AD("0.1055613423236563494")) - (b_ok * AD("0.063854174771705903402"))) # Note: original OklabToLms had + (b * AD("-0.063..."))
|
43
|
+
s_prime = AbcDecimal(l_ok - (a_ok * AD("0.089484182094965759684")) - (b_ok * AD("1.2914855378640917399"))) # Note: original OklabToLms had + (b * AD("-1.291..."))
|
46
44
|
|
47
|
-
m_ = AbcDecimal((l * AD("1.0000000088817607767")) -
|
48
|
-
(a * AD("0.1055613423236563494")) -
|
49
|
-
(b * AD("0.063854174771705903402")))
|
50
|
-
s_ = AbcDecimal((l * AD("1.000000054672410917")) -
|
51
|
-
(a * AD("0.089484182094965759684")) -
|
52
|
-
(b * AD("1.2914855378640917399")))
|
53
45
|
|
54
|
-
|
55
|
-
|
56
|
-
|
46
|
+
# Step 2: L'M'S' to LMS (cubing)
|
47
|
+
l_lms = l_prime**3
|
48
|
+
m_lms = m_prime**3
|
49
|
+
s_lms = s_prime**3
|
57
50
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
51
|
+
# Step 3: LMS to LRGB
|
52
|
+
# Using matrix M_lrgb_from_lms (OKLAB specific)
|
53
|
+
r_lrgb = (l_lms * AD("4.07674166134799")) + (m_lms * AD("-3.307711590408193")) + (s_lms * AD("0.230969928729428"))
|
54
|
+
g_lrgb = (l_lms * AD("-1.2684380040921763")) + (m_lms * AD("2.6097574006633715")) + (s_lms * AD("-0.3413193963102197"))
|
55
|
+
b_lrgb = (l_lms * AD("-0.004196086541837188"))+ (m_lms * AD("-0.7034186144594493")) + (s_lms * AD("1.7076147009309444"))
|
56
|
+
|
57
|
+
# Clamp LRGB values to be non-negative (as done in LmsToLrgb.rb)
|
58
|
+
# It's also common to clamp to [0, 1] range after conversion from a wider gamut space
|
59
|
+
# For LRGB, often just ensuring non-negative is done, and further clamping happens
|
60
|
+
# when converting to sRGB or other display spaces.
|
61
|
+
# Here, we'll ensure non-negative as per LmsToLrgb.
|
62
|
+
output_coords = [r_lrgb, g_lrgb, b_lrgb].map { |it| [it, AD(0)].max }
|
67
63
|
|
68
|
-
output_coords = [r, g, b].map { |it| [it, 0].max }
|
69
64
|
|
70
65
|
Color.new(ColorSpace.find(:lrgb), output_coords, oklab_color.alpha)
|
71
66
|
end
|
72
67
|
end
|
73
68
|
end
|
74
|
-
end
|
69
|
+
end
|
@@ -1,38 +1,75 @@
|
|
1
1
|
# Abachrome::Converters::OklchToLrgb - OKLCH to Linear RGB color space converter
|
2
2
|
#
|
3
|
-
# This converter transforms colors from the OKLCH color space to the linear RGB (LRGB) color space
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
3
|
+
# This converter transforms colors from the OKLCH color space to the linear RGB (LRGB) color space.
|
4
|
+
# The conversion is performed by first transforming OKLCH's cylindrical coordinates (Lightness, Chroma, Hue)
|
5
|
+
# into OKLAB's rectangular coordinates (L, a, b).
|
6
|
+
# Then, these OKLAB coordinates are converted to LRGB. This second part involves transforming
|
7
|
+
# OKLAB to an intermediate non-linear cone response space (L'M'S'), then to a linear
|
8
|
+
# cone response space (LMS), and finally from LMS to LRGB using appropriate matrices.
|
9
|
+
# All these steps are combined into a single direct conversion method.
|
7
10
|
#
|
8
11
|
# Key features:
|
9
|
-
# -
|
10
|
-
# -
|
11
|
-
#
|
12
|
-
# -
|
13
|
-
# -
|
14
|
-
# - Validates input color space to ensure proper OKLCH source data
|
15
|
-
#
|
16
|
-
# The linear RGB color space provides the foundation for further conversions to display-ready
|
17
|
-
# color spaces like sRGB, making this converter essential for the color transformation pipeline
|
18
|
-
# when working with OKLCH color manipulations that need to be rendered on standard displays.
|
19
|
-
|
20
|
-
require_relative "oklab_to_lrgb"
|
21
|
-
require_relative "oklch_to_oklab"
|
12
|
+
# - Direct conversion from OKLCH to LRGB.
|
13
|
+
# - Combines cylindrical to rectangular conversion (OKLCH to OKLAB)
|
14
|
+
# with the OKLAB to LRGB transformation pipeline (OKLAB -> L'M'S' -> LMS -> LRGB).
|
15
|
+
# - Uses AbcDecimal arithmetic for precise color science calculations.
|
16
|
+
# - Maintains alpha channel transparency values during conversion.
|
17
|
+
# - Validates input color space to ensure proper OKLCH source data.
|
22
18
|
|
23
19
|
module Abachrome
|
24
20
|
module Converters
|
25
21
|
class OklchToLrgb < Abachrome::Converters::Base
|
26
|
-
# Converts a color from OKLCH color space to linear RGB color space.
|
27
|
-
# This is a two-step conversion process that first converts from OKLCH to OKLAB,
|
28
|
-
# then from OKLAB to linear RGB.
|
29
|
-
#
|
30
|
-
# @param oklch_color [Abachrome::Color] A color in the OKLCH color space
|
31
|
-
# @return [Abachrome::Color] The resulting color in linear RGB color space
|
32
22
|
def self.convert(oklch_color)
|
33
|
-
|
34
|
-
|
23
|
+
raise_unless oklch_color, :oklch
|
24
|
+
|
25
|
+
l_oklch, c_oklch, h_oklch = oklch_color.coordinates.map { |_| AbcDecimal(_) }
|
26
|
+
alpha = oklch_color.alpha
|
27
|
+
|
28
|
+
# Step 1: OKLCH to OKLAB
|
29
|
+
# l_oklab is the same as l_oklch
|
30
|
+
l_oklab = l_oklch
|
31
|
+
|
32
|
+
# Convert hue from degrees to radians
|
33
|
+
# h_oklch is AbcDecimal, Math::PI is Float. AD(Math::PI) makes it AbcDecimal.
|
34
|
+
# Division by AD("180") ensures AbcDecimal arithmetic.
|
35
|
+
h_rad = (h_oklch * AD(Math::PI)) / AD("180")
|
36
|
+
|
37
|
+
# Calculate a_oklab and b_oklab
|
38
|
+
# Math.cos/sin take a float; .value of AbcDecimal is BigDecimal.
|
39
|
+
# AD(Math.cos/sin(big_decimal_value)) wraps the result back to AbcDecimal.
|
40
|
+
a_oklab = c_oklch * AD(Math.cos(h_rad.value))
|
41
|
+
b_oklab = c_oklch * AD(Math.sin(h_rad.value))
|
42
|
+
|
43
|
+
# Step 2: OKLAB to L'M'S' (cone responses, non-linear)
|
44
|
+
# Constants from the inverse of M2 matrix (OKLAB to L'M'S')
|
45
|
+
# l_oklab, a_oklab, b_oklab are already AbcDecimal.
|
46
|
+
l_prime = l_oklab + (AD("0.39633779217376785678") * a_oklab) + (AD("0.21580375806075880339") * b_oklab)
|
47
|
+
m_prime = l_oklab - (AD("0.1055613423236563494") * a_oklab) - (AD("0.063854174771705903402") * b_oklab)
|
48
|
+
s_prime = l_oklab - (AD("0.089484182094965759684") * a_oklab) - (AD("1.2914855378640917399") * b_oklab)
|
49
|
+
|
50
|
+
# Step 3: L'M'S' to LMS (cubing to linearize cone responses)
|
51
|
+
l_lms = l_prime**3
|
52
|
+
m_lms = m_prime**3
|
53
|
+
s_lms = s_prime**3
|
54
|
+
|
55
|
+
# Step 4: LMS to LRGB
|
56
|
+
# Using matrix M_lrgb_from_lms (OKLAB specific)
|
57
|
+
r_lrgb = (l_lms * AD("4.07674166134799")) + (m_lms * AD("-3.307711590408193")) + (s_lms * AD("0.230969928729428"))
|
58
|
+
g_lrgb = (l_lms * AD("-1.2684380040921763")) + (m_lms * AD("2.6097574006633715")) + (s_lms * AD("-0.3413193963102197"))
|
59
|
+
b_lrgb = (l_lms * AD("-0.004196086541837188"))+ (m_lms * AD("-0.7034186144594493")) + (s_lms * AD("1.7076147009309444"))
|
60
|
+
|
61
|
+
# Clamp LRGB values to be non-negative.
|
62
|
+
# LRGB values can be outside [0,1] but should be >= 0.
|
63
|
+
# Further clamping to [0,1] typically occurs when converting to display-referred spaces like sRGB.
|
64
|
+
zero_ad = AD("0")
|
65
|
+
output_coords = [
|
66
|
+
[r_lrgb, zero_ad].max,
|
67
|
+
[g_lrgb, zero_ad].max,
|
68
|
+
[b_lrgb, zero_ad].max
|
69
|
+
]
|
70
|
+
|
71
|
+
Color.new(ColorSpace.find(:lrgb), output_coords, alpha)
|
35
72
|
end
|
36
73
|
end
|
37
74
|
end
|
38
|
-
end
|
75
|
+
end
|
@@ -33,9 +33,9 @@ module Abachrome
|
|
33
33
|
|
34
34
|
l, c, h = oklch_color.coordinates.map { |_| AbcDecimal(_) }
|
35
35
|
|
36
|
-
h_rad = h *
|
37
|
-
a = c * Math.cos(h_rad.value)
|
38
|
-
b = c * Math.sin(h_rad.value)
|
36
|
+
h_rad = (h * Math::PI)/ AD(180)
|
37
|
+
a = c * AD(Math.cos(h_rad.value))
|
38
|
+
b = c * AD(Math.sin(h_rad.value))
|
39
39
|
|
40
40
|
Color.new(
|
41
41
|
ColorSpace.find(:oklab),
|
@@ -45,4 +45,4 @@ module Abachrome
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
|
-
end
|
48
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class OklchToXyz < Abachrome::Converters::Base
|
4
|
+
def self.convert(oklch_color)
|
5
|
+
raise_unless oklch_color, :oklch
|
6
|
+
|
7
|
+
l_oklch, c_oklch, h_oklch = oklch_color.coordinates.map { |coord| AbcDecimal(coord) }
|
8
|
+
alpha = oklch_color.alpha
|
9
|
+
|
10
|
+
# Step 1: OKLCH to OKLAB
|
11
|
+
# (l_oklab, a_oklab, b_oklab)
|
12
|
+
l_oklab = l_oklch
|
13
|
+
# h_rad = (h_oklch * Math::PI) / AD(180) # h_oklch is AbcDecimal, Math::PI is Float. Coercion happens.
|
14
|
+
# More explicit for Math::PI:
|
15
|
+
h_rad = (h_oklch * AD(Math::PI.to_s)) / AD(180)
|
16
|
+
|
17
|
+
# Standard Math.cos/sin expect float. h_rad is AbcDecimal.
|
18
|
+
# .to_f is needed for conversion from AbcDecimal/BigDecimal to Float.
|
19
|
+
cos_h_rad = AD(Math.cos(h_rad.to_f))
|
20
|
+
sin_h_rad = AD(Math.sin(h_rad.to_f))
|
21
|
+
|
22
|
+
a_oklab = c_oklch * cos_h_rad
|
23
|
+
b_oklab = c_oklch * sin_h_rad
|
24
|
+
|
25
|
+
# Step 2: OKLAB to L'M'S' (cone responses, non-linear)
|
26
|
+
# (l_prime, m_prime, s_prime)
|
27
|
+
# These are the M_lms_prime_from_oklab matrix operations.
|
28
|
+
# The AbcDecimal() wrapper on the whole sum (as in OklabToLms.rb) is not strictly necessary
|
29
|
+
# if l_oklab, a_oklab, b_oklab are already AbcDecimal, as AbcDecimal ops return AbcDecimal.
|
30
|
+
l_prime = l_oklab + (AD("0.39633779217376785678") * a_oklab) + (AD("0.21580375806075880339") * b_oklab)
|
31
|
+
m_prime = l_oklab - (a_oklab * AD("-0.1055613423236563494")) + (b_oklab * AD("-0.063854174771705903402"))
|
32
|
+
s_prime = l_oklab - (a_oklab * AD("-0.089484182094965759684")) + (b_oklab * AD("-1.2914855378640917399"))
|
33
|
+
|
34
|
+
# Step 3: L'M'S' to LMS (cubing)
|
35
|
+
# (l_lms, m_lms, s_lms)
|
36
|
+
l_lms = l_prime**3
|
37
|
+
m_lms = m_prime**3
|
38
|
+
s_lms = s_prime**3
|
39
|
+
|
40
|
+
# Step 4: LMS to LRGB
|
41
|
+
# (r_lrgb, g_lrgb, b_lrgb)
|
42
|
+
# Using matrix M_lrgb_from_lms (OKLAB specific)
|
43
|
+
r_lrgb = (l_lms * AD("4.07674166134799")) + (m_lms * AD("-3.307711590408193")) + (s_lms * AD("0.230969928729428"))
|
44
|
+
g_lrgb = (l_lms * AD("-1.2684380040921763")) + (m_lms * AD("2.6097574006633715")) + (s_lms * AD("-0.3413193963102197"))
|
45
|
+
b_lrgb = (l_lms * AD("-0.004196086541837188"))+ (m_lms * AD("-0.7034186144594493")) + (s_lms * AD("1.7076147009309444"))
|
46
|
+
|
47
|
+
# Clamp LRGB values to be non-negative (as done in LmsToLrgb.rb)
|
48
|
+
# Using the pattern [AbcDecimal, Integer].max which relies on AbcDecimal's <=> coercion.
|
49
|
+
# AD(0) is AbcDecimal zero.
|
50
|
+
zero_ad = AD(0)
|
51
|
+
r_lrgb_clamped = [r_lrgb, zero_ad].max
|
52
|
+
g_lrgb_clamped = [g_lrgb, zero_ad].max
|
53
|
+
b_lrgb_clamped = [b_lrgb, zero_ad].max
|
54
|
+
|
55
|
+
# Step 5: LRGB to XYZ
|
56
|
+
# (x_xyz, y_xyz, z_xyz)
|
57
|
+
# Using matrix M_xyz_from_lrgb (sRGB D65)
|
58
|
+
x_xyz = (r_lrgb_clamped * AD("0.4124564")) + (g_lrgb_clamped * AD("0.3575761")) + (b_lrgb_clamped * AD("0.1804375"))
|
59
|
+
y_xyz = (r_lrgb_clamped * AD("0.2126729")) + (g_lrgb_clamped * AD("0.7151522")) + (b_lrgb_clamped * AD("0.0721750"))
|
60
|
+
z_xyz = (r_lrgb_clamped * AD("0.0193339")) + (g_lrgb_clamped * AD("0.1191920")) + (b_lrgb_clamped * AD("0.9503041"))
|
61
|
+
|
62
|
+
Color.new(ColorSpace.find(:xyz), [x_xyz, y_xyz, z_xyz], alpha)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class XyzToLms < Abachrome::Converters::Base
|
4
|
+
# Converts a color from XYZ color space to LMS color space.
|
5
|
+
#
|
6
|
+
# This method implements the XYZ to LMS transformation using the standard
|
7
|
+
# transformation matrix. The LMS color space represents the response of
|
8
|
+
# the three types of cone cells in the human eye (Long, Medium, Short),
|
9
|
+
# while XYZ is the CIE 1931 color space that forms the basis for most
|
10
|
+
# other color space definitions.
|
11
|
+
#
|
12
|
+
# @param xyz_color [Abachrome::Color] The color in XYZ color space
|
13
|
+
# @raise [ArgumentError] If the input color is not in XYZ color space
|
14
|
+
# @return [Abachrome::Color] The resulting color in LMS color space with
|
15
|
+
# the same alpha as the input color
|
16
|
+
def self.convert(xyz_color)
|
17
|
+
raise_unless xyz_color, :xyz
|
18
|
+
|
19
|
+
x, y, z = xyz_color.coordinates.map { |_| AbcDecimal(_) }
|
20
|
+
|
21
|
+
# XYZ to LMS transformation matrix
|
22
|
+
l = (x * AD("0.8189330101")) + (y * AD("0.3618667424")) - (z * AD("0.1288597137"))
|
23
|
+
m = (x * AD("0.0329845436")) + (y * AD("0.9293118715")) + (z * AD("0.0361456387"))
|
24
|
+
s = (x * AD("0.0482003018")) + (y * AD("0.2643662691")) + (z * AD("0.6338517070"))
|
25
|
+
|
26
|
+
Color.new(ColorSpace.find(:lms), [l, m, s], xyz_color.alpha)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Abachrome
|
2
|
+
module Converters
|
3
|
+
class XyzToOklab < Abachrome::Converters::Base
|
4
|
+
# Converts a color from XYZ color space to OKLAB color space.
|
5
|
+
#
|
6
|
+
# This method implements the XYZ to OKLAB transformation by first
|
7
|
+
# converting XYZ coordinates to the intermediate LMS (Long, Medium, Short)
|
8
|
+
# color space, then applying the LMS to OKLAB transformation matrix.
|
9
|
+
#
|
10
|
+
# @param xyz_color [Abachrome::Color] The color in XYZ color space
|
11
|
+
# @raise [ArgumentError] If the input color is not in XYZ color space
|
12
|
+
# @return [Abachrome::Color] The resulting color in OKLAB color space with
|
13
|
+
# the same alpha as the input color
|
14
|
+
def self.convert(xyz_color)
|
15
|
+
raise_unless xyz_color, :xyz
|
16
|
+
|
17
|
+
x, y, z = xyz_color.coordinates.map { |_| AbcDecimal(_) }
|
18
|
+
|
19
|
+
# XYZ to LMS transformation matrix
|
20
|
+
l = (x * AD("0.8189330101")) + (y * AD("0.3618667424")) - (z * AD("0.1288597137"))
|
21
|
+
m = (x * AD("0.0329845436")) + (y * AD("0.9293118715")) + (z * AD("0.0361456387"))
|
22
|
+
s = (x * AD("0.0482003018")) + (y * AD("0.2643662691")) + (z * AD("0.6338517070"))
|
23
|
+
|
24
|
+
# Apply cube root transformation
|
25
|
+
l_ = AbcDecimal(l)**Rational(1, 3)
|
26
|
+
m_ = AbcDecimal(m)**Rational(1, 3)
|
27
|
+
s_ = AbcDecimal(s)**Rational(1, 3)
|
28
|
+
|
29
|
+
# LMS to OKLAB transformation matrix
|
30
|
+
lightness = (AD("0.2104542553") * l_) + (AD("0.793617785") * m_) - (AD("0.0040720468") * s_)
|
31
|
+
a = (AD("1.9779984951") * l_) - (AD("2.4285922050") * m_) + (AD("0.4505937099") * s_)
|
32
|
+
b = (AD("0.0259040371") * l_) + (AD("0.7827717662") * m_) - (AD("0.8086757660") * s_)
|
33
|
+
|
34
|
+
Color.new(ColorSpace.find(:oklab), [lightness, a, b], xyz_color.alpha)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|