abachrome 0.1.5 → 0.2.0

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/abachrome.gemspec +1 -0
  3. data/devenv.nix +1 -1
  4. data/lib/abachrome/abc_decimal.rb +42 -35
  5. data/lib/abachrome/color.rb +61 -10
  6. data/lib/abachrome/color_mixins/blend.rb +7 -5
  7. data/lib/abachrome/color_mixins/harmonies.rb +187 -0
  8. data/lib/abachrome/color_mixins/lighten.rb +8 -6
  9. data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
  10. data/lib/abachrome/color_mixins/to_colorspace.rb +10 -8
  11. data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
  12. data/lib/abachrome/color_mixins/to_lms.rb +106 -0
  13. data/lib/abachrome/color_mixins/to_lrgb.rb +14 -12
  14. data/lib/abachrome/color_mixins/to_oklab.rb +16 -14
  15. data/lib/abachrome/color_mixins/to_oklch.rb +12 -10
  16. data/lib/abachrome/color_mixins/to_srgb.rb +17 -15
  17. data/lib/abachrome/color_mixins/to_xyz.rb +106 -0
  18. data/lib/abachrome/color_mixins/wcag.rb +126 -0
  19. data/lib/abachrome/color_models/cmyk.rb +160 -0
  20. data/lib/abachrome/color_models/hsv.rb +5 -3
  21. data/lib/abachrome/color_models/lms.rb +3 -1
  22. data/lib/abachrome/color_models/oklab.rb +3 -1
  23. data/lib/abachrome/color_models/oklch.rb +6 -4
  24. data/lib/abachrome/color_models/rgb.rb +4 -2
  25. data/lib/abachrome/color_models/xyz.rb +11 -1
  26. data/lib/abachrome/color_models/yiq.rb +37 -0
  27. data/lib/abachrome/color_space.rb +28 -14
  28. data/lib/abachrome/converter.rb +10 -8
  29. data/lib/abachrome/converters/base.rb +13 -11
  30. data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
  31. data/lib/abachrome/converters/lms_to_lrgb.rb +5 -3
  32. data/lib/abachrome/converters/lms_to_oklab.rb +28 -0
  33. data/lib/abachrome/converters/lms_to_srgb.rb +6 -4
  34. data/lib/abachrome/converters/lms_to_xyz.rb +5 -3
  35. data/lib/abachrome/converters/lrgb_to_lms.rb +29 -1
  36. data/lib/abachrome/converters/lrgb_to_oklab.rb +5 -3
  37. data/lib/abachrome/converters/lrgb_to_srgb.rb +6 -4
  38. data/lib/abachrome/converters/lrgb_to_xyz.rb +5 -3
  39. data/lib/abachrome/converters/oklab_to_lms.rb +9 -7
  40. data/lib/abachrome/converters/oklab_to_lrgb.rb +7 -7
  41. data/lib/abachrome/converters/oklab_to_oklch.rb +4 -2
  42. data/lib/abachrome/converters/oklab_to_srgb.rb +4 -2
  43. data/lib/abachrome/converters/oklab_to_xyz.rb +29 -0
  44. data/lib/abachrome/converters/oklch_to_lms.rb +28 -0
  45. data/lib/abachrome/converters/oklch_to_lrgb.rb +5 -3
  46. data/lib/abachrome/converters/oklch_to_oklab.rb +5 -3
  47. data/lib/abachrome/converters/oklch_to_srgb.rb +6 -4
  48. data/lib/abachrome/converters/oklch_to_xyz.rb +6 -4
  49. data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
  50. data/lib/abachrome/converters/srgb_to_lms.rb +29 -0
  51. data/lib/abachrome/converters/srgb_to_lrgb.rb +5 -3
  52. data/lib/abachrome/converters/srgb_to_oklab.rb +4 -2
  53. data/lib/abachrome/converters/srgb_to_oklch.rb +5 -3
  54. data/lib/abachrome/converters/srgb_to_xyz.rb +30 -0
  55. data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
  56. data/lib/abachrome/converters/xyz_to_lms.rb +5 -3
  57. data/lib/abachrome/converters/xyz_to_lrgb.rb +33 -0
  58. data/lib/abachrome/converters/xyz_to_oklab.rb +5 -3
  59. data/lib/abachrome/converters/xyz_to_srgb.rb +30 -0
  60. data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
  61. data/lib/abachrome/floatify.rb +282 -0
  62. data/lib/abachrome/gamut/base.rb +3 -3
  63. data/lib/abachrome/gamut/p3.rb +3 -3
  64. data/lib/abachrome/gamut/rec2020.rb +2 -2
  65. data/lib/abachrome/gamut/srgb.rb +4 -2
  66. data/lib/abachrome/illuminants/base.rb +2 -2
  67. data/lib/abachrome/illuminants/d50.rb +2 -2
  68. data/lib/abachrome/illuminants/d55.rb +2 -2
  69. data/lib/abachrome/illuminants/d65.rb +2 -2
  70. data/lib/abachrome/illuminants/d75.rb +2 -2
  71. data/lib/abachrome/named/css.rb +149 -149
  72. data/lib/abachrome/named/tailwind.rb +265 -265
  73. data/lib/abachrome/outputs/css.rb +2 -2
  74. data/lib/abachrome/palette.rb +26 -25
  75. data/lib/abachrome/palette_mixins/interpolate.rb +3 -1
  76. data/lib/abachrome/palette_mixins/resample.rb +2 -2
  77. data/lib/abachrome/palette_mixins/stretch_luminance.rb +2 -2
  78. data/lib/abachrome/parsers/css.rb +86 -71
  79. data/lib/abachrome/parsers/hex.rb +2 -2
  80. data/lib/abachrome/parsers/tailwind.rb +8 -8
  81. data/lib/abachrome/spectral.rb +277 -0
  82. data/lib/abachrome/to_abcd.rb +4 -4
  83. data/lib/abachrome/version.rb +2 -2
  84. data/lib/abachrome.rb +66 -10
  85. metadata +41 -3
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorMixins::ToSrgb - sRGB color space conversion functionality
2
4
  #
