Created
January 1, 2020 01:45
-
-
Save zoffixznet/4a14b510ac8af02887955139cbe7a7de to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
package Color::RGB::Util; | |
our $DATE = '2019-08-20'; # DATE | |
our $VERSION = '0.599'; # VERSION | |
use 5.010001; | |
use strict; | |
use warnings; | |
#use List::Util qw(min); | |
require Exporter; | |
our @ISA = qw(Exporter); | |
our @EXPORT_OK = qw( | |
assign_rgb_color | |
assign_rgb_dark_color | |
assign_rgb_light_color | |
int2rgb | |
mix_2_rgb_colors | |
mix_rgb_colors | |
rand_rgb_color | |
rand_rgb_colors | |
reverse_rgb_color | |
rgb2grayscale | |
rgb2hsv | |
rgb2hsl | |
rgb2int | |
rgb2sepia | |
rgb_diff | |
rgb_distance | |
rgb_is_dark | |
rgb_is_light | |
rgb_luminance | |
tint_rgb_color | |
); | |
my $re_rgb = qr/\A#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})\z/; | |
sub _min { | |
$_[0] < $_[1] ? $_[0] : $_[1]; | |
} | |
sub assign_rgb_color { | |
require Digest::SHA; | |
my ($str) = @_; | |
my $sha1 = Digest::SHA::sha1_hex($str); | |
substr($sha1, 0, 2) . | |
substr($sha1, 18, 2) . | |
substr($sha1, 38, 2); | |
} | |
sub assign_rgb_dark_color { | |
my $str = shift; | |
my $rgb = assign_rgb_color($str); | |
rgb_is_dark($rgb) ? $rgb : mix_2_rgb_colors($rgb, '000000'); | |
} | |
sub assign_rgb_light_color { | |
my $str = shift; | |
my $rgb = assign_rgb_color($str); | |
rgb_is_light($rgb) ? $rgb : mix_2_rgb_colors($rgb, 'ffffff'); | |
} | |
sub int2rgb { | |
my $int = shift; | |
return sprintf("%02x%02x%02x", | |
($int & 0xff0000) >> 16, | |
($int & 0x00ff00) >> 8, | |
($int & 0x0000ff), | |
); | |
} | |
sub mix_2_rgb_colors { | |
my ($rgb1, $rgb2, $pct) = @_; | |
$pct //= 0.5; | |
my ($r1, $g1, $b1) = | |
$rgb1 =~ $re_rgb or die "Invalid rgb1 color, must be in 'ffffff' form"; | |
my ($r2, $g2, $b2) = | |
$rgb2 =~ $re_rgb or die "Invalid rgb2 color, must be in 'ffffff' form"; | |
for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ } | |
return sprintf("%02x%02x%02x", | |
$r1 + $pct*($r2-$r1), | |
$g1 + $pct*($g2-$g1), | |
$b1 + $pct*($b2-$b1), | |
); | |
} | |
sub mix_rgb_colors { | |
my (@weights, @r, @g, @b); | |
while (@_ >= 2) { | |
my ($rgb, $weight) = splice @_, 0, 2; | |
my ($r, $g, $b) = $rgb =~ $re_rgb | |
or die "Invalid rgb color '$rgb', must be in 'ffffff' form"; | |
push @r, hex $r; | |
push @g, hex $g; | |
push @b, hex $b; | |
push @weights, $weight; | |
} | |
my $tot_r = 0; for (0..$#r) { $tot_r += $r[$_]*$weights[$_] } | |
my $tot_g = 0; for (0..$#g) { $tot_g += $g[$_]*$weights[$_] } | |
my $tot_b = 0; for (0..$#b) { $tot_b += $b[$_]*$weights[$_] } | |
my $tot_weight = 0; $tot_weight += $_ for @weights; | |
die "Zero/negative total weight" unless $tot_weight > 0; | |
return sprintf("%02x%02x%02x", | |
$tot_r / $tot_weight, | |
$tot_g / $tot_weight, | |
$tot_b / $tot_weight, | |
); | |
} | |
sub rand_rgb_color { | |
my ($rgb1, $rgb2) = @_; | |
$rgb1 //= '000000'; | |
my ($r1, $g1, $b1) = | |
$rgb1 =~ $re_rgb or die "Invalid rgb1 color, must be in 'ffffff' form"; | |
$rgb2 //= 'ffffff'; | |
my ($r2, $g2, $b2) = | |
$rgb2 =~ $re_rgb or die "Invalid rgb2 color, must be in 'ffffff' form"; | |
for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ } | |
return sprintf("%02x%02x%02x", | |
$r1 + rand()*($r2-$r1+1), | |
$g1 + rand()*($g2-$g1+1), | |
$b1 + rand()*($b2-$b1+1), | |
); | |
} | |
sub rand_rgb_colors { | |
my $opts = ref $_[0] eq 'HASH' ? shift : {}; | |
my $num = shift // 1; | |
my $light_color = exists($opts->{light_color}) ? $opts->{light_color} : 1; | |
my $max_attempts = $opts->{max_attempts} // 1000; | |
my $avoid_colors = $opts->{avoid_colors}; | |
my @res; | |
while (@res < $num) { | |
my $num_attempts = 0; | |
my $rgb; | |
while (1) { | |
$rgb = rand_rgb_color(); | |
my $reject = 0; | |
REJECT: { | |
if ($light_color) { | |
do { $reject++; last } if rgb_is_dark($rgb); | |
} elsif (defined $light_color) { | |
do { $reject++; last } if rgb_is_light($rgb); | |
} | |
if ($avoid_colors && ref $avoid_colors eq 'ARRAY') { | |
do { $reject++; last } if grep { $rgb eq $_ } @$avoid_colors; | |
} | |
if ($avoid_colors && ref $avoid_colors eq 'HASH') { | |
do { $reject++; last } if $avoid_colors->{$rgb} | |
} | |
} # REJECT | |
last if !$reject; | |
last if ++$num_attempts >= $max_attempts; | |
} | |
push @res, $rgb; | |
} | |
@res; | |
} | |
sub reverse_rgb_color { | |
my ($rgb) = @_; | |
my ($r, $g, $b) = | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
for ($r, $g, $b) { $_ = hex $_ } | |
return sprintf("%02x%02x%02x", 255-$r, 255-$g, 255-$b); | |
} | |
sub rgb2grayscale { | |
my ($rgb) = @_; | |
my ($r, $g, $b) = | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
for ($r, $g, $b) { $_ = hex $_ } | |
# basically we just average the R, G, B | |
my $avg = int(($r + $g + $b)/3); | |
return sprintf("%02x%02x%02x", $avg, $avg, $avg); | |
} | |
sub rgb2int { | |
my $rgb = shift; | |
# just to check | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
hex($rgb); | |
} | |
sub rgb2sepia { | |
my ($rgb) = @_; | |
my ($r, $g, $b) = | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
for ($r, $g, $b) { $_ = hex $_ } | |
# reference: http://www.techrepublic.com/blog/howdoi/how-do-i-convert-images-to-grayscale-and-sepia-tone-using-c/120 | |
my $or = ($r*0.393) + ($g*0.769) + ($b*0.189); | |
my $og = ($r*0.349) + ($g*0.686) + ($b*0.168); | |
my $ob = ($r*0.272) + ($g*0.534) + ($b*0.131); | |
for ($or, $og, $ob) { $_ = 255 if $_ > 255 } | |
return sprintf("%02x%02x%02x", $or, $og, $ob); | |
} | |
sub rgb_diff { | |
my ($rgb1, $rgb2, $algo) = @_; | |
$algo //= 'euclidean'; | |
my ($r1, $g1, $b1) = | |
$rgb1 =~ $re_rgb or die "Invalid rgb1 color, must be in 'ffffff' form"; | |
my ($r2, $g2, $b2) = | |
$rgb2 =~ $re_rgb or die "Invalid rgb2 color, must be in 'ffffff' form"; | |
for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ } | |
my $dr2 = ($r1-$r2)**2; | |
my $dg2 = ($g1-$g2)**2; | |
my $db2 = ($b1-$b2)**2; | |
if ($algo eq 'approx1' || $algo eq 'approx2') { | |
my $rm = ($r1 + $r2)/2; | |
if ($algo eq 'approx1') { | |
return (2*$dr2 + 4*$dg2 + 3*$db2 + $rm*($dr2 - $db2)/256 )**0.5; | |
} else { # approx2 | |
if ($rm < 128) { | |
return (3*$dr2 + 4*$dg2 + 2*$db2)**0.5; | |
} else { | |
return (2*$dr2 + 4*$dg2 + 3*$db2)**0.5; | |
} | |
} | |
} elsif ($algo eq 'hsv_euclidean' || $algo eq 'hsv_hue1') { | |
my $hsv1 = rgb2hsv($rgb1); | |
my ($h1, $s1, $v1) = split / /, $hsv1; | |
my $hsv2 = rgb2hsv($rgb2); | |
my ($h2, $s2, $v2) = split / /, $hsv2; | |
my $dh2 = ( _min(abs($h2-$h1), 360-abs($h2-$h1))/180 )**2; | |
my $ds2 = ( $s2-$s1 )**2; | |
my $dv2 = ( ($v2-$v1)/255.0 )**2; | |
if ($algo eq 'hsv_hue1') { | |
return (5*$dh2 + $ds2 + $dv2)**0.5; | |
} else { # hsv_euclidean | |
return ($dh2 + $ds2 + $dv2)**0.5; | |
} | |
} else { # euclidean | |
return ($dr2 + $dg2 + $db2)**0.5; | |
} | |
} | |
sub rgb_distance { | |
my ($rgb1, $rgb2) = @_; | |
my ($r1, $g1, $b1) = | |
$rgb1 =~ $re_rgb or die "Invalid rgb1 color, must be in 'ffffff' form"; | |
my ($r2, $g2, $b2) = | |
$rgb2 =~ $re_rgb or die "Invalid rgb2 color, must be in 'ffffff' form"; | |
for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ } | |
(($r1-$r2)**2 + ($g1-$g2)**2 + ($b1-$b2)**2)**0.5; | |
} | |
sub rgb_is_dark { | |
my ($rgb) = @_; | |
rgb_distance($rgb, "000000") < rgb_distance($rgb, "ffffff") ? 1:0; | |
} | |
sub rgb_is_light { | |
my ($rgb) = @_; | |
rgb_distance($rgb, "000000") > rgb_distance($rgb, "ffffff") ? 1:0; | |
} | |
sub _rgb_luminance { | |
my ($r, $g, $b) = @_; | |
0.2126*$r/255 + 0.7152*$g/255 + 0.0722*$b/255; | |
} | |
sub rgb_luminance { | |
my ($rgb) = @_; | |
my ($r, $g, $b) = | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
for ($r, $g, $b) { $_ = hex $_ } | |
return _rgb_luminance($r, $g, $b); | |
} | |
sub tint_rgb_color { | |
my ($rgb1, $rgb2, $pct) = @_; | |
$pct //= 0.5; | |
my ($r1, $g1, $b1) = | |
$rgb1 =~ $re_rgb or die "Invalid rgb1 color, must be in 'ffffff' form"; | |
my ($r2, $g2, $b2) = | |
$rgb2 =~ $re_rgb or die "Invalid rgb2 color, must be in 'ffffff' form"; | |
for ($r1, $g1, $b1, $r2, $g2, $b2) { $_ = hex $_ } | |
my $lum = _rgb_luminance($r1, $g1, $b1); | |
return sprintf("%02x%02x%02x", | |
$r1 + $pct*($r2-$r1)*$lum, | |
$g1 + $pct*($g2-$g1)*$lum, | |
$b1 + $pct*($b2-$b1)*$lum, | |
); | |
} | |
sub rgb2hsl { | |
my ($rgb) = @_; | |
my ($r, $g, $b) = | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
for ($r, $g, $b) { $_ = hex($_)/255 } | |
my $max = $r; | |
my $maxc = 'r'; | |
my $min = $r; | |
if ($g > $max) { | |
$max = $g; | |
$maxc = 'g'; | |
} | |
if ($b > $max) { | |
$max = $b; | |
$maxc = 'b'; | |
} | |
if ($g < $min) { | |
$min = $g; | |
} | |
if ($b < $min) { | |
$min = $b; | |
} | |
my ($h, $s, $l); | |
if ($max == $min) { | |
$h = 0; | |
} elsif ($maxc eq 'r') { | |
$h = 60 * (($g - $b) / ($max - $min)) % 360; | |
} elsif ($maxc eq 'g') { | |
$h = (60 * (($b - $r) / ($max - $min)) + 120); | |
} elsif ($maxc eq 'b') { | |
$h = (60 * (($r - $g) / ($max - $min)) + 240); | |
} | |
$l = ($max + $min) / 2; | |
if ($max == $min) { | |
$s = 0; | |
} elsif($l <= .5) { | |
$s = ($max - $min) / ($max + $min); | |
} else { | |
$s = ($max - $min) / (2 - ($max + $min)); | |
} | |
return sprintf("%.3g %.3g %.3g", $h, $s, $l); | |
} | |
sub rgb2hsv { | |
my ($rgb) = @_; | |
my ($r, $g, $b) = | |
$rgb =~ $re_rgb or die "Invalid rgb color, must be in 'ffffff' form"; | |
for ($r, $g, $b) { $_ = hex($_)/255 } | |
my $max = $r; | |
my $maxc = 'r'; | |
my $min = $r; | |
if ($g > $max) { | |
$max = $g; | |
$maxc = 'g'; | |
} | |
if($b > $max) { | |
$max = $b; | |
$maxc = 'b'; | |
} | |
if($g < $min) { | |
$min = $g; | |
} | |
if($b < $min) { | |
$min = $b; | |
} | |
my ($h, $s, $v); | |
if ($max == $min) { | |
$h = 0; | |
} elsif ($maxc eq 'r') { | |
$h = 60 * (($g - $b) / ($max - $min)) % 360; | |
} elsif ($maxc eq 'g') { | |
$h = (60 * (($b - $r) / ($max - $min)) + 120); | |
} elsif ($maxc eq 'b') { | |
$h = (60 * (($r - $g) / ($max - $min)) + 240); | |
} | |
$v = $max; | |
if($max == 0) { | |
$s = 0; | |
} else { | |
$s = 1 - ($min / $max); | |
} | |
return sprintf("%.3g %.3g %.3g", $h, $s, $v); | |
} | |
1; | |
#!/usr/bin/env perl | |
# vim: set ts=2 sts=2 sw=2 expandtab smarttab: | |
# | |
# This file is part of Parse-ANSIColor-Tiny | |
# | |
# This software is copyright (c) 2011 by Randy Stauner. | |
# | |
# This is free software; you can redistribute it and/or modify it under | |
# the same terms as the Perl 5 programming language system itself. | |
# | |
use strict; | |
use warnings; | |
our %RGB_MAP = qw( | |
000000 black | |
FF0000 red | |
008000 green | |
FFFF00 yellow | |
0000FF blue | |
FF00FF magenta | |
00FFFF cyan | |
FFFFFF white | |
); | |
package Parse::ANSIColor::Tiny; | |
# git description: v0.600-2-gba6391f | |
our $AUTHORITY = 'cpan:RWSTAUNER'; | |
# ABSTRACT: Determine attributes of ANSI-Colored string | |
$Parse::ANSIColor::Tiny::VERSION = '0.601'; | |
our @COLORS = qw( black red green yellow blue magenta cyan white ); | |
our %FOREGROUND = ( | |
(map { ( $COLORS[$_] => 30 + $_ ) } 0 .. $#COLORS), | |
(map { ( 'bright_' . $COLORS[$_] => 90 + $_ ) } 0 .. $#COLORS), | |
); | |
our %BACKGROUND = ( | |
(map { ( 'on_' . $COLORS[$_] => 40 + $_ ) } 0 .. $#COLORS), | |
(map { ('on_bright_' . $COLORS[$_] => 100 + $_ ) } 0 .. $#COLORS), | |
); | |
our %ATTRIBUTES = ( | |
clear => 0, | |
reset => 0, | |
bold => 1, | |
dark => 2, | |
faint => 2, | |
underline => 4, | |
underscore => 4, | |
blink => 5, | |
reverse => 7, | |
concealed => 8, | |
reverse_off => 27, | |
reset_foreground => 39, | |
reset_background => 49, | |
%FOREGROUND, | |
%BACKGROUND, | |
); | |
# Generating the 256-color codes involves a lot of codes and offsets that are | |
# not helped by turning them into constants. | |
## no critic (ValuesAndExpressions::ProhibitMagicNumbers) | |
our @COLORS256; | |
# The first 16 256-color codes are duplicates of the 16 ANSI colors, | |
# included for completeness. | |
for my $code (0 .. 15) { | |
my $name = "ansi$code"; | |
$ATTRIBUTES{$name} = "38;5;$code"; | |
$ATTRIBUTES{"on_$name"} = "48;5;$code"; | |
push @COLORS256, $name; | |
} | |
# 256-color RGB colors. Red, green, and blue can each be values 0 through 5, | |
# and the resulting 216 colors start with color 16. | |
for my $r (0 .. 5) { | |
for my $g (0 .. 5) { | |
for my $b (0 .. 5) { | |
my $code = 16 + (6 * 6 * $r) + (6 * $g) + $b; | |
my $name = "rgb$r$g$b"; | |
$ATTRIBUTES{$name} = "38;5;$code"; | |
$ATTRIBUTES{"on_$name"} = "48;5;$code"; | |
push @COLORS256, $name; | |
} | |
} | |
} | |
# The last 256-color codes are 24 shades of grey. | |
for my $n (0 .. 23) { | |
my $code = $n + 232; | |
my $name = "grey$n"; | |
$ATTRIBUTES{$name} = "38;5;$code"; | |
$ATTRIBUTES{"on_$name"} = "48;5;$code"; | |
push @COLORS256, $name; | |
} | |
# copied from Term::ANSIColor | |
our %ATTRIBUTES_R; | |
# Reverse lookup. Alphabetically first name for a sequence is preferred. | |
for (reverse sort keys %ATTRIBUTES) { | |
$ATTRIBUTES_R{$ATTRIBUTES{$_}} = $_; | |
} | |
sub new { | |
my $class = shift; | |
my $self = { | |
remove_escapes => 1, | |
@_ == 1 ? %{ $_[0] } : @_, | |
}; | |
$self->{process} = 1 | |
if $self->{auto_reverse}; | |
# fix incorrectly specified attributes | |
($self->{background} ||= 'black') =~ s/^(on_)*/on_/; | |
($self->{foreground} ||= 'white') =~ s/^(on_)*//; | |
bless $self, $class; | |
} | |
sub colors { | |
return (@COLORS, @COLORS256); | |
} | |
sub foreground_colors { | |
return ( | |
@COLORS, | |
(map { "bright_$_" } @COLORS), | |
@COLORS256, | |
); | |
} | |
sub background_colors { | |
return ( | |
(map { "on_$_" } @COLORS), | |
(map { "on_bright_$_" } @COLORS), | |
(map { "on_$_" } @COLORS256), | |
); | |
} | |
sub __separate_and_normalize { | |
my ($codes) = @_; | |
# Treat empty as "clear". | |
defined($codes) && length($codes) | |
or return 0; | |
# Replace empty (clear) with zero to simplify parsing and return values. | |
$codes =~ s/^;/0;/; | |
$codes =~ s/;$/;0/; | |
# Insert a zero between two semicolons (use look-ahead to get /g to find all). | |
$codes =~ s/;(?=;)/;0/g; | |
# Remove any leading zeros from (sections of) codes. | |
$codes =~ s/\b0+(?=\d)//g; | |
# Return all matches (of extended sequences or digits). | |
return $codes =~ m{ ( [34]8;5;\d+ | \d+) }xg; | |
} | |
sub identify { | |
my ($self, @codes) = @_; | |
local $_; | |
return | |
grep { defined } | |
map { $ATTRIBUTES_R{ $_ } } | |
map { __separate_and_normalize($_) } | |
@codes; | |
} | |
sub normalize { | |
my $self = shift; | |
my @norm; | |
foreach my $attr ( @_ ){ | |
if( $attr eq 'clear' ){ | |
@norm = (); | |
} | |
elsif( $attr eq 'reverse_off' ){ | |
# reverse_off cancels reverse | |
@norm = grep { $_ ne 'reverse' } @norm; | |
} | |
elsif( $attr eq 'reset_foreground' ){ | |
@norm = grep { !exists $FOREGROUND{$_} } @norm; | |
} | |
elsif( $attr eq 'reset_background' ){ | |
@norm = grep { !exists $BACKGROUND{$_} } @norm; | |
} | |
else { | |
# remove previous (duplicate) occurrences of this attribute | |
@norm = grep { $_ ne $attr } @norm; | |
# new fg color overwrites previous fg | |
@norm = grep { !exists $FOREGROUND{$_} } @norm if exists $FOREGROUND{$attr}; | |
# new bg color overwrites previous bg | |
@norm = grep { !exists $BACKGROUND{$_} } @norm if exists $BACKGROUND{$attr}; | |
push @norm, $attr; | |
} | |
} | |
return @norm; | |
} | |
sub parse { | |
my ($self, $orig) = @_; | |
my $last_pos = 0; | |
my $last_attr = []; | |
my $processed = []; | |
my $parsed = []; | |
# Strip escape sequences that we aren't going to use | |
$orig = $self->remove_escape_sequences($orig) | |
if $self->{remove_escapes}; | |
while( $orig =~ m/(\e\[([0-9;]*)m)/mg ){ | |
my $seq = $1; | |
my $attrs = $2; | |
my $cur_pos = pos($orig); | |
my $len = ($cur_pos - length($seq)) - $last_pos; | |
push @$parsed, [ | |
$processed, | |
substr($orig, $last_pos, $len) | |
] | |
# don't bother with empty strings | |
if $len; | |
$last_pos = $cur_pos; | |
$last_attr = [$self->normalize(@$last_attr, $self->identify($attrs))]; | |
$processed = $self->{process} ? [$self->process(@$last_attr)] : $last_attr; | |
} | |
push @$parsed, [ | |
$processed, | |
substr($orig, $last_pos) | |
] | |
# if there's any string left | |
if $last_pos < length($orig); | |
return $parsed; | |
} | |
sub process { | |
my ($self, @attr) = @_; | |
@attr = $self->process_reverse(@attr) if $self->{auto_reverse}; | |
return @attr; | |
} | |
sub process_reverse { | |
my $self = shift; | |
my ($rev, $fg, $bg, @attr); | |
my $i = 0; | |
foreach my $attr ( @_ ){ | |
if( $attr eq 'reverse' ){ | |
$rev = 1; | |
next; | |
} | |
elsif( $FOREGROUND{ $attr } ){ | |
$fg = $i; | |
} | |
elsif( $BACKGROUND{ $attr } ){ | |
$bg = $i; | |
} | |
push @attr, $attr; | |
$i++; | |
} | |
# maintain order for consistency with other methods | |
if( $rev ){ | |
# if either color is missing then the default colors should be reversed | |
{ | |
$attr[ $fg = $i++ ] = $self->{foreground} if !defined $fg; | |
$attr[ $bg = $i++ ] = $self->{background} if !defined $bg; | |
} | |
$attr[ $fg ] = 'on_' . $attr[ $fg ] if defined $fg; | |
$attr[ $bg ] = substr( $attr[ $bg ], 3 ) if defined $bg; | |
} | |
return @attr; | |
} | |
sub remove_escape_sequences { | |
my ($self, $string) = @_; | |
# This is in no way comprehensive or accurate... | |
# it just seems like most of the sequences match this. | |
# We could certainly expand this if the need arises. | |
$string =~ s{ | |
\e\[ | |
[0-9;]* | |
[a-ln-zA-Z] | |
}{}gx; | |
return $string; | |
} | |
our @EXPORT_OK; | |
BEGIN { | |
my @funcs = qw(identify normalize parse); | |
my $suffix = '_ansicolor'; | |
local $_; | |
eval join '', ## no critic (StringyEval) | |
map { "sub ${_}$suffix { __PACKAGE__->new->$_(\@_) }" } | |
@funcs; | |
@EXPORT_OK = map { $_ . $suffix } @funcs; | |
} | |
sub import { | |
my $class = shift; | |
return unless @_; | |
my $caller = caller; | |
no strict 'refs'; ## no critic (NoStrict) | |
foreach my $arg ( @_ ){ | |
die "'$arg' is not exported by $class" | |
unless grep { $arg eq $_ } @EXPORT_OK; | |
*{"${caller}::$arg"} = *{"${class}::$arg"}{CODE}; | |
} | |
} | |
# TODO: option for blotting out 'concealed'? s/\S/ /g | |
1; | |
package IRC::FromANSI::Tiny; | |
# ABSTRACT: Convert ANSI color codes to IRC | |
our $AUTHORITY = 'cpan:ARODLAND'; # AUTHORITY | |
our $VERSION = '0.02'; # VERSION | |
use strict; | |
use warnings; | |
my %irccolors = ( | |
black => 1, | |
red => 5, | |
green => 3, | |
yellow => 7, | |
blue => 2, | |
magenta => 6, | |
cyan => 10, | |
white => 14, | |
bright_black => 15, | |
bright_red => 4, | |
bright_green => 9, | |
bright_yellow => 8, | |
bright_blue => 12, | |
bright_magenta => 13, | |
bright_cyan => 11, | |
bright_white => 0, | |
); | |
sub convert { | |
my ($text) = @_; | |
my $ret = ""; | |
my $ansi = Parse::ANSIColor::Tiny->new; | |
my $data = $ansi->parse($text); | |
my (%foregrounds, %backgrounds); | |
$foregrounds{$_} = 1 for $ansi->foreground_colors; | |
$backgrounds{$_} = 1 for $ansi->background_colors; | |
my ($foreground, $background, $underline, $bold) = (undef, undef, 0, 0); | |
for my $chunk (@$data) { | |
my ($attrs, $text) = @$chunk; | |
my ($fg) = grep $foregrounds{$_}, @$attrs; | |
my ($bg) = grep $backgrounds{$_}, @$attrs; | |
my $bb = (grep $_ eq 'bold', @$attrs) ? 1 : 0; | |
my $u = (grep $_ eq 'underline', @$attrs) ? 1 : 0; | |
my $set_color; | |
if ($fg) { | |
$foreground = ($bb ? 'bright_' : '') . $fg; | |
# use Acme::Dump::And::Dumper; | |
# warn DnD [ $foreground ]; | |
if ($foreground =~ /^rgb/) { | |
# %RGB_MAP | |
my ($r, $g, $b) = $foreground =~ /(\d)(\d)(\d)/; | |
$r = 255*$r/5; $r = 255 if $r > 255; $r = 0 if $r < 0; | |
$g = 255*$g/5; $g = 255 if $g > 255; $g = 0 if $g < 0; | |
$b = 255*$b/5; $b = 255 if $b > 255; $b = 0 if $b < 0; | |
my $wanted = sprintf "%02x%02x%02x", $r, $g, $b; | |
my $smallest_distance = 999999999999999; | |
my $smallest = 'green'; | |
# use Acme::Dump::And::Dumper; | |
for (keys %RGB_MAP) { | |
# warn DnD [ Color::RGB::Util::rgb_distance($wanted, $_), $wanted, $_ ]; | |
my $d = Color::RGB::Util::rgb_distance($wanted, $_); | |
if ($d < $smallest_distance) { | |
$smallest_distance = $d; | |
$smallest = $RGB_MAP{$_}; | |
} | |
} | |
# warn DnD [$foreground, $r, $g, $b, $wanted, $smallest]; | |
$foreground = $smallest; | |
# $foreground = 'green'; ### FIX FOR RGB | |
} | |
$set_color = "\cC$irccolors{$foreground}"; | |
$bb = 0; | |
} | |
if ($bg) { | |
$background = $bg; | |
$set_color = "\cC" . $irccolors{$foreground || "black"} . ",$irccolors{$background}"; | |
} | |
if (!$fg && !$bg && ($foreground || $background)) { | |
undef $foreground; | |
undef $background; | |
if ($text =~ /^\d/) { | |
# Use "reset all" to clear color to avoid a following number | |
# being interpreted as a color code | |
$set_color = "\cO"; | |
undef $underline; | |
undef $bold; | |
} else { | |
$set_color = "\cC"; | |
} | |
} | |
$ret .= $set_color if length $set_color; | |
if ($bb ^ $bold) { | |
$bold = $bb; | |
$ret .= "\cB"; | |
} | |
if ($u ^ $underline) { | |
$underline = $u; | |
$ret .= "\c_"; | |
} | |
if ($ret =~ /\D\d$/ && $text =~ /^\d/) { | |
# Avoid a 1-digit color code (e.g. ^C1 or ^C12,3 running into a following | |
# digit that's supposed to be part of the literal text, by making it two-digit. | |
substr($ret, -1, 0, '0'); | |
} | |
$ret .= $text; | |
} | |
return $ret; | |
} | |
1; | |
use feature 'say'; | |
say IRC::FromANSI::Tiny::convert(do { local $/; <STDIN> }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment