abachrome 0.1.1 → 0.1.2
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/Rakefile +8 -5
- 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/version.rb +1 -1
- metadata +12 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a33e1980f4b275bc95646259d6c71b01715d2a77f019828b2c05ea98b24826d8
|
4
|
+
data.tar.gz: 896c1479708b87a3862bc28bdbb1959aefc0bcb3619604980096f34ed7dd07e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6923ce91856cf5261cc52613c1e6eacd70e0fb9c3f7a1a2fe4f248b04ea326530ca8e6b56e84f07d4cc7f663ed61fb469c35ab1247ca40f6efeeb22b28952c3
|
7
|
+
data.tar.gz: db5e02222ce2f664086a1deb5b2a4cf878c3e285c9053b652935b45688cac1d64286987b5bca0a02a0a8a884110996a2f071d283ac5df3b94f8d8722a620aef7
|
data/Rakefile
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
|
-
require "
|
4
|
+
require "minitest/test_task"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
t.
|
9
|
-
t.
|
6
|
+
Minitest::TestTask.create do |t|
|
7
|
+
|
8
|
+
t.test_prelude = %(require "simplecov"; SimpleCov.start { add_filter %p }) % ['/test/']
|
9
|
+
t.test_globs = FileList["test/**/*_test.rb", 'test/**/test_*.rb']
|
10
|
+
t.framework = %(require "test/test_helper.rb")
|
10
11
|
end
|
11
12
|
|
13
|
+
|
12
14
|
task default: :test
|
15
|
+
|
@@ -23,7 +23,7 @@ module Abachrome
|
|
23
23
|
|
24
24
|
attr_accessor :value, :precision
|
25
25
|
|
26
|
-
def_delegators :@value, :to_i, :zero?, :nonzero?
|
26
|
+
def_delegators :@value, :to_i, :zero?, :nonzero?, :finite?
|
27
27
|
|
28
28
|
# Initializes a new AbcDecimal object with the specified value and precision.
|
29
29
|
#
|
@@ -352,4 +352,4 @@ end
|
|
352
352
|
# AD("2.718") # => #<Abachrome::AbcDecimal:0x... @value=2.718>
|
353
353
|
def AD(*args)
|
354
354
|
Abachrome::AbcDecimal.new(*args)
|
355
|
-
end
|
355
|
+
end
|
data/lib/abachrome/color.rb
CHANGED
@@ -62,6 +62,18 @@ module Abachrome
|
|
62
62
|
new(space, [r, g, b], a)
|
63
63
|
end
|
64
64
|
|
65
|
+
# Creates a new Color instance from LRGB values
|
66
|
+
#
|
67
|
+
# @param r [Numeric] The red component value (typically 0-1)
|
68
|
+
# @param g [Numeric] The green component value (typically 0-1)
|
69
|
+
# @param b [Numeric] The blue component value (typically 0-1)
|
70
|
+
# @param a [Numeric] The alpha (opacity) component value (0-1), defaults to 1.0 (fully opaque)
|
71
|
+
# @return [Abachrome::Color] A new Color instance in the sRGB color space
|
72
|
+
def self.from_lrgb(r, g, b, a = 1.0)
|
73
|
+
space = ColorSpace.find(:lrgb)
|
74
|
+
new(space, [r, g, b], a)
|
75
|
+
end
|
76
|
+
|
65
77
|
# Creates a new Color object with OKLAB values.
|
66
78
|
#
|
67
79
|
# @param l [Float] The lightness component (L) of the OKLAB color space
|
@@ -146,4 +158,4 @@ module Abachrome
|
|
146
158
|
"Expected #{color_space.coordinates.size} coordinates for #{color_space.name}, got #{coordinates.size}"
|
147
159
|
end
|
148
160
|
end
|
149
|
-
end
|
161
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Abachrome::ColorModels::Lms - LMS color space model definition
|
2
|
+
#
|
3
|
+
# This module defines the LMS color model within the Abachrome color manipulation library.
|
4
|
+
# LMS represents the response of the three types of cone cells in the human eye (Long, Medium, Short)
|
5
|
+
# and serves as an intermediate color space in the OKLAB transformation pipeline. The LMS color space
|
6
|
+
# provides a foundation for perceptually uniform color representations by modeling human visual perception
|
7
|
+
# at the photoreceptor level.
|
8
|
+
#
|
9
|
+
# Key features:
|
10
|
+
# - Registers the LMS color space with coordinate names [l, m, s]
|
11
|
+
# - Represents cone cell responses for Long, Medium, and Short wavelength sensitivity
|
12
|
+
# - Serves as intermediate color space for OKLAB and linear RGB conversions
|
13
|
+
# - Uses normalized values for consistency with other color models in the library
|
14
|
+
# - Maintains high precision through AbcDecimal arithmetic for color transformations
|
15
|
+
# - Provides validation for LMS coordinate ranges to ensure valid color representations
|
16
|
+
#
|
17
|
+
# The LMS model is particularly important in the color science pipeline as it bridges the gap
|
18
|
+
# between linear RGB representations and perceptually uniform color spaces like OKLAB, enabling
|
19
|
+
# accurate color transformations that better match human visual perception characteristics.
|
20
|
+
|
21
|
+
module Abachrome
|
22
|
+
module ColorModels
|
23
|
+
class Lms
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
ColorSpace.register(
|
29
|
+
:lms,
|
30
|
+
"LMS",
|
31
|
+
%w[l m s],
|
32
|
+
nil,
|
33
|
+
[]
|
34
|
+
)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Abachrome::ColorModels::Xyz - XYZ color space model definition
|
2
|
+
#
|
3
|
+
# This module defines the XYZ color model within the Abachrome color manipulation library.
|
4
|
+
# XYZ is the CIE 1931 color space that forms the basis for most other color space definitions
|
5
|
+
# and serves as a device-independent reference color space. The XYZ color space represents
|
6
|
+
# colors using tristimulus values that correspond to the response of the human visual system
|
7
|
+
# to light stimuli, making it fundamental to color science and accurate color reproduction.
|
8
|
+
#
|
9
|
+
# Key features:
|
10
|
+
# - Registers the XYZ color space with coordinate names [x, y, z]
|
11
|
+
# - Represents tristimulus values for device-independent color specification
|
12
|
+
# - Serves as intermediate color space for conversions between different color models
|
13
|
+
# - Uses normalized values for consistency with other color models in the library
|
14
|
+
# - Maintains high precision through AbcDecimal arithmetic for color transformations
|
15
|
+
# - Provides validation for XYZ coordinate ranges to ensure valid color representations
|
16
|
+
#
|
17
|
+
# The XYZ model is particularly important in the color science pipeline as it provides
|
18
|
+
# a standardized reference for color matching and serves as the foundation for defining
|
19
|
+
# other color spaces like LAB, making it essential for accurate color transformations
|
20
|
+
# that maintain consistency across different devices and viewing conditions.
|
21
|
+
|
22
|
+
module Abachrome
|
23
|
+
module ColorModels
|
24
|
+
class Xyz
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -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
|
data/lib/abachrome/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abachrome
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Durable Programming
|
@@ -54,23 +54,34 @@ files:
|
|
54
54
|
- lib/abachrome/color_mixins/to_oklch.rb
|
55
55
|
- lib/abachrome/color_mixins/to_srgb.rb
|
56
56
|
- lib/abachrome/color_models/hsv.rb
|
57
|
+
- lib/abachrome/color_models/lms.rb
|
57
58
|
- lib/abachrome/color_models/oklab.rb
|
58
59
|
- lib/abachrome/color_models/oklch.rb
|
59
60
|
- lib/abachrome/color_models/rgb.rb
|
61
|
+
- lib/abachrome/color_models/xyz.rb
|
60
62
|
- lib/abachrome/color_space.rb
|
61
63
|
- lib/abachrome/converter.rb
|
62
64
|
- lib/abachrome/converters/base.rb
|
65
|
+
- lib/abachrome/converters/lms_to_lrgb.rb
|
66
|
+
- lib/abachrome/converters/lms_to_srgb.rb
|
67
|
+
- lib/abachrome/converters/lms_to_xyz.rb
|
68
|
+
- lib/abachrome/converters/lrgb_to_lms.rb
|
63
69
|
- lib/abachrome/converters/lrgb_to_oklab.rb
|
64
70
|
- lib/abachrome/converters/lrgb_to_srgb.rb
|
71
|
+
- lib/abachrome/converters/lrgb_to_xyz.rb
|
72
|
+
- lib/abachrome/converters/oklab_to_lms.rb
|
65
73
|
- lib/abachrome/converters/oklab_to_lrgb.rb
|
66
74
|
- lib/abachrome/converters/oklab_to_oklch.rb
|
67
75
|
- lib/abachrome/converters/oklab_to_srgb.rb
|
68
76
|
- lib/abachrome/converters/oklch_to_lrgb.rb
|
69
77
|
- lib/abachrome/converters/oklch_to_oklab.rb
|
70
78
|
- lib/abachrome/converters/oklch_to_srgb.rb
|
79
|
+
- lib/abachrome/converters/oklch_to_xyz.rb
|
71
80
|
- lib/abachrome/converters/srgb_to_lrgb.rb
|
72
81
|
- lib/abachrome/converters/srgb_to_oklab.rb
|
73
82
|
- lib/abachrome/converters/srgb_to_oklch.rb
|
83
|
+
- lib/abachrome/converters/xyz_to_lms.rb
|
84
|
+
- lib/abachrome/converters/xyz_to_oklab.rb
|
74
85
|
- lib/abachrome/gamut/base.rb
|
75
86
|
- lib/abachrome/gamut/p3.rb
|
76
87
|
- lib/abachrome/gamut/rec2020.rb
|