File Coverage

lib/ICC/Support/Color.pm
Criterion Covered Total %
statement 22 589 3.7
branch 2 324 0.6
condition 1 299 0.3
subroutine 7 39 17.9
pod 9 9 100.0
total 41 1260 3.2


line stmt bran cond sub pod time code
1             package ICC::Support::Color;
2              
3 2     2   98476 use strict;
  2         11  
  2         59  
4 2     2   9 use Carp;
  2         4  
  2         148  
5              
6             our $VERSION = 0.22;
7              
8             # revised 2019-02-05
9              
10             # Copyright © 2004-2019 by William B. Birkett
11              
12             # add development directory
13 2     2   512 use lib 'lib';
  2         661  
  2         14  
14              
15             # inherit from Shared
16 2     2   650 use parent qw(ICC::Shared);
  2         285  
  2         12  
17              
18             # support modules
19 2     2   107 use File::Glob;
  2         3  
  2         113  
20              
21             # enable static variables
22 2     2   10 use feature 'state';
  2         3  
  2         16118  
23              
24             # spectral range hash
25             our $srh = {'31' => [400, 700, 10], '36' => [380, 730, 10], '38' => [360, 730, 10], '43' => [360, 780, 10], '81' => [380, 780, 5]};
26              
27             # create new Color object
28             # hash keys are: 'illuminant', 'observer', 'increment', 'method', 'imethod', 'bandpass', 'ibandpass', 'status', 'range'
29             # the method used is determined by the hash keys.
30             # for ASTM method,
31             # 'illuminant' value is a scalar, 'A', 'C', 'D50', 'D55', 'D65', 'D75', 'F2', 'F7', or 'F11', default is 'D50'
32             # 'observer' value is a scalar, '2' or '10', default is '2'
33             # 'increment' value is a scalar, '10' or '20', default is '10'
34             # 'bandpass' value is a scalar, 'astm', 'triangle', 'trapezoid' or 'six', default is no bandpass correction
35             # for CIE method,
36             # 'illuminant' value is an array reference, [], ['source', 'id'] or ['nm', 'spd']
37             # an empty array indicates no illuminant, for emissive measurements
38             # 'source' values are 'CIE', 'Philips', or a measurement file path
39             # 'id' values depend on the 'source'
40             # 'CIE' illuminants are 'A', 'C', 'D50', 'D55', 'D65', 'D75', 'ID50', 'ID65', 'FL1' to 'FL12', 'FL3.1' to 'FL3.15', 'HP1' to 'HP5', 'LED-B1' to 'LED-B5', 'LED-BH1', 'LED-RGB1', 'LED-V1', 'LED-V2', and 'E' (5 nm increment)
41             # 'CIE_HR' illuminants are 'FL1' to 'FL12', 'FL3.1' to 'FL3.15', 'LED-B1' to 'LED-B5', 'LED-BH1', 'LED-RGB1', 'LED-V1', 'LED-V2', and 'E' (1 nm increment)
42             # 'Philips' illuminants are '60_A/W', 'C100S54', 'C100S54C', 'F32T8/TL830', 'F32T8/TL835', 'F32T8/TL841', 'F32T8/TL850', 'F32T8/TL865/PLUS', 'F34/CW/RS/EW', 'F34T12WW/RS/EW', 'F40/C50', 'F40/C75', 'F40/CWX', 'F40/DX', 'F40/DXTP', 'F40/N', 'F34T12/LW/RS/EW', 'H38HT-100', 'H38JA-100/DX', 'MHC100/U/MP/3K', 'MHC100/U/MP/4K', and 'SDW-T_100W/LV'
43             # for a measurement file path, the 'id' is the sample number
44             # 'nm' and 'spd' are vectors, wavelength range and spectral power distribution
45             # 'observer' value is a scalar, '2', '10', '2P' or '10P', default '2'
46             # 'increment' value is a scalar, '1' or '5', default '1'
47             # 'method' value is a scalar, 'linear', 'cspline' or 'lagrange', default is 'cspline'
48             # 'bandpass' value is a scalar, 'astm', 'triangle' or 'trapezoid', default is no bandpass correction
49             # 'imethod' value is a scalar, 'linear', 'cspline' or 'lagrange', default is 'linear' or 'cspline', based on smoothness of the illuminant SPD
50             # 'ibandpass' value is a scalar, 'astm', 'triangle' or 'trapezoid', default is no bandpass correction
51             # for ISO 5-3 method (density),
52             # 'status' value is a scalar, 'A', 'M', 'T', 'E', or 'I', default 'T'
53             # 'increment' value is a scalar, '10' or '20', default '10'
54             # 'range' value is: [start_nm, end_nm, increment], which is added to the spectral range hash ($srh)
55             # parameters: ([ref_to_attribute_hash])
56             # returns: (object_reference)
57             sub new {
58              
59             # get object class
60 1     1 1 875 my $class = shift();
61              
62             # create empty Color object
63 1         5 my $self = [
64             {}, # object header
65             [], # illuminant (CIE)
66             [], # color-matching functions (CIE)
67             [], # color-weight functions (ASTM and ISO 5-3)
68             [], # color-weight functions (adjusted to input range and cached)
69             [] # white-point
70             ];
71              
72             # if one parameter, a hash reference
73 1 50 33     9 if (@_ == 1 && ref($_[0]) eq 'HASH') {
    50          
74            
75             # set object contents from hash
76 0         0 _new_from_hash($self, $_[0]);
77            
78             } elsif (@_) {
79            
80             # error
81 0         0 croak('invalid parameter(s)');
82            
83             }
84              
85             # return blessed object
86 1         3 return(bless($self, $class));
87              
88             }
89              
90             # get/set reference to header hash
91             # header contains keys used by 'new'
92             # parameters: ([ref_to_new_hash])
93             # returns: (ref_to_hash)
94             sub header {
95              
96             # get object reference
97 0     0 1   my $self = shift();
98              
99             # if there are parameters
100 0 0         if (@_) {
101            
102             # if one parameter, a hash reference
103 0 0 0       if (@_ == 1 && ref($_[0]) eq 'HASH') {
104            
105             # set header to copy of hash
106 0           $self->[0] = Storable::dclone(shift());
107            
108             } else {
109            
110             # error
111 0           croak('parameter must be a hash reference');
112            
113             }
114            
115             }
116              
117             # return reference
118 0           return($self->[0]);
119              
120             }
121              
122             # get/set reference to illuminant structure
123             # structure: [[start_nm, end_nm, increment], spd_vector]
124             # note: set updates the color-weight functions
125             # parameters: ([ref_to_new_structure])
126             # returns: (ref_to_structure)
127             sub illuminant {
128              
129             # get object reference
130 0     0 1   my $self = shift();
131              
132             # local variables
133 0           my ($array, $sx);
134              
135             # if there are parameters
136 0 0         if (@_) {
137            
138             # if one parameter, an array reference
139 0 0 0       if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
140            
141             # get array reference
142 0           $array = shift();
143            
144             # initialize data array
145 0           $self->[1] = [];
146            
147             # if array is not empty
148 0 0         if (@{$array}) {
  0            
149            
150             # verify array size
151 0 0         (@{$array} == 2) or croak('invalid illuminant array');
  0            
152            
153             # verify wavelength range
154 0 0 0       (ref($array->[0]) eq 'ARRAY' && 3 == @{$array->[0]} && (3 == grep {Scalar::Util::looks_like_number($_)} @{$array->[0]}) && $array->[0][2]) or croak('invalid illuminant wavelength range');
  0   0        
  0   0        
  0            
155            
156             # compute upper index of spd array
157 0 0         (($sx = ICC::Shared::round(($array->[0][1] - $array->[0][0])/$array->[0][2])) > 0) or croak('inconsistent illuminant wavelength range');
158            
159             # verify spd array
160 0 0 0       (ref($array->[1]) eq 'ARRAY' && $#{$array->[1]} == $sx && @{$array->[1]} == grep {Scalar::Util::looks_like_number($_)} @{$array->[1]}) or croak('invalid illuminant spd array');
  0   0        
  0            
  0            
  0            
161            
162             # copy array contents
163 0           $self->[1] = Storable::dclone($array);
164            
165             # if observer array defined
166 0 0         if (defined($self->[2][0])) {
167            
168             # update color-weight functions
169 0           _make_cwf($self);
170            
171             }
172            
173             }
174            
175             } else {
176            
177             # error
178 0           croak('parameter must be an array reference');
179            
180             }
181            
182             }
183              
184             # return reference
185 0           return($self->[1]);
186              
187             }
188              
189             # get/set reference to observer structure
190             # structure: [[start_nm, end_nm, increment], cmf_matrix]
191             # note: set updates the color-weight functions
192             # parameters: ([ref_to_new_structure])
193             # returns: (ref_to_structure)
194             sub observer {
195              
196             # get object reference
197 0     0 1   my $self = shift();
198              
199             # local variables
200 0           my ($array, $sx);
201              
202             # if there are parameters
203 0 0         if (@_) {
204            
205             # if one parameter, an array reference
206 0 0 0       if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
207            
208             # get array reference
209 0           $array = shift();
210            
211             # initialize data array
212 0           $self->[2] = [];
213            
214             # if array is not empty
215 0 0         if (@{$array}) {
  0            
216            
217             # verify array size
218 0 0         (@{$array} == 2) or croak('invalid observer array');
  0            
219            
220             # verify wavelength range
221 0 0 0       (ref($array->[0]) eq 'ARRAY' && 3 == @{$array->[0]} && (3 == grep {Scalar::Util::looks_like_number($_)} @{$array->[0]}) && $array->[0][2]) or croak('invalid observer wavelength range');
  0   0        
  0   0        
  0            
222            
223             # compute upper index of observer matrix
224 0 0         (($sx = ICC::Shared::round(($array->[0][1] - $array->[0][0])/$array->[0][2])) > 0) or croak('inconsistent observer wavelength range');
225            
226             # verify observer matrix
227 0 0 0       ((ref($array->[1]) eq 'ARRAY' || UNIVERSAL::isa($array->[1], 'Math::Matrix')) && ref($array->[1][0]) eq 'ARRAY' && $#{$array->[1][0]} == $sx && @{$array->[1][0]} == grep {Scalar::Util::looks_like_number($_)} @{$array->[1][0]}) or croak('invalid observer cmf matrix');
  0   0        
  0   0        
  0   0        
  0            
228            
229             # copy array contents
230 0           $self->[2] = Storable::dclone($array);
231            
232             # if illuminant array defined
233 0 0         if (defined($self->[1][0])) {
234            
235             # update color-weight functions
236 0           _make_cwf($self);
237            
238             }
239            
240             }
241            
242             } else {
243            
244             # error
245 0           croak('parameter must be an array reference');
246            
247             }
248            
249             }
250              
251             # return reference
252 0           return($self->[2]);
253              
254             }
255              
256             # get/set reference to color-weight function structure
257             # structure: [[start_nm, end_nm, increment], cwf_matrix]
258             # parameters: ([ref_to_new_structure])
259             # returns: (ref_to_structure)
260             sub cwf {
261              
262             # get object reference
263 0     0 1   my $self = shift();
264              
265             # local variables
266 0           my ($array, $sx);
267              
268             # if there are parameters
269 0 0         if (@_) {
270            
271             # if one parameter, an array reference
272 0 0 0       if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
273            
274             # get array reference
275 0           $array = shift();
276            
277             # initialize data array
278 0           $self->[3] = [];
279            
280             # if array is not empty
281 0 0         if (@{$array}) {
  0            
282            
283             # verify array size
284 0 0         (@{$array} == 2) or croak('invalid cwf array');
  0            
285            
286             # verify wavelength range
287 0 0 0       (ref($array->[0]) eq 'ARRAY' && 3 == @{$array->[0]} && (3 == grep {Scalar::Util::looks_like_number($_)} @{$array->[0]}) && $array->[0][2]) or croak('invalid cwf wavelength range');
  0   0        
  0   0        
  0            
288            
289             # compute upper index of cwf matrix
290 0 0         (($sx = ICC::Shared::round(($array->[0][1] - $array->[0][0])/$array->[0][2])) > 0) or croak('inconsistent cwf wavelength range');
291            
292             # verify cwf matrix
293 0 0 0       ((ref($array->[1]) eq 'ARRAY' || UNIVERSAL::isa($array->[1], 'Math::Matrix')) && ref($array->[1][0]) eq 'ARRAY' && $#{$array->[1][0]} == $sx && @{$array->[1][0]} == grep {Scalar::Util::looks_like_number($_)} @{$array->[1][0]}) or croak('invalid cwf weight matrix');
  0   0        
  0   0        
  0   0        
  0            
294            
295             # copy array contents
296 0           $self->[3] = Storable::dclone($array);
297            
298             # for each weight function (XYZ -or- RGBV)
299 0           for my $i (0 .. $#{$array->[1]}) {
  0            
300            
301             # update white point array
302 0           $self->[5][$i] = List::Util::sum(@{$array->[1][$i]});
  0            
303            
304             }
305            
306             }
307            
308             } else {
309            
310             # error
311 0           croak('parameter must be an array reference');
312            
313             }
314            
315             }
316              
317             # return reference
318 0           return($self->[3]);
319              
320             }
321              
322             # get illuminant white point
323             # encoding specified by 'encoding' hash key
324             # 'encoding' values are 'XYZ', 'xyz', 'ICC_XYZ', 'ICC_XYZNumber', and 'density'
325             # parameters: ([hash])
326             # returns: (XYZ_vector)
327             sub iwtpt {
328              
329             # get parameters
330 0     0 1   my ($self, $hash) = @_;
331              
332             # local variable
333 0           my ($code);
334              
335             # return white point with optional encoding
336 0 0         return(defined($code = _encoding($self, $hash)) ? [&$code(@{$self->[5]})] : $self->[5]);
  0            
337              
338             }
339              
340             # transform data
341             # hash keys are: 'range', and 'encoding'
342             # 'range' value is: [start_nm, end_nm, increment]
343             # 'encoding' values are 'XYZ', 'xyz', 'ICC_XYZ', 'ICC_XYZNumber', 'RGBV', 'rgbv' and 'density'
344             # supported input types:
345             # parameters: (list, [hash])
346             # parameters: (vector, [hash])
347             # parameters: (matrix, [hash])
348             # parameters: (Math::Matrix_object, [hash])
349             # parameters: (structure, [hash])
350             # returns: (same_type_as_input)
351             sub transform {
352              
353             # set hash value (0 or 1)
354 0 0   0 1   my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
355              
356             # verify color weight array
357 0 0 0       (defined($_[0]->[3][0]) && defined($_[0]->[3][1])) or croak('color weight array undefined');
358              
359             # if input a 'Math::Matrix' object
360 0 0 0       if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
361            
362             # call matrix transform
363 0           &_trans2;
364            
365             # if input an array reference
366             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
367            
368             # if array contains numbers (vector)
369 0 0 0       if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0        
  0            
  0            
370            
371             # call vector transform
372 0           &_trans1;
373            
374             # if array contains vectors (2-D array)
375 0 0         } elsif (ref($_[1][0]) eq 'ARRAY' && @{$_[1]} == grep {ref($_) eq 'ARRAY' && Scalar::Util::looks_like_number($_->[0])} @{$_[1]}) {
  0            
  0            
376            
377             # call matrix transform
378 0           &_trans2;
379            
380             } else {
381            
382             # call structure transform
383 0           &_trans3;
384            
385             }
386            
387             # if input a list (of numbers)
388 0           } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
389            
390             # call list transform
391 0           &_trans0;
392            
393             } else {
394            
395             # error
396 0           croak('invalid transform input');
397            
398             }
399              
400             }
401              
402             # compute Jacobian matrix
403             # hash keys are: 'range', and 'encoding'
404             # 'range' value is: [start_nm, end_nm, increment]
405             # 'encoding' values are 'XYZ', 'xyz', 'ICC_XYZ', 'ICC_XYZNumber', 'RGBV', 'rgbv' and 'density'
406             # parameters: (input_vector, [hash])
407             # returns: (Jacobian_matrix, [output_vector])
408             sub jacobian {
409              
410             # get parameters
411 0     0 1   my ($self, $in, $hash) = @_;
412              
413             # local variables
414 0           my ($range, $encoding, $jac, $out, $sf);
415              
416             # check if ICC::Support::Lapack module is loaded
417 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
418              
419             # get input spectral range
420 0 0 0       ($range = $hash->{'range'} || $srh->{@{$in}}) or croak('spectral range must be specified');
421              
422             # if data increment == cwf increment
423 0 0         if ($range->[2] == $self->[3][0][2]) {
    0          
424            
425             # use adjusted cwf
426 0           $jac = Storable::dclone(_adjust_cwf($self, $range));
427            
428             # if data increment > cwf increment
429             } elsif ($range->[2] > $self->[3][0][2]) {
430            
431             # use reduced cwf
432 0           $jac = Storable::dclone(_reduce_cwf($self, $range));
433            
434             } else {
435            
436             # error
437 0           croak('data increment < cwf increment');
438            
439             }
440              
441             # if hash contain 'encoding' other than 'XYZ' or 'RGBV'
442 0 0 0       if (defined($encoding = $hash->{'encoding'}) && $encoding ne 'XYZ' && $encoding ne 'RGBV') {
      0        
443            
444             # if encoding is 'ICC_XYZ'
445 0 0 0       if ($encoding eq 'ICC_XYZ' && @{$jac} == 3) {
  0 0 0        
    0 0        
    0 0        
    0 0        
446            
447             # for each XYZ
448 0           for my $i (0 .. 2) {
449            
450             # for each spectral value
451 0           for my $j (0 .. $#{$jac->[0]}) {
  0            
452            
453             # adjust value
454 0           $jac->[$i][$j] *= 327.68/65535;
455            
456             }
457            
458             }
459            
460             # if encoding is 'ICC_XYZNumber'
461 0           } elsif ($encoding eq 'ICC_XYZNumber' && @{$jac} == 3) {
462            
463             # for each XYZ
464 0           for my $i (0 .. 2) {
465            
466             # for each spectral value
467 0           for my $j (0 .. $#{$jac->[0]}) {
  0            
468            
469             # adjust value
470 0           $jac->[$i][$j] /= 100;
471            
472             }
473            
474             }
475            
476             # if encoding is 'xyz'
477 0           } elsif ($encoding eq 'xyz' && @{$jac} == 3) {
478            
479             # verify white point
480 0 0 0       ($self->[5][0] && $self->[5][1] && $self->[5][2]) or croak('invalid illuminant white point');
      0        
481            
482             # for each XYZ
483 0           for my $i (0 .. 2) {
484            
485             # for each spectral value
486 0           for my $j (0 .. $#{$jac->[0]}) {
  0            
487            
488             # adjust value
489 0           $jac->[$i][$j] /= $self->[5][$i];
490            
491             }
492            
493             }
494            
495             # if encoding is 'unit'
496 0           } elsif ($encoding eq 'unit' && @{$jac} == 4) {
497            
498             # for each RGBV
499 0           for my $i (0 .. 3) {
500            
501             # for each spectral value
502 0           for my $j (0 .. $#{$jac->[0]}) {
  0            
503            
504             # adjust value
505 0           $jac->[$i][$j] /= 100;
506            
507             }
508            
509             }
510            
511             # if encoding is 'density'
512 0           } elsif ($encoding eq 'density' && @{$jac} == 4) {
513            
514             # delete encoding
515 0           delete($hash->{'encoding'});
516            
517             # get the output values (RGBV)
518 0           $out = _trans1($self, $in, $hash);
519            
520             # verify output
521 0 0 0       ($out->[0] && $out->[1] && $out->[2] && $out->[3]) or croak('invalid density value');
      0        
      0        
522            
523             # for each RGBV
524 0           for my $i (0 .. 3) {
525            
526             # compute scale factor
527 0           $sf = - $out->[$i] * ICC::Shared::ln10;
528            
529             # for each spectral value
530 0           for my $j (0 .. $#{$jac->[0]}) {
  0            
531            
532             # adjust value
533 0           $jac->[$i][$j] /= $sf;
534            
535             }
536            
537             }
538            
539             # restore encoding
540 0           $hash->{'encoding'} = 'density';
541            
542             } else {
543            
544             # error
545 0           croak('unsupported XYZ/RGBV encoding');
546             }
547            
548             }
549              
550             # if output vector wanted
551 0 0         if (wantarray) {
552            
553             # return Jacobian and output vector
554 0           return($jac, _trans1($self, $in, $hash));
555            
556             } else {
557            
558             # return Jacobian only
559 0           return($jac);
560            
561             }
562            
563             }
564              
565             # print object contents to string !!! needs work !!!
566             # format is an array structure
567             # parameter: ([format])
568             # returns: (string)
569             sub sdump {
570              
571             # get parameters
572 0     0 1   my ($self, $p) = @_;
573              
574             # local variables
575 0           my ($s, $fmt);
576              
577             # resolve parameter to an array reference
578 0 0         $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
579              
580             # get format string
581 0 0 0       $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef';
582              
583             # set string to object ID
584 0           $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
585              
586             # return
587 0           return($s);
588              
589             }
590              
591             # transform list
592             # parameters: (object_reference, list, [hash])
593             # returns: (list)
594             sub _trans0 {
595              
596             # local variables
597 0     0     my ($self, $hash);
598              
599             # get object reference
600 0           $self = shift();
601              
602             # get optional hash
603 0 0         $hash = pop() if (ref($_[-1]) eq 'HASH');
604              
605             # return
606 0           return(@{_trans1($self, \@_, $hash)});
  0            
607              
608             }
609              
610             # transform vector
611             # parameters: (object_reference, vector, [hash])
612             # returns: (vector)
613             sub _trans1 {
614              
615             # get parameters
616 0     0     my ($self, $in, $hash) = @_;
617              
618             # local variables
619 0           my ($range, $cwf, $out, $code);
620              
621             # check if ICC::Support::Lapack module is loaded
622 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
623              
624             # get input spectral range
625 0 0 0       ($range = $hash->{'range'} || $srh->{@{$in}}) or croak('spectral range must be specified');
626              
627             # if data increment == cwf increment
628 0 0         if ($range->[2] == $self->[3][0][2]) {
    0          
629            
630             # adjust cwf to match data range
631 0           $cwf = _adjust_cwf($self, $range);
632            
633             # if data increment > cwf increment
634             } elsif ($range->[2] > $self->[3][0][2]) {
635            
636             # adjust cwf to match data range
637 0           $cwf = _reduce_cwf($self, $range);
638            
639             } else {
640            
641             # error
642 0           croak('data increment < cwf increment');
643            
644             }
645            
646             # if ICC::Support::Lapack module is loaded
647 0 0         if ($lapack) {
648            
649             # compute output matrix using BLAS dgemv function
650 0           $out = ICC::Support::Lapack::matf_vec_trans($in, $cwf);
651            
652             } else {
653            
654             # for each XYZ or RGBV
655 0           for my $i (0 .. $#{$cwf}) {
  0            
656            
657             # compute dot product
658 0           $out->[$i] = ICC::Shared::dotProduct($in, $cwf->[$i]);
659            
660             }
661            
662             }
663              
664             # return with optional encoding
665 0 0         return(defined($code = _encoding($self, $hash)) ? [&$code(@{$out})] : $out);
  0            
666              
667             }
668              
669             # transform matrix (2-D array -or- Math::Matrix object)
670             # parameters: (object_reference, matrix, [hash])
671             # returns: (matrix)
672             sub _trans2 {
673              
674             # get parameters
675 0     0     my ($self, $in, $hash) = @_;
676              
677             # local variables
678 0           my ($range, $cwf, $out, $code);
679              
680             # check if ICC::Support::Lapack module is loaded
681 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
682              
683             # get input spectral range
684 0 0 0       ($range = $hash->{'range'} || $srh->{@{$in->[0]}}) or croak('spectral range must be specified');
685              
686             # if data increment == cwf increment
687 0 0         if ($range->[2] == $self->[3][0][2]) {
    0          
688            
689             # adjust cwf to match data range
690 0           $cwf = _adjust_cwf($self, $range);
691            
692             # if data increment > cwf increment
693             } elsif ($range->[2] > $self->[3][0][2]) {
694            
695             # adjust cwf to match data range
696 0           $cwf = _reduce_cwf($self, $range);
697              
698             } else {
699            
700             # error
701 0           croak('data increment < cwf increment');
702            
703             }
704              
705             # if ICC::Support::Lapack module is loaded
706 0 0         if ($lapack) {
707            
708             # compute output matrix using BLAS dgemm function
709 0           $out = ICC::Support::Lapack::matf_mat_trans($in, $cwf);
710            
711             } else {
712            
713             # for each sample
714 0           for my $i (0 .. $#{$in}) {
  0            
715            
716             # for each XYZ or RGBV
717 0           for my $j (0 .. $#{$cwf}) {
  0            
718            
719             # compute dot product
720 0           $out->[$i][$j] = ICC::Shared::dotProduct($in->[$i], $cwf->[$j]);
721            
722             }
723            
724             }
725            
726             }
727              
728             # if encoding enabled
729 0 0         if ($code = _encoding($self, $hash)) {
730            
731             # for each sample
732 0           for my $i (0 .. $#{$out}) {
  0            
733            
734             # apply encoding
735 0           @{$out->[$i]} = &$code(@{$out->[$i]});
  0            
  0            
736            
737             }
738            
739             }
740              
741             # return output (Math::Matrix object or 2-D array)
742 0 0         return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
743              
744             }
745              
746             # transform structure
747             # parameters: (object_reference, structure, [hash])
748             # returns: (structure)
749             sub _trans3 {
750              
751             # get parameters
752 0     0     my ($self, $in, $hash) = @_;
753              
754             # transform the array structure
755 0           _crawl($self, \&_trans1, $in, my $out = [], $hash);
756              
757             # return output structure
758 0           return($out);
759              
760             }
761              
762             # recursive transform
763             # array structure is traversed until all vectors are found and transformed
764             # parameters: (object_reference, subroutine_reference, input_array_reference, output_array_reference, hash)
765             sub _crawl {
766              
767             # get parameters
768 0     0     my ($self, $sub, $in, $out, $hash) = @_;
769              
770             # if input is a vector (reference to a numeric array)
771 0 0         if (@{$in} == grep {Scalar::Util::looks_like_number($_)} @{$in}) {
  0            
  0            
  0            
772            
773             # transform input vector and copy to output
774 0           @{$out} = @{$sub->($self, $in, $hash)};
  0            
  0            
775            
776             } else {
777            
778             # for each input element
779 0           for my $i (0 .. $#{$in}) {
  0            
780            
781             # if an array reference
782 0 0         if (ref($in->[$i]) eq 'ARRAY') {
783            
784             # transform next level
785 0           _crawl($self, $sub, $in->[$i], $out->[$i] = [], $hash);
786            
787             } else {
788            
789             # error
790 0           croak('invalid input structure');
791            
792             }
793            
794             }
795            
796             }
797            
798             }
799              
800             # reduce cwf matrix to match data range
801             # the reduced cwf and data range is cached
802             # parameters: (object_reference, data_range)
803             # returns: (reduced_cwf)
804             sub _reduce_cwf {
805              
806             # get parameters
807 0     0     my ($self, $range_data) = @_;
808              
809             # local variables
810 0           my ($method, $bandpass, $range_cwf, $cwf, $mat);
811              
812             # if cached CWF matches data range
813 0 0 0       if (defined($self->[4][0]) && $self->[4][0][0] == $range_data->[0] && $self->[4][0][1] == $range_data->[1] && $self->[4][0][2] == $range_data->[2]) {
      0        
      0        
814            
815             # return cached CWF
816 0           return($self->[4][1]);
817            
818             }
819              
820             # check if ICC::Support::Lapack module is loaded
821 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
822              
823             # get interpolation method
824 0   0       $method = $self->[0]{'method'} // 'cspline';
825              
826             # get bandpass correction
827 0   0       $bandpass = $self->[0]{'bandpass'} // 0;
828              
829             # get cwf range
830 0           $range_cwf = $self->[3][0];
831              
832             # if method is linear
833 0 0         if ($method eq 'linear') {
    0          
    0          
834            
835             # compute linear interpolation matrix
836 0           $mat = ICC::Shared::linear_matrix($range_data, $range_cwf, 'copy');
837            
838             # if method is cubic spline
839             } elsif ($method eq 'cspline') {
840            
841             # compute cubic spline interpolation matrix
842 0           $mat = ICC::Shared::cspline_matrix($range_data, $range_cwf, 'copy');
843            
844             # if method is Lagrange
845             } elsif ($method eq 'lagrange') {
846            
847             # compute Lagrange interpolation matrix
848 0           $mat = ICC::Shared::lagrange_matrix($range_data, $range_cwf, 'copy');
849            
850             } else {
851            
852             # error
853 0           croak('invalid interpolation method');
854            
855             }
856            
857             # if ICC::Support::Lapack module is loaded
858 0 0         if ($lapack) {
859            
860             # compute cwf using BLAS dgemm function
861 0           $cwf = ICC::Support::Lapack::mat_xplus($self->[3][1], $mat);
862            
863             # if bandpass correction is enabled
864 0 0         if ($bandpass) {
865            
866             # apply correction matrix
867 0           $cwf = ICC::Support::Lapack::mat_xplus($cwf, _bandpass($range_data, $method, $bandpass));
868            
869             }
870            
871             } else {
872            
873             # compute cwf using Math::Matrix module
874 0           $cwf = bless(Storable::dclone($self->[3][1]), 'Math::Matrix') * $mat;
875            
876             # if bandpass correction is enabled
877 0 0         if ($bandpass) {
878            
879             # apply correction matrix
880 0           $cwf = $cwf * _bandpass($range_data, $method, $bandpass);
881            
882             }
883            
884             }
885              
886             # cache reduced CWF
887 0           $self->[4][0] = $range_data;
888 0           $self->[4][1] = $cwf;
889              
890             # return
891 0           return($cwf);
892              
893             }
894              
895             # make bandpass correction matrix
896             # per ASTM E 2729 or deconvolution
897             # parameters: (data_range, interpolation_method, bandpass_method)
898             # returns: (matrix)
899             sub _bandpass {
900              
901             # get parameters
902 0     0     my ($range_data, $method, $bandpass) = @_;
903              
904             # local variables
905 0           my ($ix, $jx, $range_mat, $mati, $matc, $bpf, $info, $matrix);
906              
907             # compute bandpass matrix upper index
908 0           $ix = ($range_data->[1] - $range_data->[0])/$range_data->[2];
909            
910             # set bandpass function upper index
911 0           $jx = 20;
912              
913             # if bandpass value if 'astm'
914 0 0         if ($bandpass eq 'astm') {
915            
916             # return ASTM E 2729 bandpass rectification matrix
917 0           return(_bandpass_astm($ix));
918            
919             } else {
920            
921             # compute interpolation matrix range
922 0           $range_mat = [$range_data->[0] - $range_data->[2], $range_data->[1] + $range_data->[2], $range_data->[2]/10];
923            
924             # if method is linear
925 0 0         if ($method eq 'linear') {
    0          
    0          
926            
927             # compute linear interpolation matrix
928 0           $mati = ICC::Shared::linear_matrix($range_data, $range_mat, 'linear');
929            
930             # if method is cubic spline
931             } elsif ($method eq 'cspline') {
932            
933             # compute cubic spline interpolation matrix
934 0           $mati = ICC::Shared::cspline_matrix($range_data, $range_mat, 'linear');
935            
936             # if method is Lagrange
937             } elsif ($method eq 'lagrange') {
938            
939             # compute Lagrange interpolation matrix
940 0           $mati = ICC::Shared::lagrange_matrix($range_data, $range_mat, 'linear');
941            
942             } else {
943            
944             # error
945 0           croak('invalid interpolation method');
946            
947             }
948            
949             # if bandpass value if 'triangle'
950 0 0 0       if ($bandpass eq 'triangle') {
    0          
    0          
951            
952             # compute bandpass function array
953 0           $bpf = _bandpass_fn(1, $jx);
954            
955             # if bandpass value if 'trapezoid' (combination of three triangular sub-bands)
956             } elsif ($bandpass eq 'trapezoid') {
957            
958             # compute bandpass function array
959 0           $bpf = _bandpass_fn(1/3, $jx);
960            
961             # if bandpass value is an array reference
962 0           } elsif (ref($bandpass) eq 'ARRAY' && $#{$bandpass} == $jx) {
963            
964             # set value
965 0           $bpf = $bandpass;
966            
967             } else {
968            
969             # error
970 0           croak('invalid bandpass correction');
971            
972             }
973            
974             # compute convolution matrix
975 0           $matc = _conv_matrix($bpf, $ix);
976            
977             # check if ICC::Support::Lapack module is loaded
978 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
979            
980             # if Lapack module loaded
981 0 0         if ($lapack) {
982            
983             # multiply matrices and invert using Lapack module
984 0           ($info, $matrix) = ICC::Support::Lapack::inv(ICC::Support::Lapack::mat_xplus($matc, $mati));
985            
986             # return deconvolution matrix
987 0           return(bless($matrix, 'Math::Matrix'));
988            
989             } else {
990            
991             # return deconvolution matrix
992 0           return(($matc * $mati)->invert());
993            
994             }
995            
996             }
997            
998             }
999              
1000             # make ASTM E 2729 bandpass rectification matrix
1001             # parameter: (upper_index)
1002             # returns: (matrix)
1003             sub _bandpass_astm {
1004              
1005             # get upper index
1006 0     0     my $ix = shift();
1007              
1008             # local variables
1009 0           my ($matrix, @zeros);
1010              
1011             # make array of zeros
1012 0           @zeros = (0) x ($ix + 1);
1013              
1014             # for each matrix row
1015 0           for my $i (0 .. $ix) {
1016            
1017             # set the row to zeros
1018 0           $matrix->[$i] = [@zeros];
1019            
1020             # for each matrix column
1021 0           for my $j (0 .. $ix) {
1022            
1023             # if main diagonal
1024 0 0 0       if ($i == $j) {
    0 0        
    0 0        
      0        
1025            
1026 0 0 0       if ($i == 0 || $i == $ix) {
    0 0        
1027            
1028 0           $matrix->[$i][$j] = 1;
1029            
1030             } elsif ($i == 1 || $i == $ix - 1) {
1031            
1032 0           $matrix->[$i][$j] = 1.21;
1033            
1034             } else {
1035            
1036 0           $matrix->[$i][$j] = 1.22;
1037            
1038             }
1039            
1040             # if +/- 1 diagonal, and not the first or last rows
1041             } elsif (abs($i - $j) == 1 && $i > 0 && $i < $ix) {
1042            
1043 0 0 0       if ($j == 0 || $j == $ix) {
1044            
1045 0           $matrix->[$i][$j] = -0.10;
1046            
1047             } else {
1048            
1049 0           $matrix->[$i][$j] = -0.12;
1050            
1051             }
1052            
1053             # if +/- 2 diagonal, and not the first or last rows
1054             } elsif (abs($i - $j) == 2 && $i > 0 && $i < $ix) {
1055            
1056 0           $matrix->[$i][$j] = 0.01;
1057            
1058             }
1059            
1060             }
1061            
1062             }
1063              
1064             # return Math::Matrix object
1065 0           return(bless($matrix, 'Math::Matrix'));
1066              
1067             }
1068              
1069             # make simple bandpass function
1070             # shape ranges from 0 to 1 (rectangular to triangular)
1071             # upper index must be divisible by 4
1072             # parameters: (shape, upper_index)
1073             # returns: (bandpass_array)
1074             sub _bandpass_fn {
1075              
1076             # get parameters
1077 0     0     my ($shape, $ix) = @_;
1078              
1079             # local variables
1080 0           my ($m, $v, @array, $sum);
1081              
1082             # verify shape
1083 0 0 0       ($shape >= 0 && $shape <= 1) or croak('invalid shape parameter');
1084              
1085             # verify upper index
1086 0 0 0       ($ix == int($ix) && ! ($ix % 4)) or croak('invalid upper index');
1087              
1088             # if shape not rectangular
1089 0 0         if ($shape) {
1090            
1091             # compute slope
1092 0           $m = 2/($shape * $ix);
1093            
1094             } else {
1095            
1096             # set slope to big number
1097 0           $m = 1E100;
1098            
1099             }
1100              
1101             # for each array element
1102 0           for my $i (0 .. $ix/2) {
1103            
1104             # compute value
1105 0           $v = ($i - $ix/4) * $m + 0.5;
1106            
1107             # set array elements to limited value
1108 0 0         $array[$i] = $array[$ix - $i] = $v < 0 ? 0 : $v > 1 ? 1 : $v;
    0          
1109            
1110             }
1111              
1112             # compute array sum
1113 0           $sum = List::Util::sum(@array);
1114              
1115             # return normalized array
1116 0           return([map {$_/$sum} @array]);
  0            
1117              
1118             }
1119              
1120             # make convolution matrix
1121             # parameters: (bandpass_function_vector, row_upper_index)
1122             # returns: (convolution_matrix)
1123             sub _conv_matrix {
1124              
1125             # get parameters
1126 0     0     my ($bpf, $ix) = @_;
1127              
1128             # local variables
1129 0           my ($n, $p, @zeros, $mat);
1130              
1131             # verify bandpass function
1132 0 0         (@{$bpf} % 2) or croak('bandpass function must have odd number of elements');
  0            
1133              
1134             # verify row upper index
1135 0 0 0       ($ix == int($ix) && $ix >= 0) or croak('row upper index must be an integer >= 0');
1136              
1137             # compute upper column index
1138 0           $n = $#{$bpf} * ($ix + 2)/2;
  0            
1139              
1140             # compute bandpass increment
1141 0           $p = $#{$bpf}/2;
  0            
1142              
1143             # make array of zeros
1144 0           @zeros = (0) x ($n + 1);
1145              
1146             # for each row
1147 0           for my $i (0 .. $ix) {
1148            
1149             # add row of zeros
1150 0           $mat->[$i] = [@zeros];
1151            
1152             # for each bandpass value
1153 0           for my $j (0 .. $#{$bpf}) {
  0            
1154            
1155             # copy bandpass value
1156 0           $mat->[$i][$i * $p + $j] = $bpf->[$j];
1157            
1158             }
1159            
1160             }
1161              
1162             # return
1163 0           return(bless($mat, 'Math::Matrix'));
1164              
1165             }
1166              
1167             # adjust cwf matrix to match data range
1168             # the adjusted cwf and data range is cached
1169             # parameters: (object_reference, data_range)
1170             # returns: (adjusted_cwf)
1171             sub _adjust_cwf {
1172              
1173             # get parameters
1174 0     0     my ($self, $range_data) = @_;
1175              
1176             # local variables
1177 0           my ($bandpass, $off, $cwf);
1178              
1179             # if cached CWF matches data range
1180 0 0 0       if (defined($self->[4][0]) && $self->[4][0][0] == $range_data->[0] && $self->[4][0][1] == $range_data->[1] && $self->[4][0][2] == $range_data->[2]) {
      0        
      0        
1181            
1182             # return cached CWF
1183 0           return($self->[4][1]);
1184            
1185             }
1186              
1187             # check if ICC::Support::Lapack module is loaded
1188 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
1189              
1190             # get bandpass correction
1191 0   0       $bandpass = $self->[0]{'bandpass'} // 0;
1192              
1193             # compute range offset
1194 0           $off = abs(($range_data->[0] - $self->[3][0][0])/$range_data->[2]);
1195              
1196             # verify offset is an integer
1197 0 0         (abs($off - ICC::Shared::round($off)) < 1E-12) or croak('range offset not an integer');
1198              
1199             # adjust CWF matrix
1200 0           $cwf = _adjust_matrix($self->[3][1], $self->[3][0], $range_data);
1201              
1202             # if bandpass correction enabled, and not table 6
1203 0 0 0       if ($bandpass && $bandpass ne 'six') {
1204            
1205             # if ICC::Support::Lapack module is loaded
1206 0 0         if ($lapack) {
1207            
1208             # apply correction matrix
1209 0           $cwf = ICC::Support::Lapack::mat_xplus($cwf, _bandpass($range_data, 'lagrange', $bandpass));
1210            
1211             } else {
1212            
1213             # apply correction matrix
1214 0           $cwf = bless($cwf, 'Math::Matrix') * _bandpass($range_data, 'lagrange', $bandpass);
1215            
1216             }
1217            
1218             }
1219              
1220             # cache adjusted CWF
1221 0           $self->[4][0] = $range_data;
1222 0           $self->[4][1] = $cwf;
1223              
1224             # return
1225 0           return($cwf);
1226              
1227             }
1228              
1229             # adjust matrix to match data range
1230             # parameters: (matrix, matrix_range, data_range)
1231             # returns: (adjusted_matrix)
1232             sub _adjust_matrix {
1233              
1234             # get parameters
1235 0     0     my ($matrix, $range_matrix, $range_data) = @_;
1236              
1237             # local variables
1238 0           my ($adj, $j);
1239              
1240             # clone matrix
1241 0           $adj = Storable::dclone($matrix);
1242              
1243             # for each function
1244 0           for my $i (0 .. $#{$adj}) {
  0            
1245            
1246             # if cwf start < data start
1247 0 0         if ($range_matrix->[0] < $range_data->[0]) {
    0          
1248            
1249             # compute number of elements to combine
1250 0           $j = int(($range_data->[0] - $range_matrix->[0])/$range_data->[2] + 1.5);
1251            
1252             # combine elements
1253 0           splice(@{$adj->[$i]}, 0, $j, List::Util::sum(@{$adj->[$i]}[0 .. ($j - 1)]));
  0            
  0            
1254            
1255             # if cwf start > data start
1256             } elsif ($range_matrix->[0] > $range_data->[0]) {
1257            
1258             # compute number of zeros to add
1259 0           $j = int(($range_matrix->[0] - $range_data->[0])/$range_data->[2] + 0.5);
1260            
1261             # add zeros
1262 0           unshift(@{$adj->[$i]}, (0) x $j);
  0            
1263            
1264             }
1265            
1266             # if cwf end > data end
1267 0 0         if ($range_matrix->[1] > $range_data->[1]) {
    0          
1268            
1269             # compute number of elements to combine
1270 0           $j = int(($range_matrix->[1] - $range_data->[1])/$range_data->[2] + 1.5);
1271            
1272             # combine elements
1273 0           splice(@{$adj->[$i]}, -$j, $j, List::Util::sum(@{$adj->[$i]}[-$j .. -1]));
  0            
  0            
1274            
1275             # if cwf end < data end
1276             } elsif ($range_matrix->[1] < $range_data->[1]) {
1277            
1278             # compute number of zeros to add
1279 0           $j = int(($range_data->[1] - $range_matrix->[1])/$range_data->[2] + 0.5);
1280            
1281             # add zeros
1282 0           push(@{$adj->[$i]}, (0) x $j);
  0            
1283            
1284             }
1285            
1286             }
1287              
1288             # return
1289 0           return($adj);
1290              
1291             }
1292              
1293             # compute CIE color-weight functions
1294             # parameters: (object_reference)
1295             # results are saved in the object
1296             # cwf cache is cleared
1297             sub _make_cwf {
1298              
1299             # get parameters
1300 0     0     my $self = shift();
1301              
1302             # local variables
1303 0           my ($range, $obs, $inc, $smooth, $method, $bandpass, $matc, $illum, $spd, $ks, @k, @Wx, @Wy, @Wz);
1304              
1305             # if cwf_range key defined
1306 0 0 0       if (defined($self->[0]{'cwf_range'})) {
    0          
1307            
1308             # use supplied range
1309 0           $range = $self->[0]{'cwf_range'};
1310            
1311             # interpolate observer functions, with linear extrapolation
1312 0           $obs = ICC::Support::Lapack::matf_mat_trans($self->[2][1], ICC::Shared::cspline_matrix($self->[2][0], $range, 'linear'));
1313            
1314             # if observer requires interpolation
1315             } elsif ($self->[2][0][2] != ($inc = $self->[0]{'increment'} // 1)) {
1316            
1317             # use observer range, changing increment
1318 0           $range = [$self->[2][0][0], $self->[2][0][1], $inc];
1319            
1320             # interpolate observer functions
1321 0           $obs = ICC::Support::Lapack::matf_mat_trans($self->[2][1], ICC::Shared::cspline_matrix($self->[2][0], $range));
1322            
1323             } else {
1324            
1325             # use observer range
1326 0           $range = $self->[2][0];
1327            
1328             # get observer functions
1329 0           $obs = $self->[2][1];
1330            
1331             }
1332              
1333             # if observer requires exponentiation
1334 0 0         if ($self->[0]{'observer_exp'}) {
1335            
1336             # for each spectral value
1337 0           for my $i (0 .. $#{$obs->[0]}) {
  0            
1338            
1339             # exponentiate
1340 0           $obs->[0][$i] = exp($obs->[0][$i]);
1341 0           $obs->[1][$i] = exp($obs->[1][$i]);
1342 0           $obs->[2][$i] = exp($obs->[2][$i]);
1343            
1344             }
1345            
1346             }
1347              
1348             # if illuminant is defined
1349 0 0         if (defined($self->[1][0])) {
1350            
1351             # compute illuminant smoothness
1352 0           $smooth = _smoothness($self->[1][1]);
1353            
1354             # get illuminant interpolation method
1355 0 0 0       $method = $self->[0]{'imethod'} // ($smooth == 0 || $smooth > 0.001) ? 'linear' : 'cspline';
      0        
1356            
1357             # if illuminant bandpass correction enabled
1358 0 0         if (defined($bandpass = $self->[0]{'ibandpass'})) {
1359            
1360             # compute bandpass correction matrix
1361 0           $matc = _bandpass($self->[1][0], $method, $bandpass);
1362            
1363             # apply bandpass correction matrix
1364 0           $illum = [map {ICC::Shared::dotProduct($self->[1][1], $_)} @{$matc}];
  0            
  0            
1365            
1366             } else {
1367            
1368             # use illuminant as is
1369 0           $illum = $self->[1][1];
1370            
1371             }
1372            
1373             # if method is linear
1374 0 0         if ($method eq 'linear') {
    0          
    0          
1375            
1376             # interpolate illuminant data
1377 0           $spd = ICC::Shared::linear($illum, $self->[1][0], $range, 'copy');
1378            
1379             # if method is cubic spline
1380             } elsif ($method eq 'cspline') {
1381            
1382             # interpolate illuminant data
1383 0           $spd = ICC::Shared::cspline($illum, $self->[1][0], $range, 'copy');
1384            
1385             # if method is Lagrange -or- ASTM E 2022
1386             } elsif ($method eq 'lagrange') {
1387            
1388             # interpolate illuminant data
1389 0           $spd = ICC::Shared::lagrange($illum, $self->[1][0], $range, 'copy');
1390            
1391             } else {
1392            
1393             # error
1394 0           croak('invalid illuminant interpolation method');
1395            
1396             }
1397            
1398             # get or compute summation constant
1399 0   0       $ks = $self->[0]{'cwf_ks'} // 100/ICC::Shared::dotProduct($spd, $obs->[1]);
1400            
1401             # for each spectral value
1402 0           for my $i (0 .. $#{$obs->[0]}) {
  0            
1403            
1404             # compute spectral products (k * illuminant * observer)
1405 0           $Wx[$i] = $ks * $spd->[$i] * $obs->[0][$i];
1406 0           $Wy[$i] = $ks * $spd->[$i] * $obs->[1][$i];
1407 0           $Wz[$i] = $ks * $spd->[$i] * $obs->[2][$i];
1408            
1409             }
1410            
1411             } else {
1412            
1413             # compute summation constants (slightly different for X, Y, Z)
1414 0           $k[0] = 100/List::Util::sum(@{$obs->[0]});
  0            
1415 0           $k[1] = 100/List::Util::sum(@{$obs->[1]});
  0            
1416 0           $k[2] = 100/List::Util::sum(@{$obs->[2]});
  0            
1417            
1418             # for each spectral value
1419 0           for my $i (0 .. $#{$obs->[0]}) {
  0            
1420            
1421             # compute spectral products (k * observer)
1422 0           $Wx[$i] = $k[0] * $obs->[0][$i];
1423 0           $Wy[$i] = $k[1] * $obs->[1][$i];
1424 0           $Wz[$i] = $k[2] * $obs->[2][$i];
1425            
1426             }
1427            
1428             }
1429              
1430             # set wavelength range (start, end, and increment)
1431 0           $self->[3][0] = [@{$range}];
  0            
1432              
1433             # set color-weight functions
1434 0           $self->[3][1][0] = [@Wx];
1435 0           $self->[3][1][1] = [@Wy];
1436 0           $self->[3][1][2] = [@Wz];
1437              
1438             # set illuminant white point (XYZ)
1439 0           $self->[5][0] = List::Util::sum(@Wx);
1440 0           $self->[5][1] = List::Util::sum(@Wy);
1441 0           $self->[5][2] = List::Util::sum(@Wz);
1442              
1443             # clear adjusted CWF cache
1444 0           $self->[4] = [];
1445              
1446             }
1447              
1448             # read CIE illuminant and color-matching functions
1449             # compute color-weight functions (k * illuminant * observer) and white point
1450             # illuminant, observer and increment specified in hash
1451             # parameters: (object_reference, hash)
1452             # results are saved in the object
1453             sub _cie {
1454              
1455             # get parameters
1456 0     0     my ($self, $hash) = @_;
1457              
1458             # local variables
1459 0           my ($array, @files, $illum, $s, $w, $sx);
1460 0           my ($inc, $obs, $cmf, $key);
1461              
1462             # get copy of illuminant array
1463 0           $array = Storable::dclone($hash->{'illuminant'});
1464              
1465             # if empty array (emissive)
1466 0 0         if (! @{$array}) {
  0 0          
    0          
    0          
1467            
1468             # clear illuminant
1469 0           $self->[1] = [];
1470            
1471             # if a measured illuminant (file path)
1472 0           } elsif (@files = grep {-f} File::Glob::bsd_glob($array->[0])) {
1473            
1474             # make new chart object
1475 0           $illum = ICC::Support::Chart->new($files[0]);
1476            
1477             # get sample from hash (default is 1)
1478 0 0         $s = defined($array->[1]) ? $array->[1] : 1;
1479            
1480             # verify sample
1481 0 0 0       (Scalar::Util::looks_like_number($s) && $s == int($s) && $s > 0 && $s <= $illum->size()) or croak('invalid sample number');
      0        
      0        
1482            
1483             # set wavelength range
1484 0           $self->[1][0] = $illum->nm();
1485            
1486             # set spectral values
1487 0 0         ($self->[1][1] = $illum->spectral([$s])->[0]) or croak('illuminant chart has no spectral data');
1488            
1489             # if a standard illuminant (YAML file in data folder)
1490 0           } elsif (@files = grep {-f} File::Glob::bsd_glob(ICC::Shared::getICCPath("Data/$array->[0]_illums_*.yml"))) {
1491            
1492             # load standard illuminants (YAML format)
1493 0           $illum = YAML::Tiny->read($files[0])->[0];
1494            
1495             # get wavelength vector
1496 0           $w = $illum->{'nm'};
1497            
1498             # set wavelength range
1499 0           $self->[1][0] = [$w->[0], $w->[-1], $w->[1] - $w->[0]];
1500            
1501             # set spectral values
1502 0 0         ($self->[1][1] = $illum->{$array->[1]}) or croak('standard illuminant not found');
1503            
1504             # if illuminant is two array references (wavelength range and spd vectors)
1505 0           } elsif (@{$array} == grep {ref() eq 'ARRAY'} @{$array}) {
  0            
  0            
1506            
1507             # verify wavelength range
1508 0 0 0       (ref($array->[0]) eq 'ARRAY' && 3 == @{$array->[0]} && (3 == grep {Scalar::Util::looks_like_number($_)} @{$array->[0]}) && $array->[0][2]) or croak('invalid illuminant wavelength range');
  0   0        
  0   0        
  0            
1509            
1510             # compute upper index of spd array
1511 0 0         (($sx = ICC::Shared::round(($array->[0][1] - $array->[0][0])/$array->[0][2])) > 0) or croak('inconsistent illuminant wavelength range');
1512            
1513             # verify spd array
1514 0 0 0       (ref($array->[1]) eq 'ARRAY' && $#{$array->[1]} == $sx && @{$array->[1]} == grep {Scalar::Util::looks_like_number($_)} @{$array->[1]}) or croak('invalid illuminant spd array');
  0   0        
  0            
  0            
  0            
1515            
1516             # copy array contents
1517 0           $self->[1] = Storable::dclone($array);
1518            
1519             } else {
1520            
1521             # error
1522 0           croak("invalid illuminant [@{$array}]");
  0            
1523            
1524             }
1525              
1526             # get increment from hash (default is 1)
1527 0 0         $inc = defined($hash->{'increment'}) ? $hash->{'increment'} : 1;
1528              
1529             # if increment is 1 nm
1530 0 0         if ($inc == 1) {
    0          
1531            
1532             # set wavelength range
1533 0           $self->[2][0] = [360, 830, 1];
1534            
1535             # load CIE color matching functions (YAML format)
1536 0           $cmf = YAML::Tiny->read(ICC::Shared::getICCPath('Data/CIE_cmfs_360-830_x_1.yml'))->[0];
1537            
1538             # if increment is 5 nm
1539             } elsif ($inc == 5) {
1540            
1541             # set wavelength range
1542 0           $self->[2][0] = [380, 780, 5];
1543            
1544             # load CIE color matching functions (YAML format)
1545 0           $cmf = YAML::Tiny->read(ICC::Shared::getICCPath('Data/CIE_cmfs_380-780_x_5.yml'))->[0];
1546            
1547             } else {
1548            
1549             # error
1550 0           croak('invalid spectral increment');
1551            
1552             }
1553              
1554             # get observer from hash (default is 2)
1555 0 0         $obs = defined($hash->{'observer'}) ? $hash->{'observer'} : 2;
1556              
1557             # if observer is an array reference
1558 0 0         if (ref($obs) eq 'ARRAY') {
1559            
1560             # verify array size
1561 0 0         (@{$obs} == 2) or croak('invalid observer array');
  0            
1562            
1563             # verify range
1564 0           (ref($obs->[0]) eq 'ARRAY' && (@{$obs->[0]} == grep {Scalar::Util::looks_like_number($_)} @{$obs->[0]}) &&
  0            
  0            
1565 0 0 0       @{$obs->[0]} == 3 && $obs->[0][2] && ! (($obs->[0][1] - $obs->[0][0]) % $obs->[0][2])) or croak('invalid observer wavelength range');
  0   0        
      0        
      0        
1566            
1567             # compute upper index of observer matrix
1568 0 0         (($sx = ICC::Shared::round(($obs->[0][1] - $obs->[0][0])/$obs->[0][2])) > 0) or croak('inconsistent observer wavelength range');
1569            
1570             # verify observer matrix
1571             ((ref($obs->[1]) eq 'ARRAY' || UNIVERSAL::isa($obs->[1], 'Math::Matrix')) && ref($obs->[1][0]) eq 'ARRAY' &&
1572 0 0 0       $#{$obs->[1][0]} == $sx && @{$obs->[1][0]} == grep {Scalar::Util::looks_like_number($_)} @{$obs->[1][0]}) or croak('invalid observer cmf matrix');
  0   0        
  0   0        
  0   0        
  0            
1573            
1574             # copy array contents
1575 0           $self->[2] = Storable::dclone($obs);
1576            
1577             } else {
1578            
1579             # get observer key
1580 0 0         ($key = {'2' => 'CIE1931', '10' => 'CIE1964', '2P' => 'CIE2012D2', '10P' => 'CIE2012D10'}->{$obs}) or croak('invalid observer key');
1581            
1582             # set CIE color-matching functions
1583 0           $self->[2][1][0] = $cmf->{$key . 'x'};
1584 0           $self->[2][1][1] = $cmf->{$key . 'y'};
1585 0           $self->[2][1][2] = $cmf->{$key . 'z'};
1586            
1587             }
1588              
1589             # compute color-weight functions
1590 0           _make_cwf($self);
1591              
1592             # set object type
1593 0           $self->[0]{'type'} = 'CIE';
1594              
1595             }
1596              
1597             # read ASTM color weight functions and white point
1598             # illuminant, observer, increment and bandpass specified in hash
1599             # parameters: (object_reference, hash)
1600             # results are saved in the object
1601             sub _astm {
1602              
1603             # get parameters
1604 0     0     my ($self, $hash) = @_;
1605              
1606             # local variables
1607 0           my ($table, $id, @bpc, @inc, @obs, @illum, @m, $nm);
1608              
1609             # initialize id
1610 0           $id = 1;
1611              
1612             # if increment key in hash
1613 0 0         if (defined($hash->{'increment'})) {
1614            
1615             # enumerate ASTM increments
1616 0           @inc = qw(10 20);
1617            
1618             # match observer value
1619 0 0         (@m = grep {$hash->{'increment'} eq $inc[$_]} (0 .. $#inc)) || warn('invalid increment value - using 10 nm');
  0            
1620            
1621             # adjust id for increment
1622 0 0         $id += @m ? $m[0] : 0;
1623            
1624             }
1625              
1626             # if observer key in hash
1627 0 0         if (defined($hash->{'observer'})) {
1628            
1629             # enumerate ASTM observers
1630 0           @obs = qw(2 10);
1631            
1632             # match observer value
1633 0 0         (@m = grep {$hash->{'observer'} eq $obs[$_]} (0 .. $#obs)) || warn('invalid observer value - using 1931 2º');
  0            
1634            
1635             # adjust id for observer
1636 0 0         $id += @m ? 2 * $m[0] : 0;
1637            
1638             }
1639              
1640             # if illuminant key in hash
1641 0 0         if (defined($hash->{'illuminant'})) {
1642            
1643             # enumerate ASTM illuminants
1644 0           @illum = qw(A C D50 D55 D65 D75 F2 F7 F11);
1645            
1646             # match illuminant value
1647 0 0         (@m = grep {$hash->{'illuminant'} eq $illum[$_]} (0 .. $#illum)) || warn('invalid illuminant value - using D50');
  0            
1648            
1649             # adjust id for illuminant
1650 0 0         $id += @m ? 4 * $m[0] : 8;
1651            
1652             } else {
1653            
1654             # adjust id for D50 illuminant
1655 0           $id += 8;
1656            
1657             }
1658              
1659             # set table: 5 (no bpc) -or- 6 (with bpc)
1660             # note: table 6 is deprecated in ASTM E 2729
1661 0 0 0       $table = (defined($hash->{'bandpass'}) && $hash->{'bandpass'} eq 'six') ? 6 : 5;
1662              
1663             # combine table and id
1664 0           $table .= '.' . $id;
1665              
1666             # load ASTM weight functions (YAML format)
1667 0           state $ASTM = YAML::Tiny->read(ICC::Shared::getICCPath('Data/ASTM_E308_data.yml'))->[0];
1668              
1669             # get wavelength vector
1670 0           $nm = $ASTM->{$table . 'nm'};
1671              
1672             # set wavelength start, end, and increment
1673 0           $self->[3][0] = [$nm->[0], $nm->[-1], $nm->[1] - $nm->[0]];
1674              
1675             # set color-weight functions
1676 0           $self->[3][1][0] = $ASTM->{$table . 'x'};
1677 0           $self->[3][1][1] = $ASTM->{$table . 'y'};
1678 0           $self->[3][1][2] = $ASTM->{$table . 'z'};
1679              
1680             # set illuminant white point
1681 0           $self->[5] = $ASTM->{$table . 'wp'};
1682              
1683             # set object type
1684 0           $self->[0]{'type'} = 'ASTM';
1685              
1686             }
1687              
1688             # read ISO 5-3 density weight functions
1689             # status and increment specified in hash
1690             # parameters: (object_reference, hash)
1691             # results are saved in the object
1692             sub _iso {
1693              
1694             # get parameters
1695 0     0     my ($self, $hash) = @_;
1696              
1697             # local variables
1698 0           my ($status, $inc, @m, $nm);
1699              
1700             # initialize table selectors
1701 0           $status = 'T';
1702 0           $inc = '10';
1703              
1704             # if status key in hash
1705 0 0         if (defined($hash->{'status'})) {
1706            
1707             # match status value
1708 0 0         (@m = grep {$hash->{'status'} eq $_} qw(A M T E I)) || warn('invalid status value - using T');
  0            
1709            
1710             # set status
1711 0 0         $status = @m ? $m[0] : 'T';
1712            
1713             }
1714              
1715             # if increment key in hash
1716 0 0         if (defined($hash->{'increment'})) {
1717            
1718             # match observer value
1719 0 0         (@m = grep {$hash->{'increment'} eq $_} qw(10 20)) || warn('invalid increment value - using 10 nm');
  0            
1720            
1721             # set increment
1722 0 0         $inc = @m ? $m[0] : '10';
1723            
1724             }
1725              
1726             # load ISO 5-3 weight functions (YAML format)
1727 0           state $ISO = YAML::Tiny->read(ICC::Shared::getICCPath('Data/ISO_5-3_data.yml'))->[0];
1728              
1729             # get wavelength vector
1730 0           $nm = $ISO->{$inc . 'nm'};
1731              
1732             # set wavelength start, end, and increment
1733 0           $self->[3][0] = [$nm->[0], $nm->[-1], $nm->[1] - $nm->[0]];
1734              
1735             # set density weight functions
1736 0           $self->[3][1][0] = $ISO->{$inc . $status . 'r'};
1737 0           $self->[3][1][1] = $ISO->{$inc . $status . 'g'};
1738 0           $self->[3][1][2] = $ISO->{$inc . $status . 'b'};
1739 0           $self->[3][1][3] = $ISO->{$inc . 'vis'};
1740              
1741             # set density weight function sums
1742 0           $self->[5][0] = List::Util::sum(@{$self->[3][1][0]});
  0            
1743 0           $self->[5][1] = List::Util::sum(@{$self->[3][1][1]});
  0            
1744 0           $self->[5][2] = List::Util::sum(@{$self->[3][1][2]});
  0            
1745 0           $self->[5][3] = List::Util::sum(@{$self->[3][1][3]});
  0            
1746              
1747             # set object type
1748 0           $self->[0]{'type'} = 'ISO';
1749              
1750             }
1751              
1752             # compute vector smoothness
1753             # (returns 0 if vector is linear)
1754             # parameter: (vector)
1755             # returns: (linearity)
1756             sub _smoothness {
1757              
1758             # get parameter
1759 0     0     my $v = shift();
1760              
1761             # local variables
1762 0           my ($d, $s);
1763              
1764             # return if < 3 vector elements
1765 0 0         return(0) if (@{$v} < 3);
  0            
1766              
1767             # for each triplet
1768 0           for my $i (0 .. $#{$v} - 2) {
  0            
1769            
1770             # add linear deviation
1771 0           $d += (2 * $v->[$i + 1] - $v->[$i] - $v->[$i + 2])**2;
1772            
1773             # add outer magnitudes
1774 0           $s += abs($v->[$i]) + abs($v->[$i + 2]);
1775            
1776             }
1777              
1778             # return relative rms value
1779 0 0         return($s ? sqrt($d)/$s : 0);
1780              
1781             }
1782              
1783             # get encoding CODE reference
1784             # parameter(object_reference, hash)
1785             # returns: (CODE_reference)
1786             sub _encoding {
1787              
1788             # get parameters
1789 0     0     my ($self, $hash) = @_;
1790              
1791             # local variables
1792 0           my ($encoding, $size);
1793              
1794             # if hash contain 'encoding' value, but not 'XYZ', 'RGBV' or 'linear'
1795 0 0 0       if (defined($encoding = $hash->{'encoding'}) && $encoding !~ m/^(XYZ|RGBV|linear)$/) {
1796            
1797             # get output size
1798 0           $size = @{$self->[3][1]};
  0            
1799            
1800             # if encoding is 'ICC_XYZ'
1801 0 0 0       if ($encoding eq 'ICC_XYZ' && $size == 3) {
    0 0        
    0 0        
    0 0        
    0 0        
1802            
1803             # return code reference
1804 0     0     return(sub {map {$_ * 327.68/65535} @_});
  0            
  0            
1805            
1806             # if encoding is 'ICC_XYZNumber'
1807             } elsif ($encoding eq 'ICC_XYZNumber' && $size == 3) {
1808            
1809             # return code reference
1810 0     0     return(sub {map {$_/100} @_});
  0            
  0            
1811            
1812             # if encoding is 'xyz'
1813             } elsif ($encoding eq 'xyz' && $size == 3) {
1814            
1815             # verify white point
1816 0 0 0       ($self->[5][0] && $self->[5][1] && $self->[5][2]) or croak('invalid illuminant white point');
      0        
1817            
1818             # return code reference
1819 0     0     return(sub {$_[0]/$self->[5][0], $_[1]/$self->[5][1], $_[2]/$self->[5][2]});
  0            
1820            
1821             # if encoding is 'unit'
1822             } elsif ($encoding eq 'unit' && $size == 4) {
1823            
1824             # return code reference
1825 0     0     return(sub {map {$_/100} @_});
  0            
  0            
1826            
1827             # if encoding is 'density'
1828             } elsif ($encoding eq 'density' && $size == 4) {
1829            
1830             # return code reference
1831 0 0   0     return(sub {map {$_ > 0 ? -POSIX::log10($_/100) : 99} @_});
  0            
  0            
1832            
1833             } else {
1834            
1835             # error
1836 0           croak('unsupported XYZ/RGBV encoding');
1837            
1838             }
1839            
1840             } else {
1841            
1842             # return empty
1843 0           return();
1844            
1845             }
1846            
1847             }
1848              
1849             # set object contents from parameter hash
1850             # parameters: (object_reference, ref_to_parameter_hash)
1851             sub _new_from_hash {
1852              
1853             # get parameters
1854 0     0     my ($self, $hash) = @_;
1855              
1856             # local variables
1857 0           my ($range, $ox);
1858              
1859             # save colorimetry hash in object header
1860 0           $self->[0] = Storable::dclone($hash);
1861              
1862             # if 'range' is defined
1863 0 0         if (defined($range = $hash->{'range'})) {
1864            
1865             # verify wavelength range structure
1866 0 0 0       (ref($range) eq 'ARRAY' && 3 == @{$range} && (3 == grep {Scalar::Util::looks_like_number($_)} @{$range})) or croak('invalid wavelength range structure');
  0   0        
  0            
  0            
1867            
1868             # compute upper index
1869 0           $ox = ICC::Shared::round(($range->[1] - $range->[0])/$range->[2]);
1870            
1871             # verify wavelength range values
1872 0 0 0       ($ox > 0 && abs($ox * $range->[2] - $range->[1] + $range->[0]) < 1E-12 && $range->[2] > 0) or croak('invalid wavelength range values');
      0        
1873            
1874             # add range to hash
1875 0           $srh->{$ox + 1} = $range;
1876            
1877             }
1878              
1879             # if 'status' is defined, a scalar
1880 0 0 0       if (defined($hash->{'status'}) && ! ref($hash->{'status'})) {
    0 0        
1881            
1882             # read ISO 5-3 color-weight functions (density)
1883 0           _iso($self, $hash);
1884            
1885             # if 'illuminant' is defined, an ARRAY ref
1886             } elsif (defined($hash->{'illuminant'}) && ref($hash->{'illuminant'}) eq 'ARRAY') {
1887            
1888             # read CIE illuminant and color-matching functions, compute color-weight functions
1889 0           _cie($self, $hash);
1890            
1891             } else {
1892            
1893             # read ASTM color-weight functions
1894 0           _astm($self, $hash);
1895            
1896             }
1897            
1898             }
1899              
1900             1;
1901