color_converters 0.1.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.
- checksums.yaml +7 -0
- data/README.md +152 -0
- data/Rakefile +8 -0
- data/lib/color_converter/base_converter.rb +161 -0
- data/lib/color_converter/color.rb +16 -0
- data/lib/color_converter/color_converters/cielab_converter.rb +88 -0
- data/lib/color_converter/color_converters/cmyk_converter.rb +56 -0
- data/lib/color_converter/color_converters/hex_converter.rb +32 -0
- data/lib/color_converter/color_converters/hsl_converter.rb +64 -0
- data/lib/color_converter/color_converters/hsl_string_converter.rb +26 -0
- data/lib/color_converter/color_converters/hsv_converter.rb +58 -0
- data/lib/color_converter/color_converters/name_converter.rb +183 -0
- data/lib/color_converter/color_converters/null_converter.rb +17 -0
- data/lib/color_converter/color_converters/oklch_converter.rb +52 -0
- data/lib/color_converter/color_converters/rgb_converter.rb +29 -0
- data/lib/color_converter/color_converters/rgb_string_converter.rb +33 -0
- data/lib/color_converter/color_converters/xyz_converter.rb +127 -0
- data/lib/color_converter/version.rb +5 -0
- data/lib/color_converter.rb +28 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 41c9c2e35a56c7fcbc75a4b96a923cef9c106c260b2eb567caa958404042466a
|
4
|
+
data.tar.gz: f8ee279b96b83cb0938acc291a58752e495c2a6295227f9d3ad5f88ea33ad280
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f9db0a0eec4f2380b45a21be819e5c698dbdcf73e5fbf064e2f97f90a0debd46e62e6e5fc63a07ea64b0ba1c84e2150dc6475d029a577747376f27d3acbe9fd1
|
7
|
+
data.tar.gz: e1a9aec3fee9cb4a467f6d16b42482088c5af3390dead1574015d1cea5e4bdeff69d314f73c9a60566b0ef6d0d087b696235783029888a5da993a351839761e6
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Color Converters
|
2
|
+
|
3
|
+
> Give me a color and I'll convert it.
|
4
|
+
|
5
|
+
Color Converters is an ruby gem package for use in ruby or other projects that provides conversions for colors to other color spaces.
|
6
|
+
Given a color in [Hexadecimal, RGA(A), HSL(A), HSV, HSB, CMYK, XYZ, CIELAB, or OKLCH format](https://github.com/devrieda/color_conversion), it can convert the color to those other spaces.
|
7
|
+
|
8
|
+
> Lab and LCH color spaces are special in that the perceived difference between two colors is proportional to their Euclidean distance in color space. This special property, called perceptual uniformity, makes them ideal for accurate visual encoding of data. In contrast, the more familiar RGB and HSL color spaces distort data when used for visualization.
|
9
|
+
|
10
|
+
## Converters
|
11
|
+
|
12
|
+
Colors can be converted between the following spaces:
|
13
|
+
|
14
|
+
- hex
|
15
|
+
- rgb(a)
|
16
|
+
- hsl(a)
|
17
|
+
- hsv/hsb
|
18
|
+
- cmyk
|
19
|
+
- xyz
|
20
|
+
- cielab
|
21
|
+
- oklch
|
22
|
+
- name
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Install the gem and add to the application's Gemfile by executing:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
bundle add color_converters
|
30
|
+
```
|
31
|
+
|
32
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
gem install color_converters
|
36
|
+
```
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Initialize a color:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# from hex
|
44
|
+
color = Color.new("#3366cc")
|
45
|
+
color = Color.new("#36c")
|
46
|
+
|
47
|
+
# from rgb(a)
|
48
|
+
color = Color.new(r: 51, g: 102, b: 204)
|
49
|
+
color = Color.new(r: 51, g: 102, b: 204, a: 0.5)
|
50
|
+
|
51
|
+
# from hsl(a)
|
52
|
+
color = Color.new(h: 225, s: 73, l: 57)
|
53
|
+
color = Color.new(h: 225, s: 73, l: 57, a: 0.5)
|
54
|
+
|
55
|
+
# from hsv/hsb
|
56
|
+
color = Color.new(h: 220, s: 75, v: 80)
|
57
|
+
color = Color.new(h: 220, s: 75, b: 80)
|
58
|
+
|
59
|
+
# from cmyk
|
60
|
+
color = Color.new(c: 74, m: 58, y: 22, k: 3)
|
61
|
+
|
62
|
+
# from xyz
|
63
|
+
color = Color.new(x: 16, y: 44, z: 32)
|
64
|
+
|
65
|
+
# from cielab
|
66
|
+
color = Color.new(l: 16, a: 44, b: 32)
|
67
|
+
|
68
|
+
# from oklch
|
69
|
+
color = Color.new(l: 16, c: 44, h: 32)
|
70
|
+
|
71
|
+
# from textual color
|
72
|
+
color = Color.new("blue")
|
73
|
+
|
74
|
+
# from a css rgb(a) string
|
75
|
+
color = Color.new("rgb(51, 102, 204)")
|
76
|
+
color = Color.new("rgba(51, 102, 204, 0.5)")
|
77
|
+
|
78
|
+
# from a css hsl(a) string
|
79
|
+
color = Color.new("hsl(225, 73%, 57%)")
|
80
|
+
color = Color.new("hsl(225, 73%, 57%, 0.5)")
|
81
|
+
```
|
82
|
+
|
83
|
+
Converters
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
color = Color.new(r: 70, g: 130, b: 180, a: 0.5)
|
87
|
+
|
88
|
+
color.alpha
|
89
|
+
=> 0.5
|
90
|
+
|
91
|
+
color.rgb
|
92
|
+
=> {:r=>70, :g=>130, :b=>180}
|
93
|
+
|
94
|
+
color.hsl
|
95
|
+
=> {:h=>207, :s=>44, :l=>49}
|
96
|
+
|
97
|
+
color.hsv
|
98
|
+
=> {:h=>207, :s=>61, :v=>71}
|
99
|
+
|
100
|
+
color.hsb
|
101
|
+
=> {:h=>207, :s=>61, :b=>71}
|
102
|
+
|
103
|
+
color.cmyk
|
104
|
+
=> {:c=>61, :m=>28, :y=>0, :k=>29}
|
105
|
+
|
106
|
+
color.xyz
|
107
|
+
=> {:x=>33, :y=>21, :z=>54}
|
108
|
+
|
109
|
+
color.cielab
|
110
|
+
=> {:l=>52.47, :a=>-4.08, :b=>-32.19}
|
111
|
+
|
112
|
+
color.oklch
|
113
|
+
=> {:l=>52.47, :c=>32.45, :h=>262.78}
|
114
|
+
|
115
|
+
color.hex
|
116
|
+
=> "#4682b4"
|
117
|
+
|
118
|
+
color.name
|
119
|
+
=> "steelblue"
|
120
|
+
```
|
121
|
+
|
122
|
+
## Options
|
123
|
+
|
124
|
+
### limit_override
|
125
|
+
|
126
|
+
By default all values are checked to be within the expected number ranges, i.e.; rgb between 0-255 each.
|
127
|
+
This parameter allows you to ignore those ranges and submit any values you want.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Color.new(r: 270, g: 1300, b: 380, a: 0.5, limit_override: true)
|
131
|
+
```
|
132
|
+
|
133
|
+
## Development
|
134
|
+
|
135
|
+
[Converting Colors](https://convertingcolors.com/) can be usef to help verify results. Different calculators use different exponents and standards so there can be discrepency across them (like this calculator for LCH).
|
136
|
+
|
137
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
138
|
+
|
139
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/louiswdavis/color_converters>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/louiswdavis/color_converters/blob/master/CODE_OF_CONDUCT.md).
|
144
|
+
|
145
|
+
## License
|
146
|
+
|
147
|
+
Forked from original gem by Derek DeVries.
|
148
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
149
|
+
|
150
|
+
## Code of Conduct
|
151
|
+
|
152
|
+
Everyone interacting in the ColorConverters project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/louiswdavis/color_converters/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
|
3
|
+
module ColorConverter
|
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
|
+
if limit_override == false && !self.validate_input(color_input)
|
30
|
+
raise InvalidColorError # validation method is defined in each convertor
|
31
|
+
end
|
32
|
+
|
33
|
+
@rgba = self.input_to_rgba(color_input) # conversion method is defined in each convertor
|
34
|
+
end
|
35
|
+
|
36
|
+
def rgb
|
37
|
+
{ r: @rgba[:r].round(OUTPUT_DP), g: @rgba[:g].round(OUTPUT_DP), b: @rgba[:b].round(OUTPUT_DP) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def hex
|
41
|
+
"##{'%02x' % @rgba[:r] + '%02x' % @rgba[:g] + '%02x' % @rgba[:b]}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def hsl
|
45
|
+
@r, @g, @b = self.rgb_array_frac
|
46
|
+
|
47
|
+
{ h: self.hue.round(OUTPUT_DP), s: self.hsl_saturation.round(OUTPUT_DP), l: self.hsl_lightness.round(OUTPUT_DP) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def hsv
|
51
|
+
@r, @g, @b = self.rgb_array
|
52
|
+
|
53
|
+
{ h: self.hue.round(OUTPUT_DP), s: self.hsv_saturation.round(OUTPUT_DP), v: self.hsv_value.round(OUTPUT_DP) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def hsb
|
57
|
+
hsb_hash = self.hsv
|
58
|
+
hsb_hash[:b] = hsb_hash.delete(:v)
|
59
|
+
hsb_hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def cmyk
|
63
|
+
c, m, y, k = CmykConverter.rgb_to_cmyk(self.rgb_array_frac)
|
64
|
+
|
65
|
+
{ c: c.round(OUTPUT_DP), m: m.round(OUTPUT_DP), y: y.round(OUTPUT_DP), k: k.round(OUTPUT_DP) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def xyz
|
69
|
+
x, y, z = XyzConverter.rgb_to_xyz(self.rgb_array_frac)
|
70
|
+
|
71
|
+
{ x: x.round(OUTPUT_DP), y: y.round(OUTPUT_DP), z: z.round(OUTPUT_DP) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def cielab
|
75
|
+
l, a, b = CielabConverter.xyz_to_lab(XyzConverter.rgb_to_xyz(self.rgb_array_frac))
|
76
|
+
|
77
|
+
{ l: l.round(OUTPUT_DP), a: a.round(OUTPUT_DP), b: b.round(OUTPUT_DP) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def oklch
|
81
|
+
l, c, h = OklchConverter.lab_to_lch(CielabConverter.xyz_to_lab(XyzConverter.rgb_to_xyz(self.rgb_array_frac)))
|
82
|
+
|
83
|
+
{ l: l.round(OUTPUT_DP), c: c.round(OUTPUT_DP), h: h.round(OUTPUT_DP) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def alpha
|
87
|
+
@rgba[:a]
|
88
|
+
end
|
89
|
+
|
90
|
+
def name
|
91
|
+
NameConverter.rgb_to_name(self.rgb_array_frac)
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
def rgb_array
|
97
|
+
[@rgba[:r].to_f, @rgba[:g].to_f, @rgba[:b].to_f]
|
98
|
+
end
|
99
|
+
|
100
|
+
def rgb_array_frac
|
101
|
+
[@rgba[:r] / 255.0, @rgba[:g] / 255.0, @rgba[:b] / 255.0]
|
102
|
+
end
|
103
|
+
|
104
|
+
def rgb_min
|
105
|
+
[@r, @g, @b].min
|
106
|
+
end
|
107
|
+
|
108
|
+
def rgb_max
|
109
|
+
[@r, @g, @b].max
|
110
|
+
end
|
111
|
+
|
112
|
+
def rgb_delta
|
113
|
+
self.rgb_max - self.rgb_min
|
114
|
+
end
|
115
|
+
|
116
|
+
def hue
|
117
|
+
h = 0
|
118
|
+
|
119
|
+
case true
|
120
|
+
when self.rgb_max == self.rgb_min
|
121
|
+
h = 0
|
122
|
+
when self.rgb_max == @r
|
123
|
+
h = (@g - @b) / self.rgb_delta
|
124
|
+
when self.rgb_max == @g
|
125
|
+
h = 2 + (@b - @r) / self.rgb_delta
|
126
|
+
when self.rgb_max == @b
|
127
|
+
h = 4 + (@r - @g) / self.rgb_delta
|
128
|
+
end
|
129
|
+
|
130
|
+
h = [h * 60, 360].min
|
131
|
+
h % 360
|
132
|
+
end
|
133
|
+
|
134
|
+
def hsl_saturation
|
135
|
+
s = 0
|
136
|
+
|
137
|
+
case true
|
138
|
+
when self.rgb_max == self.rgb_min
|
139
|
+
s = 0
|
140
|
+
when (self.hsl_lightness / 100.0) <= 0.5
|
141
|
+
s = self.rgb_delta / (self.rgb_max + self.rgb_min)
|
142
|
+
else
|
143
|
+
s = self.rgb_delta / (2.0 - self.rgb_max - self.rgb_min)
|
144
|
+
end
|
145
|
+
|
146
|
+
s * 100
|
147
|
+
end
|
148
|
+
|
149
|
+
def hsl_lightness
|
150
|
+
(self.rgb_min + self.rgb_max) / 2.0 * 100
|
151
|
+
end
|
152
|
+
|
153
|
+
def hsv_saturation
|
154
|
+
self.rgb_max.zero? ? 0 : ((self.rgb_delta / self.rgb_max * 1000) / 10.0)
|
155
|
+
end
|
156
|
+
|
157
|
+
def hsv_value
|
158
|
+
((self.rgb_max / 255.0) * 1000) / 10.0
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class Color
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :@converter, :rgb, :hex, :hsl, :hsv, :hsb, :cmyk, :xyz, :cielab, :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
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module ColorConverter
|
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] == []
|
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.lab_to_xyz(color_input)
|
22
|
+
XyzConverter.new(xyz_hash, limit_override: true).rgba
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.lab_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_lab(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
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class CmykConverter < BaseConverter
|
3
|
+
def self.matches?(color_input)
|
4
|
+
return false unless color_input.is_a?(Hash)
|
5
|
+
|
6
|
+
color_input.keys - [:c, :m, :y, :k] == []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bounds
|
10
|
+
{ c: [0.0, 100.0], m: [0.0, 100.0], y: [0.0, 100.0], k: [0.0, 100.0] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_input(color_input)
|
16
|
+
bounds = CmykConverter.bounds
|
17
|
+
color_input[:c].to_f.between?(*bounds[:c]) && color_input[:m].to_f.between?(*bounds[:m]) && color_input[:y].to_f.between?(*bounds[:y]) && color_input[:k].to_f.between?(*bounds[:k])
|
18
|
+
end
|
19
|
+
|
20
|
+
def input_to_rgba(color_input)
|
21
|
+
c = color_input[:c].to_f
|
22
|
+
m = color_input[:m].to_f
|
23
|
+
y = color_input[:y].to_f
|
24
|
+
k = color_input[:k].to_f
|
25
|
+
|
26
|
+
c /= 100.0
|
27
|
+
m /= 100.0
|
28
|
+
y /= 100.0
|
29
|
+
k /= 100.0
|
30
|
+
|
31
|
+
r = (255 * (1.0 - [1.0, c * (1.0 - k) + k].min)).round(IMPORT_DP)
|
32
|
+
g = (255 * (1.0 - [1.0, m * (1.0 - k) + k].min)).round(IMPORT_DP)
|
33
|
+
b = (255 * (1.0 - [1.0, y * (1.0 - k) + k].min)).round(IMPORT_DP)
|
34
|
+
|
35
|
+
{ r: r, g: g, b: b, a: 1.0 }
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.rgb_to_cmyk(rgb_array_frac)
|
39
|
+
r, g, b = rgb_array_frac
|
40
|
+
|
41
|
+
k = (1.0 - [r, g, b].max)
|
42
|
+
k_frac = k == 1.0 ? 1.0 : 1.0 - k
|
43
|
+
|
44
|
+
c = (1.0 - r - k) / k_frac
|
45
|
+
m = (1.0 - g - k) / k_frac
|
46
|
+
y = (1.0 - b - k) / k_frac
|
47
|
+
|
48
|
+
c *= 100
|
49
|
+
m *= 100
|
50
|
+
y *= 100
|
51
|
+
k *= 100
|
52
|
+
|
53
|
+
[c, m, y, k]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class HexConverter < BaseConverter
|
3
|
+
def self.matches?(color)
|
4
|
+
return false unless color.is_a?(String)
|
5
|
+
|
6
|
+
color.include?('#') && [4, 7, 9].include?(color.length)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def validate_input(color_input)
|
12
|
+
true
|
13
|
+
# color_input[:l].to_f.between?(0.0, 100.0) && color_input[:a].between?(-128.0, 127.0) && color_input[:b].between?(-128.0, 127.0)
|
14
|
+
end
|
15
|
+
|
16
|
+
def input_to_rgba(color_input)
|
17
|
+
color_input = self.normalize_hex(color_input)
|
18
|
+
|
19
|
+
r = color_input[0, 2].hex
|
20
|
+
g = color_input[2, 2].hex
|
21
|
+
b = color_input[4, 2].hex
|
22
|
+
a = color_input.length == 8 ? hex[6, 2].hex : 1.0
|
23
|
+
|
24
|
+
{ r: r, g: g, b: b, a: a }
|
25
|
+
end
|
26
|
+
|
27
|
+
def normalize_hex(hex_input)
|
28
|
+
hex_input = hex_input.gsub('#', '')
|
29
|
+
(hex_input.length == 3 ? hex_input[0, 1] * 2 + hex_input[1, 1] * 2 + hex_input[2, 1] * 2 : hex_input).downcase
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class HslConverter < BaseConverter
|
3
|
+
def self.matches?(color_input)
|
4
|
+
return false unless color_input.is_a?(Hash)
|
5
|
+
|
6
|
+
color_input.keys - [:h, :s, :l] == [] || color_input.keys - [:h, :s, :l, :a] == []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bounds
|
10
|
+
{ h: [0.0, 360.0], s: [0.0, 100.0], l: [0.0, 100.0] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_input(color_input)
|
16
|
+
bounds = HslConverter.bounds
|
17
|
+
color_input[:h].to_f.between?(*bounds[:h]) && color_input[:s].to_f.between?(*bounds[:s]) && color_input[:l].to_f.between?(*bounds[:l])
|
18
|
+
end
|
19
|
+
|
20
|
+
def input_to_rgba(color_input)
|
21
|
+
h = color_input[:h].to_s.gsub(/[^0-9.]/, '').to_f / 360.0
|
22
|
+
s = color_input[:s].to_s.gsub(/[^0-9.]/, '').to_f / 100.0
|
23
|
+
l = color_input[:l].to_s.gsub(/[^0-9.]/, '').to_f / 100.0
|
24
|
+
a = color_input[:a] ? color_input[:a].to_s.gsub(/[^0-9.]/, '').to_f : 1.0
|
25
|
+
|
26
|
+
return greyscale(l, a) if s.zero?
|
27
|
+
|
28
|
+
t2 = if l < 0.5
|
29
|
+
l * (1 + s)
|
30
|
+
else
|
31
|
+
l + s - l * s
|
32
|
+
end
|
33
|
+
|
34
|
+
t1 = 2 * l - t2
|
35
|
+
|
36
|
+
rgb = [0, 0, 0]
|
37
|
+
|
38
|
+
(0..2).each do |i|
|
39
|
+
t3 = h + 1 / 3.0 * - (i - 1)
|
40
|
+
t3 < 0 && t3 += 1
|
41
|
+
t3 > 1 && t3 -= 1
|
42
|
+
|
43
|
+
val = if 6 * t3 < 1
|
44
|
+
t1 + (t2 - t1) * 6 * t3
|
45
|
+
elsif 2 * t3 < 1
|
46
|
+
t2
|
47
|
+
elsif 3 * t3 < 2
|
48
|
+
t1 + (t2 - t1) * (2 / 3.0 - t3) * 6
|
49
|
+
else
|
50
|
+
t1
|
51
|
+
end
|
52
|
+
|
53
|
+
rgb[i] = (val * 255).round(IMPORT_DP)
|
54
|
+
end
|
55
|
+
|
56
|
+
{ r: rgb[0], g: rgb[1], b: rgb[2], a: a }
|
57
|
+
end
|
58
|
+
|
59
|
+
def greyscale(luminosity, alpha)
|
60
|
+
rgb_equal_value = (luminosity * 255).round(IMPORT_DP)
|
61
|
+
{ r: rgb_equal_value, g: rgb_equal_value, b: rgb_equal_value, a: alpha }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class HslStringConverter < BaseConverter
|
3
|
+
def self.matches?(color_input)
|
4
|
+
return false unless color_input.is_a?(String)
|
5
|
+
|
6
|
+
color_input.include?('hsl(') || color_input.include?('hsla(')
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def validate_input(color_input)
|
12
|
+
true
|
13
|
+
# color_input[:l].between?(0.0, 100.0) && color_input[:a].between?(-128.0, 127.0) && color_input[:b].between?(-128.0, 127.0)
|
14
|
+
end
|
15
|
+
|
16
|
+
def input_to_rgba(color_input)
|
17
|
+
matches = color_input.match(/hsla?\(([0-9.,%\s]+)\)/)
|
18
|
+
raise InvalidColorError unless matches
|
19
|
+
|
20
|
+
h, s, l, a = matches[1].split(',').map(&:strip)
|
21
|
+
raise InvalidColorError unless h.present? && s.present? && l.present?
|
22
|
+
|
23
|
+
HslConverter.new(h: h, s: s, l: l, a: a).rgba
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class HsvConverter < BaseConverter
|
3
|
+
def self.matches?(color_input)
|
4
|
+
return false unless color_input.is_a?(Hash)
|
5
|
+
|
6
|
+
color_input.keys - [:h, :s, :v] == [] || color_input.keys - [:h, :s, :b] == []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bounds
|
10
|
+
{ h: [0.0, 360.0], s: [-128.0, 127.0], v: [-128.0, 127.0], b: [-128.0, 127.0] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_input(color_input)
|
16
|
+
bounds = HsvConverter.bounds
|
17
|
+
light = (color_input[:v].present? && color_input[:v].to_f.between?(*bounds[:v])) || (color_input[:b].present? && color_input[:b].to_f.between?(*bounds[:v]))
|
18
|
+
color_input[:h].to_f.between?(*bounds[:h]) && color_input[:s].to_f.between?(*bounds[:s]) && light
|
19
|
+
end
|
20
|
+
|
21
|
+
def input_to_rgba(color_input)
|
22
|
+
h = color_input[:h].to_f
|
23
|
+
s = color_input[:s].to_f
|
24
|
+
v = (color_input[:v] || color_input[:b]).to_f
|
25
|
+
|
26
|
+
h /= 360
|
27
|
+
s /= 100
|
28
|
+
v /= 100
|
29
|
+
|
30
|
+
i = (h * 6).floor
|
31
|
+
f = h * 6 - i
|
32
|
+
|
33
|
+
p = v * (1 - s)
|
34
|
+
q = v * (1 - f * s)
|
35
|
+
t = v * (1 - (1 - f) * s)
|
36
|
+
|
37
|
+
p = (p * 255).round(IMPORT_DP)
|
38
|
+
q = (q * 255).round(IMPORT_DP)
|
39
|
+
t = (t * 255).round(IMPORT_DP)
|
40
|
+
v = (v * 255).round(IMPORT_DP)
|
41
|
+
|
42
|
+
case i % 6
|
43
|
+
when 0
|
44
|
+
{ r: v, g: t, b: p, a: 1.0 }
|
45
|
+
when 1
|
46
|
+
{ r: q, g: v, b: p, a: 1.0 }
|
47
|
+
when 2
|
48
|
+
{ r: p, g: v, b: t, a: 1.0 }
|
49
|
+
when 3
|
50
|
+
{ r: p, g: q, b: v, a: 1.0 }
|
51
|
+
when 4
|
52
|
+
{ r: t, g: p, b: v, a: 1.0 }
|
53
|
+
when 5
|
54
|
+
{ r: v, g: p, b: q, a: 1.0 }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module ColorConverter
|
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
|
+
true
|
20
|
+
# color_input[:l].between?(0.0, 100.0) && color_input[:a].between?(-128.0, 127.0) && color_input[:b].between?(-128.0, 127.0)
|
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
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ColorConverter
|
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
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class OklchConverter < BaseConverter
|
3
|
+
def self.matches?(color_input)
|
4
|
+
return false unless color_input.is_a?(Hash)
|
5
|
+
|
6
|
+
color_input.keys - [:l, :c, :h] == []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bounds
|
10
|
+
{ l: [0.0, 100.0], c: [0.0, 100.0], h: [0.0, 360.0] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_input(color_input)
|
16
|
+
bounds = OklchConverter.bounds
|
17
|
+
color_input[:l].to_f.between?(*bounds[:l]) && color_input[:c].to_f.between?(*bounds[:c]) && color_input[:h].to_f.between?(*bounds[:h])
|
18
|
+
end
|
19
|
+
|
20
|
+
def input_to_rgba(color_input)
|
21
|
+
lab_hash = OklchConverter.lch_to_lab(color_input)
|
22
|
+
xyz_hash = CielabConverter.lab_to_xyz(lab_hash)
|
23
|
+
XyzConverter.new(xyz_hash, limit_override: true).rgba
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.lch_to_lab(color_input)
|
27
|
+
l = color_input[:l].to_f
|
28
|
+
c = color_input[:c].to_f
|
29
|
+
h = color_input[:h].to_f
|
30
|
+
|
31
|
+
h_rad = h * (Math::PI / 180.0)
|
32
|
+
|
33
|
+
a = c * Math.cos(h_rad)
|
34
|
+
b = c * Math.sin(h_rad)
|
35
|
+
|
36
|
+
{ l: l, a: a, b: b }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.lab_to_lch(lab_array)
|
40
|
+
l, aa, bb = lab_array
|
41
|
+
|
42
|
+
c = ((aa**2) + (bb**2))**0.5
|
43
|
+
|
44
|
+
h_rad = Math.atan2(bb, aa)
|
45
|
+
h = h_rad * (180.0 / Math::PI)
|
46
|
+
|
47
|
+
h %= 360
|
48
|
+
|
49
|
+
[l, c, h]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ColorConverter
|
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
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ColorConverter
|
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
|
+
# color_input[:l].between?(0.0, 100.0) && color_input[:a].between?(-128.0, 127.0) && color_input[:b].between?(-128.0, 127.0)
|
14
|
+
end
|
15
|
+
|
16
|
+
def input_to_rgba(color_input)
|
17
|
+
matches = color_input.match(/rgba?\(([0-9.,\s]+)\)/)
|
18
|
+
raise InvalidColorError unless matches
|
19
|
+
|
20
|
+
r, g, b, a = matches[1].split(',').map(&:strip)
|
21
|
+
raise InvalidColorError unless r.present? && g.present? && b.present?
|
22
|
+
|
23
|
+
a ||= 1.0
|
24
|
+
|
25
|
+
r = r.to_f.round(IMPORT_DP)
|
26
|
+
g = g.to_f.round(IMPORT_DP)
|
27
|
+
b = b.to_f.round(IMPORT_DP)
|
28
|
+
a = a.to_f.round(IMPORT_DP)
|
29
|
+
|
30
|
+
{ r: r, g: g, b: b, a: a }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module ColorConverter
|
2
|
+
class XyzConverter < BaseConverter
|
3
|
+
def self.matches?(color_input)
|
4
|
+
return false unless color_input.is_a?(Hash)
|
5
|
+
|
6
|
+
color_input.keys - [:x, :y, :z] == []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bounds
|
10
|
+
{ x: [0.0, 95.047], y: [0.0, 100.0], z: [0.0, 108.883] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_input(color_input)
|
16
|
+
bounds = XyzConverter.bounds
|
17
|
+
color_input[:x].to_f.between?(*bounds[:x]) && color_input[:y].to_f.between?(*bounds[:y]) && color_input[:z].to_f.between?(*bounds[:z])
|
18
|
+
end
|
19
|
+
|
20
|
+
def input_to_rgba(color_input)
|
21
|
+
r, g, b = xyz_to_rgb(color_input)
|
22
|
+
|
23
|
+
{ r: r.round(IMPORT_DP), g: g.round(IMPORT_DP), b: b.round(IMPORT_DP), a: 1.0 }
|
24
|
+
end
|
25
|
+
|
26
|
+
def xyz_to_rgb(xyz_hash)
|
27
|
+
# Convert XYZ (typically with Y=100 for white) to normalized XYZ (Y=1 for white).
|
28
|
+
# The transformation matrix expects X, Y, Z values in the 0-1 range.
|
29
|
+
x = xyz_hash[:x].to_f / 100.0
|
30
|
+
y = xyz_hash[:y].to_f / 100.0
|
31
|
+
z = xyz_hash[:z].to_f / 100.0
|
32
|
+
|
33
|
+
# Convert normalized XYZ to Linear sRGB values.
|
34
|
+
# This uses the inverse sRGB matrix for D65 illuminant.
|
35
|
+
# The resulting rr, gg, bb values are linear (gamma-uncorrected) and in the 0-1 range.
|
36
|
+
rr = (x * 3.2404542) + (y * -1.5371385) + (z * -0.4985314)
|
37
|
+
gg = (x * -0.9692660) + (y * 1.8760108) + (z * 0.0415560)
|
38
|
+
bb = (x * 0.0556434) + (y * -0.2040259) + (z * 1.0572252)
|
39
|
+
|
40
|
+
# Apply sRGB Companding (gamma correction) to convert from Linear RGB to non-linear sRGB.
|
41
|
+
# This is defined by the sRGB specification (IEC 61966-2-1).
|
42
|
+
# The exponent for the non-linear segment is 1/2.4 (approximately 0.41666...).
|
43
|
+
r, g, b = [rr, gg, bb].map do
|
44
|
+
if _1 <= 0.0031308
|
45
|
+
# Linear portion of the sRGB curve
|
46
|
+
_1 * 12.92
|
47
|
+
else
|
48
|
+
# Non-linear (gamma-corrected) portion of the sRGB curve
|
49
|
+
# The sRGB specification uses an exponent of 1/2.4.
|
50
|
+
#
|
51
|
+
(1.055 * (_1**(1.0 / 2.4))) - 0.055
|
52
|
+
|
53
|
+
# IMPORTANT NUMERICAL NOTE:
|
54
|
+
# On this specific system (and confirmed by Wolfram Alpha for direct calculation),
|
55
|
+
# the inverse power function for val**2.4 yields a result that deviates from the value expected by widely-used color science libraries (like Bruce Lindbloom's).
|
56
|
+
#
|
57
|
+
# To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
|
58
|
+
# an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
|
59
|
+
#
|
60
|
+
# Choose 1/2.4 for strict adherence to the standard's definition (knowing your results may slightly deviate from common calculators),
|
61
|
+
# or choose 1/2.5 to ensure your calculated linear RGB values (and thus CIELAB) match authoritative external tools on this system.
|
62
|
+
#
|
63
|
+
# (1.055 * (_1**(1.0 / 2.5))) - 0.055
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Scale the 0-1 sRGB value to the 0-255 range for 8-bit color components.
|
68
|
+
r *= 255.0
|
69
|
+
g *= 255.0
|
70
|
+
b *= 255.0
|
71
|
+
|
72
|
+
# Clamping RGB values to prevent out-of-gamut issues and numerical errors and ensures these values stay within the valid and expected range.
|
73
|
+
r = r.clamp(0.0..255.0)
|
74
|
+
g = g.clamp(0.0..255.0)
|
75
|
+
b = b.clamp(0.0..255.0)
|
76
|
+
|
77
|
+
[r, g, b]
|
78
|
+
end
|
79
|
+
|
80
|
+
# http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
|
81
|
+
def self.rgb_to_xyz(rgb_array_frac)
|
82
|
+
r, g, b = rgb_array_frac
|
83
|
+
|
84
|
+
# Inverse sRGB companding. Linearizes RGB channels with respect to energy.
|
85
|
+
rr, gg, bb = [r, g, b].map do
|
86
|
+
if _1 <= 0.04045
|
87
|
+
_1 / 12.92
|
88
|
+
else
|
89
|
+
# sRGB Inverse Companding (Non-linear to Linear RGB)
|
90
|
+
# The sRGB specification (IEC 61966-2-1) defines the exponent as 2.4.
|
91
|
+
#
|
92
|
+
(((_1 + 0.055) / 1.055)**2.4)
|
93
|
+
|
94
|
+
# IMPORTANT NUMERICAL NOTE:
|
95
|
+
# On this specific system (and confirmed by Wolfram Alpha for direct calculation),
|
96
|
+
# the power function for val**2.4 yields a result that deviates from the value expected by widely-used color science libraries (like Bruce Lindbloom's).
|
97
|
+
#
|
98
|
+
# To compensate for this numerical discrepancy and ensure the final CIELAB values match standard online calculators and specifications,
|
99
|
+
# an empirically determined exponent of 2.5 has been found to produce the correct linearized sRGB values on this environment.
|
100
|
+
#
|
101
|
+
# Choose 2.4 for strict adherence to the standard's definition (knowing your results may slightly deviate from common calculators),
|
102
|
+
# or choose 2.5 to ensure your calculated linear RGB values (and thus CIELAB) match authoritative external tools on this system.
|
103
|
+
#
|
104
|
+
# ((_1 + 0.055) / 1.055)**2.5
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Convert using the RGB/XYZ matrix at:
|
109
|
+
# http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices
|
110
|
+
x = (rr * 0.4124564) + (gg * 0.3575761) + (bb * 0.1804375)
|
111
|
+
y = (rr * 0.2126729) + (gg * 0.7151522) + (bb * 0.0721750)
|
112
|
+
z = (rr * 0.0193339) + (gg * 0.1191920) + (bb * 0.9503041)
|
113
|
+
|
114
|
+
# Now, scale X, Y, Z so that Y for D65 white would be 100.
|
115
|
+
x *= 100.0
|
116
|
+
y *= 100.0
|
117
|
+
z *= 100.0
|
118
|
+
|
119
|
+
# Clamping XYZ values to prevent out-of-gamut issues and numerical errors and ensures these values stay within the valid and expected range.
|
120
|
+
x = x.clamp(0.0..95.047)
|
121
|
+
y = y.clamp(0.0..100.0)
|
122
|
+
z = z.clamp(0.0..108.883)
|
123
|
+
|
124
|
+
[x, y, z]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require 'color_converter/version'
|
6
|
+
|
7
|
+
require 'color_converter/color'
|
8
|
+
require 'color_converter/base_converter'
|
9
|
+
|
10
|
+
require 'color_converter/color_converters/rgb_converter'
|
11
|
+
require 'color_converter/color_converters/rgb_string_converter'
|
12
|
+
require 'color_converter/color_converters/hex_converter'
|
13
|
+
require 'color_converter/color_converters/hsl_converter'
|
14
|
+
require 'color_converter/color_converters/hsl_string_converter'
|
15
|
+
require 'color_converter/color_converters/hsv_converter'
|
16
|
+
require 'color_converter/color_converters/cmyk_converter'
|
17
|
+
require 'color_converter/color_converters/xyz_converter'
|
18
|
+
require 'color_converter/color_converters/cielab_converter'
|
19
|
+
require 'color_converter/color_converters/oklch_converter'
|
20
|
+
|
21
|
+
require 'color_converter/color_converters/name_converter'
|
22
|
+
require 'color_converter/color_converters/null_converter'
|
23
|
+
|
24
|
+
module ColorConverter
|
25
|
+
class Error < StandardError; end
|
26
|
+
class InvalidColorError < Error; end
|
27
|
+
# Your code goes here...
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: color_converters
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Louis Davis
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-06-25 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activesupport
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 8.0.2
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 8.0.2
|
26
|
+
description: Convert colors to hex/rgb/hsv/cmyk/hsl/xyz/cielab/oklch
|
27
|
+
email:
|
28
|
+
- LouisWilliamDavis@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- Rakefile
|
35
|
+
- lib/color_converter.rb
|
36
|
+
- lib/color_converter/base_converter.rb
|
37
|
+
- lib/color_converter/color.rb
|
38
|
+
- lib/color_converter/color_converters/cielab_converter.rb
|
39
|
+
- lib/color_converter/color_converters/cmyk_converter.rb
|
40
|
+
- lib/color_converter/color_converters/hex_converter.rb
|
41
|
+
- lib/color_converter/color_converters/hsl_converter.rb
|
42
|
+
- lib/color_converter/color_converters/hsl_string_converter.rb
|
43
|
+
- lib/color_converter/color_converters/hsv_converter.rb
|
44
|
+
- lib/color_converter/color_converters/name_converter.rb
|
45
|
+
- lib/color_converter/color_converters/null_converter.rb
|
46
|
+
- lib/color_converter/color_converters/oklch_converter.rb
|
47
|
+
- lib/color_converter/color_converters/rgb_converter.rb
|
48
|
+
- lib/color_converter/color_converters/rgb_string_converter.rb
|
49
|
+
- lib/color_converter/color_converters/xyz_converter.rb
|
50
|
+
- lib/color_converter/version.rb
|
51
|
+
homepage: https://github.com/louiswdavis/color_converters
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata:
|
55
|
+
homepage_uri: https://github.com/louiswdavis/color_converters
|
56
|
+
source_code_uri: https://github.com/louiswdavis/color_converters
|
57
|
+
changelog_uri: https://github.com/louiswdavis/color_converters/blob/master/CHANGELOG.md
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.9.3
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.6.3
|
73
|
+
specification_version: 4
|
74
|
+
summary: Convert colors to hex/rgb/hsv/cmyk/hsl/xyz/cielab/oklch
|
75
|
+
test_files: []
|