color_converters 0.1.2 → 0.1.4

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.
@@ -1,183 +1,67 @@
1
- module ColorConverters
2
- class NameConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(String)
5
-
6
- self.color_names.include?(color_input.downcase.to_sym)
7
- end
8
-
9
- def self.rgb_to_name(rgb_array_frac)
10
- r, g, b = rgb_array_frac
11
-
12
- name = self.color_names.find { |_k, v| v == [r, g, b] }
13
- name.present? ? name[0].to_s : nil
14
- end
15
-
16
- private
17
-
18
- def validate_input(color_input) #
19
- # TODO: validate against list of keys?
20
- true
21
- end
22
-
23
- def input_to_rgba(color_input)
24
- found_colour = self.class.color_names[color_input.downcase.to_sym]
25
- raise InvalidColorError unless found_colour.present?
26
-
27
- r, g, b = found_colour
28
- { r: r, g: g, b: b, a: 1.0 }
29
- end
30
-
31
- def self.color_names
32
- {
33
- aliceblue: [240, 248, 255],
34
- antiquewhite: [250, 235, 215],
35
- aqua: [0, 255, 255],
36
- aquamarine: [127, 255, 212],
37
- azure: [240, 255, 255],
38
- beige: [245, 245, 220],
39
- bisque: [255, 228, 196],
40
- black: [0, 0, 0],
41
- blanchedalmond: [255, 235, 205],
42
- blue: [0, 0, 255],
43
- blueviolet: [138, 43, 226],
44
- brown: [165, 42, 42],
45
- burlywood: [222, 184, 135],
46
- cadetblue: [95, 158, 160],
47
- chartreuse: [127, 255, 0],
48
- chocolate: [210, 105, 30],
49
- coral: [255, 127, 80],
50
- cornflowerblue: [100, 149, 237],
51
- cornsilk: [255, 248, 220],
52
- crimson: [220, 20, 60],
53
- cyan: [0, 255, 255],
54
- darkblue: [0, 0, 139],
55
- darkcyan: [0, 139, 139],
56
- darkgoldenrod: [184, 134, 11],
57
- darkgray: [169, 169, 169],
58
- darkgreen: [0, 100, 0],
59
- darkgrey: [169, 169, 169],
60
- darkkhaki: [189, 183, 107],
61
- darkmagenta: [139, 0, 139],
62
- darkolivegreen: [85, 107, 47],
63
- darkorange: [255, 140, 0],
64
- darkorchid: [153, 50, 204],
65
- darkred: [139, 0, 0],
66
- darksalmon: [233, 150, 122],
67
- darkseagreen: [143, 188, 143],
68
- darkslateblue: [72, 61, 139],
69
- darkslategray: [47, 79, 79],
70
- darkslategrey: [47, 79, 79],
71
- darkturquoise: [0, 206, 209],
72
- darkviolet: [148, 0, 211],
73
- deeppink: [255, 20, 147],
74
- deepskyblue: [0, 191, 255],
75
- dimgray: [105, 105, 105],
76
- dimgrey: [105, 105, 105],
77
- dodgerblue: [30, 144, 255],
78
- firebrick: [178, 34, 34],
79
- floralwhite: [255, 250, 240],
80
- forestgreen: [34, 139, 34],
81
- fuchsia: [255, 0, 255],
82
- gainsboro: [220, 220, 220],
83
- ghostwhite: [248, 248, 255],
84
- gold: [255, 215, 0],
85
- goldenrod: [218, 165, 32],
86
- gray: [128, 128, 128],
87
- green: [0, 128, 0],
88
- greenyellow: [173, 255, 47],
89
- grey: [128, 128, 128],
90
- honeydew: [240, 255, 240],
91
- hotpink: [255, 105, 180],
92
- indianred: [205, 92, 92],
93
- indigo: [75, 0, 130],
94
- ivory: [255, 255, 240],
95
- khaki: [240, 230, 140],
96
- lavender: [230, 230, 250],
97
- lavenderblush: [255, 240, 245],
98
- lawngreen: [124, 252, 0],
99
- lemonchiffon: [255, 250, 205],
100
- lightblue: [173, 216, 230],
101
- lightcoral: [240, 128, 128],
102
- lightcyan: [224, 255, 255],
103
- lightgoldenrodyellow: [250, 250, 210],
104
- lightgray: [211, 211, 211],
105
- lightgreen: [144, 238, 144],
106
- lightgrey: [211, 211, 211],
107
- lightpink: [255, 182, 193],
108
- lightsalmon: [255, 160, 122],
109
- lightseagreen: [32, 178, 170],
110
- lightskyblue: [135, 206, 250],
111
- lightslategray: [119, 136, 153],
112
- lightslategrey: [119, 136, 153],
113
- lightsteelblue: [176, 196, 222],
114
- lightyellow: [255, 255, 224],
115
- lime: [0, 255, 0],
116
- limegreen: [50, 205, 50],
117
- linen: [250, 240, 230],
118
- magenta: [255, 0, 255],
119
- maroon: [128, 0, 0],
120
- mediumaquamarine: [102, 205, 170],
121
- mediumblue: [0, 0, 205],
122
- mediumorchid: [186, 85, 211],
123
- mediumpurple: [147, 112, 219],
124
- mediumseagreen: [60, 179, 113],
125
- mediumslateblue: [123, 104, 238],
126
- mediumspringgreen: [0, 250, 154],
127
- mediumturquoise: [72, 209, 204],
128
- mediumvioletred: [199, 21, 133],
129
- midnightblue: [25, 25, 112],
130
- mintcream: [245, 255, 250],
131
- mistyrose: [255, 228, 225],
132
- moccasin: [255, 228, 181],
133
- navajowhite: [255, 222, 173],
134
- navy: [0, 0, 128],
135
- oldlace: [253, 245, 230],
136
- olive: [128, 128, 0],
137
- olivedrab: [107, 142, 35],
138
- orange: [255, 165, 0],
139
- orangered: [255, 69, 0],
140
- orchid: [218, 112, 214],
141
- palegoldenrod: [238, 232, 170],
142
- palegreen: [152, 251, 152],
143
- paleturquoise: [175, 238, 238],
144
- palevioletred: [219, 112, 147],
145
- papayawhip: [255, 239, 213],
146
- peachpuff: [255, 218, 185],
147
- peru: [205, 133, 63],
148
- pink: [255, 192, 203],
149
- plum: [221, 160, 221],
150
- powderblue: [176, 224, 230],
151
- purple: [128, 0, 128],
152
- red: [255, 0, 0],
153
- rosybrown: [188, 143, 143],
154
- royalblue: [65, 105, 225],
155
- saddlebrown: [139, 69, 19],
156
- salmon: [250, 128, 114],
157
- sandybrown: [244, 164, 96],
158
- seagreen: [46, 139, 87],
159
- seashell: [255, 245, 238],
160
- sienna: [160, 82, 45],
161
- silver: [192, 192, 192],
162
- skyblue: [135, 206, 235],
163
- slateblue: [106, 90, 205],
164
- slategray: [112, 128, 144],
165
- slategrey: [112, 128, 144],
166
- snow: [255, 250, 250],
167
- springgreen: [0, 255, 127],
168
- steelblue: [70, 130, 180],
169
- tan: [210, 180, 140],
170
- teal: [0, 128, 128],
171
- thistle: [216, 191, 216],
172
- tomato: [255, 99, 71],
173
- turquoise: [64, 224, 208],
174
- violet: [238, 130, 238],
175
- wheat: [245, 222, 179],
176
- white: [255, 255, 255],
177
- whitesmoke: [245, 245, 245],
178
- yellow: [255, 255, 0],
179
- yellowgreen: [154, 205, 50]
180
- }
181
- end
182
- end
183
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'color_swatch_collection'
4
+
5
+ module ColorConverters
6
+ class NameConverter < BaseConverter
7
+ def self.matches?(colour_input)
8
+ return false unless colour_input.is_a?(String)
9
+
10
+ !colour_input.include?('#') && !colour_input.include?('rgb') && !colour_input.include?('hsl')
11
+ end
12
+
13
+ def self.rgb_to_name(rgb_array, fuzzy = false)
14
+ if fuzzy
15
+ source_colour = ColorConverters::Color.new({ r: rgb_array[0], g: rgb_array[1], b: rgb_array[2] })
16
+
17
+ collection_colours = []
18
+
19
+ ::ColorSwatchCollection.list_collections.each do |collection_name|
20
+ collection_colours += self.add_colour_distances_to_collection(Object.const_get("::ColorSwatchCollection::#{collection_name.capitalize}").colours, source_colour)
21
+ end
22
+
23
+ collection_colours.min_by { |swatch| swatch[:distance] }.dig(:name)
24
+ else
25
+ ::ColorSwatchCollection.get_from_hex(HexConverter.rgb_to_hex(rgb_array)).dig(:name)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def validate_input(colour_input)
32
+ self.class.match_name_from_palettes(colour_input).present? ? [] : ['name could not be found across colour collections']
33
+ end
34
+
35
+ def input_to_rgba(colour_input)
36
+ found_colour = self.class.match_name_from_palettes(colour_input)
37
+
38
+ raise InvalidColorError unless found_colour.present?
39
+
40
+ HexConverter.hex_to_rgba(found_colour)
41
+ end
42
+
43
+ # this is a checking for a direct naming match against the ColorSwatchCollection
44
+ def self.match_name_from_palettes(colour_name)
45
+ ::ColorSwatchCollection.get_from_name(colour_name).dig(:hex)
46
+ end
47
+
48
+ def self.add_colour_distances_to_collection(collection_colours, source_colour)
49
+ collection_colours.map do |swatch|
50
+ swatch[:distance] = self.distance_between_colours(ColorConverters::Color.new(swatch.dig(:hex)), source_colour)
51
+ end
52
+
53
+ collection_colours
54
+ end
55
+
56
+ def self.distance_between_colours(comparison_colour, source_colour)
57
+ # https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions
58
+ # https://www.baeldung.com/cs/compute-similarity-of-colours
59
+ # TODO: allow the type of matching to be set via config. Use HSL for now as it's far faster than CIELab
60
+ conversion_1 = comparison_colour.hsl
61
+ conversion_2 = source_colour.hsl
62
+
63
+ keys = conversion_1.keys
64
+ Math.sqrt((conversion_1[keys[0]] - conversion_2[keys[0]])**2 + (conversion_1[keys[1]] - conversion_2[keys[1]])**2 + (conversion_1[keys[2]] - conversion_2[keys[2]])**2)
65
+ end
66
+ end
67
+ end
@@ -1,17 +1,19 @@
1
- module ColorConverters
2
- class NullConverter < BaseConverter
3
- def self.matches?(_color_input)
4
- true
5
- end
6
-
7
- private
8
-
9
- def validate_input(_color_input)
10
- false
11
- end
12
-
13
- def input_to_rgba(_color_input)
14
- raise InvalidColorError
15
- end
16
- end
17
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class NullConverter < BaseConverter
5
+ def self.matches?(_colour_input)
6
+ true
7
+ end
8
+
9
+ private
10
+
11
+ def validate_input(_colour_input)
12
+ ['did not recognise colour input']
13
+ end
14
+
15
+ def input_to_rgba(_colour_input)
16
+ raise InvalidColorError
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class OklabConverter < BaseConverter
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
7
+
8
+ colour_input.keys - [:l, :a, :b, :space] == [] && colour_input[:space].to_s == 'ok'
9
+ end
10
+
11
+ def self.bounds
12
+ { l: [0.0, 100.0], a: [-0.5, 0.5], b: [-0.5, 0.5] }
13
+ end
14
+
15
+ private
16
+
17
+ def validate_input(colour_input)
18
+ OklabConverter.bounds.collect do |key, range|
19
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
20
+ end.compact
21
+ end
22
+
23
+ def input_to_rgba(colour_input)
24
+ x, y, z = OklabConverter.oklab_to_xyz(colour_input)
25
+ r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
26
+
27
+ [r, g, b, 1.0]
28
+ end
29
+
30
+ def self.oklab_to_xyz(colour_input)
31
+ l = colour_input[:l].to_d
32
+ a = colour_input[:a].to_d
33
+ b = colour_input[:b].to_d
34
+
35
+ # Now, scale l to a decimal
36
+ l /= 100.0.to_d
37
+
38
+ # Convert Oklab (L*a*b*) to LMS'
39
+ lab_to_lms_matrix = ::Matrix[
40
+ [1.0000000000000000.to_d, 0.3963377773761749.to_d, 0.2158037573099136.to_d],
41
+ [1.0000000000000000.to_d, -0.1055613458156586.to_d, -0.0638541728258133.to_d],
42
+ [1.0000000000000000.to_d, -0.0894841775298119.to_d, -1.2914855480194092.to_d]
43
+ ]
44
+
45
+ l_lms, m_lms, s_lms = (lab_to_lms_matrix * ::Matrix.column_vector([l, a, b])).to_a.flatten
46
+
47
+ l_lms **= 3.0.to_d
48
+ m_lms **= 3.0.to_d
49
+ s_lms **= 3.0.to_d
50
+
51
+ lms_to_xyz_matrix = ::Matrix[
52
+ [1.2268798758459243.to_d, -0.5578149944602171.to_d, 0.2813910456659647.to_d],
53
+ [-0.0405757452148008.to_d, 1.1122868032803170.to_d, -0.0717110580655164.to_d],
54
+ [-0.0763729366746601.to_d, -0.4214933324022432.to_d, 1.5869240198367816.to_d]
55
+ ]
56
+
57
+ x, y, z = (lms_to_xyz_matrix * ::Matrix.column_vector([l_lms, m_lms, s_lms])).to_a.flatten
58
+
59
+ x *= 100.0.to_d
60
+ y *= 100.0.to_d
61
+ z *= 100.0.to_d
62
+
63
+ [x, y, z]
64
+ end
65
+
66
+ def self.xyz_to_oklab(xyz_array)
67
+ x, y, z = xyz_array.map(&:to_d)
68
+
69
+ # The transformation matrix expects normalised X, Y, Z values.
70
+ x /= 100.0.to_d
71
+ y /= 100.0.to_d
72
+ z /= 100.0.to_d
73
+
74
+ # Given XYZ relative to D65, convert to OKLab
75
+ xyz_to_lms_matrix = ::Matrix[
76
+ [0.8190224379967030.to_d, 0.3619062600528904.to_d, -0.1288737815209879.to_d],
77
+ [0.0329836539323885.to_d, 0.9292868615863434.to_d, 0.0361446663506424.to_d],
78
+ [0.0481771893596242.to_d, 0.2642395317527308.to_d, 0.6335478284694309.to_d]
79
+ ]
80
+
81
+ l_lms, m_lms, s_lms = (xyz_to_lms_matrix * ::Matrix.column_vector([x, y, z])).to_a.flatten
82
+
83
+ cube_root = (1.0.to_d / 3.0.to_d)
84
+ l_lms **= cube_root
85
+ m_lms **= cube_root
86
+ s_lms **= cube_root
87
+
88
+ lms_to_lab_matrix = ::Matrix[
89
+ [0.2104542683093140.to_d, 0.7936177747023054.to_d, -0.0040720430116193.to_d],
90
+ [1.9779985324311684.to_d, -2.4285922420485799.to_d, 0.4505937096174110.to_d],
91
+ [0.0259040424655478.to_d, 0.7827717124575296.to_d, -0.8086757549230774.to_d]
92
+ ]
93
+
94
+ l_lab, a_lab, b_lab = (lms_to_lab_matrix * ::Matrix.column_vector([l_lms, m_lms, s_lms])).to_a.flatten
95
+
96
+ # Now, scale l to a percentage
97
+ l_lab *= 100.0.to_d
98
+
99
+ [l_lab, a_lab, b_lab]
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class OklchConverter < BaseConverter
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
7
+
8
+ colour_input.keys - [:l, :c, :h, :space] == [] && colour_input[:space].to_s == 'ok'
9
+ end
10
+
11
+ def self.bounds
12
+ { l: [0.0, 100.0], c: [0.0, 500.0], h: [0.0, 360.0] }
13
+ end
14
+
15
+ private
16
+
17
+ def validate_input(colour_input)
18
+ OklchConverter.bounds.collect do |key, range|
19
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
20
+ end.compact
21
+ end
22
+
23
+ def input_to_rgba(colour_input)
24
+ l, a, b = OklchConverter.oklch_to_oklab(colour_input)
25
+ x, y, z = OklabConverter.oklab_to_xyz({ l: l, a: a, b: b })
26
+ r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
27
+
28
+ [r, g, b, 1.0]
29
+ end
30
+
31
+ def self.oklch_to_oklab(colour_input)
32
+ l = colour_input[:l].to_d
33
+ c = colour_input[:c].to_d
34
+ h = colour_input[:h].to_d
35
+
36
+ h_rad = h * (Math::PI.to_d / 180.0.to_d)
37
+
38
+ a = c * Math.cos(h_rad).to_d
39
+ b = c * Math.sin(h_rad).to_d
40
+
41
+ [l, a, b]
42
+ end
43
+
44
+ def self.oklab_to_oklch(lab_array)
45
+ l, aa, bb = lab_array.map(&:to_d)
46
+
47
+ e = 0.000015.to_d; # if chroma is smaller than this, set hue to 0 similar to CIELch
48
+
49
+ c = ((aa**2.to_d) + (bb**2.to_d))**0.5.to_d
50
+
51
+ h_rad = Math.atan2(bb, aa).to_d
52
+ h = h_rad * (180.0.to_d / Math::PI.to_d)
53
+
54
+ h %= 360
55
+
56
+ h = 0 if c < e
57
+
58
+ [l, c, h]
59
+ end
60
+ end
61
+ end
@@ -1,29 +1,109 @@
1
- module ColorConverters
2
- class RgbConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(Hash)
5
-
6
- color_input.keys - [:r, :g, :b] == [] || color_input.keys - [:r, :g, :b, :a] == []
7
- end
8
-
9
- def self.bounds
10
- { r: [0.0, 255.0], g: [0.0, 255.0], b: [0.0, 255.0] }
11
- end
12
-
13
- private
14
-
15
- def validate_input(color_input)
16
- bounds = RgbConverter.bounds
17
- color_input[:r].to_f.between?(*bounds[:r]) && color_input[:g].to_f.between?(*bounds[:g]) && color_input[:b].to_f.between?(*bounds[:b])
18
- end
19
-
20
- def input_to_rgba(color_input)
21
- r = color_input[:r].to_f
22
- g = color_input[:g].to_f
23
- b = color_input[:b].to_f
24
- a = (color_input[:a] || 1.0).to_f
25
-
26
- { r: r.round(IMPORT_DP), g: g.round(IMPORT_DP), b: b.round(IMPORT_DP), a: a.round(IMPORT_DP) }
27
- end
28
- end
29
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class RgbConverter < BaseConverter
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(Hash)
7
+
8
+ colour_input.keys - [:r, :g, :b] == [] || colour_input.keys - [:r, :g, :b, :a] == []
9
+ end
10
+
11
+ def self.bounds
12
+ { r: [0.0, 255.0], g: [0.0, 255.0], b: [0.0, 255.0], a: [0.0, 1.0] }
13
+ end
14
+
15
+ private
16
+
17
+ def validate_input(colour_input)
18
+ RgbConverter.bounds.collect do |key, range|
19
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
20
+ end.compact
21
+ end
22
+
23
+ def input_to_rgba(colour_input)
24
+ r = colour_input[:r].to_f
25
+ g = colour_input[:g].to_f
26
+ b = colour_input[:b].to_f
27
+ a = (colour_input[:a] || 1.0).to_f
28
+
29
+ [r, g, b, a]
30
+ end
31
+
32
+ def self.rgb_to_lrgb(rgb_array_frac)
33
+ # [0, 1]
34
+ r, g, b = rgb_array_frac
35
+
36
+ # Inverse sRGB companding. Linearizes RGB channels with respect to energy.
37
+ # Assumption that r, g, b are always positive
38
+ rr, gg, bb = [r, g, b].map do
39
+ if _1.to_d <= 0.04045.to_d
40
+ _1.to_d / 12.92.to_d
41
+ else
42
+ # sRGB Inverse Companding (Non-linear to Linear RGB)
43
+ # The sRGB specification (IEC 61966-2-1) defines the exponent as 2.4.
44
+ #
45
+ (((_1.to_d + 0.055.to_d) / 1.055.to_d)**2.4.to_d)
46
+
47
+ # IMPORTANT NUMERICAL NOTE:
48
+ # On this specific system (and confirmed by Wolfram Alpha for direct calculation),
49
+ # the power function for val**2.4 yields a result that deviates from the value expected by widely-used colour science libraries (like Bruce Lindbloom's).
50
+ #
51
+ # To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
52
+ # an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
53
+ #
54
+ # Choose 2.4 for strict adherence to the standard's definition (knowing your results may slightly deviate from common calculators),
55
+ # or choose 2.5 to ensure your calculated linear RGB values (and thus CIELAB) match authoritative external tools on this system.
56
+ #
57
+ # ((_1 + 0.055) / 1.055)**2.5
58
+ end
59
+ end
60
+
61
+ # [0, 1]
62
+ [rr, gg, bb]
63
+ end
64
+
65
+ def self.lrgb_to_rgb(lrgb_array)
66
+ rr, gg, bb = lrgb_array
67
+
68
+ # Apply sRGB Companding (gamma correction) to convert from Linear RGB to non-linear sRGB.
69
+ # This is defined by the sRGB specification (IEC 61966-2-1).
70
+ # The exponent for the non-linear segment is 1/2.4 (approximately 0.41666...).
71
+ # Assumption that rr, gg, bb are always positive
72
+ r, g, b = [rr, gg, bb].map do
73
+ if _1.to_d <= 0.0031308.to_d
74
+ # Linear portion of the sRGB curve
75
+ _1.to_d * 12.92.to_d
76
+ else
77
+ # Non-linear (gamma-corrected) portion of the sRGB curve
78
+ # The sRGB specification uses an exponent of 1/2.4.
79
+ #
80
+ (1.055.to_d * (_1.to_d**(1.0.to_d / 2.4.to_d))) - 0.055.to_d
81
+
82
+ # IMPORTANT NUMERICAL NOTE:
83
+ # On this specific system (and confirmed by Wolfram Alpha for direct calculation),
84
+ # the inverse power function for val**2.4 yields a result that deviates from the value expected by widely-used colour science libraries (like Bruce Lindbloom's).
85
+ #
86
+ # To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
87
+ # an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
88
+ #
89
+ # Choose 1/2.4 for strict adherence to the standard's definition (knowing your results may slightly deviate from common calculators),
90
+ # or choose 1/2.5 to ensure your calculated linear RGB values (and thus CIELAB) match authoritative external tools on this system.
91
+ #
92
+ # (1.055 * (_1**(1.0 / 2.5))) - 0.055
93
+ end
94
+ end
95
+
96
+ # Scale the 0-1 sRGB value to the 0-255 range for 8-bit colour components.
97
+ r *= 255.0.to_d
98
+ g *= 255.0.to_d
99
+ b *= 255.0.to_d
100
+
101
+ # Clamping RGB values to prevent out-of-gamut issues and numerical errors and ensures these values stay within the valid and expected range.
102
+ r = r.clamp(0.0..255.0)
103
+ g = g.clamp(0.0..255.0)
104
+ b = b.clamp(0.0..255.0)
105
+
106
+ [r, g, b]
107
+ end
108
+ end
109
+ end
@@ -1,32 +1,49 @@
1
- module ColorConverters
2
- class RgbStringConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(String)
5
-
6
- color_input.include?('rgb(') || color_input.include?('rgba(')
7
- end
8
-
9
- private
10
-
11
- def validate_input(color_input)
12
- true
13
- end
14
-
15
- def input_to_rgba(color_input)
16
- matches = color_input.match(/rgba?\(([0-9.,\s]+)\)/)
17
- raise InvalidColorError unless matches
18
-
19
- r, g, b, a = matches[1].split(',').map(&:strip)
20
- raise InvalidColorError unless r.present? && g.present? && b.present?
21
-
22
- a ||= 1.0
23
-
24
- r = r.to_f.round(IMPORT_DP)
25
- g = g.to_f.round(IMPORT_DP)
26
- b = b.to_f.round(IMPORT_DP)
27
- a = a.to_f.round(IMPORT_DP)
28
-
29
- { r: r, g: g, b: b, a: a }
30
- end
31
- end
32
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class RgbStringConverter < BaseConverter
5
+ def self.matches?(colour_input)
6
+ return false unless colour_input.is_a?(String)
7
+
8
+ colour_input.include?('rgb(') || colour_input.include?('rgba(')
9
+ end
10
+
11
+ def self.bounds
12
+ RgbConverter.bounds
13
+ end
14
+
15
+ private
16
+
17
+ def validate_input(colour_input)
18
+ keys = colour_input.include?('rgba(') ? [:r, :g, :b, :a] : [:r, :g, :b]
19
+ colour_input = RgbStringConverter.sanitize_input(colour_input)
20
+
21
+ errors = keys.collect do |key|
22
+ "#{key} must be present" if colour_input[key].blank?
23
+ end.compact
24
+
25
+ return errors if errors.present?
26
+
27
+ RgbStringConverter.bounds.collect do |key, range|
28
+ "#{key} must be between #{range[0]} and #{range[1]}" unless colour_input[key].to_f.between?(*range)
29
+ end.compact
30
+ end
31
+
32
+ def input_to_rgba(colour_input)
33
+ colour_input = RgbStringConverter.sanitize_input(colour_input)
34
+
35
+ r = colour_input[:r].to_f
36
+ g = colour_input[:g].to_f
37
+ b = colour_input[:b].to_f
38
+ a = (colour_input[:a] || 1.0).to_f
39
+
40
+ [r, g, b, a]
41
+ end
42
+
43
+ def self.sanitize_input(colour_input)
44
+ matches = colour_input.match(/rgba?\(([0-9.,%\s]+)\)/) || []
45
+ r, g, b, a = matches[1]&.split(',')&.map(&:strip)
46
+ { r: r, g: g, b: b, a: a }
47
+ end
48
+ end
49
+ end