spectrum 0.0.1

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,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