color_lib 1.4.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.
@@ -0,0 +1,265 @@
1
+ require 'color_lib/palette'
2
+ # A class that can read an Adobe ColorLib palette file (used for Photoshop
3
+ # swatches) and provide a Hash-like interface to the contents. Not all
4
+ # colour formats in ACO files are supported. Based largely off the
5
+ # information found by Larry Tesler[http://www.nomodes.com/aco.html].
6
+ #
7
+ # Not all Adobe ColorLib files have named colours; all named entries are
8
+ # returned as an array.
9
+ #
10
+ # pal = ColorLib::Palette::AdobeColorLib.from_file(my_aco_palette)
11
+ # pal[0] => ColorLib::RGB<...>
12
+ # pal["white"] => [ ColorLib::RGB<...> ]
13
+ # pal["unknown"] => [ ColorLib::RGB<...>, ColorLib::RGB<...>, ... ]
14
+ #
15
+ # AdobeColorLib palettes are always indexable by insertion order (an integer
16
+ # key).
17
+ #
18
+ # Version 2 palettes use UTF-16 colour names.
19
+ class ColorLib::Palette::AdobeColorLib
20
+ include Enumerable
21
+
22
+ class << self
23
+ # Create an AdobeColorLib palette object from the named file.
24
+ def from_file(filename)
25
+ File.open(filename, "rb") { |io| ColorLib::Palette::AdobeColorLib.from_io(io) }
26
+ end
27
+
28
+ # Create an AdobeColorLib palette object from the provided IO.
29
+ def from_io(io)
30
+ ColorLib::Palette::AdobeColorLib.new(io.read)
31
+ end
32
+ end
33
+
34
+ # Returns statistics about the nature of the colours loaded.
35
+ attr_reader :statistics
36
+ # Contains the "lost" colours in the palette. These colours could not be
37
+ # properly loaded (e.g., L*a*b* is not supported by ColorLib, so it is
38
+ # "lost") or are not understood by the algorithms.
39
+ attr_reader :lost
40
+
41
+ # Use this to convert the unsigned word to the signed word, if necessary.
42
+ UwToSw = proc { |n| (n >= (2 ** 16)) ? n - (2 ** 32) : n } #:nodoc:
43
+
44
+ # Create a new AdobeColorLib palette from the palette file as a string.
45
+ def initialize(palette)
46
+ @colors = []
47
+ @names = {}
48
+ @statistics = Hash.new(0)
49
+ @lost = []
50
+ @order = []
51
+ @version = nil
52
+
53
+ class << palette
54
+ def readwords(count = 1)
55
+ @offset ||= 0
56
+ raise IndexError if @offset >= self.size
57
+ val = self[@offset, count * 2]
58
+ raise IndexError if val.nil? or val.size < (count * 2)
59
+ val = val.unpack("n" * count)
60
+ @offset += count * 2
61
+ val
62
+ end
63
+
64
+ def readutf16(count = 1)
65
+ @offset ||= 0
66
+ raise IndexError if @offset >= self.size
67
+ val = self[@offset, count * 2]
68
+ raise IndexError if val.nil? or val.size < (count * 2)
69
+ @offset += count * 2
70
+ val
71
+ end
72
+ end
73
+
74
+ @version, count = palette.readwords 2
75
+
76
+ raise "Unknown AdobeColorLib palette version #@version." unless @version.between?(1, 2)
77
+
78
+ count.times do
79
+ space, w, x, y, z = palette.readwords 5
80
+ name = nil
81
+ if @version == 2
82
+ raise IndexError unless palette.readwords == [0]
83
+ len = palette.readwords
84
+ name = palette.readutf16(len[0] - 1)
85
+ raise IndexError unless palette.readwords == [0]
86
+ end
87
+
88
+ color = case space
89
+ when 0 then # RGB
90
+ @statistics[:rgb] += 1
91
+
92
+ ColorLib::RGB.new(w / 256, x / 256, y / 256)
93
+ when 1 then # HS[BV] -- Convert to RGB
94
+ @statistics[:hsb] += 1
95
+
96
+ h = w / 65535.0
97
+ s = x / 65535.0
98
+ v = y / 65535.0
99
+
100
+ if defined?(ColorLib::HSB)
101
+ ColorLib::HSB.from_fraction(h, s, v)
102
+ else
103
+ @statistics[:converted] += 1
104
+ if ColorLib.near_zero_or_less?(s)
105
+ ColorLib::RGB.from_fraction(v, v, v)
106
+ else
107
+ if ColorLib.near_one_or_more?(h)
108
+ vh = 0
109
+ else
110
+ vh = h * 6.0
111
+ end
112
+
113
+ vi = vh.floor
114
+ v1 = v.to_f * (1 - s.to_f)
115
+ v2 = v.to_f * (1 - s.to_f * (vh - vi))
116
+ v3 = v.to_f * (1 - s.to_f * (1 - (vh - vi)))
117
+
118
+ case vi
119
+ when 0 then
120
+ ColorLib::RGB.from_fraction(v, v3, v1)
121
+ when 1 then
122
+ ColorLib::RGB.from_fraction(v2, v, v1)
123
+ when 2 then
124
+ ColorLib::RGB.from_fraction(v1, v, v3)
125
+ when 3 then
126
+ ColorLib::RGB.from_fraction(v1, v2, v)
127
+ when 4 then
128
+ ColorLib::RGB.from_fraction(v3, v1, v)
129
+ else
130
+ ColorLib::RGB.from_fraction(v, v1, v2)
131
+ end
132
+ end
133
+ end
134
+ when 2 then # CMYK
135
+ @statistics[:cmyk] += 1
136
+ ColorLib::CMYK.from_percent(100 - (w / 655.35),
137
+ 100 - (x / 655.35),
138
+ 100 - (y / 655.35),
139
+ 100 - (z / 655.35))
140
+ when 7 then # L*a*b*
141
+ @statistics[:lab] += 1
142
+
143
+ l = [w, 10000].min / 100.0
144
+ a = [[-12800, UwToSw[x]].max, 12700].min / 100.0
145
+ b = [[-12800, UwToSw[x]].max, 12700].min / 100.0
146
+
147
+ if defined? ColorLib::Lab
148
+ ColorLib::Lab.new(l, a, b)
149
+ else
150
+ [space, w, x, y, z]
151
+ end
152
+ when 8 then # Grayscale
153
+ @statistics[:gray] += 1
154
+
155
+ g = [w, 10000].min / 100.0
156
+ ColorLib::GrayScale.new(g)
157
+ when 9 then # Wide CMYK
158
+ @statistics[:wcmyk] += 1
159
+
160
+ c = [w, 10000].min / 100.0
161
+ m = [x, 10000].min / 100.0
162
+ y = [y, 10000].min / 100.0
163
+ k = [z, 10000].min / 100.0
164
+ ColorLib::CMYK.from_percent(c, m, y, k)
165
+ else
166
+ @statistics[space] += 1
167
+ [space, w, x, y, z]
168
+ end
169
+
170
+ @order << [color, name]
171
+
172
+ if color.kind_of? Array
173
+ @lost << color
174
+ else
175
+ @colors << color
176
+
177
+ if name
178
+ @names[name] ||= []
179
+ @names[name] << color
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ # Provides the colour or colours at the provided selectors.
186
+ def values_at(*selectors)
187
+ @colors.values_at(*selectors)
188
+ end
189
+
190
+ # If a Numeric +key+ is provided, the single colour value at that position
191
+ # will be returned. If a String +key+ is provided, the colour set (an
192
+ # array) for that colour name will be returned.
193
+ def [](key)
194
+ if key.kind_of?(Numeric)
195
+ @colors[key]
196
+ else
197
+ @names[key]
198
+ end
199
+ end
200
+
201
+ # Loops through each colour.
202
+ def each
203
+ @colors.each { |el| yield el }
204
+ end
205
+
206
+ # Loops through each named colour set.
207
+ def each_name #:yields color_name, color_set:#
208
+ @names.each { |color_name, color_set| yield color_name, color_set }
209
+ end
210
+
211
+ def size
212
+ @colors.size
213
+ end
214
+
215
+ attr_reader :version
216
+
217
+ def to_aco(version = @version) #:nodoc:
218
+ res = ""
219
+
220
+ res << [version, @order.size].pack("nn")
221
+
222
+ @order.each do |cnpair|
223
+ color, name = *cnpair
224
+
225
+ # Note: HSB and CMYK formats are lost by the conversions performed on
226
+ # import. They are turned into RGB and WCMYK, respectively.
227
+
228
+ cstr = case color
229
+ when Array
230
+ color
231
+ when ColorLib::RGB
232
+ r = [(color.red * 256).round, 65535].min
233
+ g = [(color.green * 256).round, 65535].min
234
+ b = [(color.blue * 256).round, 65535].min
235
+ [0, r, g, b, 0]
236
+ when ColorLib::GrayScale
237
+ g = [(color.gray * 100).round, 10000].min
238
+ [8, g, 0, 0, 0]
239
+ when ColorLib::CMYK
240
+ c = [(color.cyan * 100).round, 10000].min
241
+ m = [(color.magenta * 100).round, 10000].min
242
+ y = [(color.yellow * 100).round, 10000].min
243
+ k = [(color.black * 100).round, 10000].min
244
+ [9, c, m, y, k]
245
+ end
246
+ cstr = cstr.pack("nnnnn")
247
+
248
+ nstr = ""
249
+
250
+ if version == 2
251
+ if (name.size / 2 * 2) == name.size # only where s[0] == byte!
252
+ nstr << [0, (name.size / 2) + 1].pack("nn")
253
+ nstr << name
254
+ nstr << [0].pack("n")
255
+ else
256
+ nstr << [0, 1, 0].pack("nnn")
257
+ end
258
+ end
259
+
260
+ res << cstr << nstr
261
+ end
262
+
263
+ res
264
+ end
265
+ end
@@ -0,0 +1,103 @@
1
+ require 'color_lib/palette'
2
+ # A class that can read a GIMP (GNU Image Manipulation Program) palette file
3
+ # and provide a Hash-like interface to the contents. GIMP colour palettes
4
+ # are RGB values only.
5
+ #
6
+ # Because two or more entries in a GIMP palette may have the same name, all
7
+ # named entries are returned as an array.
8
+ #
9
+ # pal = ColorLib::Palette::Gimp.from_file(my_gimp_palette)
10
+ # pal[0] => ColorLib::RGB<...>
11
+ # pal["white"] => [ ColorLib::RGB<...> ]
12
+ # pal["unknown"] => [ ColorLib::RGB<...>, ColorLib::RGB<...>, ... ]
13
+ #
14
+ # GIMP Palettes are always indexable by insertion order (an integer key).
15
+ class ColorLib::Palette::Gimp
16
+ include Enumerable
17
+
18
+ class << self
19
+ # Create a GIMP palette object from the named file.
20
+ def from_file(filename)
21
+ File.open(filename, "rb") { |io| ColorLib::Palette::Gimp.from_io(io) }
22
+ end
23
+
24
+ # Create a GIMP palette object from the provided IO.
25
+ def from_io(io)
26
+ ColorLib::Palette::Gimp.new(io.read)
27
+ end
28
+ end
29
+
30
+ # Create a new GIMP palette from the palette file as a string.
31
+ def initialize(palette)
32
+ @colors = []
33
+ @names = {}
34
+ @valid = false
35
+ @name = "(unnamed)"
36
+
37
+ palette.split($/).each do |line|
38
+ line.chomp!
39
+ line.gsub!(/\s*#.*\Z/, '')
40
+
41
+ next if line.empty?
42
+
43
+ if line =~ /\AGIMP Palette\Z/
44
+ @valid = true
45
+ next
46
+ end
47
+
48
+ info = /(\w+):\s(.*$)/.match(line)
49
+ if info
50
+ @name = info.captures[1] if info.captures[0] =~ /name/i
51
+ next
52
+ end
53
+
54
+ line.gsub!(/^\s+/, '')
55
+ data = line.split(/\s+/, 4)
56
+ name = data.pop.strip
57
+ data.map! { |el| el.to_i }
58
+
59
+ color = ColorLib::RGB.new(*data)
60
+
61
+ @colors << color
62
+ @names[name] ||= []
63
+ @names[name] << color
64
+ end
65
+ end
66
+
67
+ # Provides the colour or colours at the provided selectors.
68
+ def values_at(*selectors)
69
+ @colors.values_at(*selectors)
70
+ end
71
+
72
+ # If a Numeric +key+ is provided, the single colour value at that position
73
+ # will be returned. If a String +key+ is provided, the colour set (an
74
+ # array) for that colour name will be returned.
75
+ def [](key)
76
+ if key.kind_of?(Numeric)
77
+ @colors[key]
78
+ else
79
+ @names[key]
80
+ end
81
+ end
82
+
83
+ # Loops through each colour.
84
+ def each
85
+ @colors.each { |el| yield el }
86
+ end
87
+
88
+ # Loops through each named colour set.
89
+ def each_name #:yields color_name, color_set:#
90
+ @names.each { |color_name, color_set| yield color_name, color_set }
91
+ end
92
+
93
+ # Returns true if this is believed to be a valid GIMP palette.
94
+ def valid?
95
+ @valid
96
+ end
97
+
98
+ def size
99
+ @colors.size
100
+ end
101
+
102
+ attr_reader :name
103
+ end
@@ -0,0 +1,169 @@
1
+ require 'color_lib/palette'
2
+ # Generates a monochromatic constrasting colour palette for background and
3
+ # foreground. What does this mean?
4
+ #
5
+ # Monochromatic: A single colour is used to generate the base palette, and
6
+ # this colour is lightened five times and darkened five times to provide
7
+ # eleven distinct colours.
8
+ #
9
+ # Contrasting: The foreground is also generated as a monochromatic colour
10
+ # palette; however, all generated colours are tested to see that they are
11
+ # appropriately contrasting to ensure maximum readability of the foreground
12
+ # against the background.
13
+ class ColorLib::Palette::MonoContrast
14
+ # Hash of CSS background colour values.
15
+ #
16
+ # This is always 11 values:
17
+ #
18
+ # 0:: The starting colour.
19
+ # +1..+5:: Lighter colours.
20
+ # -1..-5:: Darker colours.
21
+ attr_reader :background
22
+ # Hash of CSS foreground colour values.
23
+ #
24
+ # This is always 11 values:
25
+ #
26
+ # 0:: The starting colour.
27
+ # +1..+5:: Lighter colours.
28
+ # -1..-5:: Darker colours.
29
+ attr_reader :foreground
30
+
31
+ DEFAULT_MINIMUM_BRIGHTNESS_DIFF = (125.0 / 255.0)
32
+
33
+ # The minimum brightness difference between the background and the
34
+ # foreground, and must be between 0..1. Setting this value will regenerate
35
+ # the palette based on the base colours. The default value for this is 125
36
+ # / 255.0. If this value is set to +nil+, it will be restored to the
37
+ # default.
38
+ attr_accessor :minimum_brightness_diff
39
+ remove_method :minimum_brightness_diff=;
40
+
41
+ def minimum_brightness_diff=(bd) #:nodoc:
42
+ if bd.nil?
43
+ @minimum_brightness_diff = DEFAULT_MINIMUM_BRIGHTNESS_DIFF
44
+ elsif bd > 1.0
45
+ @minimum_brightness_diff = 1.0
46
+ elsif bd < 0.0
47
+ @minimum_brightness_diff = 0.0
48
+ else
49
+ @minimum_brightness_diff = bd
50
+ end
51
+
52
+ regenerate(@background[0], @foreground[0])
53
+ end
54
+
55
+ DEFAULT_MINIMUM_COLOR_DIFF = (500.0 / 255.0)
56
+
57
+ # The minimum colour difference between the background and the foreground,
58
+ # and must be between 0..3. Setting this value will regenerate the palette
59
+ # based on the base colours. The default value for this is 500 / 255.0.
60
+ attr_accessor :minimum_color_diff
61
+ remove_method :minimum_color_diff=;
62
+
63
+ def minimum_color_diff=(cd) #:noco:
64
+ if cd.nil?
65
+ @minimum_color_diff = DEFAULT_MINIMUM_COLOR_DIFF
66
+ elsif cd > 3.0
67
+ @minimum_color_diff = 3.0
68
+ elsif cd < 0.0
69
+ @minimum_color_diff = 0.0
70
+ else
71
+ @minimum_color_diff = cd
72
+ end
73
+ regenerate(@background[0], @foreground[0])
74
+ end
75
+
76
+ # Generate the initial palette.
77
+ def initialize(background, foreground = nil)
78
+ @minimum_brightness_diff = DEFAULT_MINIMUM_BRIGHTNESS_DIFF
79
+ @minimum_color_diff = DEFAULT_MINIMUM_COLOR_DIFF
80
+
81
+ regenerate(background, foreground)
82
+ end
83
+
84
+ # Generate the colour palettes.
85
+ def regenerate(background, foreground = nil)
86
+ foreground ||= background
87
+ background = background.to_rgb
88
+ foreground = foreground.to_rgb
89
+
90
+ @background = {}
91
+ @foreground = {}
92
+
93
+ @background[-5] = background.darken_by(10)
94
+ @background[-4] = background.darken_by(25)
95
+ @background[-3] = background.darken_by(50)
96
+ @background[-2] = background.darken_by(75)
97
+ @background[-1] = background.darken_by(85)
98
+ @background[0] = background
99
+ @background[+1] = background.lighten_by(85)
100
+ @background[+2] = background.lighten_by(75)
101
+ @background[+3] = background.lighten_by(50)
102
+ @background[+4] = background.lighten_by(25)
103
+ @background[+5] = background.lighten_by(10)
104
+
105
+ @foreground[-5] = calculate_foreground(@background[-5], foreground)
106
+ @foreground[-4] = calculate_foreground(@background[-4], foreground)
107
+ @foreground[-3] = calculate_foreground(@background[-3], foreground)
108
+ @foreground[-2] = calculate_foreground(@background[-2], foreground)
109
+ @foreground[-1] = calculate_foreground(@background[-1], foreground)
110
+ @foreground[0] = calculate_foreground(@background[0], foreground)
111
+ @foreground[+1] = calculate_foreground(@background[+1], foreground)
112
+ @foreground[+2] = calculate_foreground(@background[+2], foreground)
113
+ @foreground[+3] = calculate_foreground(@background[+3], foreground)
114
+ @foreground[+4] = calculate_foreground(@background[+4], foreground)
115
+ @foreground[+5] = calculate_foreground(@background[+5], foreground)
116
+ end
117
+
118
+ # Given a background colour and a foreground colour, modifies the
119
+ # foreground colour so that it will have enough contrast to be seen
120
+ # against the background colour.
121
+ #
122
+ # Uses #mininum_brightness_diff and #minimum_color_diff.
123
+ def calculate_foreground(background, foreground)
124
+ nfg = nil
125
+ # Loop through brighter and darker versions of the foreground color. The
126
+ # numbers here represent the amount of foreground color to mix with
127
+ # black and white.
128
+ [100, 75, 50, 25, 0].each do |percent|
129
+ dfg = foreground.darken_by(percent)
130
+ lfg = foreground.lighten_by(percent)
131
+
132
+ dbd = brightness_diff(background, dfg)
133
+ lbd = brightness_diff(background, lfg)
134
+
135
+ if lbd > dbd
136
+ nfg = lfg
137
+ nbd = lbd
138
+ else
139
+ nfg = dfg
140
+ nbd = dbd
141
+ end
142
+
143
+ ncd = color_diff(background, nfg)
144
+
145
+ break if nbd >= @minimum_brightness_diff and ncd >= @minimum_color_diff
146
+ end
147
+ nfg
148
+ end
149
+
150
+ # Returns the absolute difference between the brightness levels of two
151
+ # colours. This will be a decimal value between 0 and 1. W3C accessibility
152
+ # guidelines for colour contrast[http://www.w3.org/TR/AERT#color-contrast]
153
+ # suggest that this value be at least approximately 0.49 (125 / 255.0) for
154
+ # proper contrast.
155
+ def brightness_diff(c1, c2)
156
+ (c1.brightness - c2.brightness).abs
157
+ end
158
+
159
+ # Returns the contrast between to colours, a decimal value between 0 and
160
+ # 3. W3C accessibility guidelines for colour
161
+ # contrast[http://www.w3.org/TR/AERT#color-contrast] suggest that this
162
+ # value be at least approximately 1.96 (500 / 255.0) for proper contrast.
163
+ def color_diff(c1, c2)
164
+ r = (c1.r - c2.r).abs
165
+ g = (c1.g - c2.g).abs
166
+ b = (c1.b - c2.b).abs
167
+ r + g + b
168
+ end
169
+ end