3
5
  # This mixin provides methods for converting colors to the sRGB color space, which is the
@@ -23,11 +25,11 @@ module Abachrome
23
25
  module ColorMixins
24
26
  module ToSrgb
25
27
  # Converts the current color to the sRGB color space.
26
- #
28
+ #
27
29
  # If the color is already in the sRGB color space, returns the color instance
28
30
  # unchanged. Otherwise, performs a color space conversion from the current
29
31
  # color space to sRGB.
30
- #
32
+ #
31
33
  # @return [Abachrome::Color] A new Color instance in the sRGB color space,
32
34
  # or self if already in sRGB
33
35
  def to_srgb
@@ -37,7 +39,7 @@ module Abachrome
37
39
  end
38
40
 
39
41
  # Alias for #to_srgb method.
40
- #
42
+ #
41
43
  # @return [Abachrome::Color] The color converted to sRGB color space
42
44
  def to_rgb
43
45
  # assume they mean srgb
@@ -59,7 +61,7 @@ module Abachrome
59
61
  # Converts the current color to sRGB color space in place.
60
62
  # This is an alias for {#to_srgb!} as RGB commonly refers to sRGB
61
63
  # in web and design contexts.
62
- #
64
+ #
63
65
  # @return [self] Returns self after converting to sRGB
64
66
  def to_rgb!
65
67
  # assume they mean srgb
@@ -67,7 +69,7 @@ module Abachrome
67
69
  end
68
70
 
69
71
  # Returns the red component of the color in the sRGB color space.
70
- #
72
+ #
71
73
  # @return [AbcDecimal] The red component value in the sRGB color space,
72
74
  # normalized between 0 and 1.
73
75
  def red
@@ -75,27 +77,27 @@ module Abachrome
75
77
  end
76
78
 
77
79
  # Returns the green component of the color in sRGB space.
78
- #
80
+ #
79
81
  # This method converts the current color to sRGB color space if needed,
80
82
  # then extracts the green component (second coordinate).
81
- #
83
+ #
82
84
  # @return [AbcDecimal] The green component value in the sRGB color space, typically in the range 0-1
