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.
- checksums.yaml +4 -4
- data/README.md +190 -176
- data/lib/color_converters/base_converter.rb +184 -179
- data/lib/color_converters/color.rb +18 -16
- data/lib/color_converters/converters/cielab_converter.rb +95 -92
- data/lib/color_converters/converters/cielch_converter.rb +61 -58
- data/lib/color_converters/converters/cmyk_converter.rb +59 -56
- data/lib/color_converters/converters/hex_converter.rb +45 -32
- data/lib/color_converters/converters/hsl_converter.rb +68 -65
- data/lib/color_converters/converters/hsl_string_converter.rb +46 -27
- data/lib/color_converters/converters/hsv_converter.rb +60 -58
- data/lib/color_converters/converters/name_converter.rb +67 -185
- data/lib/color_converters/converters/null_converter.rb +19 -17
- data/lib/color_converters/converters/oklab_converter.rb +102 -98
- data/lib/color_converters/converters/oklch_converter.rb +61 -58
- data/lib/color_converters/converters/rgb_converter.rb +109 -106
- data/lib/color_converters/converters/rgb_string_converter.rb +49 -32
- data/lib/color_converters/converters/xyz_converter.rb +110 -107
- data/lib/color_converters/version.rb +1 -1
- data/lib/color_converters.rb +31 -30
- metadata +18 -4
@@ -1,179 +1,184 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require '
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
if
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|