abachrome-float 0.1.6

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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/.rubocop.yml +10 -0
  4. data/CHANGELOG.md +21 -0
  5. data/CLA.md +45 -0
  6. data/CODE-OF-CONDUCT.md +9 -0
  7. data/LICENSE +19 -0
  8. data/README.md +315 -0
  9. data/Rakefile +15 -0
  10. data/SECURITY.md +94 -0
  11. data/abachrome-float.gemspec +36 -0
  12. data/demos/ncurses/plasma.rb +124 -0
  13. data/devenv.lock +171 -0
  14. data/devenv.nix +52 -0
  15. data/devenv.yaml +8 -0
  16. data/lib/abachrome/color.rb +197 -0
  17. data/lib/abachrome/color_mixins/blend.rb +100 -0
  18. data/lib/abachrome/color_mixins/lighten.rb +90 -0
  19. data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
  20. data/lib/abachrome/color_mixins/to_colorspace.rb +107 -0
  21. data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
  22. data/lib/abachrome/color_mixins/to_lrgb.rb +121 -0
  23. data/lib/abachrome/color_mixins/to_oklab.rb +117 -0
  24. data/lib/abachrome/color_mixins/to_oklch.rb +110 -0
  25. data/lib/abachrome/color_mixins/to_srgb.rb +142 -0
  26. data/lib/abachrome/color_models/cmyk.rb +159 -0
  27. data/lib/abachrome/color_models/hsv.rb +49 -0
  28. data/lib/abachrome/color_models/lms.rb +38 -0
  29. data/lib/abachrome/color_models/oklab.rb +37 -0
  30. data/lib/abachrome/color_models/oklch.rb +91 -0
  31. data/lib/abachrome/color_models/rgb.rb +58 -0
  32. data/lib/abachrome/color_models/xyz.rb +31 -0
  33. data/lib/abachrome/color_models/yiq.rb +37 -0
  34. data/lib/abachrome/color_space.rb +199 -0
  35. data/lib/abachrome/converter.rb +117 -0
  36. data/lib/abachrome/converters/base.rb +128 -0
  37. data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
  38. data/lib/abachrome/converters/lms_to_lrgb.rb +40 -0
  39. data/lib/abachrome/converters/lms_to_srgb.rb +27 -0
  40. data/lib/abachrome/converters/lms_to_xyz.rb +34 -0
  41. data/lib/abachrome/converters/lrgb_to_lms.rb +3 -0
  42. data/lib/abachrome/converters/lrgb_to_oklab.rb +57 -0
  43. data/lib/abachrome/converters/lrgb_to_srgb.rb +59 -0
  44. data/lib/abachrome/converters/lrgb_to_xyz.rb +33 -0
  45. data/lib/abachrome/converters/oklab_to_lms.rb +44 -0
  46. data/lib/abachrome/converters/oklab_to_lrgb.rb +71 -0
  47. data/lib/abachrome/converters/oklab_to_oklch.rb +56 -0
  48. data/lib/abachrome/converters/oklab_to_srgb.rb +46 -0
  49. data/lib/abachrome/converters/oklch_to_lrgb.rb +79 -0
  50. data/lib/abachrome/converters/oklch_to_oklab.rb +52 -0
  51. data/lib/abachrome/converters/oklch_to_srgb.rb +46 -0
  52. data/lib/abachrome/converters/oklch_to_xyz.rb +70 -0
  53. data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
  54. data/lib/abachrome/converters/srgb_to_lrgb.rb +55 -0
  55. data/lib/abachrome/converters/srgb_to_oklab.rb +45 -0
  56. data/lib/abachrome/converters/srgb_to_oklch.rb +47 -0
  57. data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
  58. data/lib/abachrome/converters/xyz_to_lms.rb +34 -0
  59. data/lib/abachrome/converters/xyz_to_oklab.rb +42 -0
  60. data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
  61. data/lib/abachrome/gamut/base.rb +74 -0
  62. data/lib/abachrome/gamut/p3.rb +27 -0
  63. data/lib/abachrome/gamut/rec2020.rb +25 -0
  64. data/lib/abachrome/gamut/srgb.rb +49 -0
  65. data/lib/abachrome/illuminants/base.rb +35 -0
  66. data/lib/abachrome/illuminants/d50.rb +33 -0
  67. data/lib/abachrome/illuminants/d55.rb +29 -0
  68. data/lib/abachrome/illuminants/d65.rb +37 -0
  69. data/lib/abachrome/illuminants/d75.rb +29 -0
  70. data/lib/abachrome/named/css.rb +157 -0
  71. data/lib/abachrome/named/tailwind.rb +301 -0
  72. data/lib/abachrome/outputs/css.rb +119 -0
  73. data/lib/abachrome/palette.rb +244 -0
  74. data/lib/abachrome/palette_mixins/interpolate.rb +53 -0
  75. data/lib/abachrome/palette_mixins/resample.rb +61 -0
  76. data/lib/abachrome/palette_mixins/stretch_luminance.rb +72 -0
  77. data/lib/abachrome/parsers/css.rb +452 -0
  78. data/lib/abachrome/parsers/hex.rb +52 -0
  79. data/lib/abachrome/parsers/tailwind.rb +45 -0
  80. data/lib/abachrome/spectral.rb +276 -0
  81. data/lib/abachrome/to_abcd.rb +23 -0
  82. data/lib/abachrome/version.rb +7 -0
  83. data/lib/abachrome.rb +242 -0
  84. data/logo.png +0 -0
  85. data/logo.webp +0 -0
  86. data/security/assesments/2025-10-12-SECURITY_ASSESSMENT.md +53 -0
  87. data/security/vex.json +21 -0
  88. metadata +146 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::ColorModels::Xyz - XYZ color space model definition
