File Coverage

blib/lib/Color/RGB/Util.pm
Criterion Covered Total %
statement 250 289 86.5
branch 107 164 65.2
condition 26 53 49.0
subroutine 29 30 96.6
pod 24 24 100.0
total 436 560 77.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-01-19'; # DATE
5             our $DIST = 'Color-RGB-Util'; # DIST
6             our $VERSION = '0.603'; # VERSION
7              
8 1     1   83649 use 5.010001;
  1         15  
9 1     1   5 use strict;
  1         2  
  1         137  
10 1     1   8 use warnings;
  1         1  
  1         3944  
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   1289 my $h = shift;
51 12 100       46 $h %= 360 if abs($h) > 360;
52 12 100       55 $h >= 0 ? $h : 360+$h;
53             }
54              
55             sub assign_rgb_color {
56 9     9 1 3823 require Digest::SHA;
57              
58 9         3763 my ($str) = @_;
59              
60 9         63 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 2913 my $str = shift;
68              
69 3         9 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 2864 my $str = shift;
75              
76 3         9 my $rgb = assign_rgb_color($str);
77 3 100       12 rgb_is_light($rgb) ? $rgb : mix_2_rgb_colors($rgb, 'ffffff');
78             }
79              
80             sub int2rgb {
81 5     5 1 2809 my $int = shift;
82              
83 5         40 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 3221 my ($rgb1, $rgb2, $pct) = @_;
92              
93 9   100     44 $pct //= 0.5;
94              
95 9 100       92 my ($r1, $g1, $b1) =
96             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
97 8 50       47 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         74  
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 4044 my (@weights, @r, @g, @b);
111              
112 6         18 while (@_ >= 2) {
113 10         23 my ($rgb, $weight) = splice @_, 0, 2;
114 10 100       94 my ($r, $g, $b) = $rgb =~ $re_rgb
115             or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
116 9         22 push @r, hex $r;
117 9         14 push @g, hex $g;
118 9         15 push @b, hex $b;
119 9         24 push @weights, $weight;
120             }
121 5         8 my $tot_r = 0; for (0..$#r) { $tot_r += $r[$_]*$weights[$_] }
  5         16  
  9         18  
122 5         10 my $tot_g = 0; for (0..$#g) { $tot_g += $g[$_]*$weights[$_] }
  5         9  
  9         19  
123 5         6 my $tot_b = 0; for (0..$#b) { $tot_b += $b[$_]*$weights[$_] }
  5         11  
  9         14  
124 5         8 my $tot_weight = 0; $tot_weight += $_ for @weights;
  5         13  
125 5 100       27 die "Zero/negative total weight" unless $tot_weight > 0;
126              
127 3         31 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 86     86 1 3081 my ($rgb1, $rgb2) = @_;
136              
137 86   50     293 $rgb1 //= '000000';
138 86 50       443 my ($r1, $g1, $b1) =
139             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
140 86   50     298 $rgb2 //= 'ffffff';
141 86 50       380 my ($r2, $g2, $b2) =
142             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
143 86         169 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  516         772  
144              
145 86         480 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 3919 my $opts = ref $_[0] eq 'HASH' ? shift : {};
154 31   100     100 my $num = shift // 1;
155 31 50       64 my $light_color = exists($opts->{light_color}) ? $opts->{light_color} : 1;
156 31   50     85 my $max_attempts = $opts->{max_attempts} // 1000;
157 31         43 my $avoid_colors = $opts->{avoid_colors};
158              
159 31         35 my $num_check = 10;
160 31         58 my $min_distance = rgb_diff("000000", "ffffff", "approx2") / 2 / $num;
161              
162 31         53 my @res;
163 31         56 while (@res < $num) {
164 35         50 my $num_attempts = 0;
165 35         40 my $rgb;
166 35         42 while (1) {
167 56         90 $rgb = rand_rgb_color();
168 56         211 my $reject = 0;
169             REJECT: {
170 56 50       69 if ($light_color) {
  56 0       99  
171 56 100       85 do { $reject++; last } if rgb_is_dark($rgb);
  21         33  
  21         31  
172             } elsif (defined $light_color) {
173 0 0       0 do { $reject++; last } if rgb_is_light($rgb);
  0         0  
  0         0  
174             }
175 35 50 33     75 if ($avoid_colors && ref $avoid_colors eq 'ARRAY') {
176 0 0       0 do { $reject++; last } if grep { $rgb eq $_ } @$avoid_colors;
  0         0  
  0         0  
  0         0  
177             }
178 35 50 33     72 if ($avoid_colors && ref $avoid_colors eq 'HASH') {
179 0 0       0 do { $reject++; last } if $avoid_colors->{$rgb}
  0         0  
  0         0  
180             }
181              
182 35         64 for (1..$num_check) {
183 45 100       94 last if @res-$_ < 0;
184 10         18 my $prev_rgb = $res[ @res - $_ ];
185 10 50       19 do { $reject++; last REJECT } if rgb_diff($rgb, $prev_rgb, "approx2") < $min_distance;
  0         0  
  0         0  
186             }
187              
188             } # REJECT
189 56 100       104 last if !$reject;
190 21 50       43 last if ++$num_attempts >= $max_attempts;
191             }
192 35         88 push @res, $rgb;
193             }
194 31         79 @res;
195             }
196              
197             sub reverse_rgb_color {
198 1     1 1 3311 my ($rgb) = @_;
199              
200 1 50       16 my ($r, $g, $b) =
201             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
202 1         6 for ($r, $g, $b) { $_ = hex $_ }
  3         9  
203              
204 1         13 return sprintf("%02x%02x%02x", 255-$r, 255-$g, 255-$b);
205             }
206              
207             sub rgb2grayscale {
208 1     1 1 2915 my ($rgb) = @_;
209              
210 1 50       15 my ($r, $g, $b) =
211             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
212 1         5 for ($r, $g, $b) { $_ = hex $_ }
  3         8  
213              
214             # basically we just average the R, G, B
215 1         6 my $avg = int(($r + $g + $b)/3);
216 1         11 return sprintf("%02x%02x%02x", $avg, $avg, $avg);
217             }
218              
219             sub rgb2int {
220 65     65 1 2910 my $rgb = shift;
221              
222             # just to check
223 65 50       299 $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
224              
225 65         220 hex($rgb);
226             }
227              
228             sub rgb2sepia {
229 1     1 1 2816 my ($rgb) = @_;
230              
231 1 50       13 my ($r, $g, $b) =
232             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
233 1         4 for ($r, $g, $b) { $_ = hex $_ }
  3         9  
234              
235             # reference: http://www.techrepublic.com/blog/howdoi/how-do-i-convert-images-to-grayscale-and-sepia-tone-using-c/120
236 1         6 my $or = ($r*0.393) + ($g*0.769) + ($b*0.189);
237 1         3 my $og = ($r*0.349) + ($g*0.686) + ($b*0.168);
238 1         4 my $ob = ($r*0.272) + ($g*0.534) + ($b*0.131);
239 1 50       3 for ($or, $og, $ob) { $_ = 255 if $_ > 255 }
  3         9  
240 1         12 return sprintf("%02x%02x%02x", $or, $og, $ob);
241             }
242              
243             sub rgb_diff {
244 44     44 1 2888 my ($rgb1, $rgb2, $algo) = @_;
245              
246 44   50     80 $algo //= 'euclidean';
247              
248 44 50       261 my ($r1, $g1, $b1) =
249             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
250 44 50       212 my ($r2, $g2, $b2) =
251             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
252 44         91 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  264         375  
253              
254 44         67 my $dr2 = ($r1-$r2)**2;
255 44         72 my $dg2 = ($g1-$g2)**2;
256 44         65 my $db2 = ($b1-$b2)**2;
257              
258 44 50 66     137 if ($algo eq 'approx1' || $algo eq 'approx2') {
    0 0        
259 44         77 my $rm = ($r1 + $r2)/2;
260 44 100       76 if ($algo eq 'approx1') {
261 3         30 return (2*$dr2 + 4*$dg2 + 3*$db2 + $rm*($dr2 - $db2)/256 )**0.5;
262             } else { # approx2
263 41 100       75 if ($rm < 128) {
264 35         118 return (3*$dr2 + 4*$dg2 + 2*$db2)**0.5;
265             } else {
266 6         24 return (2*$dr2 + 4*$dg2 + 3*$db2)**0.5;
267             }
268             }
269             } elsif ($algo eq 'hsv_euclidean' || $algo eq 'hsv_hue1') {
270 0         0 my $hsv1 = rgb2hsv($rgb1);
271 0         0 my ($h1, $s1, $v1) = split / /, $hsv1;
272 0         0 my $hsv2 = rgb2hsv($rgb2);
273 0         0 my ($h2, $s2, $v2) = split / /, $hsv2;
274              
275 0         0 my $dh2 = ( _min(abs($h2-$h1), 360-abs($h2-$h1))/180 )**2;
276 0         0 my $ds2 = ( $s2-$s1 )**2;
277 0         0 my $dv2 = ( ($v2-$v1)/255.0 )**2;
278              
279 0 0       0 if ($algo eq 'hsv_hue1') {
280 0         0 return (5*$dh2 + $ds2 + $dv2)**0.5;
281             } else { # hsv_euclidean
282 0         0 return ($dh2 + $ds2 + $dv2)**0.5;
283             }
284             } else { # euclidean
285 0         0 return ($dr2 + $dg2 + $db2)**0.5;
286             }
287             }
288              
289             sub rgb_distance {
290 143     143 1 3044 my ($rgb1, $rgb2) = @_;
291              
292 143 50       813 my ($r1, $g1, $b1) =
293             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
294 143 50       682 my ($r2, $g2, $b2) =
295             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
296 143         287 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  858         1185  
297              
298 143         644 (($r1-$r2)**2 + ($g1-$g2)**2 + ($b1-$b2)**2)**0.5;
299             }
300              
301             sub rgb_is_dark {
302 63     63 1 2893 my ($rgb) = @_;
303 63 100       99 rgb_distance($rgb, "000000") < rgb_distance($rgb, "ffffff") ? 1:0;
304             }
305              
306             sub rgb_is_light {
307 7     7 1 2542 my ($rgb) = @_;
308 7 100       19 rgb_distance($rgb, "000000") > rgb_distance($rgb, "ffffff") ? 1:0;
309             }
310              
311             sub _rgb_luminance {
312 8     8   16 my ($r, $g, $b) = @_;
313 8         40 0.2126*$r/255 + 0.7152*$g/255 + 0.0722*$b/255;
314             }
315              
316             sub rgb_luminance {
317 3     3 1 2557 my ($rgb) = @_;
318              
319 3 50       33 my ($r, $g, $b) =
320             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
321 3         9 for ($r, $g, $b) { $_ = hex $_ }
  9         18  
322              
323 3         11 return _rgb_luminance($r, $g, $b);
324             }
325              
326             sub tint_rgb_color {
327 5     5 1 2468 my ($rgb1, $rgb2, $pct) = @_;
328              
329 5   100     18 $pct //= 0.5;
330              
331 5 50       55 my ($r1, $g1, $b1) =
332             $rgb1 =~ $re_rgb or die "Invalid rgb1 color '$rgb1', must be in 'ffffff' form";
333 5 50       29 my ($r2, $g2, $b2) =
334             $rgb2 =~ $re_rgb or die "Invalid rgb2 color '$rgb2', must be in 'ffffff' form";
335 5         16 for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ }
  30         47  
336              
337 5         14 my $lum = _rgb_luminance($r1, $g1, $b1);
338              
339 5         54 return sprintf("%02x%02x%02x",
340             $r1 + $pct*($r2-$r1)*$lum,
341             $g1 + $pct*($g2-$g1)*$lum,
342             $b1 + $pct*($b2-$b1)*$lum,
343             );
344             }
345              
346             sub rgb2hsl {
347 3     3 1 3144 my ($rgb) = @_;
348              
349 3 50       34 my ($r, $g, $b) =
350             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
351 3         9 for ($r, $g, $b) { $_ = hex($_)/255 }
  9         23  
352              
353 3         6 my $max = $r;
354 3         5 my $maxc = 'r';
355 3         4 my $min = $r;
356              
357 3 100       11 if ($g > $max) {
358 1         4 $max = $g;
359 1         2 $maxc = 'g';
360             }
361 3 100       8 if ($b > $max) {
362 1         3 $max = $b;
363 1         2 $maxc = 'b';
364             }
365              
366 3 100       10 if ($g < $min) {
367 1         3 $min = $g;
368             }
369 3 50       17 if ($b < $min) {
370 0         0 $min = $b;
371             }
372              
373 3         7 my ($h, $s, $l);
374 3 50       15 if ($max == $min) {
    100          
    100          
    50          
375 0         0 $h = 0;
376             } elsif ($maxc eq 'r') {
377 1         4 $h = 60 * (($g - $b) / ($max - $min)) % 360;
378             } elsif ($maxc eq 'g') {
379 1         4 $h = (60 * (($b - $r) / ($max - $min)) + 120);
380             } elsif ($maxc eq 'b') {
381 1         5 $h = (60 * (($r - $g) / ($max - $min)) + 240);
382             }
383              
384 3         7 $l = ($max + $min) / 2;
385              
386 3 50       11 if ($max == $min) {
    100          
387 0         0 $s = 0;
388             } elsif($l <= .5) {
389 2         4 $s = ($max - $min) / ($max + $min);
390             } else {
391 1         3 $s = ($max - $min) / (2 - ($max + $min));
392             }
393              
394 3         53 return sprintf("%.3g %.3g %.3g", $h, $s, $l);
395             }
396              
397             sub rgb2hsv {
398 3     3 1 2823 my ($rgb) = @_;
399              
400 3 50       35 my ($r, $g, $b) =
401             $rgb =~ $re_rgb or die "Invalid rgb color '$rgb', must be in 'ffffff' form";
402 3         11 for ($r, $g, $b) { $_ = hex($_)/255 }
  9         22  
403              
404 3         5 my $max = $r;
405 3         5 my $maxc = 'r';
406 3         6 my $min = $r;
407              
408 3 100       9 if ($g > $max) {
409 1         3 $max = $g;
410 1         2 $maxc = 'g';
411             }
412 3 100       8 if($b > $max) {
413 1         2 $max = $b;
414 1         2 $maxc = 'b';
415             }
416              
417 3 100       7 if($g < $min) {
418 1         3 $min = $g;
419             }
420 3 50       8 if($b < $min) {
421 0         0 $min = $b;
422             }
423              
424 3         7 my ($h, $s, $v);
425              
426 3 50       18 if ($max == $min) {
    100          
    100          
    50          
427 0         0 $h = 0;
428             } elsif ($maxc eq 'r') {
429 1         5 $h = 60 * (($g - $b) / ($max - $min)) % 360;
430             } elsif ($maxc eq 'g') {
431 1         3 $h = (60 * (($b - $r) / ($max - $min)) + 120);
432             } elsif ($maxc eq 'b') {
433 1         4 $h = (60 * (($r - $g) / ($max - $min)) + 240);
434             }
435              
436 3         6 $v = $max;
437 3 50       8 if($max == 0) {
438 0         0 $s = 0;
439             } else {
440 3         8 $s = 1 - ($min / $max);
441             }
442              
443 3         47 return sprintf("%.3g %.3g %.3g", $h, $s, $v);
444             }
445              
446             sub hsl2hsv {
447 9     9 1 2742 my $hsl = shift;
448              
449 9         35 my ($h, $s, $l) = split / /, $hsl;
450 9 100 66     66 $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     46  
451 9         14 my $_h = $h;
452 9         18 my $_s;
453             my $_v;
454              
455 9         20 $l *= 2;
456 9 100       24 $s *= ($l <= 1) ? $l : 2-$l;
457 9         18 $_v = ($l+$s) / 2;
458 9         18 $_s = (2*$s) / ($l+$s);
459              
460 9         94 "$_h $_s $_v";
461             }
462              
463             sub hsv2hsl {
464 5     5 1 2831 my $hsv = shift;
465              
466 5         20 my ($h, $s, $v) = split / /, $hsv;
467 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     24  
  5 50 33     25  
468 5         8 my $_h = $h;
469 5         10 my $_s = $s * $v;
470 5         10 my $_l = (2-$s) * $v;
471              
472 5 50       17 $_s /= $_l <= 1 ? ($_l==0 ? 1 : $_l) : (2-$_l);
    100          
473 5         10 $_l /= 2;
474              
475 5         51 "$_h $_s $_l";
476             }
477              
478             sub hsl2rgb {
479 4     4 1 2736 hsv2rgb(hsl2hsv(shift));
480             }
481              
482             sub hsv2rgb {
483 8     8 1 2860 my $hsv = shift;
484              
485 8         30 my ($h, $s, $v) = split / /, $hsv;
486 8 100 66     45 $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     37  
487              
488 8         22 my $i = int($h/60);
489 8         16 my $f = $h/60 - $i;
490 8         15 my $p = $v * (1-$s);
491 8         17 my $q = $v * (1-$f*$s);
492 8         15 my $t = $v * (1-(1-$f)*$s);
493              
494 8         14 my ($r, $g, $b);
495 8 100       34 if ($i==0) {
    50          
    100          
    50          
    50          
496 4         6 $r = $v; $g = $t; $b = $p;
  4         8  
  4         5  
497             } elsif ($i==1) {
498 0         0 $r = $q; $g = $v; $b = $p;
  0         0  
  0         0  
499             } elsif ($i==2) {
500 2         4 $r = $p; $g = $v; $b = $t;
  2         4  
  2         4  
501             } elsif ($i==3) {
502 0         0 $r = $p; $g = $q; $b = $v;
  0         0  
  0         0  
503             } elsif ($i==4) {
504 2         5 $r = $t; $g = $p; $b = $v;
  2         3  
  2         5  
505             } else {
506 0         0 $r = $v; $g = $p; $b = $q;
  0         0  
  0         0  
507             }
508              
509 8         68 return sprintf("%02x%02x%02x", $r*255, $g*255, $b*255);
510             }
511              
512             1;
513             # ABSTRACT: Utilities related to RGB colors
514              
515             __END__