spectrum 0.0.1

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