83
85
  def green
84
86
  to_srgb.coordinates[1]
85
87
  end
86
88
 
87
89
  # Returns the blue component of the color in sRGB color space.
88
- #
90
+ #
89
91
  # This method converts the current color to sRGB if needed and
90
92
  # extracts the third coordinate value (blue).
91
- #
93
+ #
92
94
  # @return [AbcDecimal] The blue component value in sRGB space, typically in range 0-1
93
95
  def blue
94
96
  to_srgb.coordinates[2]
95
97
  end
96
98
 
97
99
  # Returns the RGB color values in the sRGB color space.
98
- #
100
+ #
99
101
  # @return [Array<AbcDecimal>] An array of three AbcDecimal values representing
100
102
  # the red, green, and blue color components in the sRGB color space.
101
103
  def srgb_values
@@ -103,18 +105,18 @@ module Abachrome
103
105
  end
104
106
 
105
107
  # Returns the RGB values of the color as coordinates in the sRGB color space.
106
- #
108
+ #
107
109
  # @return [Array<Abachrome::AbcDecimal>] The RGB coordinates (red, green, blue) in sRGB color space
108
110
  def rgb_values
109
111
  to_srgb.coordinates
110
112
  end
111
113
 
112
114
  # Returns an array of RGB values (0-255) for this color.
113
- #
115
+ #
114
116
  # This method converts the color to sRGB, then scales the component values
115
117
  # from the 0-1 range to the 0-255 range commonly used in RGB color codes.
116
118
  # Values are rounded to the nearest integer and clamped between 0 and 255.
117
- #
119
+ #
118
120
  # @return [Array<Integer>] An array of three integers representing the [R, G, B]
119
121
  # values in the 0-255 range
120
122
  def rgb_array
@@ -123,7 +125,7 @@ module Abachrome
123
125
 
124
126
  # Returns a hexadecimal representation of this color in sRGB color space.
125
127
  # Converts the color to sRGB, then formats it as a hexadecimal string.
126
- #
128
+ #
127
129
  # @return [String] A string in the format "#RRGGBB" where RR, GG, and BB are
128
130
  # the hexadecimal representations of the red, green, and blue components,
129
131
  # each ranging from 00 to FF.
@@ -137,4 +139,4 @@ module Abachrome
137
139
  end
138
140
  end
139
141
 
