File Coverage

lib/Graphics/Toolkit/Color/Space/Hub.pm
Criterion Covered Total %
statement 157 166 94.5
branch 89 126 70.6
condition 43 75 57.3
subroutine 16 16 100.0
pod 0 14 0.0
total 305 397 76.8


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 15     15   434203 use v5.12;
  15         39  
6 15     15   84 use warnings;
  15         61  
  15         28768  
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 YPbPr/,
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 OKHSL OKHSV OKHWB/);
16             add_space( require "Graphics/Toolkit/Color/Space/Instance/$_.pm" ) for @load_order;
17             my ($default_space, @search_order, %space_obj, %next_conversion_node, %space_family);
18              
19             #### space API #########################################################
20             sub is_space_name {
21 146 100   146 0 48496 (ref get_space( $default_space->normalize_name( $_[0] ))) ? 1 : 0 }
22 11     11 0 994 sub all_space_names { sort keys %space_obj }
23 18     18 0 628 sub default_space_name { $default_space_name }
24 10512     10512 0 399236 sub default_space { $default_space }
25             sub get_space { # takes only normal names or alias names
26 7550     7550 0 10301 my $name = shift;
27 7550 100       9484 return unless defined $name;
28 7548 100       11781 $name = default_space()->normalize_name( $name ) if ref $default_space;
29 7548 100       16117 exists $space_obj{ $name } ? $space_obj{ $name } : '';
30             }
31             sub try_get_space { # takes any name variant and defaults to $default_space_name
32 3026   66 3026 0 5866 my $name = shift || $default_space_name;
33 3026 100 66     5186 return $name if ref $name eq 'Graphics::Toolkit::Color::Space' and is_space_name( $name->name );
34 2966         3558 $name = default_space()->normalize_name( $name );
35 2966         4051 my $space = get_space( $name );
36 2966 100       4687 return (ref $space) ? $space
37             : "$name is an unknown color space, try one of: ".(join ', ', all_space_names());
38             }
39              
40             sub add_space {
41 496     496 0 557 my $space = shift;
42 496 50       805 return 'add_space got no Graphics::Toolkit::Color::Space object as argument' if ref $space ne 'Graphics::Toolkit::Color::Space';
43 496         759 my $name = $space->name;
44 496         750 my $alias = $space->name('alias');
45 496 50       769 return "can not add color space object without a name" unless $name;
46 496 50       710 return "color space name $name is already taken" if ref get_space( $name );
47 496 100       781 if ($name eq $default_space_name) { # there is no parent
48 15         17 $default_space = $space;
49             } else {
50 481         726 my $conversion_parent = $space->conversion_tree_parent;
51 481 50 33     1180 return "can not add color space $name, it has no converter" unless defined $conversion_parent and $conversion_parent;
52 481         648 $conversion_parent = $space->normalize_name( $conversion_parent );
53 481         638 my $parent_space = get_space( $conversion_parent );
54 481 50       672 return "color space $name does only convert into '$conversion_parent', which is no known color space" unless ref $parent_space;
55 481         617 my $parent_name = $parent_space->name;
56 481         1175 $next_conversion_node{ $parent_name }{ $name } = $name;
57 481 100       808 unless ($parent_name eq $default_space_name){
58 330         387 my $upper_space_name = $default_space_name;
59 330         566 while ($upper_space_name ne $parent_name){
60             $upper_space_name = $next_conversion_node{ $upper_space_name }{ $name }
61 660         1657 = $next_conversion_node{ $upper_space_name }{ $parent_name };
62             }
63             }
64             }
65 496         772 push @search_order, $name;
66 496         699 $space_obj{ $name } = $space;
67 496 100 66     862 $space_obj{ $alias } = $space if $alias and not ref get_space( $alias );
68 496 100       776 push @{$space_family{ $space->family }}, $space if $space->family;
  465         623  
69 496         193905 return 1;
70             }
71             sub remove_space {
72 3     3 0 631 my $name = shift;
73 3 50 33     16 return "need name of color space as argument in order to remove the space" unless defined $name and $name;
74 3         8 my $space = try_get_space( $name );
75 3 100       14 return "can not remove unknown color space: $name" if not ref $space;
76 1 50       4 return "can not remove default color space: $name" if $space->name eq $default_space_name;
77              
78 1         3 $name = $space->name;
79 1         2 my $upper_space_name = $default_space_name;
80 1         3 while ($upper_space_name ne $name){
81 1         4 $upper_space_name = delete $next_conversion_node{ $upper_space_name }{ $name };
82             }
83 1 50       3 delete $space_family{ $space->family } if $space->family;
84 1 50       3 delete $space_obj{ $space->name('alias') } if $space->name('alias');
85 1         5 delete $space_obj{ $name };
86             }
87              
88             #### tuple API ##########################################################
89             sub convert { # normalized RGB tuple, ~space_name --> |normalized tuple in wanted space
90 530     530 0 2510 my ($tuple, $target_space_name, $want_result_normalized, $source_tuple, $source_space_name) = @_;
91 530 100       977 return "need an ARRAY ref with 3 normalized RGB values as first argument in order to convert them"
92             unless $default_space->is_number_tuple( $tuple );
93 527         872 my $target_space = try_get_space( $target_space_name );
94 527 100       817 return "got unknown space name: '$target_space_name' as second argument, can not convert " unless ref $target_space;
95 526         682 my $source_space = try_get_space( $source_space_name );
96 526 50       800 return "did not found target color space !'$target_space_name'" unless ref $target_space;
97 526 50 50     2048 if ($source_space_name xor $source_tuple){
    100 66        
98 0         0 return "arguments source_space_name and source_values (nr. 4 and 5) have to be provided both or none of them";
99             } elsif ($source_space_name and $source_tuple) {
100 72 50       158 return "got unknown source color space $source_space_name" if not ref $source_space;
101 72 50       175 return "argument source_values has to be a tuple, if provided" unless $source_space->is_number_tuple( $source_tuple );
102             }
103              
104 526         1197 $tuple = [@$tuple]; # unwrap ref to avoid spooky action
105 526         621 my $current_space_name = $default_space_name; # we start in RGB
106 526         849 $target_space_name = $target_space->name; # use only normalized name
107 526   100     970 $want_result_normalized //= 0; # normal flags to start state
108 526         570 my $tuple_is_normal = 1;
109              
110 526         901 while ($current_space_name ne $target_space_name){
111 525         910 my $next_space_name = $next_conversion_node{ $current_space_name }{ $target_space_name };
112 525         659 my $next_space = get_space( $next_space_name );
113             # replace tuple with values from constructor if possible
114 525 100 100     1156 if (defined $source_space_name and $next_space->is_name($source_space_name)){
115 49         104 $tuple = [@$source_tuple];
116 49         65 $tuple_is_normal = 1;
117             } else {
118 476         854 my @normal_in_out = $next_space->converter_normal_states( 'from', $current_space_name );
119 476 0 33     709 $tuple = $next_space->normalize( $tuple ) if not $tuple_is_normal and $normal_in_out[0];
120 476 50 33     1026 $tuple = $next_space->denormalize( $tuple ) if $tuple_is_normal and not $normal_in_out[0];
121 476         766 $tuple = $next_space->convert_from( $current_space_name, $tuple );
122 476         657 $tuple_is_normal = $normal_in_out[1];
123 476 100 100     953 if (not $tuple_is_normal and $next_space_name ne $target_space_name){
124 106         99 $tuple_is_normal = 1;
125 106         217 $tuple = $next_space->normalize( $tuple );
126             }
127             }
128 525         1009 $current_space_name = $next_space_name;
129             }
130 526 100 100     940 $tuple = $target_space->normalize( $tuple ) if not $tuple_is_normal and $want_result_normalized;
131 526 100 100     1188 $tuple = $target_space->denormalize( $tuple ) if $tuple_is_normal and not $want_result_normalized;
132 526         1235 return $tuple;
133             }
134             sub deconvert { # normalized value tuple --> RGB tuple
135 214     214 0 698 my ($tuple, $original_space_name, $want_result_normalized, $source_tuple, $source_space_name) = @_;
136 214         336 my $original_space = try_get_space( $original_space_name );
137 214         297 my $source_space = try_get_space( $source_space_name );
138 214   100     384 $want_result_normalized //= 0;
139 214 100       330 return "need a space name to convert from as second argument" unless defined $original_space_name;
140 212 100       323 return "got unknown color space name as second argument" unless ref $original_space;
141 210 50       357 return "need as first argument an ARRAY with valid number of normalized values from the color space ". $original_space->name
142             unless $original_space->is_number_tuple( $tuple );
143            
144 210 50 25     879 if ($source_space_name xor $source_tuple){
    50 33        
145 0         0 return "arguments source_space_name and source_values (nr. 4 and 5) have to be provided both or none of them";
146             } elsif ($source_space_name and $source_tuple) {
147 0 0       0 return "got unknown source color space $source_space_name" if not ref $source_space;
148 0 0       0 return "argument source_values has to be a tuple, if provided" unless $source_space->is_number_tuple( $source_tuple );
149             }
150            
151             # none conversion cases
152 210 50       333 if ($original_space->name eq $default_space_name) { # nothing to convert
153 0 0       0 return ($want_result_normalized) ? $tuple : $original_space->denormalize( $tuple );
154             }
155 210         258 my $current_space = $original_space;
156 210         235 my $tuple_is_normal = 1;
157             # actual conversion
158 210         326 while ($current_space->name ne $default_space_name){
159 372         634 my ($next_space_name, @next_options) = $current_space->conversion_tree_parent;
160 372   33     698 $next_space_name = shift @next_options while @next_options and $next_space_name ne $default_space_name;
161             # replace tuple with values from constructor if possible
162 372 50 33     673 if (defined $source_space_name and $current_space->is_name( $source_space_name )){
163 0         0 $tuple = [@$source_tuple];
164 0         0 $tuple_is_normal = 1;
165             } else {
166 372         830 my @normal_in_out = $current_space->converter_normal_states( 'to', $next_space_name );
167 372 0 33     579 $tuple = $current_space->normalize( $tuple ) if not $tuple_is_normal and $normal_in_out[0];
168 372 100 66     911 $tuple = $current_space->denormalize( $tuple ) if $tuple_is_normal and not $normal_in_out[0];
169 372         650 $tuple = $current_space->convert_to( $next_space_name, $tuple);
170 372         506 $tuple_is_normal = $normal_in_out[1];
171 372 50 33     719 if (not $tuple_is_normal and $current_space->name ne $default_space_name){
172 0         0 $tuple_is_normal = 1;
173 0         0 $tuple = $current_space->normalize( $tuple );
174             }
175             }
176 372         495 $current_space = get_space( $next_space_name );
177             }
178 210 50 33     425 $tuple = $current_space->normalize( $tuple ) if not $tuple_is_normal and $want_result_normalized;
179 210 100 66     533 $tuple = $current_space->denormalize( $tuple ) if $tuple_is_normal and not $want_result_normalized;
180 210         479 return $tuple;
181             }
182              
183             sub deformat { # formatted color def --> tuple
184 2264     2264 0 3068 my ($color_def, $space_name, $suffix) = @_;
185 2264 50       2912 return 'Got no color definition!' unless defined $color_def;
186 2264         2137 my ($tuple, $original_space, $format_name);
187 2264         2656 my $color_space = get_space( $space_name );
188 2264 50       3060 return "$color_space is an unknown space!" unless ref $color_space;
189 2264         3258 ($tuple, $format_name) = $color_space->deformat( $color_def ); #, $suffix
190 2264 100       5768 return "Could not deformat color definition: '$color_def' in space: ".$color_space->name unless defined $format_name;
191 89         199 return $tuple, $color_space->name, $format_name;
192             }
193             sub deformat_search { # formatted color def --> tuple
194 142     142 0 10972 my ($color_def, $suffix) = @_;
195 142 50       238 return 'got no color definition to deformat!' unless defined $color_def;
196 142         259 for my $space_name (@search_order) {
197 2264         3053 my @tuple_space_format = deformat( $color_def, $space_name, $suffix);
198 2264 100       4187 return @tuple_space_format if @tuple_space_format > 1;
199             }
200 53 100       232 return "Could not deformat color definition in any space: '@$color_def'!" if ref $color_def eq 'ARRAY';
201 24 100       84 return "Could not deformat color definition in any space: '%$color_def'!" if ref $color_def eq 'HASH';
202 15         56 return "Could not deformat color definition in any space: '$color_def'!";
203             }
204             sub deformat_search_partial_hash { # convert partial hash into
205 49     49 0 21977 my ($value_hash, $space_name) = @_;
206 49 100       154 return unless ref $value_hash eq 'HASH';
207 48         113 my $space = try_get_space( $space_name );
208 48 50       117 return $space unless ref $space;
209 48 100 66     560 my @space_name_options = (defined $space_name and $space_name) ? ($space->name) : (@search_order);
210 48         121 for my $space_name (@space_name_options) {
211 379         549 my $color_space = try_get_space( $space_name );
212 379         695 my $tuple = $color_space->tuple_from_partial_hash( $value_hash );
213 379 100       742 next unless ref $tuple;
214 33 50       107 return wantarray ? ($tuple, $color_space->name) : $tuple;
215             }
216 15         89 return undef;
217             }
218              
219             sub distance { # @c1 @c2 -- ~space ~select @range --> +
220 28     28 0 92 my ($tuple_a, $tuple_b, $space_name, $select_axis, $range) = @_;
221 28         61 my $color_space = try_get_space( $space_name );
222 28 50       56 return $color_space unless ref $color_space;
223 28         65 $tuple_a = convert( $tuple_a, $space_name, 'normal' );
224 28         46 $tuple_b = convert( $tuple_b, $space_name, 'normal' );
225 28         61 my $delta = $color_space->delta( $tuple_a, $tuple_b );
226 28         59 $delta = $color_space->denormalize_delta( $delta, $range );
227 28 100       43 if (defined $select_axis){
228 17 100       31 $select_axis = [$select_axis] unless ref $select_axis;
229 21         37 my @selected_values = grep {defined $_} map {$delta->[$_]}
  21         29  
230 17         28 grep {defined $_} map {$color_space->pos_from_axis_name($_)} @$select_axis;
  21         33  
  21         34  
231 17         25 $delta = \@selected_values;
232             }
233 28         27 my $d = 0;
234 28         67 $d += $_ * $_ for @$delta;
235 28         171 return sqrt $d;
236             }
237              
238             1;