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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99a27f5afff0a908be9991f71c1a96ead0f226b7d0a4308e1227912e508afa82
4
- data.tar.gz: 861c7bc3b6a13cdb869d986a6b6fadd17369a960cab52c0e7a930dc019e052e5
3
+ metadata.gz: a33e1980f4b275bc95646259d6c71b01715d2a77f019828b2c05ea98b24826d8
4
+ data.tar.gz: 896c1479708b87a3862bc28bdbb1959aefc0bcb3619604980096f34ed7dd07e9
5
5
  SHA512:
6
- metadata.gz: a050c327bb501c5cd4da1855cc62b4894241eabed1869317c5d326a82d1a556c8298d5ef063d97be2b8d3f1aa9f456293f34f00926b0c9e749ef24b72c5e383b
7
- data.tar.gz: 217f2987d6df4251fb6c8a211c59c1b1f4bdb66f3107e62ed755a9b05e794d5f0c14da9653175b90a25728ade13dceb1411082f123713dca57aa0e809ed4c0f3
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 "rake/testtask"
4
+ require "minitest/test_task"
5
5
 
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb", 'test/**/test_*.rb']
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
@@ -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
@@ -110,4 +110,4 @@ module Abachrome
110
110
 
111
111
  # Auto-register all converters
112
112
  Converter.register_all_converters
113
- end
113
+ end
@@ -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 inverse transformation matrices. The conversion process applies a series of
5
- # matrix transformations and cubic operations to accurately map OKLAB coordinates back to the
6
- # linear RGB color space, which represents colors with a linear relationship to light intensity.
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 to linear RGB inverse transformation algorithm with high-precision matrices
10
- # - Converts OKLAB values through intermediate L'M'S' and LMS color space representations
11
- # - Applies cubic operations to reverse the perceptual uniformity transformations
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 the foundation for further conversions to display-ready
18
- # color spaces like sRGB, making this converter essential for the color transformation pipeline
19
- # when working with perceptually uniform OKLAB color manipulations.
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
- # The method implements the OKLAB to linear RGB transformation matrix based on
27
- # standard color science algorithms. It first confirms the input is in OKLAB
28
- # space, then applies the transformation through several steps:
29
- # 1. Converts OKLAB coordinates to intermediate L'M'S' values
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
- l, a, b = oklab_color.coordinates.map { |_| AbcDecimal(_) }
37
+ l_ok, a_ok, b_ok = oklab_color.coordinates.map { |_| AbcDecimal(_) }
42
38
 
43
- l_ = AbcDecimal((l * AD("0.99999999845051981432")) +
44
- (AD("0.39633779217376785678") * a) +
45
- (AD("0.21580375806075880339") * b))
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
- l = AbcDecimal(l_)**3
55
- m = AbcDecimal(m_)**3
56
- s = AbcDecimal(s_)**3
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
- r = (l * AD("4.07674166134799")) -
59
- (m * AD("3.307711590408193")) +
60
- (s * AD("0.230969928729428"))
61
- g = (l * AD("-1.2684380040921763")) +
62
- (m * AD("2.6097574006633715")) -
63
- (s * AD("0.3413193963102197"))
64
- b = (l * AD("-0.004196086541837188")) -
65
- (m * AD("0.7034186144594493")) +
66
- (s * AD("1.7076147009309444"))
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
- # through a two-step conversion process. The transformation first converts OKLCH cylindrical
5
- # coordinates to OKLAB rectangular coordinates, then applies the standard OKLAB to linear RGB
6
- # transformation matrices to produce the final linear RGB values.
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
- # - Two-stage conversion pipeline: OKLCH OKLAB → Linear RGB
10
- # - Leverages existing OklchToOklab and OklabToLrgb converters for modular transformation
11
- # - Converts cylindrical coordinates (lightness, chroma, hue) to linear light intensity values
12
- # - Maintains alpha channel transparency values during conversion
13
- # - Uses AbcDecimal arithmetic for precise color science calculations
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
- oklab_color = OklchToOklab.convert(oklch_color)
34
- OklabToLrgb.convert(oklab_color)
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 * Rational(Math::PI, 180)
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
@@ -1,5 +1,5 @@
1
1
  #
2
2
 
3
3
  module Abachrome
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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.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