140
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
142
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::ColorMixins::ToXyz - XYZ color space conversion functionality
4
+ #
5
+ # This mixin provides methods for converting colors to the XYZ color space, which is the
6
+ # CIE 1931 color space that forms the basis for most other color space definitions and
7
+ # serves as a device-independent reference color space. XYZ represents colors using
8
+ # tristimulus values that correspond to the response of the human visual system to light.
9
+ #
10
+ # Key features:
11
+ # - Convert colors to XYZ with automatic converter lookup
12
+ # - Both non-destructive (to_xyz) and destructive (to_xyz!) conversion methods
13
+ # - Direct access to XYZ components (x, y, z)
14
+ # - Utility methods for XYZ array and value extraction
15
+ # - Optimized to return the same object when no conversion is needed
16
+ # - High-precision decimal arithmetic for accurate color science calculations
17
+ #
18
+ # The XYZ color space uses three components: X, Y, and Z tristimulus values, providing
19
+ # a device-independent representation of colors that serves as the foundation for defining
20
+ # other color spaces, making it essential for accurate color transformations.
21
+
22
+ require_relative "../converter"
23
+
24
+ module Abachrome
25
+ module ColorMixins
26
+ module ToXyz
27
+ # Converts the current color to the XYZ color space.
28
+ #
29
+ # If the color is already in XYZ, it returns the color unchanged.
30
+ # Otherwise, it uses the Converter to transform the color to XYZ.
31
+ #
32
+ # @return [Abachrome::Color] A new Color object in the XYZ color space
33
+ def to_xyz
34
+ return self if color_space.name == :xyz
35
+
36
+ Converter.convert(self, :xyz)
37
+ end
38
+
39
+ # Converts the color to the XYZ color space in place.
40
+ # This method transforms the current color into XYZ space,
41
+ # modifying the original object by updating its color space
42
+ # and coordinates if not already in XYZ.
43
+ #
44
+ # @example
45
+ # color = Abachrome::Color.from_hex("#ff5500")
46
+ # color.to_xyz! # Color now uses XYZ color space
47
+ #
48
+ # @return [Abachrome::Color] self, with updated color space and coordinates
49
+ def to_xyz!
50
+ unless color_space.name == :xyz
51
+ xyz_color = to_xyz
52
+ @color_space = xyz_color.color_space
53
+ @coordinates = xyz_color.coordinates
54
+ end
55
+ self
56
+ end
57
+
58
+ # Returns the X component from the XYZ color space.
59
+ #
60
+ # The X component represents the mix of cone response curves chosen to be
61
+ # nonnegative and represents a scale of the CIE RGB red primary.
62
+ #
63
+ # @return [AbcDecimal] The X tristimulus value from the XYZ color space
64
+ def x
65
+ to_xyz.coordinates[0]
66
+ end
67
+
68
+ # Returns the Y component from the XYZ color space.
69
+ #
70
+ # The Y component represents luminance, which closely matches human perception
71
+ # of brightness. It corresponds to the CIE RGB green primary.
72
+ #
73
+ # @return [AbcDecimal] The Y tristimulus value from the XYZ color space
74
+ def y
75
+ to_xyz.coordinates[1]
76
+ end
77
+
78
+ # Returns the Z component from the XYZ color space.
79
+ #
80
+ # The Z component represents the CIE RGB blue primary and is roughly equal
81
+ # to blue stimulation.
82
+ #
83
+ # @return [AbcDecimal] The Z tristimulus value from the XYZ color space
84
+ def z
85
+ to_xyz.coordinates[2]
86
+ end
87
+
88
+ # Returns the XYZ color space coordinates for this color.
89
+ #
90
+ # @return [Array<AbcDecimal>] An array of XYZ coordinates [X, Y, Z] representing the color in XYZ color space
91
+ def xyz_values
92
+ to_xyz.coordinates
93
+ end
94
+
95
+ # Returns an array representation of the color's coordinates in the XYZ color space.
96
+ #
97
+ # @return [Array<AbcDecimal>] An array containing the coordinates of the color
98
+ # in the XYZ color space in the order [X, Y, Z]
99
+ def xyz_array
100
+ to_xyz.coordinates
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::ColorMixins::Wcag - WCAG Accessibility Module
4
+ #
5
+ # This module provides methods for calculating color contrast ratios and checking
6
+ # WCAG 2.0/2.1 compliance for accessibility. It implements the relative luminance
7
+ # calculation with proper sRGB linearization according to WCAG specifications.
8
+ #
9
+ # Key features:
10
+ # - Calculate relative luminance using WCAG formula: Y = 0.2126R + 0.7152G + 0.0722B
11
+ # - Compute contrast ratio between two colors
12
+ # - Check WCAG 2.0 Level AA and AAA compliance for normal and large text
13
+ # - Check WCAG 2.1 non-text contrast compliance (3:1 minimum)
14
+ # - Predicate methods for easy accessibility checks
15
+ #
16
+ # References:
17
+ # - WCAG 2.0: https://www.w3.org/TR/WCAG20/
18
+ # - WCAG 2.1: https://www.w3.org/TR/WCAG21/
19
+ # - Relative luminance: https://www.w3.org/TR/WCAG20/#relativeluminancedef
20
+ # - Contrast ratio: https://www.w3.org/TR/WCAG20/#contrast-ratiodef
21
+
22
+ module Abachrome
23
+ module ColorMixins
24
+ module Wcag
25
+ # Calculates the relative luminance of the color according to WCAG specifications.
26
+ # Uses the formula: Y = 0.2126R + 0.7152G + 0.0722B
27
+ # where R, G, B are linearized sRGB values.
28
+ #
29
+ # The linearization follows the sRGB specification:
30
+ # - For values <= 0.03928: linear_value = value / 12.92
31
+ # - For values > 0.03928: linear_value = ((value + 0.055) / 1.055) ^ 2.4
32
+ #
33
+ # @return [AbcDecimal] The relative luminance value between 0.0 (darkest) and 1.0 (lightest)
34
+ def relative_luminance
35
+ rgb = to_srgb
36
+ r, g, b = rgb.coordinates
37
+
38
+ # Linearize sRGB values
39
+ r_linear = linearize_srgb_component(r)
40
+ g_linear = linearize_srgb_component(g)
41
+ b_linear = linearize_srgb_component(b)
42
+
43
+ # Apply WCAG luminance coefficients (Rec. 709)
44
+ AbcDecimal("0.2126") * r_linear +
45
+ AbcDecimal("0.7152") * g_linear +
46
+ AbcDecimal("0.0722") * b_linear
47
+ end
48
+
49
+ # Calculates the contrast ratio between this color and another color.
50
+ # The contrast ratio is calculated according to WCAG:
51
+ # (L1 + 0.05) / (L2 + 0.05)
52
+ # where L1 is the relative luminance of the lighter color and
53
+ # L2 is the relative luminance of the darker color.
54
+ #
55
+ # @param other [Abachrome::Color] The color to compare against
56
+ # @return [AbcDecimal] The contrast ratio, ranging from 1:1 (no contrast) to 21:1 (maximum contrast)
57
+ def contrast_ratio(other)
58
+ l1 = relative_luminance
59
+ l2 = other.relative_luminance
60
+
61
+ # Ensure L1 is the lighter color
62
+ l1, l2 = l2, l1 if l1 < l2
63
+
64
+ (l1 + AbcDecimal("0.05")) / (l2 + AbcDecimal("0.05"))
65
+ end
66
+
67
+ # Checks if the contrast ratio with another color meets WCAG 2.0 Level AA standards.
68
+ # AA requirements:
69
+ # - Normal text (< 18pt or < 14pt bold): minimum 4.5:1 contrast ratio
70
+ # - Large text (≥ 18pt or ≥ 14pt bold): minimum 3:1 contrast ratio
71
+ #
72
+ # @param other [Abachrome::Color] The color to compare against
73
+ # @param large_text [Boolean] Whether the text is considered large (default: false)
74
+ # @return [Boolean] true if the contrast meets AA standards
75
+ def meets_wcag_aa?(other, large_text: false)
76
+ ratio = contrast_ratio(other)
77
+ minimum = large_text ? AbcDecimal("3.0") : AbcDecimal("4.5")
78
+ ratio >= minimum
79
+ end
80
+
81
+ # Checks if the contrast ratio with another color meets WCAG 2.0 Level AAA standards.
82
+ # AAA requirements:
83
+ # - Normal text (< 18pt or < 14pt bold): minimum 7:1 contrast ratio
84
+ # - Large text (≥ 18pt or ≥ 14pt bold): minimum 4.5:1 contrast ratio
85
+ #
86
+ # @param other [Abachrome::Color] The color to compare against
87
+ # @param large_text [Boolean] Whether the text is considered large (default: false)
88
+ # @return [Boolean] true if the contrast meets AAA standards
89
+ def meets_wcag_aaa?(other, large_text: false)
90
+ ratio = contrast_ratio(other)
91
+ minimum = large_text ? AbcDecimal("4.5") : AbcDecimal("7.0")
92
+ ratio >= minimum
93
+ end
94
+
95
+ # Checks if the contrast ratio with another color meets WCAG 2.1 non-text contrast requirements.
96
+ # Non-text contrast applies to:
97
+ # - Graphical objects (icons, graphs, infographics)
98
+ # - User interface components (buttons, form inputs, focus indicators)
99
+ # Requirement: minimum 3:1 contrast ratio
100
+ #
101
+ # @param other [Abachrome::Color] The color to compare against
102
+ # @return [Boolean] true if the contrast meets the 3:1 minimum for non-text content
103
+ def meets_wcag_non_text?(other)
104
+ ratio = contrast_ratio(other)
105
+ ratio >= AbcDecimal("3.0")
106
+ end
107
+
108
+ private
109
+
110
+ # Linearizes a single sRGB component value according to the sRGB specification.
111
+ # This is required before applying the luminance coefficients.
112
+ #
113
+ # @param component [AbcDecimal] The sRGB component value (0.0 to 1.0)
114
+ # @return [AbcDecimal] The linearized component value
115
+ def linearize_srgb_component(component)
116
+ if component <= AbcDecimal("0.03928")
117
+ component / AbcDecimal("12.92")
118
+ else
119
+ ((component + AbcDecimal("0.055")) / AbcDecimal("1.055"))**AbcDecimal("2.4")
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abachrome::ColorModels::CMYK - CMYK color space model utilities
4
+ #
5
+ # This module provides utility methods for the CMYK (Cyan, Magenta, Yellow, Key/Black)
6
+ # color model within the Abachrome color manipulation library. CMYK represents colors
7
+ # using the subtractive color synthesis model used in printing and physical media.
8
+ #
9
+ # Key features:
10
+ # - Subtractive color model (ink on paper, not light)
11
+ # - Four channels: Cyan, Magenta, Yellow, and Key (Black)
12
+ # - Supports Undercolor Removal (UCR) for ink savings
13
+ # - Supports Gray Component Replacement (GCR) for richer blacks
14
+ # - High-precision BigDecimal arithmetic for exact ink percentages
15
+ # - Essential for print media, PDF generation, and pre-press workflows
16
+ #
17
+ # The CMYK model is fundamental for professional printing, where the fourth channel
18
+ # (Black/Key) is added to achieve crisp text and deep shadows that cannot be produced
19
+ # by combining cyan, magenta, and yellow inks alone.
20
+
21
+ require_relative "../abc_decimal"
22
+
23
+ module Abachrome
24
+ module ColorModels
25
+ class CMYK
26
+ include Abachrome::ToAbcd
27
+
28
+ class << self
29
+ # Normalizes CMYK color component values to the [0,1] range.
30
+ #
31
+ # @param c [Numeric] Cyan component (0-1 or 0-100 for percentages)
32
+ # @param m [Numeric] Magenta component (0-1 or 0-100 for percentages)
33
+ # @param y [Numeric] Yellow component (0-1 or 0-100 for percentages)
34
+ # @param k [Numeric] Key/Black component (0-1 or 0-100 for percentages)
35
+ # @return [Array<AbcDecimal>] Array of four normalized components as AbcDecimal objects
36
+ def normalize(c, m, y, k)
37
+ [c, m, y, k].map do |value|
38
+ case value
39
+ when String
40
+ if value.end_with?("%")
41
+ value_without_percent = value.chomp("%")
42
+ AbcDecimal(value_without_percent) / AbcDecimal(100)
43
+ else
44
+ AbcDecimal(value)
45
+ end
46
+ when Numeric
47
+ if value > 1
48
+ AbcDecimal(value) / AbcDecimal(100)
49
+ else
50
+ AbcDecimal(value)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Converts RGB values to CMYK using the standard naive conversion.
57
+ # This produces high ink coverage and is generally not recommended for production.
58
+ #
59
+ # @param r [Numeric] Red component (0-1)
60
+ # @param g [Numeric] Green component (0-1)
61
+ # @param b [Numeric] Blue component (0-1)
62
+ # @return [Array<AbcDecimal>] Array of [c, m, y, k] values
63
+ def from_rgb_naive(r, g, b)
64
+ r = AbcDecimal(r)
65
+ g = AbcDecimal(g)
66
+ b = AbcDecimal(b)
67
+
68
+ # Simple complementary conversion
69
+ c = AbcDecimal(1) - r
70
+ m = AbcDecimal(1) - g
71
+ y = AbcDecimal(1) - b
72
+ k = AbcDecimal(0)
73
+
74
+ [c, m, y, k]
75
+ end
76
+
77
+ # Converts RGB values to CMYK using Undercolor Removal (UCR).
78
+ # This extracts the gray component to the black (K) channel, reducing ink usage.
79
+ #
80
+ # @param r [Numeric] Red component (0-1)
81
+ # @param g [Numeric] Green component (0-1)
82
+ # @param b [Numeric] Blue component (0-1)
83
+ # @param gcr_amount [Numeric] Gray Component Replacement amount (0-1), default 1.0 for full UCR
84
+ # @return [Array<AbcDecimal>] Array of [c, m, y, k] values
85
+ def from_rgb_ucr(r, g, b, gcr_amount = 1.0)
86
+ r = AbcDecimal(r)
87
+ g = AbcDecimal(g)
88
+ b = AbcDecimal(b)
89
+ gcr = AbcDecimal(gcr_amount)
90
+
91
+ # Calculate complementary CMY values
92
+ c = AbcDecimal(1) - r
93
+ m = AbcDecimal(1) - g
94
+ y = AbcDecimal(1) - b
95
+
96
+ # Find the minimum (gray component)
97
+ k = [c, m, y].min * gcr
98
+
99
+ # Remove the gray component from CMY if k > 0
100
+ if k.positive?
101
+ c -= k
102
+ m -= k
103
+ y -= k
104
+ end
105
+
106
+ [c, m, y, k]
107
+ end
108
+
109
+ # Alias for from_rgb_ucr with full GCR (the standard approach).
110
+ # GCR is essentially the same as UCR but allows control over how much
111
+ # of the gray component to extract.
112
+ #
113
+ # @param r [Numeric] Red component (0-1)
114
+ # @param g [Numeric] Green component (0-1)
115
+ # @param b [Numeric] Blue component (0-1)
116
+ # @param amount [Numeric] Amount of GCR to apply (0-1), default 1.0
117
+ # @return [Array<AbcDecimal>] Array of [c, m, y, k] values
118
+ def from_rgb_gcr(r, g, b, amount = 1.0)
119
+ from_rgb_ucr(r, g, b, amount)
120
+ end
121
+
122
+ # Converts CMYK values back to RGB.
123
+ # This is a straightforward inverse of the naive RGB→CMY conversion
124
+ # plus the addition of the black channel.
125
+ #
126
+ # @param c [Numeric] Cyan component (0-1)
127
+ # @param m [Numeric] Magenta component (0-1)
128
+ # @param y [Numeric] Yellow component (0-1)
129
+ # @param k [Numeric] Key/Black component (0-1)
130
+ # @return [Array<AbcDecimal>] Array of [r, g, b] values
131
+ def to_rgb(c, m, y, k)
132
+ c = AbcDecimal(c)
133
+ m = AbcDecimal(m)
134
+ y = AbcDecimal(y)
135
+ k = AbcDecimal(k)
136
+
137
+ # Standard CMYK to RGB conversion
138
+ r = (AbcDecimal(1) - c) * (AbcDecimal(1) - k)
139
+ g = (AbcDecimal(1) - m) * (AbcDecimal(1) - k)
140
+ b = (AbcDecimal(1) - y) * (AbcDecimal(1) - k)
141
+
142
+ [r, g, b]
143
+ end
144
+
145
+ # Calculates the Total Area Coverage (TAC) for CMYK values.
146
+ # TAC is the sum of all four ink percentages and is critical in print workflows
147
+ # to prevent excessive ink that can cause smearing or paper damage.
148
+ #
149
+ # @param c [Numeric] Cyan component (0-1)
150
+ # @param m [Numeric] Magenta component (0-1)
151
+ # @param y [Numeric] Yellow component (0-1)
152
+ # @param k [Numeric] Key/Black component (0-1)
153
+ # @return [AbcDecimal] Total ink coverage as a decimal (0-4, often expressed as 0-400%)
154
+ def total_area_coverage(c, m, y, k)
155
+ AbcDecimal(c) + AbcDecimal(m) + AbcDecimal(y) + AbcDecimal(k)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorModels::HSV - HSV color space model definition
2
4
  #