4
+ #
5
+ # This module defines the XYZ color model within the Abachrome color manipulation library.
6
+ # XYZ is the CIE 1931 color space that forms the basis for most other color space definitions
7
+ # and serves as a device-independent reference color space. The XYZ color space represents
8
+ # colors using tristimulus values that correspond to the response of the human visual system
9
+ # to light stimuli, making it fundamental to color science and accurate color reproduction.
10
+ #
11
+ # Key features:
12
+ # - Registers the XYZ color space with coordinate names [x, y, z]
13
+ # - Represents tristimulus values for device-independent color specification
14
+ # - Serves as intermediate color space for conversions between different color models
15
+ # - Uses normalized values for consistency with other color models in the library
16
+ # - Maintains high precision through AbcDecimal arithmetic for color transformations
17
+ # - Provides validation for XYZ coordinate ranges to ensure valid color representations
18
+ #
19
+ # The XYZ model is particularly important in the color science pipeline as it provides
20
+ # a standardized reference for color matching and serves as the foundation for defining
21
+ # other color spaces like LAB, making it essential for accurate color transformations
22
+ # that maintain consistency across different devices and viewing conditions.
23
+
24
+ module Abachrome
25
+ module ColorModels
26
+ class Xyz
27
+ end
28
+ end
29
+ end
30
+
31
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::ColorModels::YIQ - YIQ color space model utilities
4
+ #
5
+ # This module provides utility methods for the YIQ color model within the Abachrome
6
+ # color manipulation library. YIQ is a color space historically used by the NTSC
7
+ # television standard, separating luminance (Y) from chrominance (I and Q).
8
+ #
9
+ # Key features:
10
+ # - Separates brightness (luma) from color information
11
+ # - Y (luma): Weighted sum of RGB based on human eye sensitivity
12
+ # - I (In-phase): Orange-to-blue axis
13
+ # - Q (Quadrature): Purple-to-green axis
14
+ # - Supports legacy broadcast standards and computer vision applications
15
+ # - Uses Rec. 601 coefficients for luma calculation
16
+ #
17
+ # The YIQ model is essential for image processing algorithms that need to separate
18
+ # luminance from chrominance, including JPEG compression, grayscale conversion, and
19
+ # edge detection in computer vision applications.
20
+
21
+ module Abachrome
22
+ module ColorModels
23
+ class YIQ
24
+ class << self
25
+ # Normalizes YIQ color component values to their standard ranges.
26
+ #
27
+ # @param y [Numeric] Luma component (brightness), typically in range [0,1]
28
+ # @param i [Numeric] In-phase component (orange-blue), typically in range [-0.5957, 0.5957]
29
+ # @param q [Numeric] Quadrature component (purple-green), typically in range [-0.5226, 0.5226]
30
+ # @return [Array<AbcDecimal>] Array of three normalized components as AbcDecimal objects
31
+ def normalize(y, i, q)
32
+ [y, i, q].map { |value| value.to_f }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::ColorSpace - Core color space definition and registry system
4
+ #
5
+ # This module provides the foundation for managing color spaces within the Abachrome library.
6
+ # It implements a registry system for storing and retrieving color space definitions, along
7
+ # with the ColorSpace class that encapsulates color space properties including coordinate
8
+ # names, white points, and color models.
9
+ #
10
+ # Key features:
11
+ # - Global registry for color space registration and lookup with alias support
12
+ # - Color space definition with configurable coordinates, white points, and color models
13
+ # - Built-in registration of standard color spaces (sRGB, linear RGB, HSL, LAB, OKLAB, OKLCH)
14
+ # - Equality comparison and hash support for color space objects
15
+ # - Flexible initialization through block-based configuration
16
+ # - Support for color space aliases (e.g., :rgb as alias for :srgb)
17
+ #
18
+ # The ColorSpace class serves as the foundation for the Color class and converter system,
19
+ # providing the metadata needed for proper color representation and transformation between
20
+ # different color spaces. All registered color spaces are accessible through the registry
21
+ # class methods and can be extended with custom color space definitions.
22
+
23
+ module Abachrome
24
+ class ColorSpace
25
+ class << self
26
+ # A registry of all registered color spaces.
27
+ #
28
+ # @return [Hash] A memoized hash where keys are color space identifiers and values are the corresponding color space objects
29
+ def registry
30
+ @registry ||= {}
31
+ end
32
+
33
+ # Registers a new color space with the specified name.
34
+ #
35
+ # @param name [String, Symbol] The identifier for the color space
36
+ # @param block [Proc] A block that configures the color space properties
37
+ # @return [Abachrome::ColorSpace] The newly created color space instance added to the registry
38
+ def register(name, &block)
39
+ registry[name.to_sym] = new(name, &block)
40
+ end
41
+
42
+ # Aliases a color space name to an existing registered color space.
43
+ #
44
+ # This method creates an alias for an existing color space in the registry,
45
+ # allowing the same color space to be accessed through multiple names.
46
+ #
47
+ # @param name [Symbol, String] The existing color space name already registered
48
+ # @param aliased_name [Symbol, String] The new alias name to register
49
+ # @return [void]
50
+ def alias(name, aliased_name)
51
+ registry[aliased_name.to_sym] = registry[name.to_sym]
52
+ end
53
+
54
+ # @param name [String, Symbol] The name of the color space to find
55
+ # @return [Abachrome::ColorSpace] The color space with the given name
56
+ # @raise [ArgumentError] If no color space with the given name exists in the registry
57
+ def find(name)
58
+ registry[name.to_sym] or raise ArgumentError, "Unknown color space: #{name}"
59
+ end
60
+ end
61
+
62
+ attr_reader :name, :coordinates, :white_point, :color_model
63
+
64
+ # Initialize a new ColorSpace instance.
65
+ #
66
+ # @param name [String, Symbol] The name of the color space, which will be converted to a symbol
67
+ # @return [Abachrome::ColorSpace] A new instance of ColorSpace
68
+ # @yield [self] Yields self to the block for configuration if a block is given
69
+ def initialize(name)
70
+ @name = name.to_sym
71
+ yield self if block_given?
72
+ end
73
+
74
+ # Sets the color coordinates for the current color space.
75
+ #
76
+ # @param [Array] coords The coordinate values that define a color in this color space.
77
+ # Multiple arguments or a single flat array can be provided.
78
+ # @return [Array] The flattened array of coordinates.
79
+ def coordinates=(*coords)
80
+ @coordinates = coords.flatten
81
+ end
82
+
83
+ # Sets the white point reference used by the color space.
84
+ #
85
+ # The white point is a reference that defines what is considered "white" in a color space.
86
+ # Common values include :D50, :D65, etc.
87
+ #
88
+ # @param point [Symbol, String] The white point reference to use (will be converted to Symbol)
89
+ # @return [Symbol] The newly set white point
90
+ def white_point=(point)
91
+ @white_point = point.to_sym
92
+ end
93
+
94
+ # Sets the color model for the color space.
95
+ #
96
+ # @param model [String, Symbol] The new color model to set for this color space
97
+ # @return [Symbol] The color model as a symbolized value
98
+ def color_model=(model)
99
+ @color_model = model.to_sym
100
+ end
101
+
102
+ # Compares this ColorSpace instance with another to check for equality.
103
+ #
104
+ # Two ColorSpace objects are considered equal if they have the same name.
105
+ #
106
+ # @param other [Object] The object to compare against
107
+ # @return [Boolean] true if other is a ColorSpace with the same name, false otherwise
108
+ def ==(other)
109
+ return false unless other.is_a?(ColorSpace)
110
+
111
+ name == other.name
112
+ end
113
+
114
+ # Checks if two color spaces are equal.
115
+ #
116
+ # @param other [Abachrome::ColorSpace] The color space to compare with
117
+ # @return [Boolean] true if the color spaces are equal, false otherwise
118
+ def eql?(other)
119
+ self == other
120
+ end
121
+
122
+ # Returns a hash value for the color space based on its name.
123
+ #
124
+ # @return [Integer] A hash value computed from the color space name that can be
125
+ # used for equality comparison and as a hash key.
126
+ def hash
127
+ name.hash
128
+ end
129
+
130
+ # Returns the identifier for the color space, which is currently the same as its name.
131
+ # @return [String, Symbol] the identifier of the color space
132
+ def id
133
+ name
134
+ end
135
+ end
136
+
137
+ ColorSpace.register(:srgb) do |s|
138
+ s.coordinates = %i[red green blue]
139
+ s.white_point = :D65
140
+ s.color_model = :srgb
141
+ end
142
+ ColorSpace.alias(:srgb, :rgb)
143
+
144
+ ColorSpace.register(:lrgb) do |s|
145
+ s.coordinates = %i[red green blue]
146
+ s.white_point = :D65
147
+ s.color_model = :lrgb
148
+ end
149
+
150
+ ColorSpace.register(:hsl) do |s|
151
+ s.coordinates = %i[hue saturation lightness]
152
+ s.white_point = :D65
153
+ s.color_model = :hsl
154
+ end
155
+
156
+ ColorSpace.register(:lab) do |s|
157
+ s.coordinates = %i[lightness a b]
158
+ s.white_point = :D65
159
+ s.color_model = :lab
160
+ end
161
+
162
+ ColorSpace.register(:oklab) do |s|
163
+ s.coordinates = %i[lightness a b]
164
+ s.white_point = :D65
165
+ s.color_model = :oklab
166
+ end
167
+
168
+ ColorSpace.register(:oklch) do |s|
169
+ s.coordinates = %i[lightness chroma hue]
170
+ s.white_point = :D65
171
+ s.color_model = :oklch
172
+ end
173
+
174
+ ColorSpace.register(:xyz) do |s|
175
+ s.coordinates = %i[x y z]
176
+ s.white_point = :D65
177
+ s.color_model = :xyz
178
+ end
179
+
180
+ ColorSpace.register(:lms) do |s|
181
+ s.coordinates = %i[l m s]
182
+ s.white_point = :D65
183
+ s.color_model = :lms
184
+ end
185
+
186
+ ColorSpace.register(:yiq) do |s|
187
+ s.coordinates = %i[y i q]
188
+ s.white_point = :D65
189
+ s.color_model = :yiq
190
+ end
191
+
192
+ ColorSpace.register(:cmyk) do |s|
193
+ s.coordinates = %i[cyan magenta yellow key]
194
+ s.white_point = :D50
195
+ s.color_model = :cmyk
196
+ end
197
+ end
198
+
199
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::Converter - Color space conversion registry and orchestration system
4
+ #
5
+ # This module provides the central registry and conversion orchestration for transforming colors
6
+ # between different color spaces within the Abachrome library. It manages a registry of converter
7
+ # classes that handle transformations between specific color space pairs (e.g., sRGB to OKLAB,
8
+ # OKLCH to linear RGB) and provides the main conversion interface used throughout the library.
9
+ #
10
+ # Key features:
11
+ # - Global registry for color space converter registration with automatic discovery
12
+ # - Converter lookup and routing between source and target color spaces
13
+ # - Automatic registration of all converter classes found in the Converters namespace
14
+ # - Conversion orchestration that finds appropriate converters based on color model compatibility
15
+ # - Support for multi-step conversions through intermediate color spaces when needed
16
+ # - Integration with the ColorSpace system for proper color space identification
17
+ #
18
+ # The Converter system serves as the backbone for all color transformations in Abachrome,
19
+ # enabling seamless conversion between any registered color spaces while maintaining the
20
+ # precision and accuracy required for color science calculations. Converter classes follow
21
+ # a naming convention (FromSpaceToSpace) for automatic registration and discovery.
22
+
23
+ module Abachrome
24
+ class Converter
25
+ class << self
26
+ # Returns the registry hash used to store color space converters.
27
+ #
28
+ # This method lazily initializes and returns the hash used internally to store
29
+ # converter mappings between different color spaces. This registry is a central
30
+ # repository that maps color space pairs to their appropriate conversion functions.
31
+ #
32
+ # @return [Hash] The converter registry hash, mapping color space pairs to converter functions
33
+ def registry
34
+ @registry ||= {}
35
+ end
36
+
37
+ # Register a converter class for transforming colors between two specific color spaces.
38
+ #
39
+ # @param from_space [Symbol, String] The source color space identifier
40
+ # @param to_space [Symbol, String] The destination color space identifier
41
+ # @param converter_class [Class] The converter class that implements the transformation
42
+ # @return [Class] The registered converter class
43
+ def register(from_space, to_space, converter_class)
44
+ registry[[from_space.to_s, to_space.to_s]] = converter_class
45
+ end
46
+
47
+ # Converts a color from its current color space to the specified target color space.
48
+ #
49
+ # @param color [Abachrome::Color] The color to convert
50
+ # @param to_space_name [String, Symbol] The name of the target color space
51
+ # @return [Abachrome::Color] The color converted to the target color space
52
+ # @raise [RuntimeError] If no converter is found between the source and target color models
53
+ def convert(color, to_space_name)
54
+ to_space = ColorSpace.find(to_space_name)
55
+ return color if color.color_space == to_space
56
+
57
+ # convert model first
58
+ to_model = to_space.color_model
59
+ converter = find_converter(color.color_space.color_model, to_model.to_s)
60
+ raise "No converter found from #{color.color_space.color_model} to #{to_model}" unless converter
61
+
62
+ converter.convert(color)
63
+ end
64
+
65
+ # @api private
66
+ # @since 0.1.0
67
+ # @example
68
+ # converter = Abachrome::Converter.new
69
+ # converter.register_all_converters
70
+ #
71
+ # Automatically registers all converter classes found in the Converters namespace.
72
+ # The method iterates through constants in the Converters module, identifies classes
73
+ # with naming pattern "FromSpaceToSpace" (e.g., LrgbToOklab), extracts the source
74
+ # and destination color spaces from the class name, and registers the converter
75
+ # class for the corresponding color space conversion.
76
+ #
77
+ # @return [void]
78
+ def register_all_converters
79
+ Converters.constants.each do |const_name|
80
+ const = Converters.const_get(const_name)
81
+ next unless const.is_a?(Class)
82
+
83
+ # Parse from_space and to_space from class name (e.g., LrgbToOklab)
84
+ next unless const_name.to_s =~ /^(.+)To(.+)$/
85
+
86
+ from_space = ::Regexp.last_match(1).downcase.to_sym
87
+ to_space = ::Regexp.last_match(2).downcase.to_sym
88
+
89
+ # Register the converter
90
+ register(from_space, to_space, const)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Retrieves a converter function between two color spaces from the registry.
97
+ #
98
+ # @param from_space_name [String, Symbol] The source color space name
99
+ # @param to_space_name [String, Symbol] The target color space name
100
+ # @return [Proc, nil] The conversion function if registered, or nil if no converter exists for the specified color spaces
101
+ def find_converter(from_space_name, to_space_name)
102
+ registry[[from_space_name.to_s, to_space_name.to_s]]
103
+ end
104
+ end
105
+ end
106
+
107
+ # Load all converter files
108
+ converters_path = File.join(__dir__, "converters", "*.rb")
109
+ Dir[converters_path].each do |file|
110
+ require file
111
+ end
112
+
113
+ # Auto-register all converters
114
+ Converter.register_all_converters
115
+ end
116
+
117
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::Converters::Base - Abstract base class for color space converters
4
+ #
5
+ # This class provides the foundation for implementing color space conversion functionality
6
+ # within the Abachrome library. It defines the interface that all converter classes must
7
+ # implement and provides common validation and utility methods for color transformations.
8
+ #
9
+ # Key features:
10
+ # - Abstract conversion interface requiring subclasses to implement #convert method
11
+ # - Color model validation to ensure proper conversion compatibility
12
+ # - Converter registration and lookup system for managing conversion mappings
13
+ # - Source and target color space compatibility checking
14
+ # - Base functionality for building specific converter implementations
15
+ #
16
+ # All converter classes in the Abachrome system inherit from this base class and implement
17
+ # the specific mathematical transformations needed to convert colors between different
18
+ # color spaces such as sRGB, OKLAB, OKLCH, and linear RGB. The class follows a naming
19
+ # convention pattern (FromSpaceToSpace) for automatic registration and discovery.
20
+
21
+ module Abachrome
22
+ module Converters
23
+ class Base
24
+ attr_reader :from_space, :to_space
25
+
26
+ # Initialize a new converter between two color spaces.
27
+ #
28
+ # @param from_space [Abachrome::ColorSpace] The source color space to convert from
29
+ # @param to_space [Abachrome::ColorSpace] The target color space to convert to
30
+ # @return [Abachrome::Converters::Base] A new converter instance
31
+ def initialize(from_space, to_space)
32
+ @from_space = from_space
33
+ @to_space = to_space
34
+ end
35
+
36
+ # Converts a color from one color space to another.
37
+ #
38
+ # @abstract This is an abstract method that must be implemented by subclasses.
39
+ # @param color [Abachrome::Color] The color to convert
40
+ # @return [Abachrome::Color] The converted color
41
+ # @raise [NotImplementedError] If the subclass doesn't implement this method
42
+ def convert(color)
43
+ raise NotImplementedError, "Subclasses must implement #convert"
44
+ end
45
+
46
+ # Validates that a color uses the expected color model.
47
+ #
48
+ # @param color [Abachrome::Color] The color object to check
49
+ # @param model [Symbol, String] The expected color model
50
+ # @raise [RuntimeError] If the color's model doesn't match the expected model
51
+ # @return [nil] If the color's model matches the expected model
52
+ def self.raise_unless(color, model)
53
+ return if color.color_space.color_model == model
54
+
55
+ raise "#{color} is #{color.color_space.color_model}), expecting #{model}"
56
+ end
57
+
58
+ # Determines if the converter can handle the given color.
59
+ #
60
+ # This method checks if the color's current color space matches
61
+ # the converter's source color space.
62
+ #
63
+ # @param color [Abachrome::Color] The color to check
64
+ # @return [Boolean] true if the converter can convert from the color's current color space,
65
+ # false otherwise
66
+ def can_convert?(color)
67
+ color.color_space == from_space
68
+ end
69
+
70
+ # Register a converter class for transforming colors between two specific color spaces.
71
+ #
72
+ # @param from_space_id [Symbol] The identifier of the source color space
73
+ # @param to_space_id [Symbol] The identifier of the destination color space
74
+ # @param converter_class [Class] The converter class that handles the transformation
75
+ # @return [void]
76
+ def self.register(from_space_id, to_space_id, converter_class)
77
+ @converters ||= {}
78
+ @converters[[from_space_id, to_space_id]] = converter_class
79
+ end
80
+
81
+ # Find a converter for converting between color spaces.
82
+ #
83
+ # @param from_space_id [Symbol, String] The identifier of the source color space
84
+ # @param to_space_id [Symbol, String] The identifier of the destination color space
85
+ # @return [Converter, nil] The converter instance for the specified color spaces, or nil if no converter is found
86
+ def self.find_converter(from_space_id, to_space_id)
87
+ @converters ||= {}
88
+ @converters[[from_space_id, to_space_id]]
89
+ end
90
+
91
+ # Converts a color from its current color space to a target color space.
92
+ #
93
+ # This method finds the appropriate converter class for the given source and
94
+ # target color spaces and performs the conversion.
95
+ #
96
+ # @param color [Abachrome::Color] The color to convert
97
+ # @param to_space [Abachrome::ColorSpace] The target color space to convert to
98
+ # @return [Abachrome::Color] The converted color in the target color space
99
+ # @raise [ConversionError] If no converter is found for the given color spaces
100
+ def self.convert(color, to_space)
101
+ converter_class = find_converter(color.color_space.id, to_space.id)
102
+ unless converter_class
103
+ raise ConversionError,
104
+ "No converter found from #{color.color_space.name} to #{to_space.name}"
105
+ end
106
+
107
+ converter = converter_class.new(color.color_space, to_space)
108
+ converter.convert(color)
109
+ end
110
+
111
+ private
112
+
113
+ # Validates if a color can be converted from its current color space.
114
+ # Raises an ArgumentError if the color's space doesn't match the expected source space.
115
+ #
116
+ # @param color [Abachrome::Color] The color object to validate
117
+ # @raise [ArgumentError] If the color cannot be converted from its current color space
118
+ # @return [nil] Returns nil if the color is valid for conversion
119
+ def validate_color!(color)
120
+ return if can_convert?(color)
121
+
122
+ raise ArgumentError, "Cannot convert color from #{color.color_space.name} (expected #{from_space.name})"
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::Converters::CmykToSrgb - CMYK to sRGB color space converter
4
+ #
5
+ # This converter transforms colors from the CMYK (Cyan, Magenta, Yellow, Key/Black)
6
+ # color space back to the standard RGB (sRGB) color space for display on screens.
7
+ #
8
+ # Key features:
9
+ # - Implements the standard CMYK to RGB conversion algorithm
10
+ # - Converts from subtractive (ink) to additive (light) color model
11
+ # - Maintains alpha channel transparency values during conversion
12
+ # - Uses BigDecimal arithmetic for precise color science calculations
13
+ # - Handles colors created with UCR/GCR correctly
14
+ #
15
+ # The conversion recombines the ink components (CMYK) back into light components (RGB)
16
+ # using the standard inverse transformation. Note that some CMYK colors may be
17
+ # out of gamut for sRGB displays.
18
+
19
+ module Abachrome
20
+ module Converters
21
+ class CmykToSrgb
22
+ # Converts a color from CMYK color space to sRGB color space.
23
+ # This method applies the standard CMYK to RGB transformation.
24
+ #
25
+ # @param cmyk_color [Abachrome::Color] A color object in the CMYK color space
26
+ # @return [Abachrome::Color] A new color object in the sRGB color space
27
+ # with the same alpha value as the input color
28
+ def self.convert(cmyk_color)
29
+ c, m, y, k = cmyk_color.coordinates.map { |component| component.to_f }
30
+
31
+ # Use the CMYK color model's conversion method
32
+ r, g, b = ColorModels::CMYK.to_rgb(c, m, y, k)
33
+
34
+ Color.new(
35
+ ColorSpace.find(:srgb),
36
+ [r, g, b],
37
+ cmyk_color.alpha
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abachrome
4
+ module Converters
5
+ class LmsToLrgb < Abachrome::Converters::Base
6
+ # Converts a color from LMS color space to linear RGB color space.
7
+ #
8
+ # This method implements the final part of the OKLAB to linear RGB transformation,
9
+ # converting LMS (Long, Medium, Short) coordinates to linear RGB coordinates
10
+ # using the standard transformation matrix. The LMS color space represents
11
+ # the response of the three types of cone cells in the human eye.
12
+ #
13
+ # @param lms_color [Abachrome::Color] The color in LMS color space
14
+ # @raise [ArgumentError] If the input color is not in LMS color space
15
+ # @return [Abachrome::Color] The resulting color in linear RGB color space with
16
+ # the same alpha as the input color
17
+ def self.convert(lms_color)
18
+ raise_unless lms_color, :lms
19
+
20
+ l, m, s = lms_color.coordinates.map { |_| _.to_f }
21
+
22
+ r = (l * AD("4.07674166134799")) +
23
+ (m * AD("-3.307711590408193")) +
24
+ (s * AD("0.230969928729428"))
25
+ g = (l * AD("-1.2684380040921763")) +
26
+ (m * AD("2.6097574006633715")) +
27
+ (s * AD("-0.3413193963102197"))
28
+ b = (l * AD("-0.004196086541837188")) +
29
+ (m * AD("-0.7034186144594493")) +
30
+ (s * AD("1.7076147009309444"))
31
+
32
+ output_coords = [r, g, b].map { |it| [it, 0].max }
33
+
34
+ Color.new(ColorSpace.find(:lrgb), output_coords, lms_color.alpha)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abachrome
4
+ module Converters
5
+ class LmsToSrgb < Abachrome::Converters::Base
6
+ # Converts a color from LMS color space to sRGB color space.
7
+ #
8
+ # This method implements a two-step conversion process:
9
+ # 1. First converts from LMS to linear RGB using the standard transformation matrix
10
+ # 2. Then converts from linear RGB to sRGB by applying gamma correction
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 sRGB color space with
15
+ # the same alpha as the input color
16
+ def self.convert(lms_color)
17
+ # First convert LMS to linear RGB
18
+ lrgb_color = LmsToLrgb.convert(lms_color)
19
+
20
+ # Then convert linear RGB to sRGB
21
+ LrgbToSrgb.convert(lrgb_color)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abachrome
4
+ module Converters
5
+ class LmsToXyz < Abachrome::Converters::Base
6
+ # Converts a color from LMS color space to XYZ color space.
7
+ #
8
+ # This method implements the LMS to XYZ transformation using the standard
9
+ # transformation matrix. The LMS color space represents the response of
10
+ # the three types of cone cells in the human eye (Long, Medium, Short),
11
+ # while XYZ is the CIE 1931 color space that forms the basis for most
12
+ # other color space definitions.
13
+ #
14
+ # @param lms_color [Abachrome::Color] The color in LMS color space
15
+ # @raise [ArgumentError] If the input color is not in LMS color space
16
+ # @return [Abachrome::Color] The resulting color in XYZ color space with
17
+ # the same alpha as the input color
18
+ def self.convert(lms_color)
19
+ raise_unless lms_color, :lms
20
+
21
+ l, m, s = lms_color.coordinates.map { |_| _.to_f }
22
+
23
+ # LMS to XYZ transformation matrix
24
+ x = (l * AD("1.86006661")) - (m * AD("1.12948190")) + (s * AD("0.21989740"))
25
+ y = (l * AD("0.36122292")) + (m * AD("0.63881308")) - (s * AD("0.00000000"))
26
+ z = (l * AD("0.00000000")) - (m * AD("0.00000000")) + (s * AD("1.08906362"))
27
+
28
+ Color.new(ColorSpace.find(:xyz), [x, y, z], lms_color.alpha)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.