File Coverage

lib/Graphics/Toolkit/Color/Space/Hub.pm
Criterion Covered Total %
statement 148 157 94.2
branch 77 112 68.7
condition 43 75 57.3
subroutine 15 15 100.0
pod 10 13 76.9
total 293 372 78.7


line stmt bran cond sub pod time code
1              
2             # store all color space objects, to convert check, convert and measure color values
3              
4             package Graphics::Toolkit::Color::Space::Hub;
5 14     14   541288 use v5.12;
  14         56  
6 14     14   83 use warnings;
  14         27  
  14         34383  
7              
8             #### internal space loading ############################################
9             our $default_space_name = 'RGB';
10             our @load_order = ($default_space_name,
11             qw/RGBLinear CMY CMYK HSL HSV HSB HWB NCol YIQ YUV/,
12             qw/CIEXYZ CIERGB CIELAB CIELUV CIELCHab CIELCHuv HunterLAB/,
13             qw/AppleRGB AdobeRGB ProPhotoRGB WideGamutRGB/,
14             qw/DisplayP3Linear DisplayP3 DCIP3Linear DCIP3 Rec709 Rec2020/,
15             qw/OKLAB OKLCH/);
16             add_space( require "Graphics/Toolkit/Color/Space/Instance/$_.pm" ) for @load_order;
17             my ($default_space, @search_order, %space_obj, %next_conversion_node);
18              
19             #### space API #########################################################
20             sub is_space_name {
21 239 100   239 1 44848 (ref get_space( $default_space->normalize_name( $_[0] ))) ? 1 : 0 }
22 12     12 1 1017 sub all_space_names { sort keys %space_obj }
23 16     16 0 1114 sub default_space_name { $default_space_name }
24 2656     2656 1 480237 sub default_space { $default_space }
25             sub get_space { # takes only normal names or alias names
26 6253     6253 1 11722 my $name = shift;
27 6253 50       9673 return unless defined $name;
28 6253 100       15176 exists $space_obj{ $name } ? $space_obj{ $name } : '';
29             }
30             sub try_get_space { # takes any name variant and defaults to $default_space_name
31 2704   66 2704 1 6077 my $name = shift || $default_space_name;
32 2704 100 66     5794 return $name if ref $name eq 'Graphics::Toolkit::Color::Space' and is_space_name( $name->name );
33 2644         5045 $name = default_space()->normalize_name( $name );
34 2644         4482 my $space = get_space( $name );
35 2644 100       5418 return (ref $space) ? $space
36             : "$name is an unknown color space, try one of: ".(join ', ', all_space_names());
37             }
38              
39             sub add_space {
40 421     421 0 969 my $space = shift;
41 421 50       1065 return 'add_space got no Graphics::Toolkit::Color::Space object as argument' if ref $space ne 'Graphics::Toolkit::Color::Space';
42 421         1041 my $name = $space->name;
43 421         1142 my $alias = $space->name('alias');
44 421 50       947 return "can not add color space object without a name" unless $name;
45 421 50       806 return "color space name $name is already taken" if ref get_space( $name );
46 421 100       999 if ($name eq $default_space_name) { # there is no parent
47 14         27 $default_space = $space;
48             } else {
49 407         916 my $conversion_parent = $space->conversion_tree_parent;
50 407 50 33     1503 return "can not add color space $name, it has no converter" unless defined $conversion_parent and $conversion_parent;
51 407         901 $conversion_parent = $space->normalize_name( $conversion_parent );
52 407         843 my $parent_space = get_space( $conversion_parent );
53 407 50       875 return "color space $name does only convert into '$conversion_parent', which is no known color space" unless ref $parent_space;
54 407         920 my $parent_name = $parent_space->name;
55 407         1449 $next_conversion_node{ $parent_name }{ $name } = $name;
56 407 100       946 unless ($parent_name eq $default_space_name){
57 266         446 my $upper_space_name = $default_space_name;
58 266         647 while ($upper_space_name ne $parent_name){
59             $upper_space_name = $next_conversion_node{ $upper_space_name }{ $name }
60 560         1966 = $next_conversion_node{ $upper_space_name }{ $parent_name };
61             }
62             }
63             }
64 421         945 push @search_order, $name;
65 421         937 $space_obj{ $name } = $space;
66 421 100 66     1165 $space_obj{ $alias } = $space if $alias and not ref get_space( $alias );
67 421         224905 return 1;
68             }
69             sub remove_space {
70 3     3 0 977 my $name = shift;
71 3 50 33     23 return "need name of color space as argument in order to remove the space" unless defined $name and $name;
72 3         11 my $space = try_get_space( $name );
73 3 100       19 return "can not remove unknown color space: $name" if not ref $space;
74 1 50       42 return "can not remove default color space: $name" if $space->name eq $default_space_name;
75              
76 1         4 $name = $space->name;
77 1         3 my $upper_space_name = $default_space_name;
78 1         5 while ($upper_space_name ne $name){
79 1         5 $upper_space_name = delete $next_conversion_node{ $upper_space_name }{ $name };
80             }
81 1 50       3 delete $space_obj{ $space->name('alias') } if $space->name('alias');
82 1         7 delete $space_obj{ $name };
83             }
84              
85             #### tuple API ##########################################################
86             sub convert { # normalized RGB tuple, ~space_name --> |normalized tuple in wanted space
87 492     492 1 2545 my ($tuple, $target_space_name, $want_result_normalized, $source_tuple, $source_space_name) = @_;
88 492 100       1196 return "need an ARRAY ref with 3 normalized RGB values as first argument in order to convert them"
89             unless $default_space->is_number_tuple( $tuple );
90 489         925 my $target_space = try_get_space( $target_space_name );
91 489 100       1029 return "got unknown space name: '$target_space_name' as second argument, can not convert " unless ref $target_space;
92 488         772 my $source_space = try_get_space( $source_space_name );
93 488 50       864 return "did not found target color space !'$target_space_name'" unless ref $target_space;
94 488 50 50     2447 if ($source_space_name xor $source_tuple){
    100 66        
95 0         0 return "arguments source_space_name and source_values (nr. 4 and 5) have to be provided both or none of them";
96             } elsif ($source_space_name and $source_tuple) {
97 64 50       159 return "got unknown source color space $source_space_name" if not ref $source_space;
98 64 50       173 return "argument source_values has to be a tuple, if provided" unless $source_space->is_number_tuple( $source_tuple );
99             }
100              
101 488         1493 $tuple = [@$tuple]; # unwrap ref to avoid spooky action
102 488         735 my $current_space_name = $default_space_name; # we start in RGB
103 488         1006 $target_space_name = $target_space->name; # use only normalized name
104 488   100     1064 $want_result_normalized //= 0; # normal flags to start state
105 488         714 my $tuple_is_normal = 1;
106              
107 488         1199 while ($current_space_name ne $target_space_name){
108 203         477 my $next_space_name = $next_conversion_node{ $current_space_name }{ $target_space_name };
109             # replace tuple with values from constructor if possible
110 203 100 100     657 if (defined $source_space_name and $next_space_name eq $source_space_name){
111 43         118 $tuple = [@$source_tuple];
112 43         102 $tuple_is_normal = 1;
113             } else {
114 160         248 my $next_space = get_space( $next_space_name );
115 160         418 my @normal_in_out = $next_space->converter_normal_states( 'from', $current_space_name );
116 160 0 33     380 $tuple = $next_space->normalize( $tuple ) if not $tuple_is_normal and $normal_in_out[0];
117 160 50 33     522 $tuple = $next_space->denormalize( $tuple ) if $tuple_is_normal and not $normal_in_out[0];
118 160         363 $tuple = $next_space->convert_from( $current_space_name, $tuple );
119 160         284 $tuple_is_normal = $normal_in_out[1];
120 160 100 100     420 if (not $tuple_is_normal and $next_space_name ne $target_space_name){
121 12         16 $tuple_is_normal = 1;
122 12         24 $tuple = $next_space->normalize( $tuple );
123             }
124             }
125 203         476 $current_space_name = $next_space_name;
126             }
127 488 100 100     1065 $tuple = $target_space->normalize( $tuple ) if not $tuple_is_normal and $want_result_normalized;
128 488 100 100     1516 $tuple = $target_space->denormalize( $tuple ) if $tuple_is_normal and not $want_result_normalized;
129 488         1630 return $tuple;
130             }
131             sub deconvert { # normalized value tuple --> RGB tuple
132 151     151 1 583 my ($tuple, $original_space_name, $want_result_normalized, $source_tuple, $source_space_name) = @_;
133 151         273 my $original_space = try_get_space( $original_space_name );
134 151         236 my $source_space = try_get_space( $source_space_name );
135 151   100     326 $want_result_normalized //= 0;
136 151 100       274 return "need a space name to convert from as second argument" unless defined $original_space_name;
137 149 100       366 return "got unknown color space name as second argument" unless ref $original_space;
138 147 50       328 return "need as first argument an ARRAY with valid number of normalized values from the color space ". $original_space->name
139             unless $original_space->is_number_tuple( $tuple );
140            
141 147 50 25     770 if ($source_space_name xor $source_tuple){
    50 33        
142 0         0 return "arguments source_space_name and source_values (nr. 4 and 5) have to be provided both or none of them";
143             } elsif ($source_space_name and $source_tuple) {
144 0 0       0 return "got unknown source color space $source_space_name" if not ref $source_space;
145 0 0       0 return "argument source_values has to be a tuple, if provided" unless $source_space->is_number_tuple( $source_tuple );
146             }
147            
148             # none conversion cases
149 147 50       451 if ($original_space->name eq $default_space_name) { # nothing to convert
150 0 0       0 return ($want_result_normalized) ? $tuple : $original_space->denormalize( $tuple );
151             }
152 147         247 my $current_space = $original_space;
153 147         187 my $tuple_is_normal = 1;
154             # actual conversion
155 147         338 while ($current_space->name ne $default_space_name){
156 193         453 my ($next_space_name, @next_options) = $current_space->conversion_tree_parent;
157 193   33     517 $next_space_name = shift @next_options while @next_options and $next_space_name ne $default_space_name;
158             # replace tuple with values from constructor if possible
159 193 50 33     418 if ($source_space_name and $next_space_name eq $source_space->name){
160 0         0 $tuple = [@$source_tuple];
161 0         0 $tuple_is_normal = 1;
162             } else {
163 193         528 my @normal_in_out = $current_space->converter_normal_states( 'to', $next_space_name );
164 193 0 33     414 $tuple = $current_space->normalize( $tuple ) if not $tuple_is_normal and $normal_in_out[0];
165 193 100 66     604 $tuple = $current_space->denormalize( $tuple ) if $tuple_is_normal and not $normal_in_out[0];
166 193         458 $tuple = $current_space->convert_to( $next_space_name, $tuple);
167 193         293 $tuple_is_normal = $normal_in_out[1];
168 193 50 33     494 if (not $tuple_is_normal and $current_space->name ne $default_space_name){
169 0         0 $tuple_is_normal = 1;
170 0         0 $tuple = $current_space->normalize( $tuple );
171             }
172             }
173 193         349 $current_space = get_space( $next_space_name );
174             }
175 147 50 33     347 $tuple = $current_space->normalize( $tuple ) if not $tuple_is_normal and $want_result_normalized;
176 147 100 66     456 $tuple = $current_space->denormalize( $tuple ) if $tuple_is_normal and not $want_result_normalized;
177 147         413 return $tuple;
178             }
179              
180             sub deformat { # formatted color def --> normalized values
181 135     135 1 12077 my ($color_def, $ranges, $suffix) = @_;
182 135 50       267 return 'got no color definition' unless defined $color_def;
183 135         250 my ($tuple, $original_space, $format_name);
184 135         294 for my $space_name (@search_order) {
185 1928         2816 my $color_space = get_space( $space_name );
186 1928         3875 ($tuple, $format_name) = $color_space->deformat( $color_def );
187 1928 100       3938 if (defined $format_name){
188 89         127 $original_space = $color_space;
189 89         174 last;
190             }
191             }
192 135 100       456 return "could not deformat color definition: '$color_def'" unless ref $original_space;
193 89         296 return $tuple, $original_space->name, $format_name;
194             }
195             sub deformat_partial_hash { # convert partial hash into
196 40     40 1 16040 my ($value_hash, $space_name) = @_;
197 40 100       106 return unless ref $value_hash eq 'HASH';
198 39         67 my $space = try_get_space( $space_name );
199 39 50       69 return $space unless ref $space;
200 39 100 66     372 my @space_name_options = (defined $space_name and $space_name) ? ($space->name) : (@search_order);
201 39         68 for my $space_name (@space_name_options) {
202 314         389 my $color_space = try_get_space( $space_name );
203 314         475 my $tuple = $color_space->tuple_from_partial_hash( $value_hash );
204 314 100       504 next unless ref $tuple;
205 25 50       68 return wantarray ? ($tuple, $color_space->name) : $tuple;
206             }
207 14         68 return undef;
208             }
209              
210             sub distance { # @c1 @c2 -- ~space ~select @range --> +
211 28     28 1 103 my ($tuple_a, $tuple_b, $space_name, $select_axis, $range) = @_;
212 28         73 my $color_space = try_get_space( $space_name );
213 28 50       65 return $color_space unless ref $color_space;
214 28         124 $tuple_a = convert( $tuple_a, $space_name, 'normal' );
215 28         105 $tuple_b = convert( $tuple_b, $space_name, 'normal' );
216 28         102 my $delta = $color_space->delta( $tuple_a, $tuple_b );
217 28         96 $delta = $color_space->denormalize_delta( $delta, $range );
218 28 100       103 if (defined $select_axis){
219 17 100       55 $select_axis = [$select_axis] unless ref $select_axis;
220 21         60 my @selected_values = grep {defined $_} map {$delta->[$_]}
  21         40  
221 17         38 grep {defined $_} map {$color_space->pos_from_axis_name($_)} @$select_axis;
  21         55  
  21         59  
222 17         43 $delta = \@selected_values;
223             }
224 28         71 my $d = 0;
225 28         99 $d += $_ * $_ for @$delta;
226 28         304 return sqrt $d;
227             }
228              
229             1;
230              
231             __END__