3
5
  # This module defines the HSV (Hue, Saturation, Value) color model within the Abachrome
@@ -30,13 +32,13 @@ module Abachrome
30
32
 
31
33
  # Validates whether the coordinates are valid for the HSV color model.
32
34
  # Each component (hue, saturation, value) must be in the range [0, 1].
33
- #
35
+ #
34
36
  # @param coordinates [Array<Numeric>] An array of three values representing
35
37
  # hue (h), saturation (s), and value (v) in the range [0, 1]
36
38
  # @return [Boolean] true if all coordinates are within valid ranges, false otherwise
37
39
  def valid_coordinates?(coordinates)
38
40
  h, s, v = coordinates
39
- h >= 0 && h <= 1.0 &&
41
+ h.between?(0, 1.0) &&
40
42
  s >= 0 && s <= 1.0 &&
41
43
  v >= 0 && v <= 1.0
42
44
  end
@@ -44,4 +46,4 @@ module Abachrome
44
46
  end
45
47
  end
46
48
 
47
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
49
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorModels::Lms - LMS color space model definition
2
4
  #
3
5
  # This module defines the LMS color model within the Abachrome color manipulation library.
@@ -33,4 +35,4 @@ ColorSpace.register(
33
35
  []
34
36
  )
35
37
 
36
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
38
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorModels::Oklab - OKLAB color space model definition
2
4
  #
