File Coverage

blib/lib/Color/ANSI/Util.pm
Criterion Covered Total %
statement 91 117 77.7
branch 36 62 58.0
condition 7 11 63.6
subroutine 26 28 92.8
pod 21 21 100.0
total 181 239 75.7


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