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