3
5
  # This module defines the OKLAB color model within the Abachrome color manipulation library.
@@ -32,4 +34,4 @@ ColorSpace.register(
32
34
  ["ok-lab"]
33
35
  )
34
36
 
35
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
37
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorModels::Oklch - OKLCH color space model definition
2
4
  #
3
5
  # This module defines the OKLCH color model within the Abachrome color manipulation library.
@@ -23,7 +25,7 @@ module Abachrome
23
25
  module ColorModels
24
26
  class Oklch
25
27
  # Normalizes OKLCH color values to their standard ranges.
26
- #
28
+ #
27
29
  # @param l [Numeric] The lightness component, will be clamped to range 0-1
28
30
  # @param c [Numeric] The chroma component, will be clamped to range 0-1
29
31
  # @param h [Numeric] The hue component in degrees, will be normalized to range 0-360
@@ -45,7 +47,7 @@ module Abachrome
45
47
  end
46
48
 
47
49
  # Converts OKLCH color coordinates to OKLab color coordinates.
48
- #
50
+ #
49
51
  # @param l [Numeric] The lightness value in the OKLCH color space
50
52
  # @param c [Numeric] The chroma value in the OKLCH color space
51
53
  # @param h [Numeric] The hue value in degrees in the OKLCH color space
@@ -59,7 +61,7 @@ module Abachrome
59
61
  end
