File Coverage

lib/Graphics/Toolkit/Color.pm
Criterion Covered Total %
statement 186 212 87.7
branch 114 134 85.0
condition 62 90 68.8
subroutine 21 42 50.0
pod 13 34 38.2
total 396 512 77.3


line stmt bran cond sub pod time code
1              
2             # public user level API: docs, help and arg cleaning
3              
4             package Graphics::Toolkit::Color;
5             our $VERSION = '1.972';
6              
7 4     4   601539 use v5.12;
  4         15  
8 4     4   24 use warnings;
  4         8  
  4         295  
9 4     4   22 use Exporter 'import';
  4         29  
  4         168  
10 4     4   1003 use Graphics::Toolkit::Color::Space::Util qw/is_nr/;
  4         8  
  4         287  
11 4     4   2141 use Graphics::Toolkit::Color::SetCalculator;
  4         14  
  4         18711  
12              
13             my $default_space_name = Graphics::Toolkit::Color::Space::Hub::default_space_name();
14             our @EXPORT_OK = qw/color/;
15              
16             ## constructor #########################################################
17              
18 23     23 1 928748 sub color { Graphics::Toolkit::Color->new ( @_ ) }
19              
20             sub new {
21 86     86 1 2364 my ($pkg, @args) = @_;
22 86         273 my $help = <
23             constructor new of Graphics::Toolkit::Color object needs either:
24             1. a color name: new('red') or new('SVG:red')
25             3. RGB hex string new('#FF0000') or new('#f00')
26             4. $default_space_name array or ARRAY ref: new( 255, 0, 0 ) or new( [255, 0, 0] )
27             5. named array or ARRAY ref: new( 'HSL', 255, 0, 0 ) or new( ['HSL', 255, 0, 0 ])
28             6. named string: new( 'HSL: 0, 100, 50' ) or new( 'ncol(r0, 0%, 0%)' )
29             7. HASH or HASH ref with values from RGB or any other space:
30             new(r => 255, g => 0, b => 0) or new({ hue => 0, saturation => 100, lightness => 50 })
31             EOH
32 86         312 my $first_arg_is_color_space = Graphics::Toolkit::Color::Space::Hub::is_space_name( $args[0] );
33 86 100 100     343 @args = ([ $args[0], @{$args[1]} ]) if @args == 2 and $first_arg_is_color_space and ref $args[1] eq 'ARRAY';
  2   66     7  
34 86 100 100     430 @args = ([ @args ]) if @args == 3 or (@args > 3 and $first_arg_is_color_space);
      100        
35 86 100 100     465 @args = ({ @args }) if @args == 6 or @args == 8;
36 86 100       262 return $help unless @args == 1;
37 77         192 my $self = _new_from_scalar_def( $args[0] );
38 77 100       682 return (ref $self) ? $self : $help;
39             }
40             sub _new_from_scalar_def { # color defs of method arguments
41 135     135   230 my ($color_def) = shift;
42 135 100       344 return $color_def if ref $color_def eq __PACKAGE__;
43 117         551 return _new_from_value_obj( Graphics::Toolkit::Color::Values->new_from_any_input( $color_def ) );
44             }
45             sub _new_from_value_obj {
46 285     285   439 my ($value_obj) = @_;
47 285 100       656 return $value_obj unless ref $value_obj eq 'Graphics::Toolkit::Color::Values';
48 238         1146 return bless {values => $value_obj};
49             }
50              
51              
52             ## deprecated methods - deleted with 2.0
53 0 0   0 0 0 sub string { $_[0]{'name'} || $_[0]->{'values'}->string }
54 0     0 0 0 sub rgb { $_[0]->values( ) }
55 0     0 0 0 sub red {($_[0]->values( ))[0] }
56 0     0 0 0 sub green {($_[0]->values( ))[1] }
57 0     0 0 0 sub blue {($_[0]->values( ))[2] }
58 0     0 0 0 sub rgb_hex { $_[0]->values( as => 'hex') }
59 0     0 0 0 sub rgb_hash { $_[0]->values( as => 'hash') }
60 0     0 0 0 sub hsl { $_[0]->values( in => 'hsl') }
61 0     0 0 0 sub hue {($_[0]->values( in => 'hsl'))[0] }
62 0     0 0 0 sub set { shift->set_value( @_ ) }
63 0     0 0 0 sub add { shift->add_value( @_ ) }
64 0     0 0 0 sub saturation {($_[0]->values( in => 'hsl'))[1] }
65 0     0 0 0 sub lightness {($_[0]->values( in => 'hsl'))[2] }
66 0     0 0 0 sub hsl_hash { $_[0]->values( in => 'hsl', as => 'hash') }
67 0     0 0 0 sub distance_to { distance(@_) }
68 0     0 0 0 sub blend { mix( @_ ) }
69 0     0 0 0 sub blend_with { $_[0]->mix( with => $_[1], amount => $_[2], in => 'HSL') }
70 0     0 0 0 sub gradient_to { hsl_gradient_to( @_ ) }
71 0     0 0 0 sub rgb_gradient_to { $_[0]->gradient( to => $_[1], steps => $_[2], dynamic => $_[3], in => 'RGB' ) }
72 0     0 0 0 sub hsl_gradient_to { $_[0]->gradient( to => $_[1], steps => $_[2], dynamic => $_[3], in => 'HSL' ) }
73 0     0 0 0 sub complementary { complement(@_) }
74              
75             sub _split_named_args {
76 158     158   408 my ($raw_args, $only_parameter, $required_parameter, $optional_parameter, $parameter_alias) = @_;
77 158 50 66     614 @$raw_args = %{$raw_args->[0]} if @$raw_args == 1 and ref $raw_args->[0] eq 'HASH' and not
  0   0     0  
      33        
78             (defined $only_parameter and $only_parameter eq 'to' and ref _new_from_scalar_def( $raw_args ) );
79              
80 158 100 100     422 if (@$raw_args == 1 and defined $only_parameter and $only_parameter){
      66        
81 33 50       58 return "The one default argument can not cover multiple, required parameter !" if @$required_parameter > 1;
82 33 50 66     80 return "The default argument does not cover the required argument!"
83             if @$required_parameter and $required_parameter->[0] ne $only_parameter;
84              
85 33         103 my %defaults = %$optional_parameter;
86 33         62 delete $defaults{$only_parameter};
87 33         186 return {$only_parameter => $raw_args->[0], %defaults};
88             }
89 125         168 my %clean_arg;
90 125 100       313 if (@$raw_args % 2) {
91 2 50 33     15 return (defined $only_parameter and $only_parameter)
92             ? "Got odd number of values, please use key value pairs as arguments or one default argument !\n"
93             : "Got odd number of values, please use key value pairs as arguments !\n"
94             }
95 123         288 my %arg_hash = @$raw_args;
96 123         282 for my $parameter_name (@$required_parameter){
97 67 100 66     331 if (ref $parameter_alias eq 'HASH' and exists $parameter_alias->{ $parameter_name }
      66        
98             and exists $arg_hash{ $parameter_alias->{$parameter_name} }){
99 10         57 $arg_hash{ $parameter_name } = delete $arg_hash{ $parameter_alias->{$parameter_name} };
100             }
101 67 100       199 return "Argument '$parameter_name' is missing\n" unless exists $arg_hash{$parameter_name};
102 61         175 $clean_arg{ $parameter_name } = delete $arg_hash{ $parameter_name };
103             }
104 117         307 for my $parameter_name (keys %$optional_parameter){
105 382 50 66     699 if (ref $parameter_alias eq 'HASH' and exists $parameter_alias->{ $parameter_name }
      33        
106             and exists $arg_hash{ $parameter_alias->{$parameter_name} }){
107 0         0 $arg_hash{ $parameter_name } = delete $arg_hash{ $parameter_alias->{$parameter_name} };
108             }
109             $clean_arg{ $parameter_name } = exists $arg_hash{$parameter_name}
110             ? delete $arg_hash{ $parameter_name }
111 382 100       1062 : $optional_parameter->{ $parameter_name };
112             }
113 117 100       322 return "Inserted unknown argument(s): ".(join ',', keys %arg_hash)."\n" if %arg_hash;
114 110         280 return \%clean_arg;
115             }
116              
117             ## getter ##############################################################
118             sub values {
119 56     56 1 51622 my ($self, @args) = @_;
120 56         432 my $arg = _split_named_args( \@args, 'in', [],
121             { in => $default_space_name, as => 'list',
122             precision => undef, range => undef, suffix => undef } );
123 56         200 my $help = <
124             GTC method 'values' accepts either no arguments, one color space name or four optional, named args:
125             values ( ...
126             in => 'HSL', # color space name, defaults to "$default_space_name"
127             as => 'css_string', # output format name, default is "list"
128             range => 1, # value range (SCALAR or ARRAY), default set by space def
129             precision => 3, # value precision (SCALAR or ARRAY), default set by space
130             suffix => '%', # value suffix (SCALAR or ARRAY), default set by color space
131              
132             EOH
133 56 100       165 return $arg.$help unless ref $arg;
134 55         304 $self->{'values'}->formatted( @$arg{qw/in as suffix range precision/} );
135             }
136              
137             sub name {
138 34     34 1 10481 my ($self, @args) = @_;
139 34 100       168 return $self->{'values'}->name unless @args;
140 2         19 my $arg = _split_named_args( \@args, 'from', [], {from => 'default', all => 0, full => 0, distance => 0});
141 2         9 my $help = <
142             GTC method 'name' accepts three optional, named arguments:
143             name ( ...
144             'CSS' # color naming scheme works as only positional argument
145             from => 'CSS' # same scheme (defaults to internal: X + CSS + PantoneReport)
146             from => ['SVG', 'X'] # more color naming schemes at once, without duplicates
147             all => 1 # returns list of all names with the object's RGB values (defaults 0)
148             full => 1 # adds color scheme name to the color name. 'SVG:red' (defaults 0)
149             distance => 3 # color names from within distance of 3 (defaults 0)
150             EOH
151 2         11 return Graphics::Toolkit::Color::Name::from_values( $self->{'values'}->shaped, @$arg{qw/from all full distance/});
152             }
153              
154             sub closest_name {
155 11     11 1 17539 my ($self, @args) = @_;
156 11         81 my $arg = _split_named_args( \@args, 'from', [], {from => 'default', all => 0, full => 0});
157 11         63 my $help = <
158             GTC method 'name' accepts three optional, named arguments:
159             closest_name ( ...
160             'CSS' # color naming scheme works as only positional argument
161             from => 'CSS' # same scheme (defaults to internal: X + CSS + PantoneReport)
162             from => ['SVG', 'X'] # more color naming schemes at once, without duplicates
163             all => 1 # returns list of all names with the object's RGB values (defaults 0)
164             full => 1 # adds color scheme name to the color name. 'SVG:red' (defaults 0)
165             EOH
166             my ($name, $distance) = Graphics::Toolkit::Color::Name::closest_from_values(
167 11         59 $self->{'values'}->shaped, @$arg{qw/from all full/});
168 11 100       97 return wantarray ? ($name, $distance) : $name;
169             }
170              
171             sub distance {
172 9     9 1 2984 my ($self, @args) = @_;
173 9         69 my $arg = _split_named_args( \@args, 'to', ['to'], {in => $default_space_name, select => undef, range => undef});
174 9         37 my $help = <
175             GTC method 'distance' accepts as arguments either a scalar color definition or
176             four named arguments, only the first being required:
177             distance ( ...
178             to => 'green' # color object or color definition (required)
179             in => 'HSL' # color space name, defaults to "$default_space_name"
180             select => 'red' # axis name or names (ARRAY ref), default is none
181             range => 2**16 # value range definition, defaults come from color space def
182             EOH
183 9 100       34 return $arg.$help unless ref $arg;
184 7         24 my $target_color = _new_from_scalar_def( $arg->{'to'} );
185 7 50       19 return "target color definition: $arg->{to} is ill formed" unless ref $target_color;
186 7         25 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
187 7 50       15 return "$color_space\n".$help unless ref $color_space;
188 7 100       26 if (defined $arg->{'select'}){
189 3 100       13 if (not ref $arg->{'select'}){
    50          
190             return $arg->{'select'}." is not an axis name in color space: ".$color_space->name
191 2 50       11 unless $color_space->is_axis_name( $arg->{'select'} );
192             } elsif (ref $arg->{'select'} eq 'ARRAY'){
193 1         3 for my $axis_name (@{$arg->{'select'}}) {
  1         5  
194 2 50       6 return "$axis_name is not an axis name in color space: ".$color_space->name
195             unless $color_space->is_axis_name( $axis_name );
196             }
197 0         0 } else { return "The 'select' argument needs one axis name or an ARRAY with several axis names".
198             " from the same color space!" }
199             }
200 7         25 my $range_def = $color_space->shape->try_check_range_definition( $arg->{'range'} );
201 7 50       17 return $range_def unless ref $range_def;
202             Graphics::Toolkit::Color::Space::Hub::distance(
203 7         34 $self->{'values'}->normalized, $target_color->{'values'}->normalized, $color_space->name ,$arg->{'select'}, $range_def );
204             }
205              
206             ## single color creation methods #######################################
207             sub set_value {
208 10     10 1 6502 my ($self, @args) = @_;
209 10 50 33     30 @args = %{$args[0]} if @args == 1 and ref $args[0] eq 'HASH';
  0         0  
210 10         13 my $help = <
211             GTC method 'set_value' needs a value HASH (not a ref) whose keys are axis names or
212             short names from one color space. If the chosen axis name(s) is/are ambiguous,
213             you might add the "in" argument:
214             set_value( green => 20 ) or set( g => 20 ) or
215             set_value( hue => 240, in => 'HWB' )
216             EOH
217 10 100 66     71 return $help if @args % 2 or not @args or @args > 10;
      66        
218 9         23 my $partial_color = { @args };
219 9         14 my $space_name = delete $partial_color->{'in'};
220 9         19 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $space_name );
221 9 50       14 return "$color_space\n".$help unless ref $color_space;
222 9         82 _new_from_value_obj( $self->{'values'}->set( $partial_color, $space_name ) );
223             }
224              
225             sub add_value {
226 10     10 1 6593 my ($self, @args) = @_;
227 10 50 33     28 @args = %{$args[0]} if @args == 1 and ref $args[0] eq 'HASH';
  0         0  
228 10         16 my $help = <
229             GTC method 'add_value' needs a value HASH (not a ref) whose keys are axis names or
230             short names from one color space. If the chosen axis name(s) is/are ambiguous,
231             you might add the "in" argument:
232             add_value( blue => -10 ) or set( b => -10 )
233             add_value( hue => 100 , in => 'HWB' )
234             EOH
235 10 100 66     54 return $help if @args % 2 or not @args or @args > 10;
      66        
236 9         20 my $partial_color = { @args };
237 9         13 my $space_name = delete $partial_color->{'in'};
238 9         22 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $space_name );
239 9 50       15 return "$color_space\n".$help unless ref $color_space;
240 9         30 _new_from_value_obj( $self->{'values'}->add( $partial_color, $space_name ) );
241             }
242              
243             sub mix {
244 23     23 1 15565 my ($self, @args) = @_;
245 23         110 my $arg = _split_named_args( \@args, 'to', ['to'], {in => $default_space_name, amount => 50});
246 23         83 my $help = <
247             GTC method 'mix' accepts three named arguments, only the first being required:
248             mix ( ...
249             to => ['HSL', 240, 100, 50] # scalar color definition or ARRAY ref thereof
250             amount => 20 # percentage value or ARRAY ref thereof, default is 50
251             in => 'HSL' # color space name, defaults to "$default_space_name"
252             Please note that either both or none of the first two arguments has to be an ARRAY.
253             Both ARRAY have to have the same length. 'amount' refers to the color(s) picked with 'to'.
254             EOH
255 23 100       62 return $arg.$help unless ref $arg;
256 21         49 my $recipe = _new_from_scalar_def( $arg->{'to'} );
257 21 100       36 if (ref $recipe){
258 11         31 $recipe = [{color => $recipe->{'values'}, percent => 50}];
259 11 100       31 return "Amount argument has to be a sacalar value if only one color is mixed !\n".$help if ref $arg->{'amount'};
260 10 50       30 $recipe->[0]{'percent'} = $arg->{'amount'} if defined $arg->{'amount'};
261             } else {
262 10 100       29 if (ref $arg->{'to'} ne 'ARRAY'){
263 2         40 return "target color definition (argument 'to'): $arg->{to} is ill formed, has to be one color definition or an ARRAY of";
264             } else {
265 8         15 $recipe = [];
266 8         10 for my $color_def (@{$arg->{'to'}}){
  8         20  
267 13         21 my $color = _new_from_scalar_def( $color_def );
268 13 50       23 return "target color definition: '$color_def' is ill formed" unless ref $color;
269 13         56 push @$recipe, { color => $color->{'values'}, percent => 50};
270             }
271             return "Amount argument has to be an ARRAY of same length as argument 'to' (color definitions)!\n".$help
272 8 100 66     45 if ref $arg->{'to'} eq 'ARRAY' and ref $arg->{'amount'} eq 'ARRAY' and @{$arg->{'amount'}} != @{$arg->{'to'}};
  3   100     6  
  3         21  
273 7 100 66     67 $arg->{'amount'} = [($arg->{'amount'}) x @{$arg->{'to'}}] if ref $arg->{'to'} and not ref $arg->{'amount'};
  5         11  
274 7         11 $recipe->[$_]{'percent'} = $arg->{'amount'}[$_] for 0 .. $#{$arg->{'amount'}};
  7         31  
275             }
276             }
277 17         42 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( delete $arg->{'in'} );
278 17 100       45 return "$color_space\n".$help unless ref $color_space;
279 16         45 _new_from_value_obj( $self->{'values'}->mix( $recipe, $color_space ) );
280             }
281              
282             sub invert {
283 13     13 1 40 my ($self, @args) = @_;
284 13         78 my $arg = _split_named_args( \@args, 'in', [], {in => $default_space_name});
285 13         33 my $help = <
286             GTC method 'invert' accepts one optional argument, which can be positional or named:
287             invert ( ...
288             in => 'HSL' # color space name, defaults to "$default_space_name"
289             EOH
290 13 100       30 return $arg.$help unless ref $arg;
291 12         28 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
292 12 100       27 return "$color_space\n".$help unless ref $color_space;
293 11         43 _new_from_value_obj( $self->{'values'}->invert( $color_space ) );
294             }
295              
296             ## color set creation methods ##########################################
297             sub complement {
298 12     12 1 676 my ($self, @args) = @_;
299 12         54 my $arg = _split_named_args( \@args, 'steps', [], {steps => 1, tilt => 0, target => {}});
300 12         27 my $help = <
301             GTC method 'complement' is computed in HSL and has two named, optional arguments:
302             complement ( ...
303             steps => 20 # count of produced colors, default is 1
304             tilt => 10 # default is 0
305             target => {h => 10, s => 20, l => 3} # sub-keys are independent, default to 0
306             EOH
307 12 100       29 return $arg.$help unless ref $arg;
308 11 100       32 return "Optional argument 'steps' has to be a number !\n".$help unless is_nr($arg->{'steps'});
309 9 50       34 return "Optional argument 'steps' is zero, no complement colors will be computed !\n".$help unless $arg->{'steps'};
310 9 100       17 return "Optional argument 'tilt' has to be a number !\n".$help unless is_nr($arg->{'tilt'});
311 8 100       23 return "Optional argument 'target' has to be a HASH ref !\n".$help if ref $arg->{'target'} ne 'HASH';
312 7         9 my ($target_values, $space_name);
313 7 100       8 if (keys %{$arg->{'target'}}){
  7         17  
314 2         8 ($target_values, $space_name) = Graphics::Toolkit::Color::Space::Hub::deformat_partial_hash( $arg->{'target'}, 'HSL' );
315 2 100       14 return "Optional argument 'target' got HASH keys that do not fit HSL space (use 'h','s','l') !\n".$help
316             unless ref $target_values;
317 5         10 } else { $target_values = [] }
318 13         27 map {_new_from_value_obj( $_ )}
319 6         27 Graphics::Toolkit::Color::SetCalculator::complement( $self->{'values'}, @$arg{qw/steps tilt/}, $target_values );
320             }
321              
322             sub gradient {
323 14     14 1 1927 my ($self, @args) = @_;
324 14         92 my $arg = _split_named_args( \@args, 'to', ['to'], {steps => 10, tilt => 0, in => $default_space_name});
325 14         49 my $help = <
326             GTC method 'gradient' accepts four named arguments, only the first is required:
327             gradient ( ...
328             to => 'blue' # scalar color definition or ARRAY ref thereof
329             steps => 20 # count of produced colors, defaults to 10
330             tilt => 1 # dynamics of color change, defaults to 0
331             in => 'HSL' # color space name, defaults to "$default_space_name"
332             EOH
333 14 100       47 return $arg.$help unless ref $arg;
334 12         33 my @colors = ($self->{'values'});
335 12         37 my $target_color = _new_from_scalar_def( $arg->{'to'} );
336 12 100       29 if (ref $target_color) {
337 9         26 push @colors, $target_color->{'values'} }
338             else {
339 3 100 66     22 return "Argument 'to' contains malformed color definition!\n".$help if ref $arg->{'to'} ne 'ARRAY' or not @{$arg->{'to'}};
  2         9  
340 2         6 for my $color_def (@{$arg->{'to'}}){
  2         6  
341 5         10 my $target_color = _new_from_scalar_def( $color_def );
342 5 100       47 return "Argument 'to' contains malformed color definition: $color_def !\n".$help unless ref $target_color;
343 4         17 push @colors, $target_color->{'values'};
344             }
345             }
346             return "Argument 'steps' has to be a number greater zero !\n".$help
347 10 100 66     26 unless is_nr($arg->{'steps'}) and $arg->{'steps'} > 0;
348 9         25 $arg->{'steps'} = int $arg->{'steps'};
349 9 100       15 return "Argument 'tilt' has to be a number !\n".$help unless is_nr($arg->{'tilt'});
350 8         23 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
351 8 100       38 return "$color_space\n".$help unless ref $color_space;
352 53         73 map {_new_from_value_obj( $_ )}
353 7         32 Graphics::Toolkit::Color::SetCalculator::gradient( \@colors, @$arg{qw/steps tilt/}, $color_space);
354             }
355              
356             sub cluster {
357 18     18 1 10367 my ($self, @args) = @_;
358 18         130 my $arg = _split_named_args( \@args, undef, ['radius', 'minimal_distance'], {in => $default_space_name},
359             {radius => 'r', minimal_distance => 'min_d'} );
360 18         75 my $help = <
361             GTC method 'cluster' accepts three named arguments, the first two being required:
362             cluster ( ...
363             radius => 3 # ball shaped cluster with cuboctahedral packing or
364             r => [10, 5, 3] # cuboid shaped cluster with cubical packing
365             minimal_distance => 0.5 # minimal distance between colors in cluster
366             min_d => 0.5 # short alias for minimal distance
367             in => 'HSL' # color space name, defaults to "$default_space_name"
368             EOH
369 18 100       87 return $arg.$help unless ref $arg;
370 12         46 my $color_space = Graphics::Toolkit::Color::Space::Hub::try_get_space( $arg->{'in'} );
371 12 100       41 return "$color_space\n".$help unless ref $color_space;
372             return "Argument 'radius' has to be a number or an ARRAY of numbers".$help
373 11 100 100     38 unless is_nr($arg->{'radius'}) or $color_space->is_number_tuple( $arg->{'radius'} );
374             return "Argument 'distance' has to be a number greater zero !\n".$help
375 8 100 100     25 unless is_nr($arg->{'minimal_distance'}) and $arg->{'minimal_distance'} > 0;
376             return "Ball shaped cluster works only in spaces with three dimensions !\n".$help
377 6 100 100     22 if $color_space->axis_count > 3 and not ref $arg->{'radius'};
378 57         68 map {_new_from_value_obj( $_ )}
379 5         27 Graphics::Toolkit::Color::SetCalculator::cluster( $self->{'values'}, @$arg{qw/radius minimal_distance/}, $color_space);
380             }
381              
382             1;
383              
384             __END__