color_lib 1.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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