60
62
 
61
63
  # Converts OKLab color coordinates to OKLCH color coordinates.
62
- #
64
+ #
63
65
  # @param l [Numeric] The lightness component from OKLab.
64
66
  # @param a [Numeric] The green-red component from OKLab.
65
67
  # @param b [Numeric] The blue-yellow component from OKLab.
@@ -86,4 +88,4 @@ ColorSpace.register(
86
88
  ["ok-lch"]
87
89
  )
88
90
 
89
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
91
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorModels::RGB - RGB color space model utilities
2
4
  #
3
5
  # This module provides utility methods for the RGB color model within the Abachrome
@@ -22,7 +24,7 @@ module Abachrome
22
24
  class RGB
23
25
  class << self
24
26
  # Normalizes RGB color component values to the [0,1] range.
25
- #
27
+ #
26
28
  # @param r [String, Numeric] Red component. If string with % suffix, interpreted as percentage;
27
29
  # if string without suffix or numeric > 1, interpreted as 0-255 range value;
28
30
  # if numeric ≤ 1, used directly.
@@ -53,4 +55,4 @@ module Abachrome
53
55
  end
54
56
  end
55
57
 
56
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
58
+ # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Abachrome::ColorModels::Xyz - XYZ color space model definition
2
4
  #
3
5
  # This module defines the XYZ color model within the Abachrome color manipulation library.
@@ -26,4 +28,12 @@ module Abachrome
26
28
  end
27
29
  end
28
30
 
29
- # Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
31
+ ColorSpace.register(
32
+ :xyz,
33
+ "XYZ",
34
+ %w[x y z],
35
+ nil,
36
+ []
37
+ )
38
+
39
+ # 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| AbcDecimal(value) }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end