color_converters 0.1.3 → 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,179 +1,184 @@
1
- require 'active_support/core_ext/object/blank'
2
- require 'bigdecimal'
3
- require 'bigdecimal/util'
4
-
5
- module ColorConverters
6
- class BaseConverter
7
- IMPORT_DP = 2
8
- OUTPUT_DP = 2
9
-
10
- attr_reader :original_value, :rgba
11
-
12
- # keep track of subclasses for factory
13
- class << self
14
- attr_reader :converters
15
- end
16
-
17
- @converters = []
18
-
19
- def self.inherited(subclass)
20
- BaseConverter.converters << subclass
21
- end
22
-
23
- def self.factory(color)
24
- converter = BaseConverter.converters.find { |klass| klass.matches?(color) }
25
- converter.new(color) if converter
26
- end
27
-
28
- def initialize(color_input, limit_override = false)
29
- @original_value = color_input
30
-
31
- # self.clamp_input(color_input) if limit_clamp == true
32
-
33
- if limit_override == false && !self.validate_input(color_input)
34
- raise InvalidColorError # validation method is defined in each convertor
35
- end
36
-
37
- r, g, b, a = self.input_to_rgba(color_input) # conversion method is defined in each convertor
38
-
39
- @rgba = { r: r.to_f.round(IMPORT_DP), g: g.to_f.round(IMPORT_DP), b: b.to_f.round(IMPORT_DP), a: a.to_f.round(IMPORT_DP) }
40
- end
41
-
42
- def rgb
43
- { r: @rgba[:r].to_f.round(OUTPUT_DP), g: @rgba[:g].to_f.round(OUTPUT_DP), b: @rgba[:b].to_f.round(OUTPUT_DP) }
44
- end
45
-
46
- def hex
47
- "##{'%02x' % @rgba[:r] + '%02x' % @rgba[:g] + '%02x' % @rgba[:b]}"
48
- end
49
-
50
- def hsl
51
- @r, @g, @b = self.rgb_array_frac
52
-
53
- { h: self.hue.to_f.round(OUTPUT_DP), s: self.hsl_saturation.to_f.round(OUTPUT_DP), l: self.hsl_lightness.to_f.round(OUTPUT_DP) }
54
- end
55
-
56
- def hsv
57
- @r, @g, @b = self.rgb_array
58
-
59
- { h: self.hue.to_f.round(OUTPUT_DP), s: self.hsv_saturation.to_f.round(OUTPUT_DP), v: self.hsv_value.to_f.round(OUTPUT_DP) }
60
- end
61
-
62
- def hsb
63
- hsb_hash = self.hsv
64
- hsb_hash[:b] = hsb_hash.delete(:v)
65
- hsb_hash
66
- end
67
-
68
- def cmyk
69
- c, m, y, k = CmykConverter.rgb_to_cmyk(self.rgb_array_frac)
70
-
71
- { c: c.to_f.round(OUTPUT_DP), m: m.to_f.round(OUTPUT_DP), y: y.to_f.round(OUTPUT_DP), k: k.to_f.round(OUTPUT_DP) }
72
- end
73
-
74
- def xyz
75
- x, y, z = XyzConverter.rgb_to_xyz(self.rgb_array_frac)
76
-
77
- { x: x.to_f.round(OUTPUT_DP), y: y.to_f.round(OUTPUT_DP), z: z.to_f.round(OUTPUT_DP) }
78
- end
79
-
80
- def cielab
81
- l, a, b = CielabConverter.xyz_to_cielab(XyzConverter.rgb_to_xyz(self.rgb_array_frac))
82
-
83
- { l: l.to_f.round(OUTPUT_DP), a: a.to_f.round(OUTPUT_DP), b: b.to_f.round(OUTPUT_DP) }
84
- end
85
-
86
- def cielch
87
- l, c, h = CielchConverter.cielab_to_cielch(CielabConverter.xyz_to_cielab(XyzConverter.rgb_to_xyz(self.rgb_array_frac)))
88
-
89
- { l: l.to_f.round(OUTPUT_DP), c: c.to_f.round(OUTPUT_DP), h: h.to_f.round(OUTPUT_DP) }
90
- end
91
-
92
- def oklab
93
- l, a, b = OklabConverter.xyz_to_oklab(XyzConverter.rgb_to_xyz(self.rgb_array_frac))
94
-
95
- { l: l.to_f.round(OUTPUT_DP), a: a.to_f.round(OUTPUT_DP), b: b.to_f.round(OUTPUT_DP) }
96
- end
97
-
98
- def oklch
99
- l, c, h = OklchConverter.oklab_to_oklch(OklabConverter.xyz_to_oklab(XyzConverter.rgb_to_xyz(self.rgb_array_frac)))
100
-
101
- { l: l.to_f.round(OUTPUT_DP), c: c.to_f.round(OUTPUT_DP), h: h.to_f.round(OUTPUT_DP) }
102
- end
103
-
104
- def alpha
105
- @rgba[:a]
106
- end
107
-
108
- def name
109
- NameConverter.rgb_to_name(self.rgb_array)
110
- end
111
-
112
- protected
113
-
114
- def rgb_array
115
- [@rgba[:r].to_f, @rgba[:g].to_f, @rgba[:b].to_f]
116
- end
117
-
118
- def rgb_array_frac
119
- [@rgba[:r] / 255.0, @rgba[:g] / 255.0, @rgba[:b] / 255.0]
120
- end
121
-
122
- def rgb_min
123
- [@r, @g, @b].min
124
- end
125
-
126
- def rgb_max
127
- [@r, @g, @b].max
128
- end
129
-
130
- def rgb_delta
131
- self.rgb_max - self.rgb_min
132
- end
133
-
134
- def hue
135
- h = 0
136
-
137
- case true
138
- when self.rgb_max == self.rgb_min
139
- h = 0
140
- when self.rgb_max == @r
141
- h = (@g - @b) / self.rgb_delta
142
- when self.rgb_max == @g
143
- h = 2 + (@b - @r) / self.rgb_delta
144
- when self.rgb_max == @b
145
- h = 4 + (@r - @g) / self.rgb_delta
146
- end
147
-
148
- h = [h * 60, 360].min
149
- h % 360
150
- end
151
-
152
- def hsl_saturation
153
- s = 0
154
-
155
- case true
156
- when self.rgb_max == self.rgb_min
157
- s = 0
158
- when (self.hsl_lightness / 100.0) <= 0.5
159
- s = self.rgb_delta / (self.rgb_max + self.rgb_min)
160
- else
161
- s = self.rgb_delta / (2.0 - self.rgb_max - self.rgb_min)
162
- end
163
-
164
- s * 100
165
- end
166
-
167
- def hsl_lightness
168
- (self.rgb_min + self.rgb_max) / 2.0 * 100
169
- end
170
-
171
- def hsv_saturation
172
- self.rgb_max.zero? ? 0 : ((self.rgb_delta / self.rgb_max * 1000) / 10.0)
173
- end
174
-
175
- def hsv_value
176
- ((self.rgb_max / 255.0) * 1000) / 10.0
177
- end
178
- end
179
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'bigdecimal'
5
+ require 'bigdecimal/util'
6
+
7
+ module ColorConverters
8
+ class BaseConverter
9
+ IMPORT_DP = 8
10
+ OUTPUT_DP = 2
11
+
12
+ attr_reader :original_value, :rgba
13
+
14
+ # keep track of subclasses for factory
15
+ class << self
16
+ attr_reader :converters
17
+ end
18
+
19
+ @converters = []
20
+
21
+ def self.inherited(subclass)
22
+ BaseConverter.converters << subclass
23
+ end
24
+
25
+ def self.factory(colour)
26
+ converter = BaseConverter.converters.find { |klass| klass.matches?(colour) }
27
+ converter&.new(colour)
28
+ end
29
+
30
+ def initialize(colour_input, limit_override = false)
31
+ @original_value = colour_input
32
+
33
+ # self.clamp_input(colour_input) if limit_clamp == true
34
+
35
+ validation_errors = self.validate_input(colour_input)
36
+ if limit_override == false && validation_errors.present?
37
+ raise InvalidColorError, "Invalid color input: #{validation_errors.join(', ')}" # validation method is defined in each convertor
38
+ end
39
+
40
+ r, g, b, a = self.input_to_rgba(colour_input) # conversion method is defined in each convertor
41
+
42
+ @rgba = { r: r.to_f.round(IMPORT_DP), g: g.to_f.round(IMPORT_DP), b: b.to_f.round(IMPORT_DP), a: a.to_f.round(IMPORT_DP) }
43
+ end
44
+
45
+ def rgb
46
+ { r: @rgba[:r].to_f.round(OUTPUT_DP), g: @rgba[:g].to_f.round(OUTPUT_DP), b: @rgba[:b].to_f.round(OUTPUT_DP) }
47
+ end
48
+
49
+ def hex
50
+ HexConverter.rgb_to_hex(self.rgb_array)
51
+ end
52
+
53
+ # not refactored to SubClass methods due to needing so many of the private methods
54
+ def hsl
55
+ @r, @g, @b = self.rgb_array_frac
56
+
57
+ { h: self.hue.to_f.round(OUTPUT_DP), s: self.hsl_saturation.to_f.round(OUTPUT_DP), l: self.hsl_lightness.to_f.round(OUTPUT_DP) }
58
+ end
59
+
60
+ # not refactored to SubClass methods due to needing so many of the private methods
61
+ def hsv
62
+ @r, @g, @b = self.rgb_array
63
+
64
+ { h: self.hue.to_f.round(OUTPUT_DP), s: self.hsv_saturation.to_f.round(OUTPUT_DP), v: self.hsv_value.to_f.round(OUTPUT_DP) }
65
+ end
66
+
67
+ def hsb
68
+ hsb_hash = self.hsv
69
+ hsb_hash[:b] = hsb_hash.delete(:v)
70
+ hsb_hash
71
+ end
72
+
73
+ def cmyk
74
+ c, m, y, k = CmykConverter.rgb_to_cmyk(self.rgb_array_frac)
75
+
76
+ { c: c.to_f.round(OUTPUT_DP), m: m.to_f.round(OUTPUT_DP), y: y.to_f.round(OUTPUT_DP), k: k.to_f.round(OUTPUT_DP) }
77
+ end
78
+
79
+ def xyz
80
+ x, y, z = XyzConverter.rgb_to_xyz(self.rgb_array_frac)
81
+
82
+ { x: x.to_f.round(OUTPUT_DP), y: y.to_f.round(OUTPUT_DP), z: z.to_f.round(OUTPUT_DP) }
83
+ end
84
+
85
+ def cielab
86
+ l, a, b = CielabConverter.xyz_to_cielab(XyzConverter.rgb_to_xyz(self.rgb_array_frac))
87
+
88
+ { l: l.to_f.round(OUTPUT_DP), a: a.to_f.round(OUTPUT_DP), b: b.to_f.round(OUTPUT_DP) }
89
+ end
90
+
91
+ def cielch
92
+ l, c, h = CielchConverter.cielab_to_cielch(CielabConverter.xyz_to_cielab(XyzConverter.rgb_to_xyz(self.rgb_array_frac)))
93
+
94
+ { l: l.to_f.round(OUTPUT_DP), c: c.to_f.round(OUTPUT_DP), h: h.to_f.round(OUTPUT_DP) }
95
+ end
96
+
97
+ def oklab
98
+ l, a, b = OklabConverter.xyz_to_oklab(XyzConverter.rgb_to_xyz(self.rgb_array_frac))
99
+
100
+ { l: l.to_f.round(OUTPUT_DP), a: a.to_f.round(OUTPUT_DP), b: b.to_f.round(OUTPUT_DP) }
101
+ end
102
+
103
+ def oklch
104
+ l, c, h = OklchConverter.oklab_to_oklch(OklabConverter.xyz_to_oklab(XyzConverter.rgb_to_xyz(self.rgb_array_frac)))
105
+
106
+ { l: l.to_f.round(OUTPUT_DP), c: c.to_f.round(OUTPUT_DP), h: h.to_f.round(OUTPUT_DP) }
107
+ end
108
+
109
+ def alpha
110
+ @rgba[:a]
111
+ end
112
+
113
+ def name(fuzzy: false)
114
+ NameConverter.rgb_to_name(self.rgb_array, fuzzy)
115
+ end
116
+
117
+ protected
118
+
119
+ def rgb_array
120
+ [@rgba[:r].to_f, @rgba[:g].to_f, @rgba[:b].to_f]
121
+ end
122
+
123
+ def rgb_array_frac
124
+ [@rgba[:r] / 255.0, @rgba[:g] / 255.0, @rgba[:b] / 255.0]
125
+ end
126
+
127
+ def rgb_min
128
+ [@r, @g, @b].min
129
+ end
130
+
131
+ def rgb_max
132
+ [@r, @g, @b].max
133
+ end
134
+
135
+ def rgb_delta
136
+ self.rgb_max - self.rgb_min
137
+ end
138
+
139
+ def hue
140
+ h = 0
141
+
142
+ case true
143
+ when self.rgb_max == self.rgb_min
144
+ h = 0
145
+ when self.rgb_max == @r
146
+ h = (@g - @b) / self.rgb_delta
147
+ when self.rgb_max == @g
148
+ h = 2 + (@b - @r) / self.rgb_delta
149
+ when self.rgb_max == @b
150
+ h = 4 + (@r - @g) / self.rgb_delta
151
+ end
152
+
153
+ h = [h * 60, 360].min
154
+ h % 360
155
+ end
156
+
157
+ def hsl_saturation
158
+ s = 0
159
+
160
+ case true
161
+ when self.rgb_max == self.rgb_min
162
+ s = 0
163
+ when (self.hsl_lightness / 100.0) <= 0.5
164
+ s = self.rgb_delta / (self.rgb_max + self.rgb_min)
165
+ else
166
+ s = self.rgb_delta / (2.0 - self.rgb_max - self.rgb_min)
167
+ end
168
+
169
+ s * 100
170
+ end
171
+
172
+ def hsl_lightness
173
+ (self.rgb_min + self.rgb_max) / 2.0 * 100
174
+ end
175
+
176
+ def hsv_saturation
177
+ self.rgb_max.zero? ? 0 : ((self.rgb_delta / self.rgb_max * 1000) / 10.0)
178
+ end
179
+
180
+ def hsv_value
181
+ ((self.rgb_max / 255.0) * 1000) / 10.0
182
+ end
183
+ end
184
+ end
@@ -1,16 +1,18 @@
1
- module ColorConverters
2
- class Color
3
- extend Forwardable
4
- def_delegators :@converter, :rgb, :hex, :hsl, :hsv, :hsb, :cmyk, :xyz, :cielab, :cielch, :oklab, :oklch, :name, :alpha
5
-
6
- def initialize(color)
7
- @converter = BaseConverter.factory(color)
8
- end
9
-
10
- def ==(other)
11
- return false unless other.is_a?(Color)
12
-
13
- rgb == other.rgb && alpha == other.alpha
14
- end
15
- end
16
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class Color
5
+ extend Forwardable
6
+ def_delegators :@converter, :rgb, :hex, :hsl, :hsv, :hsb, :cmyk, :xyz, :cielab, :cielch, :oklab, :oklch, :name, :alpha
7
+
8
+ def initialize(colour)
9
+ @converter = BaseConverter.factory(colour)
10
+ end
11
+
12
+ def ==(other)
13
+ return false unless other.is_a?(Color)
14
+
15
+ rgb == other.rgb && alpha == other.alpha
16
+ end
17
+ end
18
+ end
@@ -1,92 +1,95 @@
1
- module ColorConverters
2
- class CielabConverter < BaseConverter
3
- def self.matches?(color_input)
4
- return false unless color_input.is_a?(Hash)
5
-
6
- color_input.keys - [:l, :a, :b, :space] == [] && color_input[:space].to_s == 'cie'
7
- end
8
-
9
- def self.bounds
10
- { l: [0.0, 100.0], a: [-128.0, 127.0], b: [-128.0, 127.0] }
11
- end
12
-
13
- private
14
-
15
- def validate_input(color_input)
16
- bounds = CielabConverter.bounds
17
- color_input[:l].to_f.between?(*bounds[:l]) && color_input[:a].to_f.between?(*bounds[:a]) && color_input[:b].to_f.between?(*bounds[:b])
18
- end
19
-
20
- def input_to_rgba(color_input)
21
- x, y, z = CielabConverter.cielab_to_xyz(color_input)
22
- r, g, b = XyzConverter.xyz_to_rgb({ x: x, y: y, z: z })
23
-
24
- [r, g, b, 1.0]
25
- end
26
-
27
- def self.cielab_to_xyz(color_input)
28
- l = color_input[:l].to_d
29
- a = color_input[:a].to_d
30
- b = color_input[:b].to_d
31
-
32
- yy = (l + 16.0.to_d) / 116.0.to_d
33
- xx = (a / 500.0.to_d) + yy
34
- zz = yy - (b / 200.0.to_d)
35
-
36
- e = 216.0.to_d / 24_389.0.to_d
37
-
38
- x, y, z = [xx, yy, zz].map do
39
- if _1**3.to_d <= e
40
- (3.0.to_d * (6.0.to_d / 29.0.to_d) * (6.0.to_d / 29.0.to_d) * (_1 - (4.0.to_d / 29.0.to_d)))
41
- else
42
- _1**3.to_d
43
- end
44
- end
45
-
46
- x *= 95.047.to_d
47
- y *= 100.0.to_d
48
- z *= 108.883.to_d
49
-
50
- [x, y, z]
51
- end
52
-
53
- def self.xyz_to_cielab(xyz_array)
54
- x, y, z = xyz_array.map(&:to_d)
55
-
56
- # https://www.w3.org/TR/css-color-4/#color-conversion-code
57
- # # The D50 & D65 standard illuminant white point
58
- # wp_rel = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585]
59
- wp_rel = [0.3127.to_d / 0.3290.to_d, 1.0.to_d, (1.0.to_d - 0.3127.to_d - 0.3290.to_d) / 0.3290.to_d].map { _1 * 100.0.to_d }
60
-
61
- xr, yr, zr = wp_rel
62
-
63
- # # Calculate the ratio of the XYZ values to the reference white.
64
- # # http://www.brucelindbloom.com/index.html?Equations.html
65
- rel = [x / xr, y / yr, z / zr]
66
-
67
- e = 216.0.to_d / 24_389.0.to_d
68
- k = 841.0.to_d / 108.0.to_d
69
-
70
- # And now transform
71
- # http:#en.wikipedia.org/wiki/Lab_color_space#Forward_transformation
72
- # There is a brief explanation there as far as the nature of the calculations,
73
- # as well as a much nicer looking modeling of the algebra.
74
- xx, yy, zz = rel.map do
75
- if _1 > e
76
- _1**(1.0.to_d / 3.0.to_d)
77
- else
78
- (k * _1) + (4.0.to_d / 29.0.to_d)
79
- # The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 -
80
- # 16 = 0, which is the correct value for L* with black.
81
- # ((1.0/3)*((29.0/6)**2) * t) + (4.0/29)
82
- end
83
- end
84
-
85
- l = ((116.0.to_d * yy) - 16.0.to_d)
86
- a = (500.0.to_d * (xx - yy))
87
- b = (200.0.to_d * (yy - zz))
88
-
89
- [l, a, b]
90
- end
91
- end
92
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ColorConverters
4
+ class CielabConverter < 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 == 'cie'
9
+ end
10
+
11
+ def self.bounds
12
+ { l: [0.0, 100.0], a: [-128.0, 127.0], b: [-128.0, 127.0] }
13
+ end
14
+
15
+ private
16
+
17
+ def validate_input(colour_input)
18
+ CielabConverter.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 = CielabConverter.cielab_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.cielab_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
+ yy = (l + 16.0.to_d) / 116.0.to_d
36
+ xx = (a / 500.0.to_d) + yy
37
+ zz = yy - (b / 200.0.to_d)
38
+
39
+ e = 216.0.to_d / 24_389.0.to_d
40
+
41
+ x, y, z = [xx, yy, zz].map do
42
+ if _1**3.to_d <= e
43
+ (3.0.to_d * (6.0.to_d / 29.0.to_d) * (6.0.to_d / 29.0.to_d) * (_1 - (4.0.to_d / 29.0.to_d)))
44
+ else
45
+ _1**3.to_d
46
+ end
47
+ end
48
+
49
+ x *= 95.047.to_d
50
+ y *= 100.0.to_d
51
+ z *= 108.883.to_d
52
+
53
+ [x, y, z]
54
+ end
55
+
56
+ def self.xyz_to_cielab(xyz_array)
57
+ x, y, z = xyz_array.map(&:to_d)
58
+
59
+ # https://www.w3.org/TR/css-color-4/#color-conversion-code
60
+ # # The D50 & D65 standard illuminant white point
61
+ # wp_rel = [0.3457 / 0.3585, 1.0, (1.0 - 0.3457 - 0.3585) / 0.3585]
62
+ wp_rel = [0.3127.to_d / 0.3290.to_d, 1.0.to_d, (1.0.to_d - 0.3127.to_d - 0.3290.to_d) / 0.3290.to_d].map { _1 * 100.0.to_d }
63
+
64
+ xr, yr, zr = wp_rel
65
+
66
+ # # Calculate the ratio of the XYZ values to the reference white.
67
+ # # http://www.brucelindbloom.com/index.html?Equations.html
68
+ rel = [x / xr, y / yr, z / zr]
69
+
70
+ e = 216.0.to_d / 24_389.0.to_d
71
+ k = 841.0.to_d / 108.0.to_d
72
+
73
+ # And now transform
74
+ # http:#en.wikipedia.org/wiki/Lab_color_space#Forward_transformation
75
+ # There is a brief explanation there as far as the nature of the calculations,
76
+ # as well as a much nicer looking modeling of the algebra.
77
+ xx, yy, zz = rel.map do
78
+ if _1 > e
79
+ _1**(1.0.to_d / 3.0.to_d)
80
+ else
81
+ (k * _1) + (4.0.to_d / 29.0.to_d)
82
+ # The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 -
83
+ # 16 = 0, which is the correct value for L* with black.
84
+ # ((1.0/3)*((29.0/6)**2) * t) + (4.0/29)
85
+ end
86
+ end
87
+
88
+ l = ((116.0.to_d * yy) - 16.0.to_d)
89
+ a = (500.0.to_d * (xx - yy))
90
+ b = (200.0.to_d * (yy - zz))
91
+
92
+ [l, a, b]
93
+ end
94
+ end
95
+ end