File Coverage

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