File Coverage

blib/lib/Color/RGB/Util.pm
Criterion Covered Total %
statement 257 296 86.8
branch 115 172 66.8
condition 30 55 54.5
subroutine 29 30 96.6
pod 24 24 100.0
total 455 577 78.8


line stmt bran cond sub pod time code
1             package Color::RGB::Util;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-08-06'; # DATE
5             our $DIST = 'Color-RGB-Util'; # DIST
6             our $VERSION = '0.606'; # VERSION
7              
8 1     1   83557 use 5.010001;
  1         13  
9 1     1   5 use strict;
  1         2  
  1         21  
10 1     1   5 use warnings;
  1         2  
  1         4022  
11              
12             #use List::Util qw(min);
13              
14             require Exporter;
15             our @ISA = qw(Exporter);
16             our @EXPORT_OK = qw(
17             assign_rgb_color
18             assign_rgb_dark_color
19             assign_rgb_light_color
20             hsl2hsv
21             hsl2rgb
22             hsv2hsl
23             hsv2rgb
24             int2rgb
25             mix_2_rgb_colors
26             mix_rgb_colors
27             rand_rgb_color
28             rand_rgb_colors
29             reverse_rgb_color
30             rgb2grayscale
31             rgb2hsv
32             rgb2hsl
33             rgb2int
34             rgb2sepia
35             rgb_diff
36             rgb_distance
37             rgb_is_dark
38             rgb_is_light
39             rgb_luminance
40             tint_rgb_color
41             );
42              
43             my $re_rgb = qr/\A#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})\z/;
44              
45             sub _min {
46 0 0   0   0 $_[0] < $_[1] ? $_[0] : $_[1];
47             }
48              
49             sub _wrap_h {
50 12     12   1309 my $h = shift;
51 12 100       48 $h %= 360 if abs($h) > 360;
52 12 100       58 $h >= 0 ? $h : 360+$h;
53             }
54              
55             sub assign_rgb_color {
56 9     9 1 4001 require Digest::SHA;
57              
58 9         3896 my ($str) = @_;
59              
60 9         66 my $sha1 = Digest::SHA::sha1_hex($str);
61 9         51 substr($sha1, 0, 2) .
62             substr($sha1, 18, 2) .
63             substr($sha1, 38, 2);
64             }
65              
66             sub assign_rgb_dark_color {
67 3     3 1 3110 my $str = shift;
68              
69 3         20 my $rgb = assign_rgb_color($str);
70 3 100       24 rgb_is_dark($rgb) ? $rgb : mix_2_rgb_colors($rgb, '000000');
71             }
72              
73             sub assign_rgb_light_color {
74 3     3 1 2940 my $str = shift;
75              
76 3         9 my $rgb = assign_rgb_color($str);
77 3 100       11 rgb_is_light($rgb) ? $rgb : mix_2_rgb_colors($rgb, 'ffffff');
78             }
79              
80             sub int2rgb {
81 5     5 1 3059 my $int = shift;
82              
83 5         42 return sprintf("%02x%02x%02x",
84             ($int & 0xff0000) >> 16,
85             ($int & 0x00ff00) >> 8,
86             ($int & 0x0000ff),
87             );
88             }
89              
90             sub mix_2_rgb_colors {
91 9     9 1 3331 my ($rgb1, $rgb2, $pct) = @_;
92              
93 9   100     40 $pct //= 0.5;
94              
95 9 100       111 my ($r1, $g1, $b1) =
96             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
97 8 50       51 my ($r2, $g2, $b2) =
98             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
99 8         20 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  48         75  
100              
101 8         82 return sprintf("%02x%02x%02x",
102             $r1 + $pct*($r2-$r1),
103             $g1 + $pct*($g2-$g1),
104             $b1 + $pct*($b2-$b1),
105             );
106             }
107              
108             sub mix_rgb_colors {
109              
110 6     6 1 4110 my (@weights, @r, @g, @b);
111              
112 6         20 while (@_ >= 2) {
113 10         27 my ($rgb, $weight) = splice @_, 0, 2;
114 10 100       116 my ($r, $g, $b) = $rgb =~ $re_rgb
115             or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
116 9         23 push @r, hex $r;
117 9         15 push @g, hex $g;
118 9         14 push @b, hex $b;
119 9         22 push @weights, $weight;
120             }
121 5         8 my $tot_r = 0; for (0..$#r) { $tot_r += $r[$_]*$weights[$_] }
  5         15  
  9         17  
122 5         12 my $tot_g = 0; for (0..$#g) { $tot_g += $g[$_]*$weights[$_] }
  5         12  
  9         15  
123 5         18 my $tot_b = 0; for (0..$#b) { $tot_b += $b[$_]*$weights[$_] }
  5         12  
  9         16  
124 5         7 my $tot_weight = 0; $tot_weight += $_ for @weights;
  5         12  
125 5 100       28 die "Zero/negative total weight" unless $tot_weight > 0;
126              
127 3         29 return sprintf("%02x%02x%02x",
128             $tot_r / $tot_weight,
129             $tot_g / $tot_weight,
130             $tot_b / $tot_weight,
131             );
132             }
133              
134             sub rand_rgb_color {
135 104     104 1 3322 my ($rgb1, $rgb2) = @_;
136              
137 104   50     366 $rgb1 //= '000000';
138 104 50       568 my ($r1, $g1, $b1) =
139             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
140 104   50     386 $rgb2 //= 'ffffff';
141 104 50       485 my ($r2, $g2, $b2) =
142             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
143 104         284 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  624         879  
144              
145 104         581 return sprintf("%02x%02x%02x",
146             $r1 + rand()*($r2-$r1+1),
147             $g1 + rand()*($g2-$g1+1),
148             $b1 + rand()*($b2-$b1+1),
149             );
150             }
151              
152             sub rand_rgb_colors {
153 31 50   31 1 4088 my $opts = ref $_[0] eq 'HASH' ? shift : {};
154 31   100     107 my $num = shift // 1;
155 31 50       69 my $light_color = exists($opts->{light_color}) ? $opts->{light_color} : 1;
156 31   50     95 my $max_attempts = $opts->{max_attempts} // 1000;
157 31         46 my $avoid_colors = $opts->{avoid_colors};
158 31         43 my $hash_prefix = $opts->{hash_prefix};
159              
160 31         42 my $num_check = 10;
161 31         57 my $min_distance = rgb_diff("000000", "ffffff", "approx2") / 2 / $num;
162              
163 31         51 my @res;
164 31         64 while (@res < $num) {
165 35         49 my $num_attempts = 0;
166 35         48 my $rgb;
167 35         45 while (1) {
168 74         126 $rgb = rand_rgb_color();
169 74         133 my $reject = 0;
170             REJECT: {
171 74 50       98 if ($light_color) {
  74 0       134  
172 74 100       135 do { $reject++; last } if rgb_is_dark($rgb);
  39         62  
  39         51  
173             } elsif (defined $light_color) {
174 0 0       0 do { $reject++; last } if rgb_is_light($rgb);
  0         0  
  0         0  
175             }
176 35 50 33     79 if ($avoid_colors && ref $avoid_colors eq 'ARRAY') {
177 0 0       0 do { $reject++; last } if grep { $rgb eq $_ } @$avoid_colors;
  0         0  
  0         0  
  0         0  
178             }
179 35 50 33     71 if ($avoid_colors && ref $avoid_colors eq 'HASH') {
180 0 0       0 do { $reject++; last } if $avoid_colors->{$rgb}
  0         0  
  0         0  
181             }
182              
183 35         63 for (1..$num_check) {
184 45 100       110 last if @res-$_ < 0;
185 10         16 my $prev_rgb = $res[ @res - $_ ];
186 10 50       19 do { $reject++; last REJECT } if rgb_diff($rgb, $prev_rgb, "approx2") < $min_distance;
  0         0  
  0         0  
187             }
188              
189             } # REJECT
190 74 100       138 last if !$reject;
191 39 50       75 last if ++$num_attempts >= $max_attempts;
192             }
193 35 50       161 push @res, ($hash_prefix ? "#" : "") . $rgb;
194             }
195 31         96 @res;
196             }
197              
198             sub reverse_rgb_color {
199 1     1 1 3724 my ($rgb) = @_;
200              
201 1 50       17 my ($r, $g, $b) =
202             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
203 1         5 for ($r, $g, $b) { $_ = hex $_ }
  3         9  
204              
205 1         14 return sprintf("%02x%02x%02x", 255-$r, 255-$g, 255-$b);
206             }
207              
208             sub rgb2grayscale {
209 3     3 1 3208 my ($rgb, $algo) = @_;
210              
211 3 50       39 my ($r, $g, $b) =
212             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
213 3         9 for ($r, $g, $b) { $_ = hex $_ }
  9         20  
214              
215 3   100     15 $algo //= 'average';
216 3 100       13 if ($algo eq 'weighted_average') {
    100          
217 1         6 my $avg = int(0.299*$r + 0.587*$g + 0.114*$b);
218 1         9 return sprintf("%02x%02x%02x", $avg, $avg, $avg);
219             } elsif ($algo eq 'average') {
220 1         8 my $avg = int(($r + $g + $b)/3);
221 1         15 return sprintf("%02x%02x%02x", $avg, $avg, $avg);
222             } else {
223 1         15 die "Unknown algo '$algo'";
224             }
225             }
226              
227             sub rgb2int {
228 65     65 1 2952 my $rgb = shift;
229              
230             # just to check
231 65 50       331 $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
232              
233 65         261 hex($rgb);
234             }
235              
236             sub rgb2sepia {
237 1     1 1 2917 my ($rgb) = @_;
238              
239 1 50       23 my ($r, $g, $b) =
240             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
241 1         5 for ($r, $g, $b) { $_ = hex $_ }
  3         7  
242              
243             # reference: http://www.techrepublic.com/blog/howdoi/how-do-i-convert-images-to-grayscale-and-sepia-tone-using-c/120
244 1         7 my $or = ($r*0.393) + ($g*0.769) + ($b*0.189);
245 1         3 my $og = ($r*0.349) + ($g*0.686) + ($b*0.168);
246 1         3 my $ob = ($r*0.272) + ($g*0.534) + ($b*0.131);
247 1 50       3 for ($or, $og, $ob) { $_ = 255 if $_ > 255 }
  3         10  
248 1         24 return sprintf("%02x%02x%02x", $or, $og, $ob);
249             }
250              
251             sub rgb_diff {
252 45     45 1 2991 my ($rgb1, $rgb2, $algo) = @_;
253              
254 45   50     92 $algo //= 'euclidean';
255              
256 45 50       279 my ($r1, $g1, $b1) =
257             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
258 45 50       247 my ($r2, $g2, $b2) =
259             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
260 45         163 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  270         408  
261              
262 45         76 my $dr2 = ($r1-$r2)**2;
263 45         72 my $dg2 = ($g1-$g2)**2;
264 45         67 my $db2 = ($b1-$b2)**2;
265              
266 45 100 100     246 if ($algo eq 'approx1' || $algo eq 'approx2') {
    50 33        
    50          
267 44         89 my $rm = ($r1 + $r2)/2;
268 44 100       75 if ($algo eq 'approx1') {
269 3         64 return (2*$dr2 + 4*$dg2 + 3*$db2 + $rm*($dr2 - $db2)/256 )**0.5;
270             } else { # approx2
271 41 100       95 if ($rm < 128) {
272 33         154 return (3*$dr2 + 4*$dg2 + 2*$db2)**0.5;
273             } else {
274 8         36 return (2*$dr2 + 4*$dg2 + 3*$db2)**0.5;
275             }
276             }
277             } elsif ($algo eq 'hsv_euclidean' || $algo eq 'hsv_hue1') {
278 0         0 my $hsv1 = rgb2hsv($rgb1);
279 0         0 my ($h1, $s1, $v1) = split / /, $hsv1;
280 0         0 my $hsv2 = rgb2hsv($rgb2);
281 0         0 my ($h2, $s2, $v2) = split / /, $hsv2;
282              
283 0         0 my $dh2 = ( _min(abs($h2-$h1), 360-abs($h2-$h1))/180 )**2;
284 0         0 my $ds2 = ( $s2-$s1 )**2;
285 0         0 my $dv2 = ( ($v2-$v1)/255.0 )**2;
286              
287 0 0       0 if ($algo eq 'hsv_hue1') {
288 0         0 return (5*$dh2 + $ds2 + $dv2)**0.5;
289             } else { # hsv_euclidean
290 0         0 return ($dh2 + $ds2 + $dv2)**0.5;
291             }
292             } elsif ($algo eq 'euclidean') {
293 0         0 return ($dr2 + $dg2 + $db2)**0.5;
294             } else {
295 1         15 die "Unknown algo '$algo'";
296             }
297             }
298              
299             sub rgb_distance {
300 179     179 1 3101 my ($rgb1, $rgb2) = @_;
301              
302 179 50       1045 my ($r1, $g1, $b1) =
303             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
304 179 50       899 my ($r2, $g2, $b2) =
305             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
306 179         392 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  1074         1514  
307              
308 179         795 (($r1-$r2)**2 + ($g1-$g2)**2 + ($b1-$b2)**2)**0.5;
309             }
310              
311             sub rgb_is_dark {
312 81     81 1 2916 my ($rgb) = @_;
313 81 100       139 rgb_distance($rgb, "000000") < rgb_distance($rgb, "ffffff") ? 1:0;
314             }
315              
316             sub rgb_is_light {
317 7     7 1 2554 my ($rgb) = @_;
318 7 100       20 rgb_distance($rgb, "000000") > rgb_distance($rgb, "ffffff") ? 1:0;
319             }
320              
321             sub _rgb_luminance {
322 8     8   18 my ($r, $g, $b) = @_;
323 8         79 0.2126*$r/255 + 0.7152*$g/255 + 0.0722*$b/255;
324             }
325              
326             sub rgb_luminance {
327 3     3 1 2609 my ($rgb) = @_;
328              
329 3 50       45 my ($r, $g, $b) =
330             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
331 3         11 for ($r, $g, $b) { $_ = hex $_ }
  9         19  
332              
333 3         9 return _rgb_luminance($r, $g, $b);
334             }
335              
336             sub tint_rgb_color {
337 5     5 1 2587 my ($rgb1, $rgb2, $pct) = @_;
338              
339 5   100     26 $pct //= 0.5;
340              
341 5 50       52 my ($r1, $g1, $b1) =
342             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
343 5 50       75 my ($r2, $g2, $b2) =
344             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
345 5         17 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  30         55  
346              
347 5         14 my $lum = _rgb_luminance($r1, $g1, $b1);
348              
349 5         54 return sprintf("%02x%02x%02x",
350             $r1 + $pct*($r2-$r1)*$lum,
351             $g1 + $pct*($g2-$g1)*$lum,
352             $b1 + $pct*($b2-$b1)*$lum,
353             );
354             }
355              
356             sub rgb2hsl {
357 3     3 1 2631 my ($rgb) = @_;
358              
359 3 50       37 my ($r, $g, $b) =
360             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
361 3         11 for ($r, $g, $b) { $_ = hex($_)/255 }
  9         21  
362              
363 3         6 my $max = $r;
364 3         6 my $maxc = 'r';
365 3         5 my $min = $r;
366              
367 3 100       10 if ($g > $max) {
368 1         3 $max = $g;
369 1         3 $maxc = 'g';
370             }
371 3 100       8 if ($b > $max) {
372 1         3 $max = $b;
373 1         2 $maxc = 'b';
374             }
375              
376 3 100       17 if ($g < $min) {
377 1         2 $min = $g;
378             }
379 3 50       7 if ($b < $min) {
380 0         0 $min = $b;
381             }
382              
383 3         6 my ($h, $s, $l);
384 3 50       15 if ($max == $min) {
    100          
    100          
    50          
385 0         0 $h = 0;
386             } elsif ($maxc eq 'r') {
387 1         5 $h = 60 * (($g - $b) / ($max - $min)) % 360;
388             } elsif ($maxc eq 'g') {
389 1         4 $h = (60 * (($b - $r) / ($max - $min)) + 120);
390             } elsif ($maxc eq 'b') {
391 1         5 $h = (60 * (($r - $g) / ($max - $min)) + 240);
392             }
393              
394 3         7 $l = ($max + $min) / 2;
395              
396 3 50       9 if ($max == $min) {
    100          
397 0         0 $s = 0;
398             } elsif($l <= .5) {
399 2         6 $s = ($max - $min) / ($max + $min);
400             } else {
401 1         5 $s = ($max - $min) / (2 - ($max + $min));
402             }
403              
404 3         56 return sprintf("%.3g %.3g %.3g", $h, $s, $l);
405             }
406              
407             sub rgb2hsv {
408 3     3 1 2867 my ($rgb) = @_;
409              
410 3 50       36 my ($r, $g, $b) =
411             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
412 3         12 for ($r, $g, $b) { $_ = hex($_)/255 }
  9         21  
413              
414 3         5 my $max = $r;
415 3         6 my $maxc = 'r';
416 3         5 my $min = $r;
417              
418 3 100       11 if ($g > $max) {
419 1         3 $max = $g;
420 1         4 $maxc = 'g';
421             }
422 3 100       7 if($b > $max) {
423 1         3 $max = $b;
424 1         2 $maxc = 'b';
425             }
426              
427 3 100       9 if($g < $min) {
428 1         2 $min = $g;
429             }
430 3 50       9 if($b < $min) {
431 0         0 $min = $b;
432             }
433              
434 3         6 my ($h, $s, $v);
435              
436 3 50       16 if ($max == $min) {
    100          
    100          
    50          
437 0         0 $h = 0;
438             } elsif ($maxc eq 'r') {
439 1         6 $h = 60 * (($g - $b) / ($max - $min)) % 360;
440             } elsif ($maxc eq 'g') {
441 1         4 $h = (60 * (($b - $r) / ($max - $min)) + 120);
442             } elsif ($maxc eq 'b') {
443 1         6 $h = (60 * (($r - $g) / ($max - $min)) + 240);
444             }
445              
446 3         5 $v = $max;
447 3 50       8 if($max == 0) {
448 0         0 $s = 0;
449             } else {
450 3         8 $s = 1 - ($min / $max);
451             }
452              
453 3         51 return sprintf("%.3g %.3g %.3g", $h, $s, $v);
454             }
455              
456             sub hsl2hsv {
457 9     9 1 2872 my $hsl = shift;
458              
459 9         40 my ($h, $s, $l) = split / /, $hsl;
460 9 100 66     72 $h>=0 && $h<=360 or $h = _wrap_h($h); $s>=0 && $s<=1 or die "Invalid S in HSL '$hsl', must be in 0-1"; $l>=0 && $l<=1 or die "Invalid L in HSL '$hsl', must be in 0-1";
  9 50 33     37  
  9 50 33     47  
461 9         16 my $_h = $h;
462 9         19 my $_s;
463             my $_v;
464              
465 9         14 $l *= 2;
466 9 100       36 $s *= ($l <= 1) ? $l : 2-$l;
467 9         21 $_v = ($l+$s) / 2;
468 9         19 $_s = (2*$s) / ($l+$s);
469              
470 9         110 "$_h $_s $_v";
471             }
472              
473             sub hsv2hsl {
474 5     5 1 2907 my $hsv = shift;
475              
476 5         21 my ($h, $s, $v) = split / /, $hsv;
477 5 100 66     36 $h>=0 && $h<=360 or $h = _wrap_h($h); $s>=0 && $s<=1 or die "Invalid S in HSV '$hsv', must be in 0-1"; $v>=0 && $v<=1 or die "Invalid V in HSV '$hsv', must be in 0-1";
  5 50 33     23  
  5 50 33     23  
478 5         10 my $_h = $h;
479 5         10 my $_s = $s * $v;
480 5         8 my $_l = (2-$s) * $v;
481              
482 5 50       19 $_s /= $_l <= 1 ? ($_l==0 ? 1 : $_l) : (2-$_l);
    100          
483 5         9 $_l /= 2;
484              
485 5         61 "$_h $_s $_l";
486             }
487              
488             sub hsl2rgb {
489 4     4 1 3028 hsv2rgb(hsl2hsv(shift));
490             }
491              
492             sub hsv2rgb {
493 8     8 1 2970 my $hsv = shift;
494              
495 8         29 my ($h, $s, $v) = split / /, $hsv;
496 8 100 66     47 $h>=0 && $h<=360 or $h = _wrap_h($h); $s>=0 && $s<=1 or die "Invalid S in HSV '$hsv', must be in 0-1"; $v>=0 && $v<=1 or die "Invalid V in HSV '$hsv', must be in 0-1";
  8 50 33     33  
  8 50 33     36  
497              
498 8         20 my $i = int($h/60);
499 8         18 my $f = $h/60 - $i;
500 8         12 my $p = $v * (1-$s);
501 8         17 my $q = $v * (1-$f*$s);
502 8         16 my $t = $v * (1-(1-$f)*$s);
503              
504 8         13 my ($r, $g, $b);
505 8 100       39 if ($i==0) {
    50          
    100          
    50          
    50          
506 4         8 $r = $v; $g = $t; $b = $p;
  4         7  
  4         7  
507             } elsif ($i==1) {
508 0         0 $r = $q; $g = $v; $b = $p;
  0         0  
  0         0  
509             } elsif ($i==2) {
510 2         4 $r = $p; $g = $v; $b = $t;
  2         4  
  2         5  
511             } elsif ($i==3) {
512 0         0 $r = $p; $g = $q; $b = $v;
  0         0  
  0         0  
513             } elsif ($i==4) {
514 2         4 $r = $t; $g = $p; $b = $v;
  2         5  
  2         4  
515             } else {
516 0         0 $r = $v; $g = $p; $b = $q;
  0         0  
  0         0  
517             }
518              
519 8         71 return sprintf("%02x%02x%02x", $r*255, $g*255, $b*255);
520             }
521              
522             1;
523             # ABSTRACT: Utilities related to RGB colors
524              
525             __END__