color 1.4.1 → 1.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/color/rgb.rb CHANGED
@@ -1,86 +1,24 @@
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
1
  # An RGB colour object.
14
2
  class Color::RGB
3
+ include Color
4
+
15
5
  # The format of a DeviceRGB colour for PDF. In color-tools 2.0 this will
16
6
  # be removed from this package and added back as a modification by the
17
7
  # PDF::Writer package.
18
8
  PDF_FORMAT_STR = "%.3f %.3f %.3f %s"
19
9
 
20
- class << self
21
- # Creates an RGB colour object from percentages 0..100.
22
- #
23
- # Color::RGB.from_percentage(10, 20 30)
24
- def from_percentage(r = 0, g = 0, b = 0)
25
- from_fraction(r / 100.0, g / 100.0, b / 100.0)
26
- end
27
-
28
- # Creates an RGB colour object from fractional values 0..1.
29
- #
30
- # Color::RGB.from_fraction(.3, .2, .1)
31
- def from_fraction(r = 0.0, g = 0.0, b = 0.0)
32
- colour = Color::RGB.new
33
- colour.r = r
34
- colour.g = g
35
- colour.b = b
36
- colour
37
- end
38
-
39
- # Creates an RGB colour object from an HTML colour descriptor (e.g.,
40
- # <tt>"fed"</tt> or <tt>"#cabbed;"</tt>.
41
- #
42
- # Color::RGB.from_html("fed")
43
- # Color::RGB.from_html("#fed")
44
- # Color::RGB.from_html("#cabbed")
45
- # Color::RGB.from_html("cabbed")
46
- def from_html(html_colour)
47
- html_colour = html_colour.gsub(%r{[#;]}, '')
48
- case html_colour.size
49
- when 3
50
- colours = html_colour.scan(%r{[0-9A-Fa-f]}).map { |el| (el * 2).to_i(16) }
51
- when 6
52
- colours = html_colour.scan(%r<[0-9A-Fa-f]{2}>).map { |el| el.to_i(16) }
53
- else
54
- raise ArgumentError
55
- end
56
-
57
- Color::RGB.new(*colours)
58
- end
59
- end
60
-
61
- # Compares the other colour to this one. The other colour will be
62
- # converted to RGB before comparison, so the comparison between a RGB
63
- # colour and a non-RGB colour will be approximate and based on the other
64
- # colour's default #to_rgb conversion. If there is no #to_rgb conversion,
65
- # this will raise an exception. This will report that two RGB colours are
66
- # equivalent if all component values are within COLOR_TOLERANCE of each
67
- # other.
68
- def ==(other)
69
- other = other.to_rgb
70
- other.kind_of?(Color::RGB) and
71
- ((@r - other.r).abs <= Color::COLOR_TOLERANCE) and
72
- ((@g - other.g).abs <= Color::COLOR_TOLERANCE) and
73
- ((@b - other.b).abs <= Color::COLOR_TOLERANCE)
10
+ # Coerces the other Color object into RGB.
11
+ def coerce(other)
12
+ other.to_rgb
74
13
  end
75
14
 
76
15
  # Creates an RGB colour object from the standard range 0..255.
77
16
  #
78
17
  # Color::RGB.new(32, 64, 128)
79
18
  # Color::RGB.new(0x20, 0x40, 0x80)
80
- def initialize(r = 0, g = 0, b = 0)
81
- @r = r / 255.0
82
- @g = g / 255.0
83
- @b = b / 255.0
19
+ def initialize(r = 0, g = 0, b = 0, radix = 255.0, &block) # :yields self:
20
+ @r, @g, @b = [ r, g, b ].map { |v| Color.normalize(v / radix) }
21
+ block.call(self) if block
84
22
  end
85
23
 
86
24
  # Present the colour as a DeviceRGB fill colour string for PDF. This will
@@ -95,8 +33,8 @@ class Color::RGB
95
33
  PDF_FORMAT_STR % [ @r, @g, @b, "RG" ]
96
34
  end
97
35
 
98
- # Present the colour as an HTML/CSS colour string.
99
- def html
36
+ # Present the colour as an RGB hex triplet.
37
+ def hex
100
38
  r = (@r * 255).round
101
39
  r = 255 if r > 255
102
40
 
@@ -106,7 +44,12 @@ class Color::RGB
106
44
  b = (@b * 255).round
107
45
  b = 255 if b > 255
108
46
 
109
- "#%02x%02x%02x" % [ r, g, b ]
47
+ "%02x%02x%02x" % [ r, g, b ]
48
+ end
49
+
50
+ # Present the colour as an HTML/CSS colour string.
51
+ def html
52
+ "##{hex}"
110
53
  end
111
54
 
112
55
  # Present the colour as an RGB HTML/CSS colour string (e.g., "rgb(0%, 50%,
@@ -116,11 +59,16 @@ class Color::RGB
116
59
  "rgb(%3.2f%%, %3.2f%%, %3.2f%%)" % [ red_p, green_p, blue_p ]
117
60
  end
118
61
 
119
- # Present the colour as an RGBA (with alpha) HTML/CSS colour string (e.g.,
120
- # "rgb(0%, 50%, 100%, 1)"). Note that this will perform a #to_rgb
121
- # operation using the default conversion formula.
122
- def css_rgba
123
- "rgba(%3.2f%%, %3.2f%%, %3.2f%%, %3.2f)" % [ red_p, green_p, blue_p, 1 ]
62
+ # Present the colour as an RGBA (with an optional alpha that defaults to 1)
63
+ # HTML/CSS colour string (e.g.,"rgb(0%, 50%, 100%, 1)"). Note that this will
64
+ # perform a #to_rgb operation using the default conversion formula.
65
+ #
66
+ # Color::RGB.by_hex('ff0000').css_rgba
67
+ # => 'rgba(100.00%, 0.00%, 0.00%, 1.00)'
68
+ # Color::RGB.by_hex('ff0000').css_rgba(0.2)
69
+ # => 'rgba(100.00%, 0.00%, 0.00%, 0.20)'
70
+ def css_rgba(alpha = 1)
71
+ "rgba(%3.2f%%, %3.2f%%, %3.2f%%, %3.2f)" % [ red_p, green_p, blue_p, alpha ]
124
72
  end
125
73
 
126
74
  # Present the colour as an HSL HTML/CSS colour string (e.g., "hsl(180,
@@ -230,6 +178,78 @@ class Color::RGB
230
178
  Color::HSL.from_fraction(hue, sat, lum)
231
179
  end
232
180
 
181
+ # Returns the XYZ colour encoding of the value. Based on the
182
+ # {RGB to XYZ}[http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html]
183
+ # formula presented by Bruce Lindbloom.
184
+ #
185
+ # Currently only the sRGB colour space is supported.
186
+ def to_xyz(color_space = :sRGB)
187
+ unless color_space.to_s.downcase == 'srgb'
188
+ raise ArgumentError, "Unsupported colour space #{color_space}."
189
+ end
190
+
191
+ # Inverse sRGB companding. Linearizes RGB channels with respect to
192
+ # energy.
193
+ r, g, b = [ @r, @g, @b ].map { |v|
194
+ if (v > 0.04045)
195
+ (((v + 0.055) / 1.055) ** 2.4) * 100
196
+ else
197
+ (v / 12.92) * 100
198
+ end
199
+ }
200
+
201
+ # Convert using the RGB/XYZ matrix at:
202
+ # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices
203
+ {
204
+ :x => (r * 0.4124564 + g * 0.3575761 + b * 0.1804375),
205
+ :y => (r * 0.2126729 + g * 0.7151522 + b * 0.0721750),
206
+ :z => (r * 0.0193339 + g * 0.1191920 + b * 0.9503041)
207
+ }
208
+ end
209
+
210
+ # Returns the L*a*b* colour encoding of the value via the XYZ colour
211
+ # encoding. Based on the
212
+ # {XYZ to Lab}[http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html]
213
+ # formula presented by Bruce Lindbloom.
214
+ #
215
+ # Currently only the sRGB colour space is supported and defaults to using
216
+ # a D65 reference white.
217
+ def to_lab(color_space = :sRGB, reference_white = [ 95.047, 100.00, 108.883 ])
218
+ xyz = to_xyz
219
+
220
+ # Calculate the ratio of the XYZ values to the reference white.
221
+ # http://www.brucelindbloom.com/index.html?Equations.html
222
+ xr = xyz[:x] / reference_white[0]
223
+ yr = xyz[:y] / reference_white[1]
224
+ zr = xyz[:z] / reference_white[2]
225
+
226
+ # NOTE: This should be using Rational instead of floating point values,
227
+ # otherwise there will be discontinuities.
228
+ # http://www.brucelindbloom.com/LContinuity.html
229
+ epsilon = (216 / 24389.0)
230
+ kappa = (24389 / 27.0)
231
+
232
+ # And now transform
233
+ # http://en.wikipedia.org/wiki/Lab_color_space#Forward_transformation
234
+ # There is a brief explanation there as far as the nature of the calculations,
235
+ # as well as a much nicer looking modeling of the algebra.
236
+ fx, fy, fz = [ xr, yr, zr ].map { |t|
237
+ if (t > (epsilon))
238
+ t ** (1.0 / 3)
239
+ else # t <= epsilon
240
+ ((kappa * t) + 16) / 116.0
241
+ # The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 -
242
+ # 16 = 0, which is the correct value for L* with black.
243
+ # ((1.0/3)*((29.0/6)**2) * t) + (4.0/29)
244
+ end
245
+ }
246
+ {
247
+ :L => ((116 * fy) - 16),
248
+ :a => (500 * (fx - fy)),
249
+ :b => (200 * (fy - fz))
250
+ }
251
+ end
252
+
233
253
  # Mix the RGB hue with White so that the RGB hue is the specified
234
254
  # percentage of the resulting colour. Strictly speaking, this isn't a
235
255
  # darken_by operation.
@@ -249,7 +269,7 @@ class Color::RGB
249
269
  def mix_with(mask, opacity)
250
270
  opacity /= 100.0
251
271
  rgb = self.dup
252
-
272
+
253
273
  rgb.r = (@r * opacity) + (mask.r * (1 - opacity))
254
274
  rgb.g = (@g * opacity) + (mask.g * (1 - opacity))
255
275
  rgb.b = (@b * opacity) + (mask.b * (1 - opacity))
@@ -279,11 +299,7 @@ class Color::RGB
279
299
  # Color::RGB::DarkBlue.adjust_brightness(10)
280
300
  # Color::RGB::DarkBlue.adjust_brightness(-10)
281
301
  def adjust_brightness(percent)
282
- percent /= 100.0
283
- percent += 1.0
284
- percent = [ percent, 2.0 ].min
285
- percent = [ 0.0, percent ].max
286
-
302
+ percent = normalize_percent(percent)
287
303
  hsl = to_hsl
288
304
  hsl.l *= percent
289
305
  hsl.to_rgb
@@ -296,11 +312,7 @@ class Color::RGB
296
312
  # Color::RGB::DarkBlue.adjust_saturation(10)
297
313
  # Color::RGB::DarkBlue.adjust_saturation(-10)
298
314
  def adjust_saturation(percent)
299
- percent /= 100.0
300
- percent += 1.0
301
- percent = [ percent, 2.0 ].min
302
- percent = [ 0.0, percent ].max
303
-
315
+ percent = normalize_percent(percent)
304
316
  hsl = to_hsl
305
317
  hsl.s *= percent
306
318
  hsl.to_rgb
@@ -313,16 +325,123 @@ class Color::RGB
313
325
  # Color::RGB::DarkBlue.adjust_hue(10)
314
326
  # Color::RGB::DarkBlue.adjust_hue(-10)
315
327
  def adjust_hue(percent)
316
- percent /= 100.0
317
- percent += 1.0
318
- percent = [ percent, 2.0 ].min
319
- percent = [ 0.0, percent ].max
320
-
328
+ percent = normalize_percent(percent)
321
329
  hsl = to_hsl
322
330
  hsl.h *= percent
323
331
  hsl.to_rgb
324
332
  end
325
333
 
334
+ # TODO: Identify the base colour profile used for L*a*b* and XYZ
335
+ # conversions.
336
+
337
+ # Calculates and returns the closest match to this colour from a list of
338
+ # provided colours. Returns +nil+ if +color_list+ is empty or if there is
339
+ # no colour within the +threshold_distance+.
340
+ #
341
+ # +threshold_distance+ is used to determine the minimum colour distance
342
+ # permitted. Uses the CIE Delta E 1994 algorithm (CIE94) to find near
343
+ # matches based on perceived visual colour. The default value (1000.0) is
344
+ # an arbitrarily large number. The values <tt>:jnd</tt> and
345
+ # <tt>:just_noticeable</tt> may be passed as the +threshold_distance+ to
346
+ # use the value <tt>2.3</tt>.
347
+ def closest_match(color_list, threshold_distance = 1000.0)
348
+ color_list = [ color_list ].flatten(1)
349
+ return nil if color_list.empty?
350
+
351
+ threshold_distance = case threshold_distance
352
+ when :jnd, :just_noticeable
353
+ 2.3
354
+ else
355
+ threshold_distance.to_f
356
+ end
357
+ lab = to_lab
358
+ closest_distance = threshold_distance
359
+ best_match = nil
360
+
361
+ color_list.each do |c|
362
+ distance = delta_e94(lab, c.to_lab)
363
+ if (distance < closest_distance)
364
+ closest_distance = distance
365
+ best_match = c
366
+ end
367
+ end
368
+ best_match
369
+ end
370
+
371
+ # The Delta E (CIE94) algorithm
372
+ # http://en.wikipedia.org/wiki/Color_difference#CIE94
373
+ #
374
+ # There is a newer version, CIEDE2000, that uses slightly more complicated
375
+ # math, but addresses "the perceptual uniformity issue" left lingering by
376
+ # the CIE94 algorithm. color_1 and color_2 are both L*a*b* hashes,
377
+ # rendered by #to_lab.
378
+ #
379
+ # Since our source is treated as sRGB, we use the "graphic arts" presets
380
+ # for k_L, k_1, and k_2
381
+ #
382
+ # The calculations go through LCH(ab). (?)
383
+ #
384
+ # See also http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
385
+ #
386
+ # NOTE: This should be moved to Color::Lab.
387
+ def delta_e94(color_1, color_2, weighting_type = :graphic_arts)
388
+ case weighting_type
389
+ when :graphic_arts
390
+ k_1 = 0.045
391
+ k_2 = 0.015
392
+ k_L = 1
393
+ when :textiles
394
+ k_1 = 0.048
395
+ k_2 = 0.014
396
+ k_L = 2
397
+ else
398
+ raise ArgumentError, "Unsupported weighting type #{weighting_type}."
399
+ end
400
+
401
+ # delta_E = Math.sqrt(
402
+ # ((delta_L / (k_L * s_L)) ** 2) +
403
+ # ((delta_C / (k_C * s_C)) ** 2) +
404
+ # ((delta_H / (k_H * s_H)) ** 2)
405
+ # )
406
+ #
407
+ # Under some circumstances in real computers, delta_H could be an
408
+ # imaginary number (it's a square root value), so we're going to treat
409
+ # this as:
410
+ #
411
+ # delta_E = Math.sqrt(
412
+ # ((delta_L / (k_L * s_L)) ** 2) +
413
+ # ((delta_C / (k_C * s_C)) ** 2) +
414
+ # (delta_H2 / ((k_H * s_H) ** 2)))
415
+ # )
416
+ #
417
+ # And not perform the square root when calculating delta_H2.
418
+
419
+ k_C = k_H = 1
420
+
421
+ l_1, a_1, b_1 = color_1.values_at(:L, :a, :b)
422
+ l_2, a_2, b_2 = color_2.values_at(:L, :a, :b)
423
+
424
+ delta_a = a_1 - a_2
425
+ delta_b = b_1 - b_2
426
+
427
+ c_1 = Math.sqrt((a_1 ** 2) + (b_1 ** 2))
428
+ c_2 = Math.sqrt((a_2 ** 2) + (b_2 ** 2))
429
+
430
+ delta_L = color_1[:L] - color_2[:L]
431
+ delta_C = c_1 - c_2
432
+
433
+ delta_H2 = (delta_a ** 2) + (delta_b ** 2) - (delta_C ** 2)
434
+
435
+ s_L = 1
436
+ s_C = 1 + k_1 * c_1
437
+ s_H = 1 + k_2 * c_1
438
+
439
+ composite_L = (delta_L / (k_L * s_L)) ** 2
440
+ composite_C = (delta_C / (k_C * s_C)) ** 2
441
+ composite_H = delta_H2 / ((k_H * s_H) ** 2)
442
+ Math.sqrt(composite_L + composite_C + composite_H)
443
+ end
444
+
326
445
  # Returns the red component of the colour in the normal 0 .. 255 range.
327
446
  def red
328
447
  @r * 255.0
@@ -411,14 +530,7 @@ class Color::RGB
411
530
  # The addition is done using the RGB Accessor methods to ensure a valid
412
531
  # colour in the result.
413
532
  def +(other)
414
- other = other.to_rgb
415
- rgb = self.dup
416
-
417
- rgb.r += other.r
418
- rgb.g += other.g
419
- rgb.b += other.b
420
-
421
- rgb
533
+ self.class.from_fraction(r + other.r, g + other.g, b + other.b)
422
534
  end
423
535
 
424
536
  # Subtracts another colour to the current colour. The other colour will be
@@ -427,27 +539,178 @@ class Color::RGB
427
539
  #
428
540
  # The subtraction is done using the RGB Accessor methods to ensure a valid
429
541
  # colour in the result.
430
- def -(other)
431
- other = other.to_rgb
432
- rgb = self.dup
433
-
434
- rgb.r -= other.r
435
- rgb.g -= other.g
436
- rgb.b -= other.b
437
-
438
- rgb
542
+ def -(other)
543
+ self + (-other)
439
544
  end
440
545
 
441
546
  # Retrieve the maxmum RGB value from the current colour as a GrayScale
442
547
  # colour
443
548
  def max_rgb_as_grayscale
444
- Color::GrayScale.from_fraction([@r, @g, @b].max)
549
+ Color::GrayScale.from_fraction([@r, @g, @b].max)
445
550
  end
446
551
  alias max_rgb_as_greyscale max_rgb_as_grayscale
447
552
 
448
553
  def inspect
449
554
  "RGB [#{html}]"
450
555
  end
556
+
557
+ def to_a
558
+ [ r, g, b ]
559
+ end
560
+
561
+ # Numerically negate the color. This results in a color that is only
562
+ # usable for subtraction.
563
+ def -@
564
+ rgb = self.dup
565
+ rgb.instance_variable_set(:@r, -rgb.r)
566
+ rgb.instance_variable_set(:@g, -rgb.g)
567
+ rgb.instance_variable_set(:@b, -rgb.b)
568
+ rgb
569
+ end
570
+
571
+ private
572
+ def normalize_percent(percent)
573
+ percent /= 100.0
574
+ percent += 1.0
575
+ percent = [ percent, 2.0 ].min
576
+ percent = [ 0.0, percent ].max
577
+ percent
578
+ end
579
+ end
580
+
581
+ class << Color::RGB
582
+ # Creates an RGB colour object from percentages 0..100.
583
+ #
584
+ # Color::RGB.from_percentage(10, 20, 30)
585
+ def from_percentage(r = 0, g = 0, b = 0, &block)
586
+ new(r, g, b, 100.0, &block)
587
+ end
588
+
589
+ # Creates an RGB colour object from fractional values 0..1.
590
+ #
591
+ # Color::RGB.from_fraction(.3, .2, .1)
592
+ def from_fraction(r = 0.0, g = 0.0, b = 0.0, &block)
593
+ new(r, g, b, 1.0, &block)
594
+ end
595
+
596
+ # Creates an RGB colour object from a grayscale fractional value 0..1.
597
+ def from_grayscale_fraction(l = 0.0, &block)
598
+ new(l, l, l, 1.0, &block)
599
+ end
600
+ alias_method :from_greyscale_fraction, :from_grayscale_fraction
601
+
602
+ # Creates an RGB colour object from an HTML colour descriptor (e.g.,
603
+ # <tt>"fed"</tt> or <tt>"#cabbed;"</tt>.
604
+ #
605
+ # Color::RGB.from_html("fed")
606
+ # Color::RGB.from_html("#fed")
607
+ # Color::RGB.from_html("#cabbed")
608
+ # Color::RGB.from_html("cabbed")
609
+ def from_html(html_colour, &block)
610
+ # When we can move to 1.9+ only, this will be \h
611
+ h = html_colour.scan(/[0-9a-f]/i)
612
+ case h.size
613
+ when 3
614
+ new(*h.map { |v| (v * 2).to_i(16) }, &block)
615
+ when 6
616
+ new(*h.each_slice(2).map { |v| v.join.to_i(16) }, &block)
617
+ else
618
+ raise ArgumentError, "Not a supported HTML colour type."
619
+ end
620
+ end
621
+
622
+ # Find or create a colour by an HTML hex code. This differs from the
623
+ # #from_html method in that if the colour code matches a named colour,
624
+ # the existing colour will be returned.
625
+ #
626
+ # Color::RGB.by_hex('ff0000').name # => 'red'
627
+ # Color::RGB.by_hex('ff0001').name # => nil
628
+ #
629
+ # If a block is provided, the value that is returned by the block will
630
+ # be returned instead of the exception caused by an error in providing a
631
+ # correct hex format.
632
+ def by_hex(hex, &block)
633
+ __by_hex.fetch(html_hexify(hex)) { from_html(hex) }
634
+ rescue
635
+ if block
636
+ block.call
637
+ else
638
+ raise
639
+ end
640
+ end
641
+
642
+ # Return a colour as identified by the colour name.
643
+ def by_name(name, &block)
644
+ __by_name.fetch(name.to_s.downcase, &block)
645
+ end
646
+
647
+ # Return a colour as identified by the colour name, or by hex.
648
+ def by_css(name_or_hex, &block)
649
+ by_name(name_or_hex) { by_hex(name_or_hex, &block) }
650
+ end
651
+
652
+ # Extract named or hex colours from the provided text.
653
+ def extract_colors(text, mode = :both)
654
+ text = text.downcase
655
+ regex = case mode
656
+ when :name
657
+ Regexp.union(__by_name.keys)
658
+ when :hex
659
+ Regexp.union(__by_hex.keys)
660
+ when :both
661
+ Regexp.union(__by_hex.keys + __by_name.keys)
662
+ end
663
+
664
+ text.scan(regex).map { |match|
665
+ case mode
666
+ when :name
667
+ by_name(match)
668
+ when :hex
669
+ by_hex(match)
670
+ when :both
671
+ by_css(match)
672
+ end
673
+ }
674
+ end
675
+ end
676
+
677
+ class << Color::RGB
678
+ private
679
+
680
+ def __named_color(mod, rgb, *names)
681
+ used = names - mod.constants.map(&:to_sym)
682
+ if used.length < names.length
683
+ raise ArgumentError, "#{names.join(', ')} already defined in #{mod}"
684
+ end
685
+
686
+ names.each { |n| mod.const_set(n, rgb) }
687
+
688
+ rgb.names = names
689
+ rgb.names.each { |n| __by_name[n] = rgb }
690
+ __by_hex[rgb.hex] = rgb
691
+ rgb.freeze
692
+ end
693
+
694
+ def __by_hex
695
+ @__by_hex ||= {}
696
+ end
697
+
698
+ def __by_name
699
+ @__by_name ||= {}
700
+ end
701
+
702
+ def html_hexify(hex)
703
+ # When we can move to 1.9+ only, this will be \h
704
+ h = hex.to_s.downcase.scan(/[0-9a-f]/)
705
+ case h.size
706
+ when 3
707
+ h.map { |v| (v * 2) }.join
708
+ when 6
709
+ h.join
710
+ else
711
+ raise ArgumentError, "Not a supported HTML colour type."
712
+ end
713
+ end
451
714
  end
452
715
 
453
- require 'color/rgb-colors'
716
+ require 'color/rgb/colors'