File Coverage

blib/lib/Color/ANSI/Util.pm
Criterion Covered Total %
statement 84 108 77.7
branch 33 60 55.0
condition 8 11 72.7
subroutine 23 26 88.4
pod 21 21 100.0
total 169 226 74.7


line stmt bran cond sub pod time code
1             package Color::ANSI::Util;
2              
3             our $DATE = '2018-12-02'; # DATE
4             our $VERSION = '0.162'; # VERSION
5              
6 1     1   73934 use 5.010001;
  1         10  
7 1     1   5 use strict;
  1         2  
  1         37  
8 1     1   5 use warnings;
  1         2  
  1         2169  
9              
10             require Exporter;
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(
13             ansi16_to_rgb
14             rgb_to_ansi16
15             rgb_to_ansi16_fg_code
16             ansi16fg
17             rgb_to_ansi16_bg_code
18             ansi16bg
19              
20             ansi256_to_rgb
21             rgb_to_ansi256
22             rgb_to_ansi256_fg_code
23             ansi256fg
24             rgb_to_ansi256_bg_code
25             ansi256bg
26              
27             rgb_to_ansi24b_fg_code
28             ansi24bfg
29             rgb_to_ansi24b_bg_code
30             ansi24bbg
31              
32             rgb_to_ansi_fg_code
33             ansifg
34             rgb_to_ansi_bg_code
35             ansibg
36              
37             ansi_reset
38             );
39              
40             our %SPEC;
41              
42             my %ansi16 = (
43             0 => '000000',
44             1 => '800000',
45             2 => '008000',
46             3 => '808000',
47             4 => '000080',
48             5 => '800080',
49             6 => '008080',
50             7 => 'c0c0c0',
51             8 => '808080',
52             9 => 'ff0000',
53             10 => '00ff00',
54             11 => 'ffff00',
55             12 => '0000ff',
56             13 => 'ff00ff',
57             14 => '00ffff',
58             15 => 'ffffff',
59             );
60             my @revansi16;
61             for (sort {$a<=>$b} keys %ansi16) {
62             $ansi16{$_} =~ /(..)(..)(..)/;
63             push @revansi16, [hex($1), hex($2), hex($3), $_];
64             }
65              
66             my %ansi256 = (
67             %ansi16,
68              
69             16 => '000000', 17 => '00005f', 18 => '000087', 19 => '0000af', 20 => '0000d7', 21 => '0000ff',
70             22 => '005f00', 23 => '005f5f', 24 => '005f87', 25 => '005faf', 26 => '005fd7', 27 => '005fff',
71             28 => '008700', 29 => '00875f', 30 => '008787', 31 => '0087af', 32 => '0087d7', 33 => '0087ff',
72             34 => '00af00', 35 => '00af5f', 36 => '00af87', 37 => '00afaf', 38 => '00afd7', 39 => '00afff',
73             40 => '00d700', 41 => '00d75f', 42 => '00d787', 43 => '00d7af', 44 => '00d7d7', 45 => '00d7ff',
74             46 => '00ff00', 47 => '00ff5f', 48 => '00ff87', 49 => '00ffaf', 50 => '00ffd7', 51 => '00ffff',
75             52 => '5f0000', 53 => '5f005f', 54 => '5f0087', 55 => '5f00af', 56 => '5f00d7', 57 => '5f00ff',
76             58 => '5f5f00', 59 => '5f5f5f', 60 => '5f5f87', 61 => '5f5faf', 62 => '5f5fd7', 63 => '5f5fff',
77             64 => '5f8700', 65 => '5f875f', 66 => '5f8787', 67 => '5f87af', 68 => '5f87d7', 69 => '5f87ff',
78             70 => '5faf00', 71 => '5faf5f', 72 => '5faf87', 73 => '5fafaf', 74 => '5fafd7', 75 => '5fafff',
79             76 => '5fd700', 77 => '5fd75f', 78 => '5fd787', 79 => '5fd7af', 80 => '5fd7d7', 81 => '5fd7ff',
80             82 => '5fff00', 83 => '5fff5f', 84 => '5fff87', 85 => '5fffaf', 86 => '5fffd7', 87 => '5fffff',
81             88 => '870000', 89 => '87005f', 90 => '870087', 91 => '8700af', 92 => '8700d7', 93 => '8700ff',
82             94 => '875f00', 95 => '875f5f', 96 => '875f87', 97 => '875faf', 98 => '875fd7', 99 => '875fff',
83             100 => '878700', 101 => '87875f', 102 => '878787', 103 => '8787af', 104 => '8787d7', 105 => '8787ff',
84             106 => '87af00', 107 => '87af5f', 108 => '87af87', 109 => '87afaf', 110 => '87afd7', 111 => '87afff',
85             112 => '87d700', 113 => '87d75f', 114 => '87d787', 115 => '87d7af', 116 => '87d7d7', 117 => '87d7ff',
86             118 => '87ff00', 119 => '87ff5f', 120 => '87ff87', 121 => '87ffaf', 122 => '87ffd7', 123 => '87ffff',
87             124 => 'af0000', 125 => 'af005f', 126 => 'af0087', 127 => 'af00af', 128 => 'af00d7', 129 => 'af00ff',
88             130 => 'af5f00', 131 => 'af5f5f', 132 => 'af5f87', 133 => 'af5faf', 134 => 'af5fd7', 135 => 'af5fff',
89             136 => 'af8700', 137 => 'af875f', 138 => 'af8787', 139 => 'af87af', 140 => 'af87d7', 141 => 'af87ff',
90             142 => 'afaf00', 143 => 'afaf5f', 144 => 'afaf87', 145 => 'afafaf', 146 => 'afafd7', 147 => 'afafff',
91             148 => 'afd700', 149 => 'afd75f', 150 => 'afd787', 151 => 'afd7af', 152 => 'afd7d7', 153 => 'afd7ff',
92             154 => 'afff00', 155 => 'afff5f', 156 => 'afff87', 157 => 'afffaf', 158 => 'afffd7', 159 => 'afffff',
93             160 => 'd70000', 161 => 'd7005f', 162 => 'd70087', 163 => 'd700af', 164 => 'd700d7', 165 => 'd700ff',
94             166 => 'd75f00', 167 => 'd75f5f', 168 => 'd75f87', 169 => 'd75faf', 170 => 'd75fd7', 171 => 'd75fff',
95             172 => 'd78700', 173 => 'd7875f', 174 => 'd78787', 175 => 'd787af', 176 => 'd787d7', 177 => 'd787ff',
96             178 => 'd7af00', 179 => 'd7af5f', 180 => 'd7af87', 181 => 'd7afaf', 182 => 'd7afd7', 183 => 'd7afff',
97             184 => 'd7d700', 185 => 'd7d75f', 186 => 'd7d787', 187 => 'd7d7af', 188 => 'd7d7d7', 189 => 'd7d7ff',
98             190 => 'd7ff00', 191 => 'd7ff5f', 192 => 'd7ff87', 193 => 'd7ffaf', 194 => 'd7ffd7', 195 => 'd7ffff',
99             196 => 'ff0000', 197 => 'ff005f', 198 => 'ff0087', 199 => 'ff00af', 200 => 'ff00d7', 201 => 'ff00ff',
100             202 => 'ff5f00', 203 => 'ff5f5f', 204 => 'ff5f87', 205 => 'ff5faf', 206 => 'ff5fd7', 207 => 'ff5fff',
101             208 => 'ff8700', 209 => 'ff875f', 210 => 'ff8787', 211 => 'ff87af', 212 => 'ff87d7', 213 => 'ff87ff',
102             214 => 'ffaf00', 215 => 'ffaf5f', 216 => 'ffaf87', 217 => 'ffafaf', 218 => 'ffafd7', 219 => 'ffafff',
103             220 => 'ffd700', 221 => 'ffd75f', 222 => 'ffd787', 223 => 'ffd7af', 224 => 'ffd7d7', 225 => 'ffd7ff',
104             226 => 'ffff00', 227 => 'ffff5f', 228 => 'ffff87', 229 => 'ffffaf', 230 => 'ffffd7', 231 => 'ffffff',
105              
106             232 => '080808', 233 => '121212', 234 => '1c1c1c', 235 => '262626', 236 => '303030', 237 => '3a3a3a',
107             238 => '444444', 239 => '4e4e4e', 240 => '585858', 241 => '606060', 242 => '666666', 243 => '767676',
108             244 => '808080', 245 => '8a8a8a', 246 => '949494', 247 => '9e9e9e', 248 => 'a8a8a8', 249 => 'b2b2b2',
109             250 => 'bcbcbc', 251 => 'c6c6c6', 252 => 'd0d0d0', 253 => 'dadada', 254 => 'e4e4e4', 255 => 'eeeeee',
110             );
111             my @revansi256;
112             for (sort {$a<=>$b} keys %ansi256) {
113             $ansi256{$_} =~ /(..)(..)(..)/;
114             push @revansi256, [hex($1), hex($2), hex($3), $_];
115             }
116              
117             $SPEC{ansi16_to_rgb} = {
118             v => 1.1,
119             summary => 'Convert ANSI-16 color to RGB',
120             description => <<'_',
121              
122             Returns 6-hexdigit, e.g. 'ff00cc'.
123              
124             _
125             args => {
126             color => {
127             schema => 'color::ansi16*',
128             req => 1,
129             pos => 0,
130             },
131             },
132             args_as => 'array',
133             result => {
134             schema => 'color::rgb24*',
135             },
136             result_naked => 1,
137             };
138             sub ansi16_to_rgb {
139 4     4 1 931 my ($input) = @_;
140              
141 4 100       26 if ($input =~ /^\d+$/) {
    50          
142 2 50 33     9 if ($input >= 0 && $input <= 15) {
143 2         11 return $ansi16{$input + 0}; # to remove prefix zero e.g. "06"
144             } else {
145 0         0 die "Invalid ANSI 16-color number '$input'";
146             }
147             } elsif ($input =~ /^(?:(bold|bright) \s )?(black|red|green|yellow|blue|magenta|cyan|white)$/ix) {
148 2   100     13 my ($bold, $col) = (lc($1 // ""), lc($2));
149 2         3 my $i;
150 2 50       6 if ($col eq 'black') {
    50          
    0          
    0          
    0          
    0          
    0          
    0          
151 0         0 $i = 0;
152             } elsif ($col eq 'red') {
153 2         3 $i = 1;
154             } elsif ($col eq 'green') {
155 0         0 $i = 2;
156             } elsif ($col eq 'yellow') {
157 0         0 $i = 3;
158             } elsif ($col eq 'blue') {
159 0         0 $i = 4;
160             } elsif ($col eq 'magenta') {
161 0         0 $i = 5;
162             } elsif ($col eq 'cyan') {
163 0         0 $i = 6;
164             } elsif ($col eq 'white') {
165 0         0 $i = 7;
166             }
167 2 100       4 $i += 8 if $bold;
168 2         9 return $ansi16{$i};
169             } else {
170 0         0 die "Invalid ANSI 16-color name '$input'";
171             }
172             }
173              
174             sub _rgb_to_indexed {
175 16     16   25 my ($rgb, $table) = @_;
176              
177 16 50       84 $rgb =~ /^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/
178             or die "Invalid RGB input '$rgb'";
179 16         39 my $r = hex($1);
180 16         26 my $g = hex($2);
181 16         21 my $b = hex($3);
182              
183 16         22 my ($minsqdist, $res);
184 16         28 for my $e (@$table) {
185 730         1096 my $sqdist =
186             abs($e->[0]-$r)**2 + abs($e->[1]-$g)**2 + abs($e->[2]-$b)**2;
187             # exact match, return immediately
188 730 100       961 return $e->[3] if $sqdist == 0;
189 729 100 100     1596 if (!defined($minsqdist) || $minsqdist > $sqdist) {
190             #say "D:sqdist=$sqdist";
191 58         64 $minsqdist = $sqdist;
192 58         81 $res = $e->[3];
193             }
194             }
195 15         36 return $res;
196             }
197              
198             $SPEC{ansi256_to_rgb} = {
199             v => 1.1,
200             summary => 'Convert ANSI-256 color to RGB',
201             args => {
202             color => {
203             schema => 'color::ansi256*',
204             req => 1,
205             pos => 0,
206             },
207             },
208             args_as => 'array',
209             result => {
210             schema => 'color::rgb24',
211             },
212             result_naked => 1,
213             };
214             sub ansi256_to_rgb {
215 1     1 1 2202 my ($input) = @_;
216              
217 1         3 $input += 0;
218 1 50       5 exists($ansi256{$input}) or die "Invalid ANSI 256-color index '$input'";
219 1         4 $ansi256{$input};
220             }
221              
222             $SPEC{rgb_to_ansi16} = {
223             v => 1.1,
224             summary => 'Convert RGB to ANSI-16 color',
225             args => {
226             color => {
227             schema => 'color::rgb24*',
228             req => 1,
229             pos => 0,
230             },
231             },
232             args_as => 'array',
233             result => {
234             schema => 'color::ansi16*',
235             },
236             result_naked => 1,
237             };
238             sub rgb_to_ansi16 {
239 2     2 1 3 my ($input) = @_;
240 2         6 _rgb_to_indexed($input, \@revansi16);
241             }
242              
243             $SPEC{rgb_to_ansi256} = {
244             v => 1.1,
245             summary => 'Convert RGB to ANSI-256 color',
246             args => {
247             color => {
248             schema => 'color::rgb24*',
249             req => 1,
250             pos => 0,
251             },
252             },
253             args_as => 'array',
254             result => {
255             schema => 'color::ansi256*',
256             },
257             result_naked => 1,
258             };
259             sub rgb_to_ansi256 {
260 3     3 1 6 my ($input) = @_;
261 3         7 _rgb_to_indexed($input, \@revansi256);
262             }
263              
264             $SPEC{rgb_to_ansi16_fg_code} = {
265             v => 1.1,
266             summary => 'Convert RGB to ANSI-16 color escape sequence to change foreground color',
267             args => {
268             color => {
269             schema => 'color::rgb24*',
270             req => 1,
271             pos => 0,
272             },
273             },
274             args_as => 'array',
275             result => {
276             schema => 'str*',
277             },
278             result_naked => 1,
279             };
280             sub rgb_to_ansi16_fg_code {
281 4     4 1 9 my ($input) = @_;
282              
283 4         10 my $res = _rgb_to_indexed($input, \@revansi16);
284 4 100       22 return "\e[" . ($res >= 8 ? ($res+30-8) . ";1" : ($res+30)) . "m";
285             }
286              
287 1     1 1 4 sub ansi16fg { goto &rgb_to_ansi16_fg_code }
288              
289             $SPEC{rgb_to_ansi16_bg_code} = {
290             v => 1.1,
291             summary => 'Convert RGB to ANSI-16 color escape sequence to change background color',
292             args => {
293             color => {
294             schema => 'color::rgb24*',
295             req => 1,
296             pos => 0,
297             },
298             },
299             args_as => 'array',
300             result => {
301             schema => 'str*',
302             },
303             result_naked => 1,
304             };
305             sub rgb_to_ansi16_bg_code {
306 2     2 1 5 my ($input) = @_;
307              
308 2         5 my $res = _rgb_to_indexed($input, \@revansi16);
309 2 100       11 return "\e[" . ($res >= 8 ? ($res+40-8) : ($res+40)) . "m";
310             }
311              
312 1     1 1 3 sub ansi16bg { goto &rgb_to_ansi16_bg_code }
313              
314             $SPEC{rgb_to_ansi256_fg_code} = {
315             v => 1.1,
316             summary => 'Convert RGB to ANSI-256 color escape sequence to change foreground color',
317             args => {
318             color => {
319             schema => 'color::rgb24*',
320             req => 1,
321             pos => 0,
322             },
323             },
324             args_as => 'array',
325             result => {
326             schema => 'str*',
327             },
328             result_naked => 1,
329             };
330             sub rgb_to_ansi256_fg_code {
331 3     3 1 15 my ($input) = @_;
332              
333 3         8 my $res = _rgb_to_indexed($input, \@revansi16);
334 3         18 return "\e[38;5;${res}m";
335             }
336              
337 1     1 1 4 sub ansi256fg { goto &rgb_to_ansi256_fg_code }
338              
339             $SPEC{rgb_to_ansi256_bg_code} = {
340             v => 1.1,
341             summary => 'Convert RGB to ANSI-256 color escape sequence to change background color',
342             args => {
343             color => {
344             schema => 'color::rgb24*',
345             req => 1,
346             pos => 0,
347             },
348             },
349             args_as => 'array',
350             result => {
351             schema => 'str*',
352             },
353             result_naked => 1,
354             };
355             sub rgb_to_ansi256_bg_code {
356 2     2 1 3 my ($input) = @_;
357              
358 2         5 my $res = _rgb_to_indexed($input, \@revansi16);
359 2         12 return "\e[48;5;${res}m";
360             }
361              
362 1     1 1 4 sub ansi256bg { goto &rgb_to_ansi256_bg_code }
363              
364             $SPEC{rgb_to_ansi24b_fg_code} = {
365             v => 1.1,
366             summary => 'Convert RGB to ANSI 24bit-color escape sequence to change foreground color',
367             args => {
368             color => {
369             schema => 'color::rgb24*',
370             req => 1,
371             pos => 0,
372             },
373             },
374             args_as => 'array',
375             result => {
376             schema => 'str*',
377             },
378             result_naked => 1,
379             };
380             sub rgb_to_ansi24b_fg_code {
381 4     4 1 2169 my ($rgb) = @_;
382              
383 4         27 return sprintf("\e[38;2;%d;%d;%dm",
384             hex(substr($rgb, 0, 2)),
385             hex(substr($rgb, 2, 2)),
386             hex(substr($rgb, 4, 2)));
387             }
388              
389 1     1 1 3 sub ansi24bfg { goto &rgb_to_ansi24b_fg_code }
390              
391             $SPEC{rgb_to_ansi24b_bg_code} = {
392             v => 1.1,
393             summary => 'Convert RGB to ANSI 24bit-color escape sequence to change background color',
394             args => {
395             color => {
396             schema => 'color::rgb24*',
397             req => 1,
398             pos => 0,
399             },
400             },
401             args_as => 'array',
402             result => {
403             schema => 'str*',
404             },
405             result_naked => 1,
406             };
407             sub rgb_to_ansi24b_bg_code {
408 2     2 1 4 my ($rgb) = @_;
409              
410 2         15 return sprintf("\e[48;2;%d;%d;%dm",
411             hex(substr($rgb, 0, 2)),
412             hex(substr($rgb, 2, 2)),
413             hex(substr($rgb, 4, 2)));
414             }
415              
416 1     1 1 3 sub ansi24bbg { goto &rgb_to_ansi24b_bg_code }
417              
418             our $_use_termdetsw = 1;
419             our $_color_depth; # cache, can be set during testing
420             sub _color_depth {
421 7 50   7   17 unless (defined $_color_depth) {
422             {
423 7 50       10 if (exists $ENV{NO_COLOR}) {
  7         16  
424 0         0 $_color_depth = 0;
425 0         0 last;
426             }
427 7 100 66     27 if (defined $ENV{COLOR} && !$ENV{COLOR}) {
428 1         2 $_color_depth = 0;
429 1         2 last;
430             }
431 6 100       12 if (defined $ENV{COLOR_DEPTH}) {
432 4         8 $_color_depth = $ENV{COLOR_DEPTH};
433 4         5 last;
434             }
435 2 50       6 if ($_use_termdetsw) {
436 0         0 eval { require Term::Detect::Software };
  0         0  
437 0 0       0 if (!$@) {
438 0         0 $_color_depth = Term::Detect::Software::detect_terminal_cached()->{color_depth};
439 0         0 last;
440             }
441             }
442             # simple heuristic
443 2 100       5 if ($ENV{KONSOLE_DBUS_SERVICE}) {
444 1         2 $_color_depth = 2**24;
445 1         2 last;
446             }
447             # safe value
448 1         1 $_color_depth = 16;
449             }
450             };
451 7         14 $_color_depth;
452             }
453              
454             $SPEC{rgb_to_ansi_fg_code} = {
455             v => 1.1,
456             summary => 'Convert RGB to ANSI color escape sequence to change foreground color',
457             description => <<'_',
458              
459             Autodetect terminal capability and can return either empty string, 16-color,
460             256-color, or 24bit-code.
461              
462             Color depth used is determined by `COLOR_DEPTH` environment setting or from
463             if that module is available. In other words, this
464             function automatically chooses rgb_to_ansi{24b,256,16}_fg_code().
465              
466             _
467             args => {
468             color => {
469             schema => 'color::rgb24*',
470             req => 1,
471             pos => 0,
472             },
473             },
474             args_as => 'array',
475             result => {
476             schema => 'str*',
477             },
478             result_naked => 1,
479             };
480             sub rgb_to_ansi_fg_code {
481 7     7 1 13 my ($rgb) = @_;
482 7         11 my $cd = _color_depth();
483 7 100       24 if ($cd >= 2**24) {
    100          
    100          
484 2         3 rgb_to_ansi24b_fg_code($rgb);
485             } elsif ($cd >= 256) {
486 1         3 rgb_to_ansi256_fg_code($rgb);
487             } elsif ($cd >= 16) {
488 2         4 rgb_to_ansi16_fg_code($rgb);
489             } else {
490 2         7 "";
491             }
492             }
493              
494 7     7 1 4518 sub ansifg { goto &rgb_to_ansi_fg_code }
495              
496             $SPEC{rgb_to_ansi_bg_code} = {
497             v => 1.1,
498             summary => 'Convert RGB to ANSI color escape sequence to change background color',
499             description => <<'_',
500              
501             Autodetect terminal capability and can return either empty string, 16-color,
502             256-color, or 24bit-code.
503              
504             Which color depth used is determined by `COLOR_DEPTH` environment setting or
505             from if that module is available). In other words,
506             this function automatically chooses rgb_to_ansi{24b,256,16}_bg_code().
507              
508             _
509             args => {
510             color => {
511             schema => 'color::rgb24*',
512             req => 1,
513             pos => 0,
514             },
515             },
516             args_as => 'array',
517             result => {
518             schema => 'str*',
519             },
520             result_naked => 1,
521             };
522             sub rgb_to_ansi_bg_code {
523 0     0 1   my ($rgb) = @_;
524 0           my $cd = _color_depth();
525 0 0         if ($cd >= 2**24) {
    0          
526 0           rgb_to_ansi24b_bg_code($rgb);
527             } elsif ($cd >= 256) {
528 0           rgb_to_ansi256_bg_code($rgb);
529             } else {
530 0           rgb_to_ansi16_bg_code($rgb);
531             }
532             }
533              
534 0     0 1   sub ansibg { goto &rgb_to_ansi_bg_code }
535              
536 0     0 1   sub ansi_reset { "\e[0m" }
537              
538             1;
539             # ABSTRACT: Routines for dealing with ANSI colors
540              
541             __END__