File Coverage

blib/lib/ICC/Support/Chart.pm
Criterion Covered Total %
statement 822 4658 17.6
branch 342 2650 12.9
condition 75 1145 6.5
subroutine 62 198 31.3
pod 67 70 95.7
total 1368 8721 15.6


line stmt bran cond sub pod time code
1             package ICC::Support::Chart;
2              
3 2     2   80819 use strict;
  2         10  
  2         48  
4 2     2   8 use Carp;
  2         3  
  2         117  
5              
6             our $VERSION = 2.02;
7              
8             # revised 2020-02-06
9             #
10             # Copyright © 2004-2020 by William B. Birkett
11              
12             # inherit from Shared
13 2     2   347 use parent qw(ICC::Shared);
  2         244  
  2         8  
14              
15             # support modules
16 2     2   83 use Config;
  2         3  
  2         66  
17 2     2   457 use Encode;
  2         8162  
  2         123  
18 2     2   12 use File::Glob;
  2         2  
  2         111  
19 2     2   10 use POSIX ();
  2         3  
  2         23  
20 2     2   406 use Time::Piece;
  2         7546  
  2         17  
21 2     2   651 use XML::LibXML;
  2         35394  
  2         13  
22              
23             # enable static variables
24 2     2   281 use feature 'state';
  2         3  
  2         114817  
25              
26             # create new chart object
27             # parameters: ([hash])
28             # parameters: (ref_to_data_array, [hash])
29             # parameters: (path_to_file, [hash])
30             # parameters: (path_to_folder, [hash])
31             # returns: (ref_to_chart_object) -or- (ref_to_chart_object, error_string)
32             sub new {
33              
34             # get object class
35 29     29 1 236911 my $class = shift();
36              
37             # local variables
38 29         50 my ($self, $hash, $array, $format, $offset, $path, $files, $result, $error);
39              
40             # create empty chart object
41 29         72 $self = [
42             {}, # object header
43             [[]], # chart data
44             [[]], # colorimetry data
45             [], # header lines
46             {}, # SAMPLE_ID hash
47             ];
48              
49             # get optional hash
50 29 100       78 $hash = pop() if (ref($_[-1]) eq 'HASH');
51              
52             # if there are additional parameters
53 29 100       62 if (@_) {
    50          
54            
55             # if first parameter is an array or a Math::Matrix object
56 22 100 66     212 if (ref($_[0]) eq 'ARRAY' || UNIVERSAL::isa($_[0], 'Math::Matrix')) {
    50          
57            
58             # copy array/matrix and optional format
59 1         10 add_cols($self, shift(), $hash->{'format'});
60            
61             # if first parameter is a scalar
62             } elsif (! ref($_[0])) {
63            
64             # get path
65 21         38 $path = shift();
66            
67             # save path in object header
68 21         39 $self->[0]{'file_path'} = $path;
69            
70             # get file list
71 21         48 $files = _files($path);
72            
73             # no files
74 21 50       26 if (@{$files} == 0) {
  21 100       43  
75            
76             # invalid path
77 0         0 carp($error = "no files in path: $path\n");
78            
79             # one file
80 21         40 } elsif (@{$files} == 1) {
81            
82             # read chart
83 19 50       47 ($result = _readChart($self, $files->[0], $hash)) && carp($error = "chart $files->[0] $result\n");
84            
85             # add colorimetric metadata
86 19         40 _addColorMeta($self);
87            
88             # multiple files
89             } else {
90            
91             # get folder handling
92 2   100     11 $self->[0]{'folder_handling'} = $hash->{'folder'} // 'AVG';
93            
94             # if folder handling undefined or 'AVG'
95 2 100       8 if ($self->[0]{'folder_handling'} eq 'AVG') {
    50          
    0          
96            
97             # read average chart
98 1 50       5 _readChartAvg($self, $files, $hash) or carp($error = "no valid charts in path: $path\n");
99            
100             # if folder handling 'APPEND'
101             } elsif ($self->[0]{'folder_handling'} eq 'APPEND') {
102            
103             # read appended chart
104 1 50       5 _readChartAppend($self, $files, $hash) or carp($error = "no valid charts in path: $path\n");
105            
106             # if folder handling 'MERGE'
107             } elsif ($self->[0]{'folder_handling'} eq 'MERGE') {
108            
109             # read appended chart
110 0 0       0 _readChartMerge($self, $files, $hash) or carp($error = "no valid charts in path: $path\n");
111            
112             } else {
113            
114             # invalid folder handling
115 0         0 carp($error = "invalid folder handling: $self->[0]{'folder_handling'}\n");
116            
117             }
118            
119             }
120            
121             } else {
122            
123             # invalid parameter(s)
124 0         0 carp($error = "invalid parameter(s)");
125            
126             }
127            
128             # make SAMPLE_ID hash
129 22         59 _makeSampleID($self);
130            
131             # if hash defined
132             } elsif (defined($hash)) {
133            
134             # make patch set
135 0 0       0 ($result = _makePatchSet($self, $hash)) && carp($error = "failed making patch set - $result\n");
136            
137             }
138              
139             # add initial_size to object header
140 29         63 $self->[0]{'initial_size'} = [$#{$self->[1]}, $#{$self->[1][0]} + 1];
  29         43  
  29         83  
141              
142             # bless object
143 29         78 bless($self, $class);
144              
145             # return
146 29 50       416 return(wantarray() ? ($self, $error) : $self);
147              
148             }
149              
150             # get/set reference to header hash
151             # parameters: ([ref_to_new_hash])
152             # returns: (ref_to_hash)
153             sub header {
154              
155             # get object reference
156 2     2 1 1560 my $self = shift();
157              
158             # if there are parameters
159 2 100       6 if (@_) {
160            
161             # if one parameter, a hash reference
162 1 50 33     6 if (@_ == 1 && ref($_[0]) eq 'HASH') {
163            
164             # set header to copy of hash
165 1         3 $self->[0] = {%{shift()}};
  1         5  
166            
167             } else {
168            
169             # error
170 0         0 croak('parameter must be a hash reference');
171            
172             }
173            
174             }
175              
176             # return reference
177 2         6 return($self->[0]);
178              
179             }
180              
181             # get/set reference to data array
182             # note: row 0 contains the DATA_FORMAT field names
183             # note: set updates the SAMPLE_ID hash and colorimetry array
184             # parameters: ([ref_to_new_array])
185             # returns: (ref_to_array)
186             sub array {
187              
188             # get object reference
189 2     2 1 4 my $self = shift();
190              
191             # if there are parameters
192 2 100       6 if (@_) {
193            
194             # if one parameter, an array reference
195 1 50 33     6 if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
196            
197             # get array reference
198 1         2 my $array = shift();
199            
200             # initialize data array
201 1         2 $self->[1] = [];
202            
203             # if array is not empty
204 1 50       2 if (@{$array}) {
  1         3  
205            
206             # for each row
207 1         3 for my $i (0 .. $#{$array}) {
  1         4  
208            
209             # copy to object
210 4         5 $self->[1][$i] = [@{$array->[$i]}];
  4         8  
211            
212             }
213            
214             # make SAMPLE_ID hash
215 1         7 _makeSampleID($self);
216            
217             # add colorimetric metadata
218 1         4 _addColorMeta($self);
219            
220             }
221            
222             } else {
223            
224             # error
225 0         0 croak('parameter must be an array reference');
226            
227             }
228            
229             }
230              
231             # return reference
232 2         5 return($self->[1]);
233              
234             }
235              
236             # get data array size
237             # if flag is true, initial size is returned
238             # parameters: ([flag])
239             # returns: (number_rows)
240             # returns: (number_rows, number_columns)
241             sub size {
242              
243             # get parameters
244 0     0 1 0 my ($self, $flag) = @_;
245              
246             # if flag is true
247 0 0 0     0 if ($flag && defined($self->[0]{'initial_size'})) {
248            
249             # return initial size, array or scalar
250 0 0       0 return(wantarray ? @{$self->[0]{'initial_size'}} : $self->[0]{'initial_size'}->[0]);
  0         0  
251            
252             } else {
253            
254             # return current size, array or scalar
255 0 0       0 return(wantarray ? ($#{$self->[1]}, $#{$self->[1][0]} + 1) : $#{$self->[1]});
  0         0  
  0         0  
  0         0  
256            
257             }
258              
259             }
260              
261             # get data matrix size
262             # returns: (number_rows)
263             # returns: (number_rows, number_columns)
264             sub matrix_size {
265              
266             # get object reference
267 0     0 1 0 my $self = shift();
268              
269             # get row length
270 0   0     0 my $rows = _getRowLength($self) || $#{$self->[1]};
271              
272             # compute columns
273 0         0 my $cols = POSIX::ceil($#{$self->[1]}/$rows);
  0         0  
274              
275             # return array or scalar
276 0 0       0 wantarray ? return($rows, $cols) : return($rows);
277              
278             }
279              
280             # get row slice from SAMPLE_ID values
281             # id_keys is a list of scalars and/or array references
282             # row_slice is reference to an array of row indices
283             # note: returns undef if any key is missing
284             # parameters: (id_keys)
285             # returns: (row_slice)
286             sub rows {
287              
288             # get object reference
289 3     3 1 962 my $self = shift();
290              
291             # local variable
292 3         5 my (@keys, @rows);
293              
294             # flatten id key list
295 3         4 @keys = @{ICC::Shared::flatten(@_)};
  3         7  
296              
297             # get row list using SAMPLE_ID hash
298 3         5 @rows = @{$self->[4]}{@keys};
  3         10  
299              
300             # return row slice or undef if any rows are missing
301 3 100       6 return((grep {! defined()} @rows) ? undef : \@rows);
  12         31  
302              
303             }
304              
305             # get column slice from DATA_FORMAT keys
306             # format_keys is a list of scalars and/or array references
307             # column_slice is reference to an array of column indices
308             # note: tries to match ignoring context if exact match fails
309             # note: returns 'undef' if any column is missing
310             # parameters: (format_keys)
311             # returns: (column_slice)
312             sub cols {
313              
314             # get object reference
315 305     305 1 400 my $self = shift();
316              
317             # local variables
318 305         375 my (@keys, %fmt, @cols);
319              
320             # flatten format key list
321 305         334 @keys = @{ICC::Shared::flatten(@_)};
  305         604  
322              
323             # make lookup hash
324 305 50       493 %fmt = map {defined($self->[1][0][$_]) ? ($self->[1][0][$_], $_) : ()} (0 .. $#{$self->[1][0]});
  8930         14974  
  305         654  
325              
326             # lookup format keys in hash
327 305         1037 @cols = @fmt{@keys};
328              
329             # if any columns undefined
330 305 100       410 if (grep {! defined()} @cols) {
  1105         1660  
331            
332             # make lookup hash without context prefixes
333 206 50 33     253 %fmt = map {(defined($self->[1][0][$_]) && $self->[1][0][$_] =~ m/^(.*?)\|?([^\|\n]*)$/) ? ($2, $_) : ()} (0 .. $#{$self->[1][0]});
  6276         54130  
  206         342  
334            
335             # lookup format keys in hash
336 206         845 @cols = @fmt{@keys};
337            
338             }
339              
340             # return column slice or undef if any columns undefined
341 305 100       458 return((grep {! defined()} @cols) ? undef : \@cols);
  1105         2405  
342              
343             }
344              
345             # get DATA_FORMAT keys from column slice
346             # column_slice is a list of scalars and/or array references
347             # format_keys is an array reference
348             # parameters: ([column_slice])
349             # returns: (format_keys)
350             sub fmt_keys {
351              
352             # get object reference
353 6     6 1 13 my $self = shift();
354              
355             # if no parameter -or- column slice an empty array reference ([])
356 6 100 33     27 if (@_ == 0 || (@_ == 1 && ref($_[0]) eq 'ARRAY' && @{$_[0]} == 0)) {
  0   33     0  
      66        
357            
358             # return all format keys
359 1         2 return([@{$self->[1][0]}]);
  1         9  
360            
361             } else {
362            
363             # return format keys slice
364 5         8 return([@{$self->[1][0]}[@{ICC::Shared::flatten(@_)}]]);
  5         56  
  5         11  
365            
366             }
367              
368             }
369              
370             # get/set context
371             # 'undef' indicates no context (get or set)
372             # returned context may be a scalar or an array
373             # parameter: (column_slice) => returns: (context)
374             # parameters: (column_slice, context) => returns: (modified_keys)
375             sub context {
376              
377             # get object reference
378 7     7 1 973 my $self = shift();
379              
380             # local variables
381 7         9 my ($cols, $context, @cx);
382              
383             # return if no parameters supplied
384 7 50       15 return(undef) if (@_ == 0);
385              
386             # get column slice
387 7         17 $cols = ICC::Shared::flatten(shift());
388              
389             # use all columns if slice is empty
390 7 50       8 $cols = [0 .. $#{$self->[1][0]}] if (@{$cols} == 0);
  0         0  
  7         14  
391              
392             # if no context parameter
393 7 100       13 if (@_ == 0) {
394            
395             # match contexts
396 4 100       5 @cx = map {$self->[1][0][$_] =~ m/^(.*)\|/ ? $1 : undef} @{$cols};
  16         54  
  4         7  
397            
398             # return array if wanted
399 4 100       26 return(@cx) if (wantarray);
400            
401             # warn if columns have different contexts
402 2 100 66     17 (@cx == grep {(! defined($cx[0]) && ! defined($_)) || ($cx[0] eq $_)} @cx) || warn('columns have different contexts');
  8 50       33  
403            
404             # return context of first column
405 2         10 return($cx[0]);
406            
407             } else {
408            
409             # get context
410 3         5 $context = shift();
411            
412             # for each column
413 3         5 for my $i (0 .. $#{$cols}) {
  3         7  
414            
415             # if context is defined
416 12 100       20 if (defined($context)) {
417            
418             # replace current context
419 8         42 $self->[1][0][$cols->[$i]] =~ s/^(?:.*\|)?(.*)$/$context\|$1/;
420            
421             # if context is 'undef'
422             } else {
423            
424             # remove current context
425 4         15 $self->[1][0][$cols->[$i]] =~ s/^.*\|//;
426            
427             }
428            
429             }
430            
431             # return modified keys
432 3         6 return([@{$self->[1][0]}[@{$cols}]]);
  3         11  
  3         4  
433            
434             }
435            
436             }
437              
438             # test for a specified data class
439             # string format is '[context][|][class]'
440             # 'class' - match class, any context
441             # '|class' - match class, no context
442             # 'context|class' - match context and class
443             # 'context|' - match context, any class
444             # returns list of matched indices or count
445             # parameters: (string)
446             # returns: (list -or- count)
447             sub test {
448              
449             # get parameters
450 255     255 1 712 my ($self, $string) = @_;
451              
452             # local variables
453 255         274 my (@w, $class, @fields);
454              
455             # hash of compiled regex
456 255         325 state $regex = {
457             'RGB' => qr/^(?:(.*)\|)?RGB_[RGB]$/,
458             'CMYK' => qr/^(?:(.*)\|)?CMYK_[CMYK]$/,
459             'XYZ' => qr/^(?:(.*)\|)?XYZ_[XYZ]$/,
460             'XYY' => qr/^(?:(.*)\|)?XYY_(?:X|Y|CAPY)$/,
461             'LAB' => qr/^(?:(.*)\|)?LAB_[LAB]$/,
462             'LCH' => qr/^(?:(.*)\|)?LAB_[LCH]$/,
463             'NCLR' => qr/^(?:(.*)\|)?(?:[2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/,
464             'SPECTRAL' => qr/^(?:(.*)\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3}$/,
465             'SPOT' => qr/^(?:(.*)\|)?SPOT_\d+$/,
466             'DENSITY' => qr/^(?:(.*)\|)?D_(?:RED|GREEN|BLUE|VIS)$/,
467             'REFL' => qr/^(?:(.*)\|)?R_(?:RED|GREEN|BLUE|VIS)$/,
468             'STDEVXYZ' => qr/^(?:(.*)\|)?STDEV_[XYZ]$/,
469             'STDEVLAB' => qr/^(?:(.*)\|)?STDEV_[LAB]$/,
470             'MEAN_DE' => qr/^(?:(.*)\|)?MEAN_DE$/,
471             'ID' => qr/^(?:(.*)\|)?(?:SAMPLE_ID|SampleID)$/,
472             'NAME' => qr/^(?:(.*)\|)?SAMPLE_NAME$/,
473             'DEVICE' => qr/^(?:(.*)\|)?(?:RGB_[RGB]|CMYK_[CMYK]|[2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/,
474             'ANY' => qr/^(?:(.*)\|)?/,
475             };
476              
477             # verify string parameter
478 255 50       365 (! ref($string)) or croak('invalid string parameter');
479              
480             # return, if null string
481 255 0 33     648 return(wantarray() ? () : 0) if (! defined($string) || $string eq '');
    50          
482              
483             # split string on '|' character (string is '[context]|[class]')
484 255         420 @w = split(/\|/, $string);
485              
486             # set empty context, if array is empty (string is '|')
487 255 50       382 @w = ('') if (! @w);
488              
489             # add 'ANY' class, if string ends in '|' (string is '|' or 'context|')
490 255 50       404 push(@w, 'ANY') if (substr($string, -1, 1) eq '|');
491              
492             # make class uppercase
493 255         348 $class = uc($w[-1]);
494              
495             # verify class
496 255 50 33     615 (! ref($class) && exists($regex->{$class})) or croak('invalid data class');
497              
498             # if no '|' character
499 255 100       382 if (@w == 1) {
    50          
500            
501             # match format fields (any context)
502 120         131 @fields = grep {$self->[1][0][$_] =~ /$regex->{$class}/} (0 .. $#{$self->[1][0]});
  4005         9909  
  120         223  
503            
504             # if single '|' character
505             } elsif (@w == 2) {
506            
507             # if empty context
508 135 50       193 if ($w[0] eq '') {
509            
510             # match format fields (no context)
511 0 0       0 @fields = grep {$self->[1][0][$_] =~ /$regex->{$class}/ && ! defined($1)} (0 .. $#{$self->[1][0]});
  0         0  
  0         0  
512            
513             } else {
514            
515             # match format fields (matching context and class)
516 135 100 100     151 @fields = grep {$self->[1][0][$_] =~ /$regex->{$class}/ && defined($1) && ($1 eq $w[0])} (0 .. $#{$self->[1][0]});
  4020         14138  
  135         245  
517            
518             }
519            
520             } else {
521            
522             # error
523 0         0 croak('data class contains multiple \'|\' characters');
524            
525             }
526              
527             # return (list -or- count)
528 255 50       929 return(wantarray() ? @fields : scalar(@fields));
529              
530             }
531              
532             # get/set keyword value(s)
533             # CGATS ASCII file header lines are stored as an array in the object header
534             # most lines contain a keyword followed by a value, which this methods gets/sets
535             # a keyword may be used more than once, so the value parameter is an array
536             # if the keyword doesn't exist, a new line is added when setting its value
537             # if the keyword is enclosed by angle brackets, existing lines are removed
538             # parameters: () => returns: (file_header_array_reference)
539             # parameters: (keyword) => returns: (value_array)
540             # parameters: (keyword, value_array) => returns: (original_value_array)
541             sub keyword {
542              
543             # get parameters
544 66     66 1 108 my ($self, $key, @values) = @_;
545              
546             # local variables
547 66         73 my ($del, @ix, @current);
548              
549             # if no keyword, return file header array reference
550 66 50       95 (defined($key)) || return($self->[3]);
551              
552             # set delete flag, stripping angle brackets (if any)
553 66         99 $del = ($key =~ s/^<(.*)>$/$1/);
554              
555             # get indices of existing keyword (if any)
556 66         88 @ix = grep {$self->[3][$_][0] eq $key} (0 .. $#{$self->[3]});
  734         980  
  66         115  
557              
558             # get current values array (if any)
559 66         112 @current = map {$self->[3][$_][1]} @ix;
  26         51  
560              
561             # if delete flag set
562 66 50       90 if ($del) {
563            
564             # while indices
565 0         0 while (@ix) {
566            
567             # delete array element
568 0         0 splice(@{$self->[3]}, pop(@ix), 1);
  0         0  
569            
570             }
571            
572             }
573              
574             # disable replacement if keyword is 'KEYWORD'
575 66 50       100 @ix = () if ($key eq 'KEYWORD');
576              
577             # if there are supplied values
578 66 50       88 if (@values) {
579            
580             # for each value
581 0         0 for (@values) {
582            
583             # if not a number or already quoted
584 0 0       0 if (! m/^([\d.-]+|".*")$/) {
585            
586             # remove any quotes
587 0         0 s/"//g;
588            
589             # enclose in quotes
590 0         0 $_ = "\"$_\"";
591            
592             }
593            
594             }
595            
596             # while indices and values
597 0   0     0 while (@ix && @values) {
598            
599             # replace keyword/value entry
600 0         0 $self->[3][shift(@ix)] = [$key, shift(@values)];
601            
602             }
603            
604             # for each remaining value (if any)
605 0         0 for (@values) {
606            
607             # add keyword/value entry
608 0         0 push(@{$self->[3]}, [$key, $_]);
  0         0  
609            
610             }
611            
612             }
613              
614             # return current values array, or scalar
615 66 100       213 return(wantarray ? @current : $current[0]);
616              
617             }
618              
619             # get/set CREATED value
620             # adds CREATED keyword when setting, if none
621             # parameters: () # gets date/time from CREATED value
622             # parameters: (string) # sets/adds CREATED keyword/value
623             # parameters: (Time::Piece_object) # sets/adds CREATED keyword/value
624             # returns: (Time::Piece_object) # default is localtime
625             sub created {
626              
627             # get parameters
628 0     0 1 0 my ($self, $t) = @_;
629              
630             # local variables
631 0         0 my (@ix, $datetime);
632              
633             # get indices of existing CREATED lines (if any)
634 0         0 @ix = grep {$self->[3][$_][0] eq 'CREATED'} (0 .. $#{$self->[3]});
  0         0  
  0         0  
635              
636             # print warning if more than one CREATED line
637 0 0       0 print "warning: more than one CREATED keyword\n" if (@ix > 1);
638              
639             # if date/time parameter given
640 0 0       0 if (defined($t)) {
641            
642             # make Time::Piece object if reference is a scalar
643 0 0       0 $t = _makeTimePiece($t) if (! ref($t));
644            
645             # if not a Time::Piece object
646 0 0       0 if (ref($t) ne 'Time::Piece') {
647            
648             # print warning
649 0         0 print "warning: invalid date/time parameter, using localtime instead\n";
650            
651             # use local time
652 0         0 $t = localtime();
653            
654             }
655            
656             # make ISO 8601 datetime string from Time::Piece object
657 0         0 $datetime = sprintf("%s%+03d:00", $t->datetime, $t->tzoffset->hours);
658            
659             # if CREATED lines
660 0 0       0 if (@ix) {
661            
662             # replace value in first CREATED line
663 0         0 $self->[3][$ix[0]][1] = "\"$datetime\"";
664            
665             } else {
666            
667             # if keyword lines exist
668 0 0       0 if (@{$self->[3]}) {
  0         0  
669            
670             # insert CREATED line (as second line)
671 0         0 splice(@{$self->[3]}, 1, 0, ['CREATED', "\"$datetime\""]);
  0         0  
672            
673             } else {
674            
675             # add CREATED line
676 0         0 $self->[3][0] = ['CREATED', "\"$datetime\""];
677            
678             }
679            
680             }
681            
682             # no parameter
683             } else {
684            
685             # if CREATED lines
686 0 0       0 if (@ix) {
687            
688             # make Time::Piece object from first CREATED value
689 0         0 $t = _makeTimePiece($self->[3][$ix[0]][1]);
690            
691             } else {
692            
693             # print warning
694 0         0 print "warning: no CREATED keyword, returning localtime instead\n";
695            
696             # use local time
697 0         0 $t = localtime();
698            
699             }
700            
701             }
702              
703             # return Time::Piece object
704 0         0 return($t);
705              
706             }
707              
708             # get/set data array slice
709             # row_slice and column_slice may be either a scalar or array reference
710             # replacement_data is reference to a 2-D array of replacement values
711             # replacement data dimensions must match size of row_slice and column_slice
712             # data_slice is reference to a 2-D array, selected by row_slice and column_slice
713             # parameters: ([row_slice, [column_slice, [replacement_data]]])
714             # return: (data_slice)
715             sub slice {
716              
717             # get parameters
718 5     5 1 17765 my ($self, $rows, $cols, $data) = @_;
719              
720             # select all rows if row slice undefined
721 5 100       14 $rows = [] if (! defined($rows));
722              
723             # select all fields if column slice undefined
724 5 100       12 $cols = [] if (! defined($cols));
725              
726             # call get/set subroutine
727 5         11 _getset($self, 1, $rows, $cols, $data);
728              
729             }
730              
731             # get/set colorimetry array slice
732             # row_slice and column_slice may be either a scalar or array reference
733             # replacement_data is reference to a 2-D array of replacement values
734             # replacement data dimensions must match size of row_slice and column_slice
735             # data_slice is reference to a 2-D array, selected by row_slice and column_slice
736             # parameters: ([row_slice, [column_slice, [replacement_data]]])
737             # return: (data_slice)
738             sub colorimetry {
739              
740             # get parameters
741 0     0 1 0 my ($self, $rows, $cols, $data) = @_;
742              
743             # flatten row slice
744 0 0       0 $rows = defined($rows) ? ICC::Shared::flatten($rows) : [];
745              
746             # select all rows if row slice empty
747 0 0       0 $rows = [0 .. $#{$self->[2]}] if (@{$rows} == 0);
  0         0  
  0         0  
748              
749             # flatten column slice
750 0 0       0 $cols = defined($cols) ? ICC::Shared::flatten($cols) : [];
751              
752             # select all fields if column slice empty
753 0 0       0 $cols = [0 .. $#{$self->[1][0]}] if (@{$cols} == 0);
  0         0  
  0         0  
754              
755             # call get/set subroutine
756 0         0 _getset($self, 2, $rows, $cols, $data);
757              
758             }
759              
760             # get/set SAMPLE_ID data
761             # optional hash contains supplementary parameters
762             # row_slice and column_slice are 1-D array references
763             # data_slice is a Math::Matrix object (2-D array)
764             # replacement_data is a Math::Matrix object or 2-D array
765             # parameters: ([hash]) => returns: (column_slice)
766             # parameters: (row_slice, [hash]) => returns: (data_slice)
767             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
768             sub id {
769              
770             # local variables
771 23     23 1 40515 my ($hash, %fmt, $cols);
772              
773             # get optional hash
774 23 100       65 $hash = pop() if (ref($_[-1]) eq 'HASH');
775              
776             # get remaining parameters
777 23         39 my ($self, $rows, $data) = @_;
778              
779             # make lookup hash (context| -or- '||' => column)
780 23 50       36 %fmt = map {($self->[1][0][$_] =~ m/^(.*\|)?(?:SAMPLE_ID|SampleID|ID)$/) ? (defined($1) ? $1 : '||', $_) : ()} (0 .. $#{$self->[1][0]});
  607 100       1373  
  23         65  
781              
782             # if context defined
783 23 100       70 if (defined($hash->{'context'})) {
784            
785             # get id column with context
786 9         22 $cols = $fmt{"$hash->{'context'}|"};
787            
788             } else {
789            
790             # get id column without context
791 14         20 $cols = $fmt{'||'};
792            
793             # if id column undefined
794 14 100       29 if (! defined($cols)) {
795            
796             # make lookup hash ignoring context ('||' => column)
797 4 50       5 %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID|ID)$/) ? ('||', $_) : ()} (0 .. $#{$self->[1][0]});
  128         273  
  4         16  
798            
799             # get id column
800 4         8 $cols = $fmt{'||'};
801            
802             }
803            
804             }
805              
806             # call get/set subroutine
807 23         53 _getset($self, 1, $rows, $cols, $data);
808              
809             }
810              
811             # get/set SAMPLE_NAME data
812             # optional hash contains supplementary parameters
813             # row_slice and column_slice are 1-D array references
814             # data_slice is a Math::Matrix object (2-D array)
815             # replacement_data is a Math::Matrix object or 2-D array
816             # parameters: ([hash]) => returns: (column_slice)
817             # parameters: (row_slice, [hash]) => returns: (data_slice)
818             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
819             sub name {
820              
821             # local variables
822 23     23 1 3959 my ($hash, $cols);
823              
824             # get optional hash
825 23 100       52 $hash = pop() if (ref($_[-1]) eq 'HASH');
826              
827             # get remaining parameters
828 23         32 my ($self, $rows, $data) = @_;
829              
830             # get column slice, adding optional context prefix
831 23 100       63 $cols = cols($self, defined($hash->{'context'}) ? "$hash->{'context'}|SAMPLE_NAME" : 'SAMPLE_NAME');
832              
833             # call get/set subroutine
834 23         49 _getset($self, 1, $rows, $cols, $data);
835              
836             }
837              
838             # get/set RGB data
839             # optional hash contains supplementary parameters
840             # row_slice and column_slice are 1-D array references
841             # data_slice is a Math::Matrix object (2-D array)
842             # replacement_data is a Math::Matrix object or 2-D array
843             # parameters: ([hash]) => returns: (column_slice)
844             # parameters: (row_slice, [hash]) => returns: (data_slice)
845             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
846             sub rgb {
847              
848             # local variables
849 67     67 1 9187 my ($hash, $cols);
850              
851             # get optional hash
852 67 100       135 $hash = pop() if (ref($_[-1]) eq 'HASH');
853              
854             # get remaining parameters
855 67         106 my ($self, $rows, $data) = @_;
856              
857             # get column slice, adding optional context prefix
858 67 100       90 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(RGB_R RGB_G RGB_B));
  201         428  
859              
860             # call get/set subroutine
861 67         163 _getset($self, 1, $rows, $cols, $data);
862              
863             }
864              
865             # get/set CMYK data
866             # optional hash contains supplementary parameters
867             # row_slice and column_slice are 1-D array references
868             # data_slice is a Math::Matrix object (2-D array)
869             # replacement_data is a Math::Matrix object or 2-D array
870             # parameters: ([hash]) => returns: (column_slice)
871             # parameters: (row_slice, [hash]) => returns: (data_slice)
872             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
873             sub cmyk {
874              
875             # local variables
876 67     67 1 28132 my ($hash, $cols);
877              
878             # get optional hash
879 67 100       147 $hash = pop() if (ref($_[-1]) eq 'HASH');
880              
881             # get remaining parameters
882 67         126 my ($self, $rows, $data) = @_;
883              
884             # get column slice, adding optional context prefix
885 67 100       102 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(CMYK_C CMYK_M CMYK_Y CMYK_K));
  268         496  
886              
887             # call get/set subroutine
888 67         143 _getset($self, 1, $rows, $cols, $data);
889              
890             }
891              
892             # get/set 6CLR data
893             # optional hash contains supplementary parameters
894             # row_slice and column_slice are 1-D array references
895             # data_slice is a Math::Matrix object (2-D array)
896             # replacement_data is a Math::Matrix object or 2-D array
897             # parameters: ([hash]) => returns: (column_slice)
898             # parameters: (row_slice, [hash]) => returns: (data_slice)
899             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
900             sub hex {
901              
902             # local variables
903 33     33 1 20485 my ($hash, $cols);
904              
905             # get optional hash
906 33 100       75 $hash = pop() if (ref($_[-1]) eq 'HASH');
907              
908             # get remaining parameters
909 33         48 my ($self, $rows, $data) = @_;
910              
911             # get column slice, adding optional context prefix
912 33 100       59 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|6CLR_$_" : "6CLR_$_"} (1 .. 6)); # X-Rite notation
  198         430  
913 33 100 66     87 $cols = $cols // cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|PC6_$_" : "PC6_$_"} (1 .. 6)); # Ekso notation
  84         180  
914              
915             # call get/set subroutine
916 33         71 _getset($self, 1, $rows, $cols, $data);
917              
918             }
919              
920             # get/set nCLR data
921             # optional hash contains supplementary parameters
922             # row_slice and column_slice are 1-D array references
923             # data_slice and replacement_data are 2-D array references
924             # parameters: ([hash]) => returns: (column_slice)
925             # parameters: (row_slice, [hash]) => returns: (data_slice)
926             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
927             sub nCLR {
928              
929             # local variables
930 38     38 1 9082 my ($hash, $context, %fmt, %fmt2, $chan, @cols);
931              
932             # get optional hash
933 38 100       82 $hash = pop() if (ref($_[-1]) eq 'HASH');
934              
935             # get remaining parameters
936 38         63 my ($self, $rows, $data) = @_;
937              
938             # get the context
939 38         64 $context = $hash->{'context'};
940              
941             # make lookup hash (key => column)
942 38 100       53 %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?([2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/) ? ($self->[1][0][$_], $_) : ()} (0 .. $#{$self->[1][0]});
  1333         3014  
  38         82  
943              
944             # make lookup hash (prefix -or- '||' => last key)
945 38 100       83 %fmt2 = map {($self->[1][0][$_] =~ m/^(.*\|)?([2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/) ? (defined($1) ? ($1, $2) : ('||', $2)) : ()} (0 .. $#{$self->[1][0]});
  1333 100       3027  
  38         69  
946              
947             # if context defined
948 38 100       98 if (defined($context)) {
949            
950             # get the last key for this context
951 21 100       93 ($chan = $fmt2{"$context|"}) || return();
952            
953             # remove the channel number
954 7         17 chop($chan);
955            
956             # get column slice (selected from %fmt columns)
957 7         17 @cols = grep {$self->[1][0][$_] =~ m/^$context\|$chan[1-9A-F]$/} values(%fmt);
  42         158  
958            
959             } else {
960            
961             # if all keys have a context
962 17 100       41 if (! defined($chan = $fmt2{'||'})) {
963            
964             # make lookup hash ignoring context (key => column)
965 8 100       10 %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?([2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/) ? ($1, $_) : ()} (0 .. $#{$self->[1][0]});
  265         578  
  8         15  
966            
967             # make lookup hash ('||' => last key)
968 8 100       15 %fmt2 = map {($self->[1][0][$_] =~ m/^(?:.*\|)?([2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/) ? ('||', $1) : ()} (0 .. $#{$self->[1][0]});
  265         561  
  8         15  
969            
970             # get last key for any context
971 8 100       32 ($chan = $fmt2{'||'}) || return();
972            
973             # remove the channel number
974 2         4 chop($chan);
975            
976             # get column slice (selected from %fmt columns)
977 2         4 @cols = grep {$self->[1][0][$_] =~ m/^(?:.*\|)?$chan[1-9A-F]$/} values(%fmt);
  12         76  
978            
979             } else {
980            
981             # remove the channel number
982 9         17 chop($chan);
983            
984             # get column slice (selected from %fmt columns)
985 9         18 @cols = grep {$self->[1][0][$_] =~ m/^$chan[1-9A-F]$/} values(%fmt);
  54         245  
986            
987             }
988            
989             }
990              
991             # sort by color channel (1-9, A-F)
992 18         55 @cols = sort {substr($self->[1][0][$a], -1) cmp substr($self->[1][0][$b], -1)} @cols;
  178         257  
993              
994             # match last format key
995 18         66 $self->[1][0][$cols[-1]] =~ m/([2-9A-F])(?:CLR_|_)([1-9A-F])$/;
996              
997             # verify number of format keys
998 18 50 33     101 (CORE::hex($1) == @cols && CORE::hex($2) == @cols) or croak('wrong number of nCLR keys');
999              
1000             # call get/set subroutine
1001 18         45 _getset($self, 1, $rows, \@cols, $data);
1002              
1003             }
1004              
1005             # get/set spot color data
1006             # optional hash contains supplementary parameters
1007             # row_slice and column_slice are 1-D array references
1008             # data_slice is a Math::Matrix object (2-D array)
1009             # replacement_data is a Math::Matrix object or 2-D array
1010             # parameters: ([hash]) => returns: (column_slice)
1011             # parameters: (row_slice, [hash]) => returns: (data_slice)
1012             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1013             sub spot {
1014              
1015             # local variables
1016 6     6 1 12 my ($hash, $context, @cols);
1017              
1018             # get optional hash
1019 6 50       15 $hash = pop() if (ref($_[-1]) eq 'HASH');
1020              
1021             # get remaining parameters
1022 6         13 my ($self, $rows, $data) = @_;
1023              
1024             # if context defined
1025 6 50       15 if (defined($context = $hash->{'context'})) {
1026            
1027             # select spot color columns with specified context
1028 6         12 @cols = grep {$self->[1][0][$_] =~ m/^$context\|SPOT_[1-9A-F]$/} (0 .. $#{$self->[1][0]});
  152         345  
  6         13  
1029            
1030             } else {
1031            
1032             # if any spot colors, regardless of context
1033 0 0       0 if (@cols = grep {$self->[1][0][$_] =~ m/SPOT_[1-9A-F]$/} (0 .. $#{$self->[1][0]})) {
  0         0  
  0         0  
1034            
1035             # match context of last column
1036 0         0 $self->[1][0][$cols[-1]] =~ m/^(.*\|)/;
1037            
1038             # select columns with last context (could be none)
1039 0 0       0 @cols = grep {defined($1) ? $self->[1][0][$_] =~ m/^$1/ : $self->[1][0][$_] !~ m/\|/} @cols;
  0         0  
1040            
1041             }
1042            
1043             }
1044              
1045             # sort by format
1046 6         13 @cols = sort {$self->[1][0][$a] cmp $self->[1][0][$b]} @cols;
  0         0  
1047              
1048             # call get/set subroutine
1049 6 50       15 _getset($self, 1, $rows, @cols ? \@cols : undef, $data);
1050              
1051             }
1052              
1053             # get/set device data
1054             # device data is either RGB, CMYK or nCLR
1055             # device values have range (0 - 1)
1056             # optional hash contains supplementary parameters
1057             # row_slice and column_slice are 1-D array references
1058             # data_slice is a Math::Matrix object (2-D array)
1059             # replacement_data is a Math::Matrix object or 2-D array
1060             # parameters: ([hash]) => returns: (column_slice)
1061             # parameters: (row_slice, [hash]) => returns: (data_slice)
1062             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1063             sub device {
1064              
1065             # local variables
1066 36     36 1 21282 my ($hash, $cols, $mult);
1067              
1068             # get optional hash
1069 36 100       89 $hash = pop() if (ref($_[-1]) eq 'HASH');
1070              
1071             # get remaining parameters
1072 36         55 my ($self, $rows, $data) = @_;
1073              
1074             # get column slice or return empty
1075 36   100     60 $cols = rgb($self, $hash) || cmyk($self, $hash) || nCLR($self, $hash) || spot($self, $hash) || return();
1076              
1077             # set multiplier (255 if RGB, else 100)
1078 30 100       109 $mult = ($self->[1][0][$cols->[0]] =~ m/RGB_R$/) ? 255 : 100;
1079              
1080             # call get/set subroutine
1081 30 50   78   129 _getset($self, 1, $rows, $cols, $data, sub {map {defined($_) ? $_/$mult : $_} @_}, sub {map {defined($_) ? $_ * $mult : $_} @_});
  78 50       98  
  338         541  
  18         21  
  78         114  
1082              
1083             }
1084              
1085             # get/set CTV data
1086             # optional hash contains supplementary parameters
1087             # row_slice and column_slice are 1-D array references
1088             # data_slice is a Math::Matrix object (2-D array)
1089             # replacement_data is a Math::Matrix object or 2-D array
1090             # parameters: ([hash]) => returns: (column_slice)
1091             # parameters: (row_slice, [hash]) => returns: (data_slice)
1092             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1093             sub ctv {
1094              
1095             # local variables
1096 17     17 1 116 my ($hash, $cols);
1097              
1098             # get optional hash
1099 17 100       34 $hash = pop() if (ref($_[-1]) eq 'HASH');
1100              
1101             # get remaining parameters
1102 17         28 my ($self, $rows, $data) = @_;
1103              
1104             # get column slice, adding optional context prefix
1105 17 100       25 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(CTV));
  17         54  
1106              
1107             # call get/set subroutine
1108 17         39 _getset($self, 1, $rows, $cols, $data);
1109              
1110             }
1111              
1112             # get/set L*a*b* data
1113             # optional hash contains supplementary parameters
1114             # row_slice and column_slice are 1-D array references
1115             # data_slice is a Math::Matrix object (2-D array)
1116             # replacement_data is a Math::Matrix object or 2-D array
1117             # parameters: ([hash]) => returns: (column_slice)
1118             # parameters: (row_slice, [hash]) => returns: (data_slice)
1119             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1120             sub lab {
1121              
1122             # local variables
1123 29     29 1 9336 my ($hash, $cols);
1124              
1125             # get optional hash
1126 29 100       68 $hash = pop() if (ref($_[-1]) eq 'HASH');
1127              
1128             # get remaining parameters
1129 29         38 my ($self, $rows, $data) = @_;
1130              
1131             # get column slice, adding optional context prefix
1132 29 100       44 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(LAB_L LAB_A LAB_B));
  87         182  
1133              
1134             # call get/set subroutine
1135 29         80 _getset($self, 1, $rows, $cols, $data, _lab_encoding($self, $hash));
1136              
1137             }
1138              
1139             # get/set XYZ data
1140             # optional hash contains supplementary parameters
1141             # row_slice and column_slice are 1-D array references
1142             # data_slice is a Math::Matrix object (2-D array)
1143             # replacement_data is a Math::Matrix object or 2-D array
1144             # parameters: ([hash]) => returns: (column_slice)
1145             # parameters: (row_slice, [hash]) => returns: (data_slice)
1146             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1147             sub xyz {
1148              
1149             # local variables
1150 23     23 1 4367 my ($hash, $cols);
1151              
1152             # get optional hash
1153 23 100       53 $hash = pop() if (ref($_[-1]) eq 'HASH');
1154              
1155             # get remaining parameters
1156 23         37 my ($self, $rows, $data) = @_;
1157              
1158             # get column slice, adding optional context prefix
1159 23 100       31 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z));
  69         141  
1160              
1161             # call get/set subroutine
1162 23         59 _getset($self, 1, $rows, $cols, $data, _xyz_encoding($self, $cols, $hash));
1163              
1164             }
1165              
1166             # get/set density data
1167             # optional hash contains supplementary parameters
1168             # row_slice and column_slice are 1-D array references
1169             # data_slice is a Math::Matrix object (2-D array)
1170             # replacement_data is a Math::Matrix object or 2-D array
1171             # parameters: ([hash]) => returns: (column_slice)
1172             # parameters: (row_slice, [hash]) => returns: (data_slice)
1173             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1174             sub density {
1175              
1176             # local variables
1177 17     17 1 106 my ($hash, $cols);
1178              
1179             # get optional hash
1180 17 100       38 $hash = pop() if (ref($_[-1]) eq 'HASH');
1181              
1182             # get remaining parameters
1183 17         25 my ($self, $rows, $data) = @_;
1184              
1185             # get column slice
1186 17 100       29 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(D_RED D_GREEN D_BLUE D_VIS));
  68         128  
1187              
1188             # call get/set subroutine
1189 17         46 _getset($self, 1, $rows, $cols, $data, _density_encoding($self, $hash));
1190              
1191             }
1192              
1193             # get/set reflectance/transmittance data
1194             # optional hash contains supplementary parameters
1195             # row_slice and column_slice are 1-D array references
1196             # data_slice is a Math::Matrix object (2-D array)
1197             # replacement_data is a Math::Matrix object or 2-D array
1198             # parameters: ([hash]) => returns: (column_slice)
1199             # parameters: (row_slice, [hash]) => returns: (data_slice)
1200             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1201             sub rgbv {
1202              
1203             # local variables
1204 0     0 1 0 my ($hash, $cols);
1205              
1206             # get optional hash
1207 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
1208              
1209             # get remaining parameters
1210 0         0 my ($self, $rows, $data) = @_;
1211              
1212             # get column slice
1213 0 0       0 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(R_RED R_GREEN R_BLUE R_VIS));
  0         0  
1214              
1215             # call get/set subroutine
1216 0         0 _getset($self, 1, $rows, $cols, $data, _rgbv_encoding($self, $hash));
1217              
1218             }
1219              
1220             # get/set spectral data
1221             # optional hash contains supplementary parameters
1222             # row_slice and column_slice are 1-D array references
1223             # data_slice is a Math::Matrix object (2-D array)
1224             # replacement_data is a Math::Matrix object or 2-D array
1225             # parameters: ([hash]) => returns: (column_slice)
1226             # parameters: (row_slice, [hash]) => returns: (data_slice)
1227             # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice)
1228             sub spectral {
1229              
1230             # local variables
1231 45     45 1 24507 my ($hash, $fields, $cols);
1232              
1233             # get optional hash
1234 45 100       118 $hash = pop() if (ref($_[-1]) eq 'HASH');
1235              
1236             # get remaining parameters
1237 45         66 my ($self, $rows, $data) = @_;
1238              
1239             # get spectral fields array
1240 45         120 $fields = _spectral($self, $hash->{'context'});
1241              
1242             # get column slice from spectral fields array
1243 45 100       89 $cols = defined($fields) ? [map {$_->[0]} @{$fields}] : undef;
  1332         1495  
  37         55  
1244              
1245             # call get/set subroutine
1246 45         122 _getset($self, 1, $rows, $cols, $data);
1247              
1248             }
1249              
1250             # get spectral wavelength array
1251             # array is sorted (low to high)
1252             # parameters: ([hash])
1253             # returns: (ref_to_wavelength_array)
1254             sub wavelength {
1255              
1256             # get parameters
1257 0     0 1 0 my ($self, $hash) = @_;
1258              
1259             # get spectral fields array or return empty
1260 0   0     0 my $fields = _spectral($self, $hash->{'context'}) || return();
1261              
1262             # return
1263 0         0 return([map {$_->[1]} @{$fields}]);
  0         0  
  0         0  
1264              
1265             }
1266              
1267             # get spectral wavelength range
1268             # structure is [start_nm, end_nm, increment]
1269             # parameters: ([hash])
1270             # returns: (range)
1271             sub nm {
1272              
1273             # get parameters
1274 0     0 1 0 my ($self, $hash) = @_;
1275            
1276             # local variables
1277 0         0 my ($fields, $inc);
1278              
1279             # get spectral fields array or return empty
1280 0   0     0 $fields = _spectral($self, $hash->{'context'}) || return();
1281              
1282             # compute increment
1283 0         0 $inc = $fields->[1][1] - $fields->[0][1];
1284              
1285             # verify wavelength increment
1286 0 0 0     0 ($inc > 0 && abs($#{$fields} * $inc - $fields->[-1][1] + $fields->[0][1]) < 1E-12) || warn('inconsistent wavelength values');
  0         0  
1287              
1288             # return range
1289 0         0 return([$fields->[0][1], $fields->[-1][1], $inc]);
1290              
1291             }
1292              
1293             # get illuminant white point
1294             # parameters: ([hash])
1295             # returns: (XYZ_vector)
1296             sub iwtpt {
1297              
1298             # get parameters
1299 0     0 1 0 my ($self, $hash) = @_;
1300              
1301             # local variables
1302 0         0 my ($encode, $cols, $iwtpt, $get);
1303              
1304             # extract encoding hash
1305 0         0 $encode = {'encoding' => delete($hash->{'encoding'})};
1306              
1307             # get XYZ or L*a*b* column slice
1308 0 0 0     0 $cols = xyz($self, $hash) || lab($self, $hash) or croak('illuminant white point XYZ or L*a*b* column slice undefined');
1309              
1310             # get illuminant white point
1311 0         0 $iwtpt = _illumWP($self, $cols, $hash);
1312              
1313             # get code reference
1314 0         0 ($get) = _xyz_encoding($self, $cols, $encode);
1315              
1316             # return encoded XYZ vector
1317 0         0 return([&$get(@{$iwtpt})]);
  0         0  
1318              
1319             }
1320              
1321             # get media white point
1322             # parameters: ([hash])
1323             # returns: (XYZ_vector)
1324             sub wtpt {
1325              
1326             # get parameters
1327 0     0 1 0 my ($self, $hash) = @_;
1328              
1329             # local variables
1330 0         0 my ($encode, $cols, $get);
1331              
1332             # extract encoding hash
1333 0         0 $encode = {'encoding' => delete($hash->{'encoding'})};
1334              
1335             # get XYZ or L*a*b* column slice
1336 0 0 0     0 $cols = xyz($self, $hash) || lab($self, $hash) or croak('white point XYZ or L*a*b* column slice undefined');
1337              
1338             # if media white point undefined in colorimetry array
1339 0 0       0 if (! defined($self->[2][3][$cols->[0]])) {
1340            
1341             # compute media white point or return undefined
1342 0 0       0 (_mediaWP($self, $cols, $hash)) || return();
1343            
1344             }
1345              
1346             # get code reference
1347 0         0 ($get) = _xyz_encoding($self, $cols, $encode);
1348              
1349             # return encoded XYZ vector
1350 0         0 return([&$get(@{$self->[2][3]}[@{$cols}])]);
  0         0  
  0         0  
1351              
1352             }
1353              
1354             # get media black point
1355             # parameters: ([hash])
1356             # returns: (XYZ_vector)
1357             sub bkpt {
1358              
1359             # get parameters
1360 0     0 1 0 my ($self, $hash) = @_;
1361              
1362             # local variables
1363 0         0 my ($encode, $cols, $get);
1364              
1365             # extract encoding hash
1366 0         0 $encode = {'encoding' => delete($hash->{'encoding'})};
1367              
1368             # get XYZ or L*a*b* column slice
1369 0 0 0     0 $cols = xyz($self, $hash) || lab($self, $hash) or croak('black point XYZ or L*a*b* column slice undefined');
1370              
1371             # if media black point undefined in colorimetry array
1372 0 0       0 if (! defined($self->[2][4][$cols->[0]])) {
1373            
1374             # compute media black point or return undefined
1375 0 0       0 (_mediaBP($self, $cols, $hash)) || return();
1376            
1377             }
1378              
1379             # get code reference
1380 0         0 ($get) = _xyz_encoding($self, $cols, $encode);
1381              
1382             # return encoded XYZ vector
1383 0         0 return([&$get(@{$self->[2][4]}[@{$cols}])]);
  0         0  
  0         0  
1384              
1385             }
1386              
1387             # compute media OBA index
1388             # requires M1 and M2 measurements
1389             # requires device values -or- sample number
1390             # optional hash keys are 'sample', 'device', and 'context'
1391             # parameters: ([hash])
1392             # returns: (oba_index)
1393             # returns: (M1_XYZ_vector, M2_XYZ_vector)
1394             sub oba_index {
1395              
1396             # get parameters
1397 0     0 1 0 my ($self, $hash) = @_;
1398              
1399             # local variables
1400 0         0 my ($sample, $dev, $mwv, $wps, $wpdata, $context1, $context2, $m1, $m2, @xyz1, @xyz2, $nm, $color);
1401              
1402             # if 'sample' defined
1403 0 0       0 if (defined($hash->{'sample'})) {
1404            
1405             # get sample from hash
1406 0         0 $sample = $hash->{'sample'};
1407            
1408             # if valid sample number
1409 0 0 0     0 if (Scalar::Util::looks_like_number($sample) && $sample == int($sample) && $sample > 0 && $sample <= $#{$self->[1]}) {
  0   0     0  
      0        
1410            
1411             # get sample data row
1412 0         0 $wpdata = $self->[1][$sample];
1413            
1414             } else {
1415            
1416             # warn
1417 0         0 warn('invalid sample number');
1418            
1419             # return empty
1420 0         0 return();
1421            
1422             }
1423            
1424             } else {
1425            
1426             # if device data (using 'device' context)
1427 0 0       0 if ($dev = device($self, {'context' => $hash->{'device'}})) {
1428            
1429             # set media white device value (1 if RGB, 0 otherwise)
1430 0 0       0 $mwv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 255 : 0;
1431            
1432             # find paper white samples
1433 0     0   0 $wps = find($self, sub {@_ == grep {$_ == $mwv} @_}, [], $dev);
  0         0  
  0         0  
1434            
1435             # if samples found
1436 0 0       0 if (@{$wps}) {
  0         0  
1437            
1438             # add average paper white sample row
1439 0         0 add_avg($self, $wps);
1440            
1441             # get sample data row
1442 0         0 $wpdata = pop(@{$self->[1]});
  0         0  
1443            
1444             } else {
1445            
1446             # warn
1447 0         0 warn('no paper white samples');
1448            
1449             # return empty
1450 0         0 return();
1451            
1452             }
1453            
1454             } else {
1455            
1456             # warn
1457 0         0 warn('no sample value or device data');
1458            
1459             # return empty
1460 0         0 return();
1461            
1462             }
1463            
1464             }
1465              
1466             # if 'context' defined
1467 0 0       0 if (defined($hash->{'context'})) {
1468            
1469             # if context an array reference containing two scalars
1470 0 0 0     0 if (ref($hash->{'context'}) eq 'ARRAY' && ! ref($hash->{'context'}->[0]) && ! ref($hash->{'context'}->[1])) {
      0        
1471            
1472             # get specified 'M1' context
1473 0         0 $context1 = $hash->{'context'}->[0];
1474            
1475             # get specified 'M2' context
1476 0         0 $context2 = $hash->{'context'}->[1];
1477            
1478             } else {
1479            
1480             # warn
1481 0         0 warn('OBA context is an array reference containing M1 and M2 contexts');
1482            
1483             # return empty
1484 0         0 return();
1485            
1486             }
1487            
1488             } else {
1489            
1490             # use standard 'M1' context
1491 0         0 $context1 = 'M1_Measurement';
1492            
1493             # use standard 'M2' context
1494 0         0 $context2 = 'M2_Measurement';
1495            
1496             }
1497              
1498             # if M1 and M2 spectral data
1499 0 0 0     0 if (($m1 = spectral($self, {'context' => $context1})) && ($m2 = spectral($self, {'context' => $context2}))) {
    0 0        
    0 0        
1500            
1501             # get spectral range
1502 0         0 $nm = nm($self, {'context' => $context1});
1503            
1504             # if increment is 10 or 20 nm
1505 0 0 0     0 if ($nm->[2] == 10 || $nm->[2] == 20) {
1506            
1507             # make ASTM color object
1508 0         0 $color = ICC::Support::Color->new({'illuminant' => 'D50', 'increment' => $nm->[2]});
1509            
1510             } else {
1511            
1512             # make CIE color object
1513 0         0 $color = ICC::Support::Color->new({'illuminant' => ['CIE', 'D50'], 'increment' => $nm->[2]});
1514            
1515             }
1516            
1517             # compute M1 and M2 XYZ values
1518 0         0 @xyz1 = $color->transform(@{$wpdata}[@{$m1}]);
  0         0  
  0         0  
1519 0         0 @xyz2 = $color->transform(@{$wpdata}[@{$m2}]);
  0         0  
  0         0  
1520            
1521             # if M1 and M2 XYZ data
1522             } elsif (($m1 = xyz($self, {'context' => $context1})) && ($m2 = xyz($self, {'context' => $context2}))) {
1523            
1524             # get M1 and M2 XYZ values (assumes D50 illumination)
1525 0         0 @xyz1 = @{$wpdata}[@{$m1}];
  0         0  
  0         0  
1526 0         0 @xyz2 = @{$wpdata}[@{$m2}];
  0         0  
  0         0  
1527            
1528             # if M1 and M2 L*a*b* data
1529             } elsif (($m1 = lab($self, {'context' => $context1})) && ($m2 = lab($self, {'context' => $context2}))) {
1530            
1531             # compute M1 and M2 XYZ values (D50 illumination)
1532 0         0 @xyz1 = ICC::Shared::_Lab2XYZ(@{$wpdata}[@{$m1}], ICC::Shared::D50);
  0         0  
  0         0  
1533 0         0 @xyz2 = ICC::Shared::_Lab2XYZ(@{$wpdata}[@{$m2}], ICC::Shared::D50);
  0         0  
  0         0  
1534            
1535             } else {
1536            
1537             # warn
1538 0         0 warn('M1 and M2 data required for OBA index');
1539            
1540             # return empty
1541 0         0 return();
1542            
1543             }
1544              
1545             # return array (XYZ media white points) or scalar (OBA index)
1546 0 0       0 return(wantarray ? (\@xyz1, \@xyz2) : ($xyz1[2] - $xyz2[2])/82.49);
1547              
1548             }
1549              
1550             # get chromatic adaptation transform (CAT) object
1551             # a CAT is optionally created when adding XYZ data
1552             # optional hash contains supplementary parameters
1553             # parameters: ([hash])
1554             # returns: (CAT_object)
1555             sub cat {
1556              
1557             # get parameters
1558 0     0 1 0 my ($self, $hash) = @_;
1559              
1560             # local variables
1561 0         0 my ($cols, $cat);
1562              
1563             # get column slice, adding optional context prefix
1564 0 0       0 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z));
  0         0  
1565              
1566             # return if slice undefined
1567 0 0       0 return() if (! defined($cols));
1568              
1569             # get CAT or illuminant
1570 0         0 $cat = $self->[2][2][$cols->[0]];
1571              
1572             # return CAT if defined
1573 0 0 0     0 return((defined($cat) && UNIVERSAL::isa($cat, 'ICC::Profile::matf')) ? $cat : ());
1574              
1575             }
1576              
1577             # get Color object
1578             # a Color object is created when adding XYZ data from spectral data
1579             # optional hash contains supplementary parameters
1580             # parameters: ([hash])
1581             # returns: (Color_object)
1582             sub color {
1583              
1584             # get parameters
1585 0     0 1 0 my ($self, $hash) = @_;
1586              
1587             # local variables
1588 0         0 my ($cols, $color);
1589              
1590             # get column slice, adding optional context prefix
1591 0 0       0 $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z));
  0         0  
1592              
1593             # return if slice undefined
1594 0 0       0 return() if (! defined($cols));
1595              
1596             # get CAT or illuminant
1597 0         0 $color = $self->[2][1][$cols->[0]];
1598              
1599             # return CAT if defined
1600 0 0 0     0 return((defined($color) && UNIVERSAL::isa($color, 'ICC::Support::Color')) ? $color : ());
1601              
1602             }
1603              
1604             # append rows to data array
1605             # data matrix is the 2-D array of data values to be appended
1606             # column slice is a reference to an array of data matrix column indices
1607             # parameters: (data_matrix, [column_slice])
1608             # returns: (row_slice)
1609             sub add_rows {
1610              
1611             # get parameters
1612 0     0 1 0 my ($self, $matrix, $cols) = @_;
1613              
1614             # if object contains data
1615 0 0 0     0 if ($#{$self->[1]} || @{$self->[1][0]}) {
  0         0  
  0         0  
1616            
1617             # set offset to upper index + 1
1618 0         0 my $offset = $#{$self->[1]} + 1;
  0         0  
1619            
1620             # call 'splice_rows'
1621 0         0 splice_rows($self, $offset, 0, $matrix, $cols);
1622            
1623             # return row slice
1624 0         0 return([$offset .. ($offset + $#{$matrix})]);
  0         0  
1625            
1626             # new object
1627             } else {
1628            
1629             # call 'splice_rows' (deleting first row)
1630 0         0 splice_rows($self, 0, 1, $matrix, $cols);
1631            
1632             # return row slice
1633 0         0 return([0 .. $#{$matrix}]);
  0         0  
1634            
1635             }
1636            
1637             }
1638              
1639             # append columns to data array
1640             # data matrix is the 2-D array of data values to be appended
1641             # format is a reference to an array of DATA_FORMAT keywords
1642             # parameters: (data_matrix, [format])
1643             # returns: (column_slice)
1644             sub add_cols {
1645              
1646             # get parameters
1647 1     1 1 4 my ($self, $matrix, $format) = @_;
1648              
1649             # verify matrix a 2-D array or Math::Matrix object
1650 1 50 33     8 (ref($matrix) eq 'ARRAY' && ref($matrix->[0]) eq 'ARRAY') || (UNIVERSAL::isa($matrix, 'Math::Matrix')) || croak ('invalid matrix parameter');
      33        
1651              
1652             # if format supplied
1653 1 50       3 if (defined($format)) {
1654            
1655             # verify format is 1-D array of scalars
1656 0 0 0     0 (ref($format) eq 'ARRAY' && @{$format} == grep {! ref()} @{$format}) or croak('invalid format parameter');
  0         0  
  0         0  
  0         0  
1657            
1658             # verify format and matrix have same number of columns
1659 0 0       0 (@{$format} == @{$matrix->[0]}) or croak('format and matrix have different number of columns');
  0         0  
  0         0  
1660            
1661             # add format to matrix
1662 0         0 $matrix = [$format, @{$matrix}];
  0         0  
1663            
1664             }
1665            
1666             # if object contains data
1667 1 50 33     2 if ($#{$self->[1]} || @{$self->[1][0]}) {
  1         4  
  1         4  
1668            
1669             # warn if matrix and object have different number of rows
1670 0 0       0 (@{$matrix} == @{$self->[1]}) or carp('matrix and object have different number of rows');
  0         0  
  0         0  
1671            
1672             # set offset to upper index + 1
1673 0         0 my $offset = $#{$self->[1][0]} + 1;
  0         0  
1674            
1675             # call 'splice_cols'
1676 0         0 splice_cols($self, $offset, 0, $matrix);
1677            
1678             # return column slice
1679 0         0 return([$offset .. ($offset + $#{$matrix->[0]})]);
  0         0  
1680            
1681             # new object
1682             } else {
1683            
1684             # call 'splice_rows' (deleting first row)
1685 1         17 splice_rows($self, 0, 1, $matrix);
1686            
1687             # return column slice
1688 1         2 return([0 .. $#{$self->[1][0]}]);
  1         3  
1689            
1690             }
1691            
1692             }
1693              
1694             # add average sample
1695             # assumes device values (if any) are same for each sample
1696             # averages measurement values - spectral, XYZ, L*a*b* or density
1697             # L*a*b* values are converted to xyz for averaging, then back to L*a*b*
1698             # density values are converted to reflectance for averaging, then back to density
1699             # returns row slice of the appended average sample
1700             # parameters: (row_slice, [hash])
1701             # returns: (row_slice)
1702             sub add_avg {
1703              
1704             # get parameters
1705 0     0 1 0 my ($self, $rows, $hash) = @_;
1706              
1707             # local variables
1708 0         0 my ($c1, $c2, $c3, @id, @name);
1709              
1710             # flatten row slice
1711 0         0 $rows = ICC::Shared::flatten($rows);
1712              
1713             # resolve empty row slice
1714 0 0       0 $rows = [1 .. $#{$self->[1]}] if (@{$rows} == 0);
  0         0  
  0         0  
1715              
1716             # get averaging groups
1717 0         0 ($c1, $c2, $c3) = _avg_groups($self, $hash);
1718              
1719             # for each format field
1720 0         0 for my $i (0 .. $#{$self->[1][0]}) {
  0         0  
1721            
1722             # add column if SAMPLE_ID field
1723 0 0       0 push(@id, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID)$/);
1724            
1725             # add column if SAMPLE_NAME field
1726 0 0       0 push(@name, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?SAMPLE_NAME$/);
1727            
1728             }
1729              
1730             # return average sample
1731 0         0 return([_add_avg($self, $rows, $c1, $c2, $c3, \@id, \@name, $hash)]);
1732              
1733             }
1734              
1735             # add format keys
1736             # keys are appended to row 0 of the data array
1737             # note: format_keys is a list of scalars and/or array references
1738             # note: format_keys are saved as given, with or without context
1739             # parameters: (format_keys)
1740             # returns: (column_slice)
1741             sub add_fmt {
1742              
1743             # get parameters
1744 16     16 1 26 my $self = shift();
1745              
1746             # local variables
1747 16         23 my (@keys, $i, %fmt);
1748              
1749             # flatten format key list
1750 16         21 @keys = @{ICC::Shared::flatten(@_)};
  16         41  
1751              
1752             # get upper column index
1753 16         32 $i = $#{$self->[1][0]};
  16         26  
1754              
1755             # make format lookup hash of existing keys
1756 16         27 %fmt = map {$self->[1][0][$_], $_} (0 .. $#{$self->[1][0]});
  34         73  
  16         26  
1757              
1758             # warn if duplicate keys
1759 16 50       27 warn('adding duplicate format key(s)') if (grep {exists($fmt{$_})} @keys);
  256         348  
1760              
1761             # push format keys onto format row
1762 16         19 push(@{$self->[1][0]}, @keys);
  16         50  
1763              
1764             # return slice array reference
1765 16         25 return([$i + 1 .. $#{$self->[1][0]}]);
  16         68  
1766              
1767             }
1768              
1769             # append CTV data to data array
1770             # computed from L*a*b* data, XYZ data, or spectral data
1771             # if CTV data already exists, return those slices
1772             # adds L*a*b* data, and XYZ data if missing
1773             # parameters: ([hash])
1774             # returns: (column_slice)
1775             sub add_ctv {
1776              
1777             # get parameters
1778 0     0 1 0 my ($self, $hash) = @_;
1779              
1780             # local variables
1781 0         0 my ($context, $added, $cf, $cols, $Lab, $color);
1782 0         0 my ($iwtpt, $WPxyz, @wtpt, $dev, $mwv, $coef, @Ls);
1783 0         0 my ($den, $a, $b, $c, $d, $e, $f, $mat);
1784              
1785             # get base context
1786 0         0 $context = $hash->{'context'};
1787              
1788             # get added context
1789 0 0       0 $added = defined($hash->{'added'}) ? $hash->{'added'} : $context;
1790              
1791             # get colorimetry flag
1792 0   0     0 $cf = defined($hash->{'illuminant'}) || defined($hash->{'observer'}) || defined($hash->{'cat'});
1793              
1794             # return column slice if no colorimetry and CTV data already exists
1795 0 0 0     0 return($cols) if (! $cf && ($cols = _cols($self, map {defined($added) ? "$added|$_" : $_} qw(CTV))));
  0 0       0  
1796              
1797             # if L*a*b* exists, or is added
1798 0 0 0     0 if ($Lab = (_cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)) || add_lab($self, $hash))) {
1799            
1800             # get L*a*b* colorimetry hash
1801 0         0 $color = $self->[2][6][$Lab->[0]];
1802            
1803             # for each possible colorimetry key
1804 0         0 for my $key (qw(illuminant observer increment bandpass cat)) {
1805            
1806             # if key is specified
1807 0 0       0 if (defined($hash->{$key})) {
1808            
1809             # if YAML strings differ
1810 0 0       0 if (YAML::Tiny::Dump($hash->{$key}) ne YAML::Tiny::Dump($color->{$key})) {
1811            
1812             # print warning
1813 0         0 warn("$key parameter differs from source");
1814            
1815             }
1816            
1817             }
1818            
1819             }
1820            
1821             # if 'context' and 'added' keys are undefined, and L*a*b* source has context
1822 0 0 0     0 if (! defined($added) && $self->[1][0][$Lab->[0]] =~ m/^(.*)\|/) {
1823            
1824             # set 'added' to L*a*b* context
1825 0         0 $added = $1;
1826            
1827             }
1828            
1829             # add CTV column slice
1830 0 0       0 $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(CTV));
  0         0  
1831            
1832             # get supplied illuminant white point
1833 0         0 $iwtpt = $hash->{'iwtpt'};
1834            
1835             # if supplied illuminant white point is valid
1836 0 0 0     0 if (defined($iwtpt) && (3 == grep {defined() && ! ref() && $_ > 0} @{$iwtpt})) {
  0 0 0     0  
  0 0       0  
1837            
1838             # use it
1839 0         0 $WPxyz = $iwtpt;
1840            
1841             # if XYZ illuminant white point is valid
1842 0 0 0     0 } elsif (3 == grep {defined() && ! ref() && $_ > 0} @{$self->[2][2]}[@{$Lab}]) {
  0         0  
  0         0  
1843            
1844             # use it
1845 0         0 $WPxyz = [@{$self->[2][2]}[@{$Lab}]];
  0         0  
  0         0  
1846            
1847             } else {
1848            
1849             # use D50
1850 0         0 $WPxyz = ICC::Shared::D50;
1851            
1852             }
1853            
1854             # if media white point undefined in colorimetry array
1855 0 0       0 if (! defined($self->[2][3][$Lab->[0]])) {
1856            
1857             # compute media white point or return undefined
1858 0 0       0 (_mediaWP($self, $Lab, $hash)) || return();
1859            
1860             }
1861            
1862             # get media white point (Lx, Ly, Lz)
1863 0         0 @wtpt = ICC::Shared::_xyz2Lxyz($self->[2][3][$Lab->[0]]/$WPxyz->[0], $self->[2][3][$Lab->[1]]/$WPxyz->[1], $self->[2][3][$Lab->[2]]/$WPxyz->[2]);
1864            
1865             # get device column slice
1866 0         0 $dev = device($self, {'context' => $hash->{'device'}});
1867            
1868             # set media white device value (255 if RGB, 0 otherwise)
1869 0 0       0 $mwv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 255 : 0;
1870            
1871             # set origin
1872 0         0 $self->[2][0][$cols->[0]] = $Lab;
1873            
1874             # save media white CTV (0)
1875 0         0 $self->[2][3][$cols->[0]] = 0;
1876            
1877             # save colorimetry hash
1878 0         0 @{$self->[2][6]}[$cols->[0]] = $color;
  0         0  
1879            
1880             # get coefficient array
1881 0 0       0 $coef = defined($hash->{'coef'}) ? $hash->{'coef'} : [1, 1, 1, 0, 0, 0];
1882            
1883             # compute denominator
1884 0         0 $den = $coef->[0]**2 + $coef->[1]**2 + $coef->[2]**2;
1885            
1886             # compute matrix elements
1887 0         0 $a = ($coef->[0]**2 + $coef->[4]**2 + $coef->[5]**2)/$den;
1888 0         0 $b = ($coef->[1]**2 + $coef->[3]**2 + $coef->[5]**2)/$den;
1889 0         0 $c = ($coef->[2]**2 + $coef->[3]**2 + $coef->[4]**2)/$den;
1890 0         0 $d = -$coef->[5]**2/$den;
1891 0         0 $e = -$coef->[4]**2/$den;
1892 0         0 $f = -$coef->[3]**2/$den;
1893            
1894             # make Mahalanobis matrix
1895 0         0 $mat = [
1896             [$a, $d, $e],
1897             [$d, $b, $f],
1898             [$e, $f, $c]
1899             ];
1900            
1901             # bless the object
1902 0         0 bless($mat, 'Math::Matrix');
1903            
1904             # for each sample
1905 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
1906            
1907             # if all device channels are white
1908 0 0       0 if (@{$dev} == grep {$_ == $mwv} @{$self->[1][$i]}[@{$dev}]) {
  0         0  
  0         0  
  0         0  
  0         0  
1909            
1910             # save CTV (0)
1911 0         0 $self->[1][$i][$cols->[0]] = 0;
1912            
1913             } else {
1914            
1915             # compute sample Lx, Ly, Lz values
1916 0         0 @Ls = ICC::Shared::_Lab2Lxyz(@{$self->[1][$i]}[@{$Lab}]);
  0         0  
  0         0  
1917            
1918             # save CTV (computed as Mahalanobis distance)
1919 0         0 $self->[1][$i][$cols->[0]] = _mahal(\@wtpt, \@Ls, $mat);
1920            
1921             }
1922            
1923             }
1924            
1925             } else {
1926            
1927             # warn
1928 0         0 warn('spectral, XYZ or L*a*b* data is required');
1929            
1930             # return empty
1931 0         0 return();
1932            
1933             }
1934              
1935             # return column slice
1936 0         0 return($cols);
1937              
1938             }
1939              
1940             # append L*a*b* data to data array
1941             # computed from XYZ data or spectral data
1942             # if L*a*b* data already exists, returns that slice
1943             # adds XYZ data, if only spectral data exists
1944             # parameter: ([hash])
1945             # returns: (column_slice)
1946             sub add_lab {
1947              
1948             # get parameters
1949 0     0 1 0 my ($self, $hash) = @_;
1950              
1951             # local variables
1952 0         0 my ($context, $added, $cf, $cols, $xyz, $color, $iwtpt, $WPxyz);
1953              
1954             # get base context
1955 0         0 $context = $hash->{'context'};
1956              
1957             # get added context
1958 0 0       0 $added = defined($hash->{'added'}) ? $hash->{'added'} : $context;
1959              
1960             # get colorimetry flag
1961 0   0     0 $cf = defined($hash->{'illuminant'}) || defined($hash->{'observer'}) || defined($hash->{'cat'});
1962              
1963             # return column slice if no colorimetry and L*a*b* data already exists
1964 0 0 0     0 return($cols) if (! $cf && ($cols = _cols($self, map {defined($added) ? "$added|$_" : $_} qw(LAB_L LAB_A LAB_B))));
  0 0       0  
1965              
1966             # if XYZ data exists, or is added
1967 0 0 0     0 if ($xyz = (_cols($self, map {defined($context) ? "$context|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)) || add_xyz($self, $hash))) {
1968            
1969             # get XYZ colorimetry hash
1970 0         0 $color = $self->[2][6][$xyz->[0]];
1971            
1972             # for each possible colorimetry key
1973 0         0 for my $key (qw(illuminant observer increment bandpass cat)) {
1974            
1975             # if key is specified
1976 0 0       0 if (defined($hash->{$key})) {
1977            
1978             # if YAML strings differ
1979 0 0       0 if (YAML::Tiny::Dump($hash->{$key}) ne YAML::Tiny::Dump($color->{$key})) {
1980            
1981             # print warning
1982 0         0 warn("$key parameter differs from source");
1983            
1984             }
1985            
1986             }
1987            
1988             }
1989            
1990             # if 'context' and 'added' keys are undefined, and XYZ source has context
1991 0 0 0     0 if (! defined($added) && $self->[1][0][$xyz->[0]] =~ m/^(.*)\|/) {
1992            
1993             # set 'added' to XYZ context
1994 0         0 $added = $1;
1995            
1996             }
1997            
1998             # add L*a*b* columns slice
1999 0 0       0 $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(LAB_L LAB_A LAB_B));
  0         0  
2000            
2001             # get supplied illuminant white point
2002 0         0 $iwtpt = $hash->{'iwtpt'};
2003            
2004             # if supplied illuminant white point is valid
2005 0 0 0     0 if (defined($iwtpt) && (3 == grep {defined() && ! ref() && $_ > 0} @{$iwtpt})) {
  0 0 0     0  
  0 0       0  
2006            
2007             # use it
2008 0         0 $WPxyz = $iwtpt;
2009            
2010             # if XYZ illuminant white point is valid
2011 0 0 0     0 } elsif (3 == grep {defined() && ! ref() && $_ > 0} @{$self->[2][2]}[@{$xyz}]) {
  0         0  
  0         0  
2012            
2013             # use it
2014 0         0 $WPxyz = [@{$self->[2][2]}[@{$xyz}]];
  0         0  
  0         0  
2015            
2016             } else {
2017            
2018             # use D50
2019 0         0 $WPxyz = ICC::Shared::D50;
2020            
2021             }
2022            
2023             # set origin
2024 0         0 @{$self->[2][0]}[@{$cols}] = ($xyz) x 3;
  0         0  
  0         0  
2025            
2026             # save illuminant white point
2027 0         0 @{$self->[2][2]}[@{$cols}] = @{$WPxyz};
  0         0  
  0         0  
  0         0  
2028            
2029             # save colorimetry hash
2030 0         0 @{$self->[2][6]}[@{$cols}] = ($color) x 3;
  0         0  
  0         0  
2031            
2032             # for each sample
2033 0         0 for my $s (1 .. $#{$self->[1]}) {
  0         0  
2034            
2035             # compute L*a*b* values from XYZ values
2036 0         0 @{$self->[1][$s]}[@{$cols}] = ICC::Shared::_XYZ2Lab(@{$self->[1][$s]}[@{$xyz}], $WPxyz);
  0         0  
  0         0  
  0         0  
  0         0  
2037            
2038             }
2039            
2040             } else {
2041            
2042             # warn
2043 0         0 warn('spectral or XYZ data is required');
2044            
2045             # return empty
2046 0         0 return();
2047            
2048             }
2049              
2050             # return column slice
2051 0         0 return($cols);
2052              
2053             }
2054              
2055             # append XYZ data to data array
2056             # computed from spectral data or L*a*b* data
2057             # if XYZ data already exists, returns that slice
2058             # default colorimetry is D50, 2 degree observer
2059             # parameters: ([hash])
2060             # returns: (column_slice)
2061             sub add_xyz {
2062              
2063             # get parameters
2064 0     0 1 0 my ($self, $hash) = @_;
2065              
2066             # local variables
2067 0         0 my ($oba, $spec1, $spec2, $context, $added, $cf);
2068 0         0 my ($spec, $color, $illum, $specv, $nm, $cols);
2069 0         0 my ($cat, $spectral, $xyz, $Lab, @WPlab, $WPxyz);
2070              
2071             # if 'oba' defined
2072 0 0       0 if (defined($hash->{'oba'})) {
2073            
2074             # get oba factor
2075 0         0 $oba = $hash->{'oba'};
2076            
2077             # if 'context' defined
2078 0 0       0 if (defined($hash->{'context'})) {
2079            
2080             # if context an array reference containing two scalars
2081 0 0 0     0 if (ref($hash->{'context'}) eq 'ARRAY' && ! ref($hash->{'context'}->[0]) && ! ref($hash->{'context'}->[1])) {
      0        
2082            
2083             # get specified 'M1' spectral slice
2084 0         0 $spec1 = spectral($self, {'context' => $hash->{'context'}->[0]});
2085            
2086             # get specified 'M2' spectral slice
2087 0         0 $spec2 = spectral($self, {'context' => $hash->{'context'}->[1]});
2088            
2089             # use specified 'M2' context
2090 0         0 $context = $hash->{'context'}->[1];
2091            
2092             } else {
2093            
2094             # warn
2095 0         0 warn('OBA context is an array reference containing M1 and M2 contexts');
2096            
2097             # return empty
2098 0         0 return();
2099            
2100             }
2101            
2102             } else {
2103            
2104             # get spectral slice using standard 'M1' context
2105 0         0 $spec1 = spectral($self, {'context' => 'M1_Measurement'});
2106            
2107             # get spectral slice using standard 'M2' context
2108 0         0 $spec2 = spectral($self, {'context' => 'M2_Measurement'});
2109            
2110             # use standard 'M2' context
2111 0         0 $context = 'M2_Measurement';
2112            
2113             }
2114            
2115             # verify spectral slices
2116 0 0 0     0 if (! $spec1 || ! $spec2 || $#{$spec1} != $#{$spec2}) {
  0   0     0  
  0         0  
2117            
2118             # warn
2119 0         0 warn('M1 and M2 spectral data required for OBA effect');
2120            
2121             # return empty
2122 0         0 return();
2123            
2124             }
2125            
2126             # get added context
2127 0 0       0 $added = defined($hash->{'added'}) ? $hash->{'added'} : 'OBA';
2128            
2129             } else {
2130            
2131             # get base context
2132 0         0 $context = $hash->{'context'};
2133            
2134             # get added context
2135 0 0       0 $added = defined($hash->{'added'}) ? $hash->{'added'} : $context;
2136            
2137             }
2138              
2139             # get colorimetry flag
2140 0   0     0 $cf = defined($hash->{'illuminant'}) || defined($hash->{'observer'}) || defined($hash->{'cat'});
2141              
2142             # return column slice if no colorimetry and XYZ data already exists
2143 0 0 0     0 return($cols) if (! $cf && ($cols = _cols($self, map {defined($added) ? "$added|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z))));
  0 0       0  
2144              
2145             # if spectral data exists
2146 0 0       0 if (test($self, 'SPECTRAL', $context)) {
    0          
2147            
2148             # get spectral slice
2149 0         0 $spec = spectral($self, {'context' => $context});
2150            
2151             # add chart wavelength range to hash
2152 0         0 $hash->{'range'} = nm($self, {'context' => $context});
2153            
2154             # make empty 'Color.pm' object
2155 0         0 $color = ICC::Support::Color->new();
2156            
2157             # if illuminant is defined, an array reference
2158 0 0 0     0 if (defined($hash->{'illuminant'}) && ref($hash->{'illuminant'}) eq 'ARRAY') {
2159            
2160             # if illuminant is ['DATA'] (ProfileMaker convention)
2161 0 0 0     0 if (defined($hash->{'illuminant'}->[0]) && $hash->{'illuminant'}->[0] eq 'DATA') {
2162            
2163             # verify chart object contains illuminant data
2164 0 0 0     0 (defined($self->[0]{'illuminant'}) && ref($self->[0]{'illuminant'}) eq 'ARRAY') or croak('no illuminant data');
2165            
2166             # make new chart object from illuminant data
2167 0         0 $illum = ICC::Support::Chart->new($self->[0]{'illuminant'});
2168            
2169             # get spectral values
2170 0 0       0 ($specv = $illum->spectral([1])->[0]) or croak('illuminant chart has no spectral data');
2171            
2172             # get wavelength range
2173 0         0 $nm = $illum->nm();
2174            
2175             # update 'illuminant' value in hash
2176 0         0 $hash->{'illuminant'} = [$nm, $specv];
2177            
2178             }
2179            
2180             # initialize object for CIE method
2181 0         0 ICC::Support::Color::_cie($color, $hash);
2182            
2183             } else {
2184            
2185             # initialize object for ASTM method
2186 0         0 ICC::Support::Color::_astm($color, $hash);
2187            
2188             }
2189            
2190             # if 'context' and 'added' keys are undefined, and spectral source has context
2191 0 0 0     0 if (! defined($added) && $self->[1][0][$spec->[0]] =~ m/^(.*)\|/) {
2192            
2193             # set 'added' to spectral context
2194 0         0 $added = $1;
2195            
2196             }
2197            
2198             # add XYZ columns slice
2199 0 0       0 $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z));
  0         0  
2200            
2201             # set origin
2202 0         0 @{$self->[2][0]}[@{$cols}] = ($spec) x 3;
  0         0  
  0         0  
2203            
2204             # save reference to Color.pm object
2205 0         0 @{$self->[2][1]}[@{$cols}] = ($color) x 3;
  0         0  
  0         0  
2206            
2207             # if chromatic adaptation transform (cat) is specified
2208 0 0       0 if (defined($hash->{'cat'})) {
2209            
2210             # if cat is 'matf' object
2211 0 0       0 if (UNIVERSAL::isa($hash->{'cat'}, 'ICC::Profile::matf')) {
    0          
    0          
    0          
2212            
2213             # use it
2214 0         0 $cat = $hash->{'cat'};
2215            
2216             # if cat is 'bradford'
2217             } elsif ($hash->{'cat'} eq 'bradford') {
2218            
2219             # make 'bradford' object
2220 0         0 $cat = ICC::Profile::matf->bradford($color->iwtpt());
2221            
2222             # if cat is 'cat02'
2223             } elsif ($hash->{'cat'} eq 'cat02') {
2224            
2225             # make 'cat02' object
2226 0         0 $cat = ICC::Profile::matf->cat02($color->iwtpt());
2227            
2228             # if cat is 'quasi'
2229             } elsif ($hash->{'cat'} eq 'quasi') {
2230            
2231             # make 'quasi' object
2232 0         0 $cat = ICC::Profile::matf->quasi($color->iwtpt());
2233            
2234             } else {
2235            
2236             # warn
2237 0         0 warn('invalid cat type');
2238            
2239             }
2240            
2241             }
2242            
2243             # if cat defined
2244 0 0       0 if (defined($cat)) {
2245            
2246             # save cat reference
2247 0         0 @{$self->[2][2]}[@{$cols}] = ($cat) x 3;
  0         0  
  0         0  
2248            
2249             } else {
2250            
2251             # save white point
2252 0         0 @{$self->[2][2]}[@{$cols}] = @{$color->iwtpt()};
  0         0  
  0         0  
  0         0  
2253            
2254             }
2255            
2256             # save colorimetry hash
2257 0 0       0 @{$self->[2][6]}[@{$cols}] = ({map {defined($hash->{$_}) ? ($_, $hash->{$_}) : ()} qw(illuminant observer bandpass method ibandpass imethod oba cat increment range encoding)}) x 3;
  0         0  
  0         0  
  0         0  
2258            
2259             # for each sample
2260 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
2261            
2262             # get spectral slice
2263 0         0 $spectral->[$i - 1] = [@{$self->[1][$i]}[@{$spec}]];
  0         0  
  0         0  
2264            
2265             }
2266            
2267             # transform to XYZ data (hash may contain 'encoding' key)
2268 0         0 $xyz = ICC::Support::Color::_trans2($color, $spectral, $hash);
2269            
2270             # add OBA effect, if enabled
2271 0 0       0 _add_oba($self, $spec1, $spec2, $xyz, $oba, $hash) if $oba;
2272            
2273             # for each sample
2274 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
2275            
2276             # if cat defined
2277 0 0       0 if (defined($cat)) {
2278            
2279             # set XYZ slice with cat
2280 0         0 @{$self->[1][$i]}[@{$cols}] = ICC::Profile::matf::_trans0($cat, @{$xyz->[$i - 1]});
  0         0  
  0         0  
  0         0  
2281            
2282             } else {
2283            
2284             # set XYZ slice
2285 0         0 @{$self->[1][$i]}[@{$cols}] = @{$xyz->[$i - 1]};
  0         0  
  0         0  
  0         0  
2286            
2287             }
2288            
2289             }
2290            
2291             # if L*a*b* data exists
2292             } elsif (test($self, 'LAB', $context)) {
2293            
2294             # warn if illuminant is specified
2295 0 0       0 (! defined($hash->{'illuminant'})) || warn('illuminant specified but no spectral data!');
2296            
2297             # get L*a*b* slice
2298 0 0       0 $Lab = cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B));
  0         0  
2299            
2300             # if 'context' and 'added' keys are undefined, and L*a*b* source has context
2301 0 0 0     0 if (! defined($added) && $self->[1][0][$Lab->[0]] =~ m/^(.*)\|/) {
2302            
2303             # set 'added' to L*a*b* context
2304 0         0 $added = $1;
2305            
2306             }
2307            
2308             # get L*a*b* white point values
2309 0         0 @WPlab = @{$self->[2][2]}[@{$Lab}];
  0         0  
  0         0  
2310            
2311             # use scalar values or D50
2312 0 0 0     0 $WPxyz = (3 == grep {defined() && ! ref() && $_ > 0} @WPlab) ? [@WPlab] : ICC::Shared::D50;
  0 0       0  
2313            
2314             # add XYZ columns slice
2315 0 0       0 $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z));
  0         0  
2316            
2317             # set origin
2318 0         0 @{$self->[2][0]}[@{$cols}] = ($Lab) x 3;
  0         0  
  0         0  
2319            
2320             # save illuminant white point
2321 0         0 @{$self->[2][2]}[@{$cols}] = @{$WPxyz};
  0         0  
  0         0  
  0         0  
2322            
2323             # for each sample
2324 0         0 for my $s (1 .. $#{$self->[1]}) {
  0         0  
2325            
2326             # compute XYZ values from L*a*b* values
2327 0         0 @{$self->[1][$s]}[@{$cols}] = ICC::Shared::_Lab2XYZ(@{$self->[1][$s]}[@{$Lab}], $WPxyz);
  0         0  
  0         0  
  0         0  
  0         0  
2328            
2329             }
2330            
2331             } else {
2332            
2333             # warn
2334 0         0 warn('spectral or L*a*b* data is required');
2335            
2336             # return empty
2337 0         0 return();
2338            
2339             }
2340              
2341             # return column slice
2342 0         0 return($cols);
2343              
2344             }
2345              
2346             # append ISO 5-3 density data to data array
2347             # computed from spectral data only
2348             # if density data already exists, return that slice
2349             # default status is 'T', encoding is 'density'
2350             # parameters: ([hash])
2351             # returns: (column_slice)
2352             sub add_density {
2353              
2354             # get parameters
2355 0     0 1 0 my ($self, $hash) = @_;
2356              
2357             # local variables
2358 0         0 my ($oba, $spec1, $spec2, $context, $added);
2359 0         0 my ($encode, $fp, $cols);
2360 0         0 my ($spec, $temp, $color, $spectral, $rgbv);
2361              
2362             # if 'oba' defined
2363 0 0       0 if (defined($hash->{'oba'})) {
2364            
2365             # get oba factor
2366 0         0 $oba = $hash->{'oba'};
2367            
2368             # if 'context' defined
2369 0 0       0 if (defined($hash->{'context'})) {
2370            
2371             # if context an array reference containing two scalars
2372 0 0 0     0 if (ref($hash->{'context'}) eq 'ARRAY' && ! ref($hash->{'context'}->[0]) && ! ref($hash->{'context'}->[1])) {
      0        
2373            
2374             # get specified 'M1' spectral slice
2375 0         0 $spec1 = spectral($self, {'context' => $hash->{'context'}->[0]});
2376            
2377             # get specified 'M2' spectral slice
2378 0         0 $spec2 = spectral($self, {'context' => $hash->{'context'}->[1]});
2379            
2380             # use specified 'M2' context
2381 0         0 $context = $hash->{'context'}->[1];
2382            
2383             } else {
2384            
2385             # warn
2386 0         0 warn('OBA context is an array reference containing M1 and M2 contexts');
2387            
2388             # return empty
2389 0         0 return();
2390            
2391             }
2392            
2393             } else {
2394            
2395             # get spectral slice using standard 'M1' context
2396 0         0 $spec1 = spectral($self, {'context' => 'M1_Measurement'});
2397            
2398             # get spectral slice using standard 'M2' context
2399 0         0 $spec2 = spectral($self, {'context' => 'M2_Measurement'});
2400            
2401             # use standard 'M2' context
2402 0         0 $context = 'M2_Measurement';
2403            
2404             }
2405            
2406             # verify spectral slices
2407 0 0 0     0 if (! $spec1 || ! $spec2 || $#{$spec1} != $#{$spec2}) {
  0   0     0  
  0         0  
2408            
2409             # warn
2410 0         0 warn('M1 and M2 spectral data required for OBA effect');
2411            
2412             # return empty
2413 0         0 return();
2414            
2415             }
2416            
2417             # get added context
2418 0 0       0 $added = defined($hash->{'added'}) ? $hash->{'added'} : 'OBA';
2419            
2420             } else {
2421            
2422             # get base context
2423 0         0 $context = $hash->{'context'};
2424            
2425             # get added context
2426 0 0       0 $added = defined($hash->{'added'}) ? $hash->{'added'} : $context;
2427            
2428             }
2429              
2430             # get encoding
2431 0   0     0 $encode = $hash->{'encoding'} // 'density';
2432              
2433             # if invalid encoding
2434 0 0 0     0 if ($encode ne 'density' && $encode ne 'linear') {
2435            
2436             # warn
2437 0         0 warn('invalid density encoding, using \'density\'');
2438            
2439             # set encoding
2440 0         0 $encode = 'density';
2441            
2442             }
2443              
2444             # set format prefix
2445 0 0       0 $fp = $encode eq 'density' ? 'D' : 'R';
2446              
2447             # return column slice if density/reflectance data already exists
2448 0 0       0 return($cols) if ($cols = cols($self, map {defined($added) ? "$added|$fp$_" : "$fp$_"} qw(_RED _GREEN _BLUE _VIS)));
  0 0       0  
2449              
2450             # if spectral data
2451 0 0       0 if (test($self, 'SPECTRAL', $context)) {
2452            
2453             # get spectral slice
2454 0         0 $spec = spectral($self, $hash);
2455            
2456             # make copy of hash
2457 0         0 $temp = Storable::dclone($hash);
2458            
2459             # add chart wavelength range to hash
2460 0         0 $temp->{'range'} = nm($self, $hash);
2461            
2462             # make empty 'Color.pm' object
2463 0         0 $color = ICC::Support::Color->new();
2464            
2465             # initialize object for ISO 5-3 method
2466 0         0 ICC::Support::Color::_iso($color, $temp);
2467            
2468             # if 'context' and 'added' keys are undefined, and spectral source has context
2469 0 0 0     0 if (! defined($added) && $self->[1][0][$spec->[0]] =~ m/^(.*)\|/) {
2470            
2471             # set 'added' to spectral context
2472 0         0 $added = $1;
2473            
2474             }
2475            
2476             # add density/reflectance columns slice
2477 0 0       0 $cols = add_fmt($self, map {defined($added) ? "$added|$fp$_" : "$fp$_"} qw(_RED _GREEN _BLUE _VIS));
  0         0  
2478            
2479             # set origin
2480 0         0 @{$self->[2][0]}[@{$cols}] = ($spec) x 4;
  0         0  
  0         0  
2481            
2482             # save reference to Color.pm object
2483 0         0 @{$self->[2][1]}[@{$cols}] = ($color) x 4;
  0         0  
  0         0  
2484            
2485             # save colorimetry hash
2486 0 0       0 @{$self->[2][6]}[@{$cols}] = ({map {defined($temp->{$_}) ? ($_, $temp->{$_}) : ()} qw(status increment range encoding)}) x 4;
  0         0  
  0         0  
  0         0  
2487            
2488             # for each sample
2489 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
2490            
2491             # if oba defined
2492 0 0       0 if (defined($oba)) {
2493            
2494             # for each wavelength
2495 0         0 for my $j (0 .. $#{$spec1}) {
  0         0  
2496            
2497             # compute blended spectral value
2498 0         0 $spectral->[$i - 1][$j] = $oba * $self->[1][$i][$spec1->[$j]] + (1 - $oba) * $self->[1][$i][$spec2->[$j]];
2499            
2500             }
2501            
2502             } else {
2503            
2504             # get spectral slice
2505 0         0 $spectral->[$i - 1] = [@{$self->[1][$i]}[@{$spec}]];
  0         0  
  0         0  
2506            
2507             }
2508            
2509             }
2510            
2511             # set encoding
2512 0         0 $temp->{'encoding'} = $encode;
2513            
2514             # transform to density/reflectance data (per encoding)
2515 0         0 $rgbv = ICC::Support::Color::_trans2($color, $spectral, $temp);
2516            
2517             # for each sample
2518 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
2519            
2520             # set data values
2521 0         0 @{$self->[1][$i]}[@{$cols}] = @{$rgbv->[$i - 1]};
  0         0  
  0         0  
  0         0  
2522            
2523             }
2524            
2525             } else {
2526            
2527             # warn
2528 0         0 warn('spectral data is required');
2529            
2530             # return empty
2531 0         0 return();
2532            
2533             }
2534              
2535             # return column slice
2536 0         0 return($cols);
2537              
2538             }
2539              
2540             # add computed values to data array
2541             # processing is done by a user-defined function (udf)
2542             # data groups are defined by one or more column slice(s)
2543             # supported hash keys: 'element', 'sample', 'device', 'rows', 'start', 'added'
2544             # either an 'element' udf or a 'sample' udf are required, but not both
2545             # an 'element' udf computes a single value from single slice value(s)
2546             # a 'sample' udf computes all values at once from slice value array(s)
2547             # setting the 'device' flag converts RGB/CMYK/nCLR values to device values
2548             # the 'rows' parameter is the row slice computed, default is all rows
2549             # the 'start' parameter is the first column computed, default is to append
2550             # the 'added' parameter may be a scalar or an array reference
2551             # an 'added' scalar will be used as a context prefix
2552             # an 'added' array must be the same size as the columns added
2553             # parameters: (column_slice_0, column_slice_1, ... hash)
2554             # returns: (added_column_slice)
2555             sub add_udf {
2556              
2557             # local variables
2558 0     0 1 0 my ($self, $hash, @cs, $rows, $m, $n, @div, $udfe, $udfs);
2559 0         0 my (@p, @u, @s, $cx, $added);
2560              
2561             # get object reference
2562 0         0 $self = shift();
2563              
2564             # get parameter hash
2565 0         0 $hash = pop();
2566              
2567             # verify a hash reference
2568 0 0       0 (ref($hash) eq 'HASH') or croak('last parameter must be a hash reference');
2569              
2570             # verify number of slices
2571 0 0       0 (@cs = @_) or croak('one or more column slices are required');
2572              
2573             # get row slice, all rows by default
2574 0 0       0 $rows = defined($hash->{'rows'}) ? $hash->{'rows'} : [];
2575              
2576             # if row slice an empty array reference
2577 0 0 0     0 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  0         0  
2578            
2579             # use all rows
2580 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
2581            
2582             } else {
2583            
2584             # flatten row slice
2585 0         0 $rows = ICC::Shared::flatten($rows);
2586            
2587             # verify row slice contents
2588 0 0 0     0 (@{$rows} == grep {Scalar::Util::looks_like_number($_) && int($_) == $_ && $_ > 0 && $_ <= $#{$self->[1]}} @{$rows}) or croak('invalid row slice');
  0 0 0     0  
  0         0  
  0         0  
  0         0  
2589            
2590             }
2591              
2592             # get starting column, append by default
2593 0 0       0 $n = defined($hash->{'start'}) ? $hash->{'start'} : $#{$self->[1][0]} + 1;
  0         0  
2594              
2595             # if an array reference (slice), use the first value
2596 0 0       0 $n = $n->[0] if (ref($n) eq 'ARRAY');
2597              
2598             # verify starting column
2599 0 0 0     0 (Scalar::Util::looks_like_number($n) && int($n) eq $n && $n >= 0) or croak('invalid \'start\' parameter');
      0        
2600              
2601             # if 'device' flag
2602 0 0       0 if ($hash->{'device'}) {
2603            
2604             # for each data format
2605 0         0 for my $i (0 .. $#{$self->[1][0]}) {
  0         0  
2606            
2607             # set divisor to 255 for RGB data
2608 0 0       0 $div[$i] = 255 if ($self->[1][0][$i] =~ m/RGB_[RGB]$/);
2609            
2610             # set divisor to 100 for CMYK data
2611 0 0       0 $div[$i] = 100 if ($self->[1][0][$i] =~ m/CMYK_[CMYK]$/);
2612            
2613             # set divisor to 100 for nCLR data
2614 0 0       0 $div[$i] = 100 if ($self->[1][0][$i] =~ m/(?:[2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/);
2615             }
2616            
2617             }
2618              
2619             # get udf CODE refs
2620 0         0 $udfe = $hash->{'element'};
2621 0         0 $udfs = $hash->{'sample'};
2622              
2623             # if both udfs defined
2624 0 0 0     0 if (defined($udfe) && defined($udfs)) {
    0          
    0          
2625            
2626             # error
2627 0         0 croak('both \'element\' and \'sample\' udfs are defined');
2628            
2629             # if 'element' udf defined
2630             } elsif (defined($udfe)) {
2631            
2632             # verify udf is a code reference
2633 0 0       0 (ref($udfe) eq 'CODE') or croak('\'element\' udf is not a CODE reference');
2634            
2635             # for each parameter
2636 0         0 for my $i (0 .. $#cs) {
2637            
2638             # if an array reference
2639 0 0       0 if (ref($cs[$i]) eq 'ARRAY') {
    0          
2640            
2641             # if first slice
2642 0 0       0 if (! defined($m)) {
2643            
2644             # get upper index
2645 0         0 $m = $#{$cs[0]};
  0         0  
2646            
2647             # compute added slice
2648 0         0 @s = ($n .. $n + $m);
2649            
2650             } else {
2651            
2652             # verify slice size
2653 0 0       0 ($#{$cs[$i]} == $m) or croak('column slices are different sizes');
  0         0  
2654            
2655             }
2656            
2657             # verify a valid column slice
2658 0 0 0     0 (ref($cs[$i]) eq 'ARRAY' || @{$cs[$i]} == grep {Scalar::Util::looks_like_number($_) && int($_) == $_ && $_ >= 0 && $_ <= $#{$self->[1][0]}} @{$cs[$i]}) or croak('invalid column slice');
  0 0 0     0  
  0   0     0  
  0         0  
  0         0  
2659            
2660             # if a scalar
2661             } elsif (! ref($cs[$i])) {
2662            
2663             # verify a valid column index
2664 0 0 0     0 (Scalar::Util::looks_like_number($cs[$i]) && int($cs[$i]) == $cs[$i] && $cs[$i] >= 0 && $cs[$i] <= $#{$self->[1][0]}) or croak('invalid column index');
  0   0     0  
      0        
2665            
2666             } else {
2667            
2668             # error
2669 0         0 croak('parameter must be a scalar or an array reference');
2670            
2671             }
2672            
2673             }
2674            
2675             # verify at least one column slice parameter
2676 0 0       0 (defined($m)) or croak('at least one column slice is required');
2677            
2678             # for each sample
2679 0         0 for my $i (@{$rows}) {
  0         0  
2680            
2681             # for each column index
2682 0         0 for my $j (0 .. $m) {
2683            
2684             # for each parameter
2685 0         0 for my $k (0 .. $#cs) {
2686            
2687             # get column index (slice -or- scalar)
2688 0 0       0 $cx = (ref($cs[$k]) eq 'ARRAY') ? $cs[$k][$j] : $cs[$k];
2689            
2690             # get parameter value
2691 0         0 $p[$k] = $self->[1][$i][$cx];
2692            
2693             # adjust device values, if divisor defined
2694 0 0       0 $p[$k] /= $div[$cx] if defined($div[$cx]);
2695            
2696             }
2697            
2698             # call 'element' udf
2699 0         0 $self->[1][$i][$n + $j] = &$udfe(@p, $j);
2700            
2701             }
2702            
2703             }
2704            
2705             # if 'sample' udf defined
2706             } elsif (defined($udfs)) {
2707            
2708             # verify udf is a code reference
2709 0 0       0 (ref($udfs) eq 'CODE') or croak('\'sample\' udf is not a CODE reference');
2710            
2711             # for each parameter
2712 0         0 for my $i (0 .. $#cs) {
2713            
2714             # verify a valid column slice
2715 0 0 0     0 (ref($cs[$i]) eq 'ARRAY' || @{$cs[$i]} == grep {Scalar::Util::looks_like_number($_) && int($_) == $_ && $_ >= 0 && $_ <= $#{$self->[1][0]}} @{$cs[$i]}) or croak('invalid column slice');
  0 0 0     0  
  0   0     0  
  0         0  
  0         0  
2716            
2717             }
2718            
2719             # verify at least one parameter
2720 0 0       0 (@cs) or croak('at least one column slice is required');
2721            
2722             # for each sample
2723 0         0 for my $i (@{$rows}) {
  0         0  
2724            
2725             # for each column slice
2726 0         0 for my $j (0 .. $#cs) {
2727            
2728             # for each slice element
2729 0         0 for my $k (0 .. $#{$cs[$j]}) {
  0         0  
2730            
2731             # get column index
2732 0         0 $cx = $cs[$j][$k];
2733            
2734             # get parameter value
2735 0         0 $p[$j][$k] = $self->[1][$i][$cx];
2736            
2737             # adjust device values, if divisor defined
2738 0 0       0 $p[$j][$k] /= $div[$cx] if defined($div[$cx]);
2739            
2740             }
2741            
2742             }
2743            
2744             # if first sample
2745 0 0       0 if (! defined($m)) {
2746            
2747             # call 'sample' udf
2748 0         0 @u = &$udfs(@p);
2749            
2750             # get upper index
2751 0         0 $m = $#u;
2752            
2753             # compute added slice
2754 0         0 @s = ($n .. $n + $m);
2755            
2756             # copy values to object
2757 0         0 @{$self->[1][$i]}[@s] = @u;
  0         0  
2758            
2759             } else {
2760            
2761             # call 'sample' udf
2762 0         0 @{$self->[1][$i]}[@s] = &$udfs(@p);
  0         0  
2763            
2764             }
2765            
2766             }
2767            
2768             } else {
2769            
2770             # error
2771 0         0 croak('no udf is defined');
2772            
2773             }
2774              
2775             # get 'added' parameter, default is 'udf', could be undefined
2776 0 0       0 $added = exists($hash->{'added'}) ? $hash->{'added'} : 'udf';
2777              
2778             # if 'added' is undefined
2779 0 0 0     0 if (! defined($added)) {
    0          
    0          
2780            
2781             # if 'element' udf -or- size of first column slice equals number of added columns
2782 0 0 0     0 if (defined($udfe) || @{$cs[0]} == @s) {
  0         0  
2783            
2784             # add data format stripping context from first column slice
2785 0         0 @{$self->[1][0]}[@s] = map {m/^(?:.*\|)?(.*)$/; $1} @{$self->[1][0]}[@{$cs[0]}];
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
2786            
2787             } else {
2788            
2789             # add data format as 'colxxx'
2790 0         0 @{$self->[1][0]}[@s] = map {"col$_"} @s;
  0         0  
  0         0  
2791            
2792             }
2793            
2794             # if 'added' a scalar
2795             } elsif (! ref($added)) {
2796            
2797             # if 'element' udf -or- size of first column slice equals number of added columns
2798 0 0 0     0 if (defined($udfe) || @{$cs[0]} == @s) {
  0         0  
2799            
2800             # add data format using 'added' as context with first column slice format keys
2801 0         0 @{$self->[1][0]}[@s] = map {m/^(?:.*\|)?(.*)$/; "$added|$1"} @{$self->[1][0]}[@{$cs[0]}];
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
2802            
2803             } else {
2804            
2805             # add data format using 'added' as context to 'colxxx'
2806 0         0 @{$self->[1][0]}[@s] = map {"$added|col$_"} @s;
  0         0  
  0         0  
2807            
2808             }
2809            
2810             # if 'added' is an array ref and size equals number of added columns
2811 0         0 } elsif (ref($added) eq 'ARRAY' && @{$added} == @s) {
2812            
2813             # add data format using 'added' as array
2814 0         0 @{$self->[1][0]}[@s] = @{$added};
  0         0  
  0         0  
2815            
2816             } else {
2817            
2818             # error
2819 0         0 croak('invalid \'added\' parameter');
2820            
2821             }
2822              
2823             # return added column slice
2824 0         0 return([@s]);
2825              
2826             }
2827              
2828             # append date column to data array
2829             # adds same date/time to each sample
2830             # supported hash keys: 'date', 'format', 'added'
2831             # parameter: ([hash])
2832             # returns: (column_slice)
2833             sub add_date {
2834              
2835             # get parameters
2836 0     0 1 0 my ($self, $hash) = @_;
2837              
2838             # local variables
2839 0         0 my ($cols, $added, $date, $fmt, $str);
2840              
2841             # get added context
2842 0         0 $added = $hash->{'added'};
2843              
2844             # return column slice if date column already exists
2845 0 0       0 return($cols) if ($cols = _cols($self, defined($added) ? "$added|CREATED" : 'CREATED'));
    0          
2846              
2847             # add date column slice
2848 0 0       0 $cols = add_fmt($self, defined($added) ? "$added|CREATED" : 'CREATED');
2849              
2850             # if date supplied
2851 0 0       0 if (defined($date = $hash->{'date'})) {
2852            
2853             # if date is a number
2854 0 0       0 if (Scalar::Util::looks_like_number($date)) {
    0          
2855            
2856             # make Time::Piece object
2857 0         0 $date = localtime($date);
2858            
2859             # if date not a Time::Piece object
2860             } elsif (ref($date) ne 'Time::Piece') {
2861            
2862             # error
2863 0         0 croak('invalid date parameter');
2864             }
2865            
2866             } else {
2867            
2868             # use 'CREATED' value from chart
2869 0         0 $date = created($self);
2870            
2871             }
2872              
2873             # compute the date/time string (same for each sample)
2874 0 0       0 $str = defined($fmt = $hash->{'format'}) ? $date->strftime($fmt) : $date->epoch();
2875              
2876             # for each row
2877 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
2878            
2879             # set the date time string
2880 0         0 $self->[1][$i][$cols->[0]] = $str;
2881            
2882             }
2883              
2884             # return column slice
2885 0         0 return($cols);
2886              
2887             }
2888              
2889             # splice rows into data array
2890             # offset and length are as used by Perl's 'splice' function
2891             # data matrix is the 2-D array of data values to be spliced
2892             # column slice is a reference to an array of data matrix column indices
2893             # parameters: ([offset, [length, [data_matrix, [column_slice]]]])
2894             # returns: (removed_data_matrix)
2895             sub splice_rows {
2896              
2897             # get parameters
2898 1     1 1 3 my ($self, $offset, $length, $matrix, $cols) = @_;
2899              
2900             # local variables
2901 1         17 my (@ix, @list, @s, $removed);
2902              
2903             # if offset supplied
2904 1 50       5 if (defined($offset)) {
2905            
2906             # verify offset a scalar
2907 1 50 33     5 (! ref($offset) && (int($offset) == $offset)) or croak('invalid offset parameter');
2908            
2909             }
2910              
2911             # if length supplied
2912 1 50       2 if (defined($length)) {
2913            
2914             # verify length an integer scalar
2915 1 50 33     4 (! ref($length) && int($length) == $length) or croak('invalid length parameter');
2916            
2917             }
2918              
2919             # if matrix supplied
2920 1 50       3 if (defined($matrix)) {
2921            
2922             # verify matrix a 2-D array or Math::Matrix object
2923 1 50 33     8 (ref($matrix) eq 'ARRAY' && ref($matrix->[0]) eq 'ARRAY') || (UNIVERSAL::isa($matrix, 'Math::Matrix')) || croak ('invalid matrix parameter');
      33        
2924            
2925             }
2926              
2927             # if column slice supplied
2928 1 50       3 if (defined($cols)) {
2929            
2930             # verify column slice an array reference
2931 0 0       0 (ref($cols) eq 'ARRAY') or croak('invalid cols parameter');
2932            
2933             # verify length, offset and matrix supplied
2934 0 0 0     0 (defined($length) && defined($offset) && defined($matrix)) or croak('cols requires length, offset and matrix');
      0        
2935            
2936             # flatten column slice
2937 0         0 @ix = @{ICC::Shared::flatten($cols)};
  0         0  
2938            
2939             # make splice list using column slice
2940 0         0 @list = map {@s[@ix] = @{$_}; [@s]} @{$matrix};
  0         0  
  0         0  
  0         0  
  0         0  
2941            
2942             # splice the data
2943 0         0 $removed = [splice(@{$self->[1]}, $offset, $length, @list)];
  0         0  
2944            
2945             } else {
2946            
2947             # if matrix supplied
2948 1 50       3 if (defined($matrix)) {
2949            
2950             # verify length, offset
2951 1 50 33     3 (defined($length) && defined($offset)) or croak('matrix requires length and offset');
2952            
2953             # make splice list from full matrix
2954 1         2 @list = map {[@{$_}]} @{$matrix};
  4         4  
  4         8  
  1         2  
2955            
2956             # splice the data
2957 1         2 $removed = [splice(@{$self->[1]}, $offset, $length, @list)];
  1         4  
2958            
2959             } else {
2960            
2961             # if length supplied
2962 0 0       0 if (defined($length)) {
2963            
2964             # verify offset supplied
2965 0 0       0 (defined($offset)) or croak('length requires offset');
2966              
2967             # splice the data
2968 0         0 $removed = [splice(@{$self->[1]}, $offset, $length)];
  0         0  
2969            
2970             } else {
2971            
2972             # if offset supplied
2973 0 0       0 if (defined($offset)) {
2974            
2975             # splice the data
2976 0         0 $removed = [splice(@{$self->[1]}, $offset)];
  0         0  
2977            
2978             } else {
2979            
2980             # get data array reference
2981 0         0 $removed = $self->[1];
2982            
2983             # init data array
2984 0         0 $self->[1] = [[]];
2985            
2986             # init colorimetry array
2987 0         0 $self->[2] = [[]];
2988            
2989             }
2990            
2991             }
2992            
2993             }
2994            
2995             }
2996              
2997             # update the SAMPLE_ID hash
2998 1         4 _makeSampleID($self);
2999              
3000             # return removed data
3001 1         8 return(bless($removed, 'Math::Matrix'));
3002              
3003             }
3004              
3005             # splice columns into data array
3006             # offset and length are as used by Perl's 'splice' function
3007             # data matrix is the 2-D array of data values to be spliced
3008             # row slice is a reference to an array of data matrix row indices
3009             # parameters: ([offset, [length, [data_matrix, [row_slice]]]])
3010             # returns: (removed_data_matrix)
3011             sub splice_cols {
3012              
3013             # get parameters
3014 0     0 1 0 my ($self, $offset, $length, $matrix, $rows) = @_;
3015              
3016             # local variables
3017 0         0 my (@ix, @s, @filler, $removed);
3018              
3019             # if offset supplied
3020 0 0       0 if (defined($offset)) {
3021            
3022             # verify offset a scalar
3023 0 0 0     0 (! ref($offset) && (int($offset) == $offset)) or croak('invalid offset parameter');
3024            
3025             }
3026              
3027             # if length supplied
3028 0 0       0 if (defined($length)) {
3029            
3030             # verify length an integer scalar
3031 0 0 0     0 (! ref($length) && int($length) == $length) or croak('invalid length parameter');
3032            
3033             }
3034              
3035             # if matrix supplied
3036 0 0       0 if (defined($matrix)) {
3037            
3038             # verify matrix a 2-D array or Math::Matrix object
3039 0 0 0     0 (ref($matrix) eq 'ARRAY' && ref($matrix->[0]) eq 'ARRAY') || (UNIVERSAL::isa($matrix, 'Math::Matrix')) || croak ('invalid matrix parameter');
      0        
3040            
3041             }
3042              
3043             # if row slice supplied
3044 0 0       0 if (defined($rows)) {
3045            
3046             # verify row slice an array reference
3047 0 0       0 (ref($rows) eq 'ARRAY') or croak('invalid cols parameter');
3048            
3049             # verify length, offset and matrix supplied
3050 0 0 0     0 (defined($length) && defined($offset) && defined($matrix)) or croak('rows requires length, offset and matrix');
      0        
3051            
3052             # flatten row slice
3053 0         0 @ix = @{ICC::Shared::flatten($rows)};
  0         0  
3054            
3055             # make list of matrix row refs
3056 0         0 @s[@ix] = @{$matrix};
  0         0  
3057            
3058             # make filler data
3059 0         0 @filler = (undef) x @{$matrix->[0]};
  0         0  
3060            
3061             # for each data row
3062 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
3063            
3064             # if matrix data defined
3065 0 0       0 if (defined($s[$i])) {
3066            
3067             # splice matrix data
3068 0         0 $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @{$s[$i]})];
  0         0  
  0         0  
3069            
3070             } else {
3071            
3072             # splice filler data
3073 0         0 $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @filler)];
  0         0  
3074            
3075             }
3076            
3077             }
3078            
3079             # for each colorimetry row
3080 0         0 for my $i (0 .. $#{$self->[2]}) {
  0         0  
3081            
3082             # splice filler data
3083 0 0       0 splice(@{$self->[2][$i]}, $offset, $length, @filler) if (defined($self->[2][$i][$offset]));
  0         0  
3084            
3085             }
3086            
3087             } else {
3088            
3089             # if matrix supplied
3090 0 0       0 if (defined($matrix)) {
3091            
3092             # verify length, offset
3093 0 0 0     0 (defined($length) && defined($offset)) or croak('matrix requires length and offset');
3094            
3095             # make filler data
3096 0         0 @filler = (undef) x @{$matrix->[0]};
  0         0  
3097            
3098             # for each data row
3099 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
3100            
3101             # if matrix data defined
3102 0 0       0 if (defined($matrix->[$i])) {
3103            
3104             # splice matrix data
3105 0         0 $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @{$matrix->[$i]})];
  0         0  
  0         0  
3106            
3107             } else {
3108            
3109             # splice filler data
3110 0         0 $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @filler)];
  0         0  
3111            
3112             }
3113            
3114             }
3115            
3116             # for each colorimetry row
3117 0         0 for my $i (0 .. $#{$self->[2]}) {
  0         0  
3118            
3119             # splice filler data
3120 0 0       0 splice(@{$self->[2][$i]}, $offset, $length, @filler) if (defined($self->[2][$i][$offset]));
  0         0  
3121            
3122             }
3123            
3124             } else {
3125            
3126             # if length supplied
3127 0 0       0 if (defined($length)) {
3128            
3129             # verify offset supplied
3130 0 0       0 (defined($offset)) or croak('length requires offset');
3131            
3132             # for each data row
3133 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
3134            
3135             # splice the data
3136 0         0 $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length)];
  0         0  
3137            
3138             }
3139            
3140             # for each colorimetry row
3141 0         0 for my $i (0 .. $#{$self->[2]}) {
  0         0  
3142            
3143             # splice filler data
3144 0 0       0 splice(@{$self->[2][$i]}, $offset, $length) if (defined($self->[2][$i][$offset]));
  0         0  
3145            
3146             }
3147            
3148             } else {
3149            
3150             # if offset supplied
3151 0 0       0 if (defined($offset)) {
3152            
3153             # for each data row
3154 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
3155            
3156             # splice the data
3157 0         0 $removed->[$i] = [splice(@{$self->[1][$i]}, $offset)];
  0         0  
3158            
3159             }
3160            
3161             # for each colorimetry row
3162 0         0 for my $i (0 .. $#{$self->[2]}) {
  0         0  
3163            
3164             # splice filler data
3165 0 0       0 splice(@{$self->[2][$i]}, $offset) if (defined($self->[2][$i][$offset]));
  0         0  
3166            
3167             }
3168            
3169             } else {
3170            
3171             # get data array reference
3172 0         0 $removed = $self->[1];
3173            
3174             # init data array
3175 0         0 $self->[1] = [[]];
3176            
3177             # init colorimetry array
3178 0         0 $self->[2] = [[]];
3179            
3180             }
3181            
3182             }
3183            
3184             }
3185            
3186             }
3187              
3188             # initialize SAMPLE_ID hash if no SAMPLE_ID field
3189 0 0       0 $self->[4] = {} if (0 == test($self, 'ID'));
3190              
3191             # return removed data
3192 0         0 return(bless($removed, 'Math::Matrix'));
3193              
3194             }
3195              
3196             # remove rows from data array
3197             # parameters: (row_slice)
3198             # returns: (removed_data_matrix)
3199             sub remove_rows {
3200              
3201             # get parameters
3202 0     0 1 0 my ($self, $rows) = @_;
3203              
3204             # local variables
3205 0         0 my ($f, $up, @r, @s, $removed);
3206              
3207             # return empty matrix if row slice undefined
3208 0 0       0 return(bless([[]], 'Math::Matrix')) if (! defined($rows));
3209              
3210             # flatten row slice
3211 0         0 $f = ICC::Shared::flatten($rows);
3212              
3213             # if row slice is empty
3214 0 0       0 if (! defined($f->[0])) {
3215            
3216             # remove all rows, except row 0 (DATA_FORMAT)
3217 0         0 $removed = [splice(@{$self->[1]}, 1)];
  0         0  
3218            
3219             # clear SAMPLE_ID hash
3220 0         0 $self->[4] = {};
3221            
3222             # return removed data
3223 0         0 return(bless($removed, 'Math::Matrix'));
3224            
3225             }
3226              
3227             # get upper row index
3228 0         0 $up = $#{$self->[1]};
  0         0  
3229              
3230             # verify row slice
3231 0 0 0     0 (grep {$_ != int($_) || $_ < 1 || $_ > $up} @{$f}) && carp('row slice contains invalid index value(s)');
  0 0       0  
  0         0  
3232              
3233             # initialize slice (always keep row 0)
3234 0         0 @s = (0);
3235              
3236             # for each row
3237 0         0 for my $i (1 .. $up) {
3238            
3239             # if index contained in row slice
3240 0 0       0 if (grep {$i == $_} @{$f}) {
  0         0  
  0         0  
3241            
3242             # add to slice (remove)
3243 0         0 push(@r, $i);
3244            
3245             } else {
3246            
3247             # add to slice (keep)
3248 0         0 push(@s, $i)
3249            
3250             }
3251            
3252             }
3253              
3254             # if rows to remove
3255 0 0       0 if (@r) {
3256            
3257             # set removed data (@r)
3258 0         0 $removed = [@{$self->[1]}[@r]];
  0         0  
3259            
3260             # set kept data (@s)
3261 0         0 $self->[1] = [@{$self->[1]}[@s]];
  0         0  
3262            
3263             # update the SAMPLE_ID hash
3264 0         0 _makeSampleID($self);
3265            
3266             } else {
3267            
3268             # set removed data (none)
3269 0         0 $removed = [[]];
3270            
3271             }
3272              
3273             # return removed data
3274 0         0 return(bless($removed, 'Math::Matrix'));
3275              
3276             }
3277              
3278             # remove columns from data array
3279             # parameters: (column_slice)
3280             # returns: (removed_data_matrix)
3281             sub remove_cols {
3282              
3283             # get parameters
3284 0     0 1 0 my ($self, $cols) = @_;
3285              
3286             # local variables
3287 0         0 my ($f, $up, @r, @s, $removed, $kept, $color);
3288              
3289             # return empty matrix if column slice undefined
3290 0 0       0 return(bless([[]], 'Math::Matrix')) if (! defined($cols));
3291              
3292             # flatten column slice
3293 0         0 $f = ICC::Shared::flatten($cols);
3294              
3295             # map column slice, converting non-numeric values with 'test' method
3296 0 0       0 @{$f} = map {Scalar::Util::looks_like_number($_) ? $_ : $self->test($_)} @{$f};
  0         0  
  0         0  
  0         0  
3297              
3298             # if columns slice is empty
3299 0 0       0 if (! defined($f->[0])) {
3300            
3301             # copy all rows
3302 0         0 $removed = [@{$self->[1]}];
  0         0  
3303            
3304             # clear data array
3305 0         0 $self->[1] =[[]];
3306            
3307             # clear colorimetry array
3308 0         0 $self->[2] = [[]];
3309            
3310             # clear SAMPLE_ID hash
3311 0         0 $self->[4] = {};
3312            
3313             # return removed data
3314 0         0 return(bless($removed, 'Math::Matrix'));
3315            
3316             }
3317              
3318             # get upper column index
3319 0         0 $up = $#{$self->[1][0]};
  0         0  
3320              
3321             # verify column slice
3322 0 0 0     0 (grep {$_ != int($_) || $_ < 0 || $_ > $up} @{$f}) && carp('column slice contains invalid index value(s)');
  0 0       0  
  0         0  
3323              
3324             # for each column
3325 0         0 for my $i (0 .. $up) {
3326            
3327             # if index contained in column slice
3328 0 0       0 if (grep {$i == $_} @{$f}) {
  0         0  
  0         0  
3329            
3330             # add to slice (remove)
3331 0         0 push(@r, $i);
3332            
3333             } else {
3334            
3335             # add to slice (keep)
3336 0         0 push(@s, $i)
3337            
3338             }
3339            
3340             }
3341              
3342             # if columns to remove
3343 0 0       0 if (@r) {
3344            
3345             # for each data row
3346 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
3347            
3348             # set removed data (@r)
3349 0         0 $removed->[$i] = [@{$self->[1][$i]}[@r]];
  0         0  
3350            
3351             # set kept data (@s)
3352 0         0 $kept->[$i] = [@{$self->[1][$i]}[@s]];
  0         0  
3353            
3354             }
3355            
3356             # update object data
3357 0         0 $self->[1] = $kept;
3358            
3359             # for each colorimetry row
3360 0         0 for my $i (0 .. $#{$self->[2]}) {
  0         0  
3361            
3362             # set kept colorimetry (@s)
3363 0         0 $color->[$i] = [@{$self->[2][$i]}[@s]];
  0         0  
3364            
3365             }
3366            
3367             # update colorimetry data
3368 0         0 $self->[2] = $color;
3369            
3370             # initialize SAMPLE_ID hash if no SAMPLE_ID field
3371 0 0       0 $self->[4] = {} if (0 == test($self, 'ID'));
3372            
3373             } else {
3374            
3375             # set removed data (none)
3376 0         0 $removed = [[]];
3377            
3378             }
3379              
3380             # return removed data
3381 0         0 return(bless($removed, 'Math::Matrix'));
3382              
3383             }
3384              
3385             # get sample selection using tokens
3386             # tokens are provided as a text string
3387             # supported hash keys: 'A2B', 'B2A', 'sort', 'ink_map', 'invert_rgb'
3388             # sample slice is sorted, duplicates are removed
3389             # context may be specified with parameter hash
3390             # parameters: (token_string, [hash])
3391             # returns: (sample_slice)
3392             sub select_token {
3393              
3394             # get parameters
3395 0     0 1 0 my ($self, $select, $hash) = @_;
3396              
3397             # local variables
3398 0         0 my ($ctx, $n, $sub, $sort, $row_length, $sz, $nx);
3399 0         0 my ($map, @m, @ms, @mx, @sp, @si);
3400 0         0 my ($sel, @nr, @samples, $token, @p);
3401 0         0 my ($mat, @dev, @cmy, $c, $my, $lim, %it8, @Lab, $A2B, $B2A, $pcs, $ci);
3402 0         0 my (%unique, $code, $dev, @sn, %minus, @max, $fnk, $fns, $fnb);
3403              
3404             # get context prefix
3405 0 0       0 $ctx = defined($hash->{'context'}) ? "$hash->{'context'}|" : '';
3406              
3407             # verify chart has device data
3408 0 0       0 ($n = test($self, $ctx . 'DEVICE')) or croak("device data is required for 'select_token' method");
3409              
3410             # get substrate device value (1 if RGB, otherwise 0)
3411 0 0       0 $sub = test($self, $ctx . 'RGB') ? 1 : 0;
3412              
3413             # get sort vector from hash
3414 0         0 $sort = $hash->{'sort'};
3415              
3416             # get row length, if specified in chart
3417 0         0 $row_length = _getRowLength($self, {'undef' => 1});
3418              
3419             # get initial chart size
3420 0         0 $sz = size($self, 1);
3421              
3422             # device upper index
3423 0         0 $nx = $n - 1;
3424              
3425             # the map slice (@m) contains the device indices for (C, M, Y, K, extra colors)
3426             # by default, @m = (0, 1, 2, 3, ... N - 1), where N is the number of ink channels
3427             # when there is ink mapping, the process colors may be arranged differently
3428              
3429             # if ink map provided
3430 0 0       0 if (defined($map = $hash->{'ink_map'})) {
3431            
3432             # verify ink map
3433 0 0       0 ($n == @{$map}) or croak("ink map is wrong sized");
  0         0  
3434            
3435             # make profile mapping slices from ink map
3436 0 0       0 @sp = grep {$map->[$_] =~ m/^(\d+)$/ && push(@si, $1)} (0 .. $#{$map});
  0         0  
  0         0  
3437            
3438             # make inverted map slice
3439 0         0 @m = @{_invert_ink_map($map)};
  0         0  
3440            
3441             } else {
3442            
3443             # make default map slice
3444 0         0 @m = (0 .. $nx);
3445            
3446             }
3447              
3448             # parse token string
3449 0         0 $sel = ICC::Shared::parse_tokens($select);
3450            
3451             # if 'nr' token(s)
3452 0 0       0 if (@nr = grep {$sel->[$_] eq 'nr'} (0 .. $#{$sel})) {
  0         0  
  0         0  
3453            
3454             # move first 'nr' token and parameter to beginning of array
3455 0         0 unshift(@{$sel}, splice(@{$sel}, $nr[0], 2));
  0         0  
  0         0  
3456            
3457             # warn if more than one 'nr' token
3458 0 0       0 print "multiple 'nr' selection tokens\n" if (@nr > 1);
3459            
3460             }
3461              
3462             # initialize sample slice
3463 0         0 @samples = ();
3464              
3465             # for each selection token
3466 0         0 for (my $i = 0; $i <= $#{$sel}; $i++) {
  0         0  
3467            
3468             # get the lowercase value of current token
3469 0         0 $token = lc($sel->[$i]);
3470            
3471             # set parameter array, filtering out non-numeric elements
3472 0 0       0 @p = (ref($sel->[$i + 1]) eq 'ARRAY') ? grep {Scalar::Util::looks_like_number($_)} @{$sel->[++$i]} : ();
  0         0  
  0         0  
3473            
3474             # if 'all'
3475 0 0       0 if ($token eq 'all') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
3476            
3477             # select all samples
3478 0         0 @samples = (1 .. $sz);
3479            
3480             # if 'sort'
3481             } elsif ($token eq 'sort') {
3482            
3483             # if numeric parameters
3484 0 0       0 if (@p) {
3485            
3486             # set the sort vector
3487 0         0 $sort = [@p];
3488            
3489             } else {
3490            
3491             # print warning
3492 0         0 print "selection token 'sort' requires numeric parameter(s)\n";
3493            
3494             }
3495            
3496             # if 'rows'
3497             } elsif ($token eq 'rows') {
3498            
3499             # if numeric parameters
3500 0 0       0 if (@p) {
3501            
3502             # if row length defined
3503 0 0       0 if (defined($row_length)) {
3504            
3505             # for each row
3506 0         0 for my $i (0 .. $#p) {
3507            
3508             # add row samples
3509 0         0 push(@samples, grep {($_ - 1) % $row_length == ($p[$i] - 1)} (1 .. $sz));
  0         0  
3510            
3511             }
3512            
3513             } else {
3514            
3515             # print warning
3516 0         0 print "selection token 'rows' requires row length (use 'nr' token)\n";
3517            
3518             }
3519            
3520             } else {
3521            
3522             # print warning
3523 0         0 print "selection token 'rows' requires numeric parameter(s)\n";
3524            
3525             }
3526            
3527             # if 'cols'
3528             } elsif ($token eq 'cols') {
3529            
3530             # if numeric parameters
3531 0 0       0 if (@p) {
3532            
3533             # if row length defined
3534 0 0       0 if (defined($row_length)) {
3535            
3536             # for each column
3537 0         0 for my $i (0 .. $#p) {
3538            
3539             # add column samples
3540 0         0 push(@samples, map {$row_length * ($p[$i] - 1) + $_} (1 .. $row_length));
  0         0  
3541            
3542             }
3543            
3544             } else {
3545            
3546             # print warning
3547 0         0 print "selection token 'cols' requires row length (use 'nr' token)\n";
3548            
3549             }
3550            
3551             } else {
3552            
3553             # print warning
3554 0         0 print "selection token 'cols' requires numeric parameter(s)\n";
3555            
3556             }
3557            
3558             # if 'rect'
3559             } elsif ($token eq 'rect') {
3560            
3561             # if 4 numeric parameters
3562 0 0       0 if (@p == 4) {
3563            
3564             # if row length defined
3565 0 0       0 if (defined($row_length)) {
3566            
3567             # get samples
3568 0         0 $mat = select_matrix($self, @p, $row_length);
3569            
3570             # add column samples (flatten matrix)
3571 0         0 push(@samples, map {@{$_}} @{$mat});
  0         0  
  0         0  
  0         0  
3572            
3573             } else {
3574            
3575             # print warning
3576 0         0 print "selection token 'rect' requires row length (use 'nr' token)\n";
3577            
3578             }
3579            
3580             } else {
3581            
3582             # print warning
3583 0         0 print "selection token 'rect' requires 4 numeric parameters\n";
3584            
3585             }
3586            
3587             # if 'nr'
3588             } elsif ($token eq 'nr') {
3589            
3590             # if numeric parameters
3591 0 0       0 if (@p) {
3592            
3593             # set the number of rows
3594 0         0 $row_length = $p[0];
3595            
3596             } else {
3597            
3598             # print warning
3599 0         0 print "selection token 'nr' requires a numeric parameter\n";
3600            
3601             }
3602            
3603             # if 'iso'
3604             } elsif ($token eq 'iso') {
3605            
3606             # if CMY channels defined
3607 0 0       0 if (3 == grep {defined($m[$_])} (0 .. 2)) {
  0         0  
3608            
3609             # get cmy limit value
3610 0   0     0 $lim = ($p[0] // 100)/100;
3611            
3612             # add isometric samples (C == M == Y, all other channels == 0) and CMY ≤ limit
3613 0 0 0 0   0 push(@samples, @{ramp($self, sub {@cmy = @_[@m[0 .. 2]]; $cmy[0] == $cmy[1] && $cmy[1] == $cmy[2] && $cmy[0] <= $lim && (0 == grep {$_} @_[@m[3 .. $nx]])}, $hash)});
  0   0     0  
  0         0  
  0         0  
  0         0  
3614            
3615             } else {
3616            
3617             # print warning
3618 0         0 print "selection token 'iso', missing ink channel(s)\n";
3619            
3620             }
3621            
3622             # if 'g7'
3623             } elsif ($token eq 'g7') {
3624            
3625             # if CMY channels defined
3626 0 0       0 if (3 == grep {defined($m[$_])} (0 .. 2)) {
  0         0  
3627            
3628             # get cyan limit value
3629 0   0     0 $lim = ($p[0] // 100)/100;
3630            
3631             # add gray cmy samples, using TR015 formula
3632 0 0 0 0   0 push(@samples, @{ramp($self, sub {$c = $_[$m[0]]; $my = 0.747 * $c - 0.041 * $c**2 + 0.294 * $c**3; @cmy = @_[@m[0 .. 2]]; $cmy[0] <= $lim && abs($cmy[1] - $my) <= 0.002 && $cmy[2] == $cmy[1] && (0 == grep {$_} @_[@m[3 .. $nx]])}, $hash)});
  0   0     0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
3633            
3634             } else {
3635            
3636             # print warning
3637 0         0 print "selection token 'g7', missing ink channel(s)\n";
3638            
3639             }
3640            
3641             # if 'it8'
3642             } elsif ($token eq 'it8') {
3643            
3644             # if CMY channels defined
3645 0 0       0 if (3 == grep {defined($m[$_])} (0 .. 2)) {
  0         0  
3646            
3647             # make gray balance hash, as used in IT8.7/3, IT8.7/4, IT8.7/5 charts
3648 0         0 %it8 = (0, 0, 5, 3, 10, 6, 20, 12, 40, 27, 60, 45, 80, 65, 100, 85);
3649            
3650             # get cyan limit value
3651 0   0     0 $lim = ($p[0] // 100)/100;
3652            
3653             # add gray cmy samples
3654 0 0 0 0   0 push(@samples, @{ramp($self, sub {@cmy = @_[@m[0 .. 2]]; $c = int(100 * $cmy[0] + 0.5); $cmy[0] <= $lim && exists($it8{$c}) && abs($it8{$c}/100 - $cmy[1]) < 0.002 && $cmy[1] == $cmy[2] && (0 == grep {$_} @_[@m[3 .. $nx]])}, $hash)});
  0   0     0  
  0   0     0  
  0         0  
  0         0  
  0         0  
3655            
3656             } else {
3657            
3658             # print warning
3659 0         0 print "selection token 'it8', missing ink channel(s)\n";
3660            
3661             }
3662            
3663             # if 'cmy'
3664             } elsif ($token eq 'cmy') {
3665            
3666             # if CMY channels defined
3667 0 0       0 if (3 == grep {defined($m[$_])} (0 .. 2)) {
  0         0  
3668            
3669             # get cmy limit value
3670 0   0     0 $lim = ($p[0] // 0)/100;
3671            
3672             # add cmy ramp samples with value ≥ limit
3673 0 0 0 0   0 push(@samples, @{ramp($self, sub {@cmy = @_[@m[0 .. 2]]; (1 >= grep {$_} @cmy) && (! $lim || (1 == grep {$_ >= $lim} @cmy)) && (0 == grep {$_} @_[@m[3 .. $nx]])}, $hash)});
  0   0     0  
  0         0  
  0         0  
  0         0  
  0         0  
3674            
3675             } else {
3676            
3677             # print warning
3678 0         0 print "selection token 'cmy', missing ink channel(s)\n";
3679            
3680             }
3681            
3682             # if 'c+m+y'
3683             } elsif ($token eq 'c+m+y') {
3684            
3685             # if CMY channels defined
3686 0 0       0 if (3 == grep {defined($m[$_])} (0 .. 2)) {
  0         0  
3687            
3688             # if numeric parameters
3689 0 0       0 if (@p) {
3690            
3691             # add samples with c+m+y ≤ limit
3692 0     0   0 push(@samples, @{ramp($self, sub {100 * ($_[$m[0]] + $_[$m[1]] + $_[$m[2]]) <= $p[0]}, $hash)});
  0         0  
  0         0  
3693            
3694             } else {
3695            
3696             # print warning
3697 0         0 print "selection token 'c+m+y' requires a numeric parameter\n";
3698            
3699             }
3700            
3701             } else {
3702            
3703             # print warning
3704 0         0 print "selection token 'c+m+y', missing ink channel(s)\n";
3705            
3706             }
3707            
3708             # if 'tac'
3709             } elsif ($token eq 'tac') {
3710            
3711             # if numeric parameters
3712 0 0       0 if (@p) {
3713            
3714             # add samples with TAC ≤ limit
3715 0     0   0 push(@samples, @{ramp($self, sub {100 * List::Util::sum(@_) <= $p[0]}, $hash)});
  0         0  
  0         0  
3716            
3717             } else {
3718            
3719             # print warning
3720 0         0 print "selection token 'tac' requires a numeric parameter\n";
3721            
3722             }
3723            
3724             # if 'gray'
3725             } elsif ($token eq 'gray') {
3726            
3727             # if CMY channels defined
3728 0 0       0 if (3 == grep {defined($m[$_])} (0 .. 2)) {
  0         0  
3729            
3730             # if the A2B tag is supplied in hash
3731 0 0       0 if (defined($A2B = $hash->{'A2B'})) {
3732            
3733             # make PCS object (profile to L*a*b*, relative colorimetric)
3734 0         0 $pcs = ICC::Support::PCS->new([$A2B->pcs], [3]);
3735            
3736             # get number of input channels
3737 0 0       0 $ci = $A2B->cin if (defined($map));
3738            
3739             # if one parameter (C* limit)
3740 0 0       0 if (@p == 1) {
    0          
3741            
3742             # add gray samples (cmy samples only)
3743 0 0   0   0 push(@samples, @{ramp($self, sub {@Lab = $pcs->transform($A2B->transform(_map_array($ci, \@sp, \@si, @_))); sqrt($Lab[1]**2 + $Lab[2]**2) <= $p[0] && (0 == grep {$_} @_[@m[3 .. $nx]])}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3744            
3745             # if two parameters (C* and L* limits)
3746             } elsif (@p) {
3747            
3748             # add gray samples (cmy samples only)
3749 0 0 0 0   0 push(@samples, @{ramp($self, sub {@Lab = $pcs->transform($A2B->transform(_map_array($ci, \@sp, \@si, @_))); sqrt($Lab[1]**2 + $Lab[2]**2) <= $p[0] && $Lab[0] >= $p[1] && (0 == grep {$_} @_[@m[3 .. $nx]])}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3750            
3751             } else {
3752            
3753             # print warning
3754 0         0 print "selection token 'gray' requires numeric parameter(s)\n";
3755            
3756             }
3757            
3758             } else {
3759            
3760             # print warning
3761 0         0 print "selection token 'gray' requires A2B profile tag in hash\n";
3762            
3763             }
3764            
3765             } else {
3766            
3767             # print warning
3768 0         0 print "selection token 'gray', missing ink channel(s)\n";
3769            
3770             }
3771            
3772             # if 'rt'
3773             } elsif ($token eq 'rt') {
3774            
3775             # if CMYK channels defined
3776 0 0       0 if (4 == grep {defined($m[$_])} (0 .. 3)) {
  0         0  
3777            
3778             # if the A2B and B2A tags are supplied in hash
3779 0 0 0     0 if (defined($A2B = $hash->{'A2B'}) && defined($B2A = $hash->{'B2A'})) {
3780            
3781             # get number of input channels
3782 0 0       0 $ci = $A2B->cin if (defined($map));
3783            
3784             # if numeric parameters
3785 0 0       0 if (@p) {
3786            
3787             # add samples with round-trip black change less than the limit
3788 0     0   0 push(@samples, @{ramp($self, sub {@dev = $B2A->transform($A2B->transform(_map_array($ci, \@sp, \@si, @_))); 100 * abs($_[$m[3]] - $dev[3]) <= $p[0]}, $hash)});
  0         0  
  0         0  
  0         0  
3789            
3790             } else {
3791            
3792             # print warning
3793 0         0 print "selection token 'rt' requires a numeric parameter\n";
3794            
3795             }
3796            
3797             } else {
3798            
3799             # print warning
3800 0         0 print "selection token 'rt' requires A2B and B2A profile tags in hash\n";
3801            
3802             }
3803            
3804             } else {
3805            
3806             # print warning
3807 0         0 print "selection token 'rt', missing ink channel(s)\n";
3808            
3809             }
3810            
3811             # if 'c'
3812             } elsif ($token eq 'c') {
3813            
3814             # if cyan channel defined
3815 0 0       0 if (defined($m[0])) {
3816            
3817             # add cyan-only samples
3818 0     0   0 push(@samples, @{ramp($self, sub {$_[$m[0]] = 0; 0 == grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3819            
3820             } else {
3821            
3822             # print warning
3823 0         0 print "selection token 'c', missing ink channel(s)\n";
3824            
3825             }
3826            
3827             # if 'm'
3828             } elsif ($token eq 'm') {
3829            
3830             # if magenta channel defined
3831 0 0       0 if (defined($m[1])) {
3832            
3833             # add magenta-only samples
3834 0     0   0 push(@samples, @{ramp($self, sub {$_[$m[1]] = 0; 0 == grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3835            
3836             } else {
3837            
3838             # print warning
3839 0         0 print "selection token 'm', missing ink channel(s)\n";
3840            
3841             }
3842            
3843             # if 'y'
3844             } elsif ($token eq 'y') {
3845            
3846             # if yellow channel defined
3847 0 0       0 if (defined($m[2])) {
3848            
3849             # add yellow-only samples
3850 0     0   0 push(@samples, @{ramp($self, sub {$_[$m[2]] = 0; 0 == grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3851            
3852             } else {
3853            
3854             # print warning
3855 0         0 print "selection token 'y', missing ink channel(s)\n";
3856            
3857             }
3858            
3859             # if 'k'
3860             } elsif ($token eq 'k') {
3861            
3862             # if black channel defined
3863 0 0       0 if (defined($m[3])) {
3864            
3865             # add black-only samples
3866 0     0   0 push(@samples, @{ramp($self, sub {$_[$m[3]] = 0; 0 == grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3867            
3868             } else {
3869            
3870             # print warning
3871 0         0 print "selection token 'k', missing ink channel(s)\n";
3872            
3873             }
3874            
3875             # if 'cmyk'
3876             } elsif ($token eq 'cmyk') {
3877            
3878             # if CMYK channels defined
3879 0 0       0 if (4 == grep {defined($m[$_])} (0 .. 3)) {
  0         0  
3880            
3881             # add cmyk ramps (number of non-zero CMYK channels ≤ 1, all extra channels == 0)
3882 0 0   0   0 push(@samples, @{ramp($self, sub {(1 >= grep {$_} @_[@m[0 .. 3]]) && (0 == grep {$_} @_[@m[4 .. $nx]])}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3883            
3884             } else {
3885            
3886             # print warning
3887 0         0 print "selection token 'cmyk', missing ink channel(s)\n";
3888            
3889             }
3890            
3891             # if 'ramps'
3892             } elsif ($token eq 'ramps') {
3893            
3894             # if numeric parameters
3895 0 0       0 if (@p) {
3896            
3897             # for each parameter
3898 0         0 for my $i (0 .. $#p) {
3899            
3900             # if parameter a valid ink channel
3901 0 0 0     0 if ($p[$i] == int($p[$i]) && $p[$i] > 0 && $p[$i] <= $n) {
      0        
3902            
3903             # add ramp($i)
3904 0     0   0 push(@samples, @{ramp($self, sub {$_[$p[$i] - 1] = 0; 0 == grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3905            
3906             } else {
3907            
3908             # print warning
3909 0         0 print "selection token 'ramps', channel $p[$i] is invalid\n";
3910            
3911             }
3912            
3913             }
3914            
3915             } else {
3916            
3917             # add all device ramps (number of non-zero channels ≤ 1)
3918 0     0   0 push(@samples, @{ramp($self, sub {1 >= grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
3919            
3920             }
3921            
3922             # if 'inks'
3923             } elsif ($token eq 'inks') {
3924            
3925             # if numeric parameters
3926 0 0       0 if (@p) {
3927            
3928             # if valid ink channels
3929 0 0 0     0 if (@p == grep {$_ == int($_) && $_ > 0 && $_ <= $n} @p) {
  0 0       0  
3930            
3931             # convert ink channels to array indices
3932 0         0 @p = map {$_ - 1} @p;
  0         0  
3933            
3934             # add samples containing these inks
3935 0     0   0 push(@samples, @{ramp($self, sub {@_[@p] = ((0) x @p); 0 == grep {$_} @_}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3936            
3937             } else {
3938            
3939             # print warning
3940 0         0 print "selection token 'inks', invalid ink channel(s)\n";
3941            
3942             }
3943            
3944             } else {
3945            
3946             # print warning
3947 0         0 print "selection token 'inks' requires numeric parameter(s)\n";
3948            
3949             }
3950            
3951             # if 'binary'
3952             } elsif ($token eq 'binary') {
3953            
3954             # add binary samples (device value ≈ 0 or 1)
3955 0 0   0   0 push(@samples, @{ramp($self, sub {@_ == grep {abs($_) < 1E-9 || abs($_ - 1) < 1E-9} @_}, $hash)});
  0         0  
  0         0  
  0         0  
3956            
3957             # if 'solid'
3958             } elsif ($token eq 'solid') {
3959            
3960             # add solid ink samples (one device value == 1, all others == 0)
3961 0 0   0   0 push(@samples, @{ramp($self, sub {(1 == grep {$_ == 1} @_) && (@_ - 1 == grep {$_ == 0} @_)}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3962            
3963             # if 'sub'
3964             } elsif ($token eq 'sub') {
3965            
3966             # add substrate samples (all device values == 0)
3967 0     0   0 push(@samples, @{ramp($self, sub {@_ == grep {$_ == 0} @_}, $hash)});
  0         0  
  0         0  
  0         0  
3968            
3969             # if 'acro'
3970             } elsif ($token eq 'acro') {
3971            
3972             # if CMYK channels defined
3973 0 0       0 if (4 == grep {defined($m[$_])} (0 .. 3)) {
  0         0  
3974            
3975             # add achromatic samples (one or more cmy channels == 0, all extra channels == 0)
3976 0 0   0   0 push(@samples, @{ramp($self, sub {(1 <= grep {$_ == 0} @_[@m[0 .. 2]]) && (0 == grep {$_} @_[@m[4 .. $nx]])}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3977            
3978             } else {
3979            
3980             # print warning
3981 0         0 print "selection token 'acro', missing ink channel(s)\n";
3982            
3983             }
3984            
3985             # if 'gamut'
3986             } elsif ($token eq 'gamut') {
3987            
3988             # if CMYK channels defined
3989 0 0       0 if (4 == grep {defined($m[$_])} (0 .. 3)) {
  0         0  
3990            
3991             # add gamut samples (k == 0)
3992 0 0   0   0 push(@samples, @{ramp($self, sub {@cmy = @_[@m[0 .. 2]]; $_[$m[3]] == 0 && 0 < grep {$_ == 0} @cmy}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3993            
3994             # add gamut samples (k == 1)
3995 0 0   0   0 push(@samples, @{ramp($self, sub {@cmy = @_[@m[0 .. 2]]; $_[$m[3]] == 1 && 0 < grep {$_ == 1} @cmy}, $hash)});
  0         0  
  0         0  
  0         0  
  0         0  
3996            
3997             # add gamut samples (0 < k < 1)
3998 0 0 0 0   0 push(@samples, @{ramp($self, sub {@cmy = @_[@m[0 .. 2]]; $_[$m[3]] > 0 && $_[$m[3]] < 1 && (0 < grep {$_ == 0} @cmy) && (0 < grep {$_ == 1} @cmy)}, $hash)});
  0   0     0  
  0         0  
  0         0  
  0         0  
  0         0  
3999            
4000             } else {
4001            
4002             # print warning
4003 0         0 print "selection token 'gamut', missing ink channel(s)\n";
4004            
4005             }
4006            
4007             # if 'plus'
4008             } elsif ($token eq 'plus') {
4009            
4010             # if numeric parameters
4011 0 0       0 if (@p) {
4012            
4013             # select valid sample numbers
4014 0 0 0     0 @sn = grep {$_ == int($_) && $_ >= 1 && $_ <= $sz} @p;
  0         0  
4015            
4016             # print warning if invalid sample numbers
4017 0 0       0 print "selection token 'plus' has invalid sample numbers\n" if (@sn != @p);
4018            
4019             # add samples
4020 0         0 push(@samples, @sn);
4021            
4022             } else {
4023            
4024             # print warning
4025 0         0 print "selection token 'plus' requires numeric parameter(s)\n";
4026            
4027             }
4028            
4029             # if 'minus'
4030             } elsif ($token eq 'minus') {
4031            
4032             # if numeric parameters
4033 0 0       0 if (@p) {
4034            
4035             # select valid sample numbers, and add to hash
4036 0 0 0     0 @sn = grep {$_ == int($_) && $_ >= 1 && $_ <= $sz && ($minus{$_} = 1)} @p;
  0   0     0  
4037            
4038             # print warning if invalid sample numbers
4039 0 0       0 print "selection token 'minus' has invalid sample numbers\n" if (@sn != @p);
4040            
4041             } else {
4042            
4043             # print warning
4044 0         0 print "selection token 'minus' requires numeric parameter(s)\n";
4045            
4046             }
4047            
4048             # if 'max'
4049             } elsif ($token eq 'max') {
4050            
4051             # if numeric parameters
4052 0 0       0 if (@p) {
4053            
4054             # verify ink limits
4055 0 0 0     0 @max = map {$_/100} grep {Scalar::Util::looks_like_number($_) && $_ >= 0 && $_ <= 100} @p;
  0         0  
  0         0  
4056            
4057             # if ink limit(s) invalid
4058 0 0       0 if (@max != @p) {
    0          
4059            
4060             # print warning if invalid ink limits
4061 0         0 print "selection token 'max' has invalid ink limit(s)\n";
4062            
4063             # if wrong number of values
4064             } elsif (@max != $n) {
4065            
4066             # print warning if wrong number of values
4067 0         0 print "selection token 'max' has wrong number of values\n";
4068            
4069             }
4070            
4071             } else {
4072            
4073             # print warning
4074 0         0 print "selection token 'max' requires numeric parameter(s)\n";
4075            
4076             }
4077            
4078             # if 'nok' (no samples containing black)
4079             } elsif ($token eq 'nok') {
4080            
4081             # set flag
4082 0         0 $fnk = 1;
4083            
4084             # if 'nosub' (no substrate samples)
4085             } elsif ($token eq 'nosub') {
4086            
4087             # set flag
4088 0         0 $fns = 1;
4089            
4090             # if 'nobin' (no binary samples)
4091             } elsif ($token eq 'nobin') {
4092            
4093             # set flag
4094 0         0 $fnb = 1;
4095            
4096             } else {
4097            
4098             # print warning
4099 0         0 printf "selection token '%s' not recognized\n", $token;
4100            
4101             }
4102            
4103             }
4104              
4105             # initialize hash
4106 0         0 %unique = ();
4107              
4108             # remove duplicates and sort
4109 0 0 0     0 @samples = sort {$a <=> $b} grep {$_ >= 1 && $_ <= $sz && ++$unique{$_} == 1} @samples;
  0         0  
  0         0  
4110              
4111             # remove 'minus' samples
4112 0         0 @samples = grep {! exists($minus{$_})} @samples;
  0         0  
4113              
4114             # remove samples containing black, if flag set
4115 0 0 0 0   0 @samples = @{ramp($self, sub {abs($_[$m[3]]) < 1E-9}, \@samples, $hash)} if (@samples && $fnk && defined($m[3]));
  0   0     0  
  0         0  
4116              
4117             # remove substrate samples, if flag set
4118 0 0 0 0   0 @samples = @{ramp($self, sub {grep {abs($_ - $sub) >= 1E-9} @_}, \@samples, $hash)} if (@samples && $fns);
  0         0  
  0         0  
  0         0  
4119              
4120             # remove binary samples, if flag set
4121 0 0 0 0   0 @samples = @{ramp($self, sub {grep {abs($_) >= 1E-9 && abs($_ - 1) >= 1E-9} @_}, \@samples, $hash)} if (@samples && $fnb);
  0 0       0  
  0         0  
  0         0  
4122              
4123             # remove max samples, if max array defined
4124 0 0 0 0   0 @samples = @{ramp($self, sub {! grep {$_[$_] > $max[$_]} (0 .. $nx)}, \@samples, $hash)} if (@samples && @max);
  0         0  
  0         0  
  0         0  
4125              
4126             # sort samples by device values, if sort vector provided (in hash or as a token)
4127 0 0 0     0 @samples = @{ICC::Support::Chart::sort($self, \@samples, $sort)} if (@samples && $sort);
  0         0  
4128              
4129             # return
4130 0         0 return(\@samples);
4131              
4132             }
4133              
4134             # get sample selection based on 2-D location
4135             # indices are one-based, with origin at the upper left
4136             # row matrix slice may contain indices of undefined rows
4137             # entire chart is used when the row and column indices are omitted
4138             # chart row length is provided as a parameter, or obtained from the data
4139             # parameters: ([upper_row_index, lower_row_index, left_column_index, right_column_index], [chart_row_length])
4140             # returns: (row_matrix_slice)
4141             sub select_matrix {
4142              
4143             # get object reference
4144 0     0 1 0 my $self = shift();
4145              
4146             # local variables
4147 0         0 my ($sn, $cmax, @rows, @cols, $matrix);
4148 0         0 my ($row_length, $upper, $lower, $left, $right);
4149              
4150             # get number of samples
4151 0         0 $sn = $#{$self->[1]};
  0         0  
4152              
4153             # if 0 or 4 parameters
4154 0 0 0     0 if (@_ == 0 || @_ == 4) {
    0 0        
4155            
4156             # get row length from data
4157 0         0 $row_length = _getRowLength($self);
4158            
4159             # if 1 or 5 parameters
4160             } elsif (@_ == 1 || @_ == 5) {
4161            
4162             # get row length
4163 0         0 $row_length = pop();
4164            
4165             # verify row length
4166 0 0 0     0 (Scalar::Util::looks_like_number($row_length) && $row_length == int($row_length) && $row_length > 0) or croak('invalid chart row length');
      0        
4167            
4168             } else {
4169            
4170             # error
4171 0         0 croak('wrong number of parameters');
4172            
4173             }
4174            
4175             # if row and column parameters provided
4176 0 0       0 if (@_) {
4177            
4178             # get row and column parameters
4179 0         0 ($upper, $lower, $left, $right) = @_;
4180            
4181             # verify upper and lower indices
4182 0 0 0     0 (! ref($upper) && $upper == int($upper) && $upper > 0 && $upper <= $row_length) || warn('invalid upper row index');
      0        
      0        
4183 0 0 0     0 (! ref($lower) && $lower == int($lower) && $lower > 0 && $lower <= $row_length) || warn('invalid lower row index');
      0        
      0        
4184            
4185             # get maximum column index
4186 0 0       0 $cmax = $sn % $row_length ? int($sn/$row_length) + 1 : int($sn/$row_length);
4187            
4188             # verify left and right indices
4189 0 0 0     0 (! ref($left) && $left == int($left) && $left > 0 && $left <= $cmax) || warn('invalid left column index');
      0        
      0        
4190 0 0 0     0 (! ref($right) && $right == int($right) && $right > 0 && $right <= $cmax) || warn('invalid right column index');
      0        
      0        
4191            
4192             # if upper index < lower index
4193 0 0       0 if ($upper < $lower) {
4194            
4195             # make rows array
4196 0         0 @rows = ($upper .. $lower);
4197            
4198             } else {
4199            
4200             # make rows array
4201 0         0 @rows = reverse($lower .. $upper);
4202            
4203             }
4204            
4205             # if left index < right index
4206 0 0       0 if ($left < $right) {
4207            
4208             # make columns array
4209 0         0 @cols = ($left .. $right);
4210            
4211             } else {
4212            
4213             # make columns array
4214 0         0 @cols = reverse($right .. $left);
4215            
4216             }
4217            
4218             # use entire chart
4219             } else {
4220            
4221             # make rows array
4222 0         0 @rows = (1 .. $row_length);
4223            
4224             # if chart is rectangular
4225 0 0       0 if ($sn % $row_length == 0) {
4226            
4227             # make columns array
4228 0         0 @cols = (1 .. $sn/$row_length);
4229            
4230             } else {
4231            
4232             # warning
4233 0         0 warn('chart is not rectangular');
4234            
4235             # make columns array
4236 0         0 @cols = (1 .. int($sn/$row_length) + 1);
4237            
4238             }
4239            
4240             }
4241              
4242             # for each row
4243 0         0 for my $i (0 .. $#rows) {
4244            
4245             # for each column
4246 0         0 for my $j (0 .. $#cols) {
4247            
4248             # set matrix element
4249 0         0 $matrix->[$j][$i] = ($cols[$j] - 1) * $row_length + $rows[$i];
4250            
4251             }
4252            
4253             }
4254              
4255             # return row matrix slice
4256 0         0 return(bless($matrix, 'Math::Matrix'));
4257              
4258             }
4259              
4260             # get sample selection using template
4261             # samples are matched by their device values
4262             # supported hash keys: 'dups', 'rows', 'context', 'template_context', 'sid_context', 'method', 'copy'
4263             # duplicate handling: 0 - sample average (default), 1 - FIFO, 2 - LIFO, 3 - first sample, 4 - last sample
4264             # parameters: (template_chart_object, [hash])
4265             # returns: (row_matrix_slice, [sid_matrix_slice])
4266             sub select_template {
4267              
4268             # get parameters
4269 0     0 1 0 my ($self, $template, $hash) = @_;
4270              
4271             # local variables
4272 0         0 my ($row_length, $dups, $copys, $copyt);
4273 0         0 my ($devcs, $devct, $devs, $devt);
4274 0         0 my ($sx, $c1, $c2, $c3, $n, @src, $cmp);
4275 0         0 my ($target, $low, $high, $interval, @m, $nomatch);
4276 0         0 my ($rows, $avg, $matrix, $devp, $sidt, $sid);
4277              
4278             # verify template is a chart object
4279 0 0       0 (UNIVERSAL::isa($template, 'ICC::Support::Chart')) or croak('template not an ICC::Support::Chart object');
4280              
4281             # get template row length
4282 0         0 $row_length = _getRowLength($template, $hash);
4283              
4284             # set duplicate handling
4285 0 0       0 $dups = defined($hash->{'dups'}) ? $hash->{'dups'} : 0;
4286              
4287             # if copy slice is defined
4288 0 0       0 if (defined($hash->{'copy'})) {
4289            
4290             # flatten the copy slice
4291 0         0 $copys = ICC::Shared::flatten($hash->{'copy'});
4292            
4293             # add copied fields to template
4294 0         0 $copyt = add_fmt($template, @{$self->[1][0]}[@{$copys}]);
  0         0  
  0         0  
4295            
4296             }
4297              
4298             # verify parameters
4299 0 0 0     0 (! ref($row_length) && $row_length == int($row_length) && $row_length > 0) or croak('invalid chart_row_length parameter');
      0        
4300 0 0 0     0 ($dups == int($dups) && $dups >= 0 && $dups <= 4) or croak('invalid duplicate_handling parameter');
      0        
4301              
4302             # get object device column slice
4303 0         0 $devcs = device($self, $hash);
4304              
4305             # get template device column slice
4306 0         0 $devct = device($template, {'context' => $hash->{'template_context'}});
4307              
4308             # verify object and template column slices
4309 0 0       0 (defined($devcs)) || croak ('object device data missing');
4310 0 0       0 (defined($devct)) || croak ('template device data missing');
4311 0 0       0 ($#{$devcs} == $#{$devct}) or croak('object and template have different number of channels');
  0         0  
  0         0  
4312              
4313             # get object device values
4314 0         0 $devs = device($self, [], $hash);
4315              
4316             # get template device values
4317 0         0 $devt = device($template, [], {'context' => $hash->{'template_context'}});
4318              
4319             # get index of next object sample
4320 0         0 $sx = $#{$self->[1]} + 1;
  0         0  
4321              
4322             # get averaging groups if duplicates are averaged
4323 0 0       0 ($c1, $c2, $c3) = _avg_groups($self, $hash) if ($dups == 0);
4324              
4325             # get number of channels
4326 0         0 $n = @{$devcs};
  0         0  
4327              
4328             # initialize sample list
4329 0         0 @src = ();
4330              
4331             # for each sample
4332 0         0 for my $i (0 .. $#{$devs}) {
  0         0  
4333            
4334             # if all device values defined
4335 0 0       0 if ($n == grep {defined()} @{$devs->[$i]}) {
  0         0  
  0         0  
4336            
4337             # add sample to source list
4338 0         0 push(@src, [@{$devs->[$i]}, $i + 1]);
  0         0  
4339            
4340             }
4341            
4342             }
4343              
4344             # sort object device values
4345             @src = sort {
4346            
4347             # for each channel
4348 0         0 for my $i (0 .. $#{$a}) {
  0         0  
  0         0  
4349            
4350             # quit loop if device values are unequal
4351 0 0       0 last if ($cmp = $a->[$i] <=> $b->[$i])
4352            
4353             # use last comparison for sort test
4354             } $cmp
4355            
4356             } @src;
4357              
4358             # for each template sample
4359 0         0 for my $i (0 .. $#{$devt}) {
  0         0  
4360            
4361             # initialize search indices
4362 0         0 $low = 0;
4363 0         0 $high = $#src;
4364            
4365             # initialize no match flag
4366 0         0 $nomatch = 0;
4367            
4368             # for each channel
4369 0         0 for my $j (0 .. $#{$devt->[0]}) {
  0         0  
4370            
4371             # get the target value
4372 0         0 $target = $devt->[$i][$j];
4373            
4374             # locate interval containing or bounding the target value
4375 0         0 $interval = _bin_search(\@src, $target, $j, $low, $high);
4376            
4377             # find indices matching the target value
4378 0         0 @m = grep {$src[$_][$j] == $target} @{$interval};
  0         0  
  0         0  
4379            
4380             # if no object values exactly match the target value
4381 0 0       0 if (@m == 0) {
4382            
4383             # sort interval indices by distance to target value
4384 0         0 @m = sort {$a->[1] <=> $b->[1]} map {[$_, abs($src[$_][$j] - $target)]} @{$interval};
  0         0  
  0         0  
  0         0  
4385            
4386             # if distance to closest object value > 0.00201
4387 0 0       0 if (abs($target - $src[$m[0][0]][$j]) > 0.00201) {
4388            
4389             # print warning
4390 0         0 print "no match to template sample $i\n";
4391 0         0 print "device values: @{$devt->[$i]}\n";
  0         0  
4392            
4393             # set no match flag
4394 0         0 $nomatch = 1;
4395            
4396             # quit channel loop
4397 0         0 last;
4398            
4399             }
4400            
4401             # set target to closest object value
4402 0         0 $target = $src[$m[0][0]][$j];
4403            
4404             # locate interval containing the target value
4405 0         0 $interval = _bin_search(\@src, $target, $j, $low, $high);
4406            
4407             # find indices matching the target value
4408 0         0 @m = grep {$src[$_][$j] == $target} @{$interval};
  0         0  
  0         0  
4409            
4410             }
4411            
4412             # update interval
4413 0         0 $low = $m[0];
4414 0         0 $high = $m[-1];
4415            
4416             }
4417            
4418             # if no match found
4419 0 0       0 if ($nomatch) {
4420            
4421             # locate nearest object sample(s) using linear search
4422 0         0 ($low, $high) = _lin_search(\@src, $devt->[$i]);
4423            
4424             # print message
4425 0         0 print "closest match is object sample $src[$low][-1]\n";
4426 0         0 print "device values @{$src[$low]}[0 .. $#{$devt->[0]}]\n";
  0         0  
  0         0  
4427            
4428             }
4429            
4430             # single sample
4431 0 0       0 if ($low == $high) {
4432            
4433             # set matrix element to first row matching object sample
4434 0         0 $matrix->[$i/$row_length][$i % $row_length] = $src[$low][-1];
4435            
4436             # duplicate samples
4437             } else {
4438            
4439             # duplicates are averaged
4440 0 0       0 if ($dups == 0) {
    0          
    0          
    0          
    0          
4441            
4442             # for each appended avg sample
4443 0         0 for my $j ($sx .. $#{$self->[1]}) {
  0         0  
4444            
4445             # set avg
4446 0         0 $avg = $j;
4447            
4448             # get device values
4449 0         0 $devp = $self->device([$j]);
4450            
4451             # for each channel
4452 0         0 for my $k (0 .. $#{$devp->[0]}) {
  0         0  
4453            
4454             # clear avg if device values differ
4455 0 0       0 $avg = 0 if ($devp->[0][$k] != $devt->[$i][$k]);
4456            
4457             }
4458            
4459             # quit loop if device values match
4460 0 0       0 last if ($avg);
4461            
4462             }
4463            
4464             # if existing avg sample found
4465 0 0       0 if ($avg) {
4466            
4467             # set matrix element to existing avg sample
4468 0         0 $matrix->[$i/$row_length][$i % $row_length] = $avg;
4469            
4470             } else {
4471            
4472             # make row slice of duplicate samples
4473 0         0 $rows = [map {$src[$_][-1]} ($low .. $high)];
  0         0  
4474            
4475             # set matrix element to new avg sample
4476 0         0 $matrix->[$i/$row_length][$i % $row_length] = _add_avg($self, $rows, $c1, $c2, $c3);
4477            
4478             }
4479            
4480             # use FIFO sample
4481             } elsif ($dups == 1) {
4482            
4483             # from low to high
4484 0         0 for my $j ($low .. $high) {
4485            
4486             # if index > 0
4487 0 0       0 if ($src[$j][-1] > 0) {
4488            
4489             # set matrix element to object sample index
4490 0         0 $matrix->[$i/$row_length][$i % $row_length] = $src[$j][-1];
4491            
4492             # invert sample index to indicate it was used
4493 0         0 $src[$j][-1] = - $src[$j][-1];
4494            
4495             # quit loop
4496 0         0 last;
4497            
4498             }
4499            
4500             }
4501            
4502             # if matrix element undefined
4503 0 0       0 if (! defined($matrix->[$i/$row_length][$i % $row_length])) {
4504            
4505             # print message
4506 0         0 print "FIFO stack empty for @{$devt->[$i]}\n";
  0         0  
4507 0         0 print "using last stack sample\n";
4508            
4509             # set matrix element to last row matching object sample
4510 0         0 $matrix->[$i/$row_length][$i % $row_length] = - $src[$high][-1];
4511            
4512             }
4513            
4514             # use LIFO sample
4515             } elsif ($dups == 2) {
4516            
4517             # from high to low
4518 0         0 for my $j (reverse($low .. $high)) {
4519            
4520             # if index > 0
4521 0 0       0 if ($src[$j][-1] > 0) {
4522            
4523             # set matrix element to object sample index
4524 0         0 $matrix->[$i/$row_length][$i % $row_length] = $src[$j][-1];
4525            
4526             # invert sample index to indicate it was used
4527 0         0 $src[$j][-1] = - $src[$j][-1];
4528            
4529             # quit loop
4530 0         0 last;
4531            
4532             }
4533            
4534             }
4535            
4536             # if matrix element undefined
4537 0 0       0 if (! defined($matrix->[$i/$row_length][$i % $row_length])) {
4538            
4539             # print message
4540 0         0 print "LIFO stack empty for @{$devt->[$i]}\n";
  0         0  
4541 0         0 print "using last stack sample\n";
4542            
4543             # set matrix element to first row matching object sample
4544 0         0 $matrix->[$i/$row_length][$i % $row_length] = - $src[$low][-1];
4545            
4546             }
4547            
4548             # use first duplicate sample
4549             } elsif ($dups == 3) {
4550            
4551             # set matrix element to first row matching object sample
4552 0         0 $matrix->[$i/$row_length][$i % $row_length] = $src[$low][-1];
4553            
4554             # use last duplicate sample
4555             } elsif ($dups == 4) {
4556            
4557             # set matrix element to last row matching object sample
4558 0         0 $matrix->[$i/$row_length][$i % $row_length] = $src[$high][-1];
4559            
4560             } else {
4561            
4562             # error
4563 0         0 croak('invalid duplicate handling');
4564            
4565             }
4566            
4567             }
4568            
4569             # if 'copy' slice defined
4570 0 0       0 if (defined($copys)) {
4571            
4572             # get the object row
4573 0         0 $n = $matrix->[$i/$row_length][$i % $row_length];
4574            
4575             # copy selected values from object to template
4576 0         0 @{$template->[1][$i + 1]}[@{$copyt}] = @{$self->[1][$n]}[@{$copys}];
  0         0  
  0         0  
  0         0  
  0         0  
4577            
4578             # if device values differ
4579 0 0       0 if ($nomatch) {
4580            
4581             # copy device values from object to template
4582 0         0 @{$template->[1][$i + 1]}[@{$devct}] = @{$self->[1][$n]}[@{$devcs}];
  0         0  
  0         0  
  0         0  
  0         0  
4583            
4584             }
4585            
4586             }
4587            
4588             }
4589              
4590             # if sid-matrix is wanted and template has sid values
4591 0 0 0     0 if (wantarray() && ($sidt = id($template, [], {'context' => $hash->{'sid_context'}}))) {
4592            
4593             # for each template sample
4594 0         0 for my $i (0 .. $#{$sidt}) {
  0         0  
4595            
4596             # set sid matrix element to sid slice value
4597 0         0 $sid->[$i/$row_length][$i % $row_length] = $sidt->[$i][0];
4598            
4599             }
4600            
4601             # return row matrix slice and sid matrix slice
4602 0         0 return(bless($matrix, 'Math::Matrix'), bless($sid, 'Math::Matrix'));
4603            
4604             } else {
4605            
4606             # return row matrix slice
4607 0         0 return(bless($matrix, 'Math::Matrix'));
4608            
4609             }
4610              
4611             }
4612              
4613             # get sample selection
4614             # array of data values is supplied to code block
4615             # sample is included if code block returns 'true' value
4616             # default row_slice is all samples
4617             # default column_slice is all columns
4618             # parameters: (code_reference, row_slice, column_slice)
4619             # returns: (row_slice)
4620             sub find {
4621              
4622             # get parameters
4623 0     0 1 0 my ($self, $code, $rows, $cols) = @_;
4624              
4625             # verify code reference
4626 0 0       0 (ref($code) eq 'CODE') or croak('selection parameter must be a code reference');
4627              
4628             # if row slice undefined or empty
4629 0 0 0     0 if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) {
  0   0     0  
4630            
4631             # use all rows
4632 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
4633            
4634             } else {
4635            
4636             # flatten slice
4637 0         0 $rows = ICC::Shared::flatten($rows);
4638            
4639             }
4640              
4641             # if column slice undefined or empty
4642 0 0 0     0 if (! defined($cols) || (ref($cols) eq 'ARRAY' && @{$cols} == 0)) {
  0   0     0  
4643            
4644             # use all columns
4645 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
4646            
4647             } else {
4648            
4649             # flatten slice
4650 0         0 $cols = ICC::Shared::flatten($cols);
4651            
4652             }
4653              
4654             # return selection slice
4655 0         0 return([grep {&$code(@{$self->[1][$_]}[@{$cols}])} @{$rows}]);
  0         0  
  0         0  
  0         0  
  0         0  
4656              
4657             }
4658              
4659             # get sample selection based on device values
4660             # array of device values is supplied to code block
4661             # sample is included if code block returns 'true' value
4662             # default row_slice is all samples
4663             # context may be specified with parameter hash
4664             # parameters: (code_reference, [row_slice], [hash])
4665             # returns: (row_slice)
4666             sub ramp {
4667              
4668             # local variables
4669 0     0 1 0 my ($hash, $cols, $mult);
4670              
4671             # get optional hash
4672 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
4673              
4674             # get remaining parameters
4675 0         0 my ($self, $code, $rows) = @_;
4676              
4677             # verify code reference
4678 0 0       0 (ref($code) eq 'CODE') or croak('selection parameter must be a code reference');
4679              
4680             # if row slice undefined or empty
4681 0 0 0     0 if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) {
  0   0     0  
4682            
4683             # use all rows
4684 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
4685            
4686             } else {
4687            
4688             # flatten slice
4689 0         0 $rows = ICC::Shared::flatten($rows);
4690            
4691             }
4692              
4693             # get device column slice
4694 0 0       0 (defined($cols = device($self, $hash))) or croak('device values required');
4695              
4696             # set multiplier (255 if RGB, otherwise 100)
4697 0 0       0 $mult = ($self->[1][0][$cols->[0]] =~ m/RGB_R$/) ? 255 : 100;
4698              
4699             # if RGB data -and- 'invert_rgb' flag
4700 0 0 0     0 if ($mult == 255 && $hash->{'invert_rgb'}) {
4701            
4702             # return selection slice
4703 0         0 return([grep {&$code(map {1 - $_/$mult} @{$self->[1][$_]}[@{$cols}])} @{$rows}]);
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
4704            
4705             } else {
4706            
4707             # return selection slice
4708 0         0 return([grep {&$code(map {$_/$mult} @{$self->[1][$_]}[@{$cols}])} @{$rows}]);
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
4709            
4710             }
4711              
4712             }
4713              
4714             # get sample selection based on L*a*b* values
4715             # array of L*a*b* values is supplied to code block
4716             # sample is included if code block returns 'true' value
4717             # default row_slice is all samples
4718             # context may be specified with parameter hash
4719             # parameters: (code_reference, [row_slice], [hash])
4720             # returns: (row_slice)
4721             sub range {
4722              
4723             # local variables
4724 0     0 1 0 my ($hash, $cols);
4725              
4726             # get optional hash
4727 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
4728              
4729             # get remaining parameters
4730 0         0 my ($self, $code, $rows) = @_;
4731              
4732             # verify code reference
4733 0 0       0 (ref($code) eq 'CODE') or croak('selection parameter must be a code reference');
4734              
4735             # if row slice undefined or empty
4736 0 0 0     0 if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) {
  0   0     0  
4737            
4738             # use all rows
4739 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
4740            
4741             } else {
4742            
4743             # flatten slice
4744 0         0 $rows = ICC::Shared::flatten($rows);
4745            
4746             }
4747              
4748             # get L*a*b* column slice
4749 0 0       0 (defined($cols = lab($self, $hash))) or croak('L*a*b* values required');
4750              
4751             # return selection slice
4752 0         0 return([grep {&$code(@{$self->[1][$_]}[@{$cols}])} @{$rows}]);
  0         0  
  0         0  
  0         0  
  0         0  
4753              
4754             }
4755              
4756             # generate randomized sample slice
4757             # parameter: ([row_slice])
4758             # returns: (row_slice)
4759             sub randomize {
4760              
4761             # get parameters
4762 0     0 1 0 my ($self, $rows) = @_;
4763              
4764             # if row slice undefined -or- an empty array reference
4765 0 0 0     0 if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) {
  0   0     0  
4766            
4767             # use all rows
4768 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
4769            
4770             } else {
4771            
4772             # flatten row slice
4773 0         0 $rows = ICC::Shared::flatten($rows);
4774            
4775             # verify row slice contents
4776 0 0 0     0 (@{$rows} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$rows}) or croak('invalid row slice');
  0 0       0  
  0         0  
  0         0  
4777            
4778             }
4779              
4780             # return row slice, randomized
4781 0         0 return([List::Util::shuffle(@{$rows})]);
  0         0  
4782              
4783             }
4784              
4785             # sort sample slice by device values
4786             # note: returns undef if no device values
4787             # parameters: ([row_slice, sort_vector])
4788             # returns: (sorted_row_slice)
4789             # returns: (sorted_row_slice, sorted_device_values)
4790             sub sort {
4791              
4792             # get parameters
4793 0     0 1 0 my ($self, $rows, $sort) = @_;
4794              
4795             # local variables
4796 0         0 my ($dev, $n, $code, $sorted);
4797              
4798             # if row slice undefined -or- an empty array reference
4799 0 0 0     0 if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) {
  0   0     0  
4800            
4801             # use all rows
4802 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
4803            
4804             } else {
4805            
4806             # flatten row slice
4807 0         0 $rows = ICC::Shared::flatten($rows);
4808            
4809             # verify row slice contents
4810 0 0 0     0 (@{$rows} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$rows}) or croak('invalid row slice');
  0 0       0  
  0         0  
  0         0  
4811            
4812             }
4813              
4814             # get device values
4815 0 0       0 if (! ($dev = device($self, $rows))) {
4816            
4817             # display warning
4818 0         0 carp("no device values, sorting failed\n");
4819            
4820             # return
4821 0         0 return();
4822            
4823             }
4824              
4825             # get number of device channels
4826 0         0 $n = @{device($self)};
  0         0  
4827              
4828             # default sort vector, e.g. [4, 3, 2, 1]
4829 0 0       0 $sort = [reverse(1 .. $n)] if (! defined($sort));
4830              
4831             # verify sort parameter
4832 0 0 0     0 if (ICC::Shared::is_num_vector($sort) && @{$sort} == grep {$_ && $_ == int($_) && abs($_) <= $n} @{$sort}) {
  0 0 0     0  
  0         0  
  0         0  
4833            
4834             # for each sample
4835 0         0 for my $i (0 .. $#{$dev}) {
  0         0  
4836            
4837             # insert sample number
4838 0   0     0 unshift(@{$dev->[$i]}, $rows->[$i] // $i + 1);
  0         0  
4839            
4840             }
4841            
4842             # make sort code fragment
4843 0 0       0 $code = '@{$dev} = sort {' . join(' || ', map {my $dir = m/-/; my $col = abs($_); $dir ? "\$b->[$col] <=> \$a->[$col]" : "\$a->[$col] <=> \$b->[$col]"} @{$sort}) . '} @{$dev}';
  0         0  
  0         0  
  0         0  
  0         0  
4844            
4845             # evaluate code fragment
4846 0         0 eval($code);
4847            
4848             # for each sample
4849 0         0 for my $i (0 .. $#{$dev}) {
  0         0  
4850            
4851             # extract sample number
4852 0         0 $sorted->[$i] = shift(@{$dev->[$i]});
  0         0  
4853            
4854             }
4855            
4856             } else {
4857            
4858             # display warning
4859 0         0 carp("invalid sort parameter, sorting failed\n");
4860            
4861             }
4862              
4863             # return, array or scalar
4864 0 0       0 return(wantarray ? ($sorted, $dev) : $sorted);
4865              
4866             }
4867              
4868             # analyze chart device values
4869             # creates an array structure with an element for each device channel.
4870             # each element contains a hash, a keys array, and a ramp array.
4871             # hash keys are device values, and hash values are arrays of samples.
4872             # if row-slice is omitted, all samples are used.
4873             # if the dup_flag is false (default), a new sample is added
4874             # containing average measurement values, and the new sample
4875             # is substituted for the anonymous array of duplicates.
4876             # if the dup_flag is true, duplicate samples are included in
4877             # array of samples grouped within anonymous arrays.
4878             # dup_flag and/or device context are specified with parameter hash
4879             # parameters: ([row_slice], [hash])
4880             # returns: (ref_to_structure)
4881             sub analyze {
4882              
4883             # get object reference
4884 0     0 1 0 my $self = shift();
4885              
4886             # local variables
4887 0         0 my ($hash, $rows, $dup, $ramp, $dev, $c1, $c2, $c3, @id, @name, $mult);
4888 0         0 my (@d, %dev_hash, $key, $avg, $value, $struct);
4889              
4890             # get optional hash
4891 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
4892              
4893             # get device column slice
4894 0 0       0 ($dev = device($self, $hash)) or croak('chart has no device values');
4895              
4896             # get row slice
4897 0 0       0 $rows = shift() if (ref($_[0]) eq 'ARRAY');
4898              
4899             # flatten row slice
4900 0 0       0 $rows = $rows ? ICC::Shared::flatten($rows) : [];
4901              
4902             # use all samples if slice is empty
4903 0 0       0 $rows = [1 .. $#{$self->[1]}] if (@{$rows} == 0);
  0         0  
  0         0  
4904              
4905             # get dup flag
4906 0 0       0 $dup = defined($hash->{'dups'}) ? $hash->{'dups'} : 0;
4907              
4908             # get ramp value
4909 0 0       0 $ramp = defined($hash->{'ramp'}) ? $hash->{'ramp'} : 0;
4910              
4911             # get averaging groups
4912 0         0 ($c1, $c2, $c3) = _avg_groups($self, $hash);
4913              
4914             # for each column
4915 0         0 for my $i (0 .. $#{$self->[1][0]}) {
  0         0  
4916            
4917             # add column if SAMPLE_ID field
4918 0 0       0 push(@id, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID)$/);
4919            
4920             # add column if SAMPLE_NAME field
4921 0 0       0 push(@name, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?SAMPLE_NAME$/);
4922            
4923             }
4924              
4925             # set device multiplier (255 for RGB values, otherwise 100)
4926 0 0       0 $mult = ($self->[1][0][$dev->[0]] =~ m/^(?:.*\|)?RGB_[RGB]$/) ? 255 : 100;
4927              
4928             # for each sample
4929 0         0 for my $i (0 .. $#{$rows}) {
  0         0  
4930            
4931             # get device values
4932 0         0 @d = @{$self->[1][$rows->[$i]]}[@{$dev}];
  0         0  
  0         0  
4933            
4934             # divide by multiplier (setting -0 to 0)
4935 0 0       0 @d = map {$_ == 0 ? 0 : $_/$mult} @d;
  0         0  
4936            
4937             # make device value key
4938 0         0 $key = join(':', @d);
4939            
4940             # if key exists
4941 0 0       0 if (exists($dev_hash{$key})) {
4942            
4943             # add sample to existing hash entry
4944 0         0 push(@{$dev_hash{$key}}, $rows->[$i]);
  0         0  
4945            
4946             } else {
4947            
4948             # add device hash entry
4949 0         0 $dev_hash{$key} = [$rows->[$i]];
4950            
4951             }
4952            
4953             }
4954              
4955             # if dup flag is not set
4956 0 0       0 if (! $dup) {
4957            
4958             # for each key
4959 0         0 for my $key (keys(%dev_hash)) {
4960            
4961             # if duplicate samples
4962 0 0       0 if (@{$dev_hash{$key}} > 1) {
  0         0  
4963            
4964             # if measurement data
4965 0 0 0     0 if (@{$c1} || @{$c2} || @{$c3}) {
  0   0     0  
  0         0  
  0         0  
4966            
4967             # add average sample
4968 0         0 $avg = _add_avg($self, $dev_hash{$key}, $c1, $c2, $c3, \@id, \@name);
4969            
4970             # update hash to average sample
4971 0         0 $dev_hash{$key} = [$avg];
4972            
4973             } else {
4974            
4975             # update hash to first sample
4976 0         0 $dev_hash{$key} = [$dev_hash{$key}[0]];
4977            
4978             }
4979            
4980             }
4981            
4982             }
4983            
4984             # update the SAMPLE_ID hash
4985 0         0 _makeSampleID($self);
4986            
4987             }
4988              
4989             # make empty structure
4990 0         0 $struct = [map {[{}, [], []]} (0 .. $#{$dev})];
  0         0  
  0         0  
4991              
4992             # for each key
4993 0         0 for my $key (keys(%dev_hash)) {
4994            
4995             # split key to device values
4996 0         0 @d = split(/:/, $key);
4997            
4998             # get value
4999 0         0 $value = $dev_hash{$key};
5000            
5001             # resolve single value to scalar
5002 0 0       0 $value = $value->[0] if (@{$value} == 1);
  0         0  
5003            
5004             # for each device channel
5005 0         0 for my $i (0 .. $#d) {
5006            
5007             # if key exists
5008 0 0       0 if (exists($struct->[$i][0]{$d[$i]})) {
5009            
5010             # add sample to hash entry
5011 0         0 push(@{$struct->[$i][0]{$d[$i]}}, $value);
  0         0  
5012            
5013             } else {
5014            
5015             # add hash entry
5016 0         0 $struct->[$i][0]{$d[$i]} = [$value];
5017            
5018             # add device value to keys array
5019 0         0 push(@{$struct->[$i][1]}, $d[$i]);
  0         0  
5020            
5021             }
5022            
5023             # if all other device values equal ramp value
5024 0 0       0 if (@d == grep {$_ == $i || $d[$_] == $ramp} (0 .. $#d)) {
  0 0       0  
5025            
5026             # add sample to ramp array
5027 0         0 push(@{$struct->[$i][2]}, $value);
  0         0  
5028            
5029             }
5030            
5031             }
5032            
5033             }
5034              
5035             # for each device channel
5036 0         0 for my $i (0 .. $#{$dev}) {
  0         0  
5037            
5038             # sort keys array (decreasing frequency)
5039 0         0 $struct->[$i][1] = [sort {@{$struct->[$i][0]{$b}} <=> @{$struct->[$i][0]{$a}}} @{$struct->[$i][1]}];
  0         0  
  0         0  
  0         0  
  0         0  
5040            
5041             # sort ramp array (increasing values)
5042 0 0       0 $struct->[$i][2] = [sort {$self->[1][(! ref($a) ? $a : $a->[0])][$dev->[$i]] <=> $self->[1][(! ref($b) ? $b : $b->[0])][$dev->[$i]]} @{$struct->[$i][2]}];
  0 0       0  
  0         0  
5043            
5044             }
5045              
5046             # return
5047 0         0 return($struct);
5048              
5049             }
5050              
5051             # chart signature
5052             # signature is a 32 character hex string computed from device values
5053             # returns undef if no device values
5054             # parameter: ([row_slice])
5055             # returns: (signature)
5056             sub signature {
5057              
5058             # get parameters
5059 0     0 1 0 my ($self, $rows) = @_;
5060              
5061             # local variables
5062 0         0 my ($dev, $str, $val);
5063              
5064             # if chart contains device values
5065 0 0       0 if ($dev = device($self)) {
5066            
5067             # initialize string
5068 0         0 $str = '';
5069            
5070             # get default row slice, if undefined
5071 0 0       0 $rows = [1 .. size($self, 1)] if (! defined($rows));
5072            
5073             # for each row
5074 0         0 for my $i (@{$rows}) {
  0         0  
5075            
5076             # for each device value
5077 0         0 for my $j (@{$dev}) {
  0         0  
5078            
5079             # get device value
5080 0         0 $val = $self->[1][$i][$j];
5081            
5082             # if value starts with 99.9 (P2P targets)
5083 0 0       0 if ($val =~ m/^99\.9/) {
5084            
5085             # add '100'
5086 0         0 $str .= '100';
5087            
5088             } else {
5089            
5090             # add integer value (e.g. 50% adds '050')
5091 0         0 $str .= sprintf('%03s', int($self->[1][$i][$j]));
5092            
5093             }
5094            
5095             }
5096            
5097             }
5098            
5099             # return MD5 hex digest
5100 0         0 return(Digest::MD5::md5_hex($str));
5101            
5102             }
5103              
5104             # return
5105 0         0 return();
5106              
5107             }
5108              
5109             # identify chart format from device values
5110             # returns an array reference containing the chart name and nominal size
5111             # returns undef if format unknown
5112             # returns: (identity)
5113             sub identity {
5114              
5115             # get object reference
5116 0     0 1 0 my $self = shift();
5117              
5118             # local variables
5119 0         0 my ($dev, $sorted);
5120              
5121             # verify device values
5122 0 0       0 (test($self, 'DEVICE')) or return();
5123              
5124             # signature table (sorted, no white samples)
5125 0         0 state $table = {
5126             # CMYK charts
5127             '8b5b7006b31df16eeb0e3d3ced60051f' => ['IT8.7/3', 928],
5128             'b9c1f31582d682c23983aab1f1b75b4f' => ['IT8.7/4', 1617],
5129             '15aa45ec6118dea7daddeb4ef2b54386' => ['IT8.7/5', 1617],
5130             'aba61c4d5774da33b67b582cdb7b073c' => ['ECI2002', 1485],
5131             'de55394b26fb64635db213042e3a5d28' => ['TC3.5', 432],
5132             '7a1fda1429838505eadf492249ad630c' => ['TC3.5+Cal', 520],
5133             '6c5d838867d23a76132fc1108d93c272' => ['PressCal5', 125],
5134             '1eb64d5eba56285f124dc60065bf431e' => ['PressCalG5', 125],
5135             '06469a5bb67ef64f0f03563e79a42992' => ['PressCal8', 225],
5136             '41eb5955ed2abb9c7a0f6fecc5bba7fd' => ['PressCalF', 475],
5137             'dc9d5a6302ccb62f2be359dd916a0c1f' => ['PressCalG', 475],
5138             '48cf188554c85df6685cff6e71e360ed' => ['PressCalX', 400],
5139             'd176de53229117268a2eb956380369e4' => ['PressCalY', 475],
5140             'a5facefa8dc2c30f471aecdabbea6bb9' => ['PressCalZ', 625],
5141             '4657c95764bc3fb9209f77395b658bd4' => ['P2P25', 300],
5142             '87e9c04c2d804d87dd6127d99efb95c0' => ['P2P51', 300],
5143             '63f4daa1011e14bc04cbd11ac7b89849' => ['P2P53', 300],
5144             '75af26579af3b3c0cce93b09d95c0fed' => ['microP2P', 96],
5145             'c442e077c30e1918338da856d2b6168b' => ['miniP2P', 125],
5146             '8545bb6b881e3b2864753c87f8439e5a' => ['miniP2P53', 125],
5147             'f11fae3e797b2e0c5a2ab9fc1788597d' => ['G7 Verifier', 56],
5148             '905aecb1bfd469ecf6e6b7cec210460a' => ['LimitFinder', 460],
5149             'e60bf0937c637a2b97d95961a3f650a2' => ['TC1617', 1617],
5150             'da6a16373eeb868bac2c9682f7fb206f' => ['HC2052F', 2052],
5151             '1671b192dc8a9f4219662bff28378ff9' => ['Curve OneRun', 1807],
5152             'c27b03ad4289e7769dbef8f7a57330e1' => ['UGRA/FOGRA MediaWedge v2', 46],
5153             '7915773af8e52e03924421a09ed9a104' => ['UGRA/FOGRA MediaWedge v3', 72],
5154             '58e02386295caf078fa1b851cb5393dd' => ['Idealliance 12647-7 2009', 54],
5155             'a06de1b8592e0b7e959a085fc7bd8086' => ['Idealliance 12647-7 2013', 84],
5156             'a9b9bb266227df349c7630643beb1f38' => ['SpotOn Control Strip', 33],
5157             '758d9fcec64ad9d3a7ca4562cb274e82' => ['Japan Color Control Strip', 54],
5158             '18792374fb80709dfae611dfeeaf6bcf' => ['EFI Color Verifier US', 32],
5159             '2207946e9fea7acf3ae990df8f49e1eb' => ['EFI Color Verifier', 15],
5160             'cfc0196dbb05985261ba51fb91f9eccc' => ['FieryColorBar', 26],
5161             '33bda2f014f4d99939c4ff15ceacff30' => ['Monaco CMYK 378', 378],
5162             '9ba653e395b1199ef1d4375026a0303c' => ['Monaco CMYK 530', 530],
5163             'd6a584f1b30c245fc48b578738bb8c18' => ['Monaco CMYK 917', 917],
5164             '3346663bf34bca87ec09762a31bf84e2' => ['Monaco CMYK 1379', 1379],
5165             '24b08a49124eabf0aa871987391a3e11' => ['Monaco CMYK 2989', 2989],
5166            
5167             # RGB charts
5168             '7f80a2d564d0ec66fd751cbc122d0090' => ['TC2.83', 283],
5169             'fe57e61b61ab53456082d3b9df984357' => ['TC9.18', 918],
5170             '26113f02441add8f0b483c860b887023' => ['TC1331', 1331],
5171             'faf68027b76bd5f9d65020c0dae5e25a' => ['Monaco RGB 343', 343],
5172             'd2e094757c363969f5fbf2557b3b05b0' => ['Monaco RGB 729', 729],
5173             'e44b07f1cc89876d488d3559b8a44488' => ['Monaco RGB 1728', 1728],
5174             };
5175              
5176             # make chart object containing device values only
5177 0         0 $dev = ICC::Support::Chart->new(slice($self, [0 .. $#{$self->[1]}], device($self)));
  0         0  
5178              
5179             # for each row
5180 0         0 for my $i (1 .. $#{$dev->[1]}) {
  0         0  
5181            
5182             # change any 99.9xx% values to 100% (P2P charts!)
5183 0 0       0 @{$dev->[1][$i]} = map {m/^99.9/ ? 100 : $_} @{$dev->[1][$i]};
  0         0  
  0         0  
  0         0  
5184            
5185             }
5186              
5187             # get sorted samples
5188 0         0 $sorted = ICC::Support::Chart::sort($dev);
5189              
5190             # if RGB device values
5191 0 0       0 if ($dev->[1][0][0] =~ m/RGB_R$/) {
5192            
5193             # remove white samples (RGB)
5194 0         0 @{$sorted} = grep {grep {$_ != 255} @{$dev->[1][$_]}} @{$sorted};
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
5195            
5196             } else {
5197            
5198             # remove white samples (non-RGB)
5199 0         0 @{$sorted} = grep {grep {$_ != 0} @{$dev->[1][$_]}} @{$sorted};
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
5200            
5201             }
5202              
5203             # return chart identity, undefined if hash lookup fails
5204 0         0 return($table->{signature($dev, $sorted)});
5205              
5206             }
5207              
5208             # write chart to ISO 28178 (CGATS.17) ASCII file
5209             # optional slice parameters are either scalars, array references or 'Math::Matrix' objects
5210             # optional hash parameter keys: 'sid', 'append', 'standard'
5211             # parameters: (path_to_file, [row_slice, [column_slice]], [hash])
5212             sub write {
5213              
5214             # local variables
5215 0     0 1 0 my ($hash, $row_length, $m, $n, $sid, $fh, $rs, @fields);
5216 0         0 my (%cspec, %keys, $keyword, $value, $source, $std_key, @s, $sidx, $append);
5217 0         0 my ($null, $undef);
5218              
5219             # get optional hash parameter
5220 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
5221              
5222             # get remaining parameters
5223 0         0 my ($self, $path, $rows, $cols) = @_;
5224              
5225             # if row slice defined
5226 0 0       0 if (defined($rows)) {
5227            
5228             # if row slice an empty array reference
5229 0 0 0     0 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  0         0  
5230            
5231             # use all rows
5232 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5233            
5234             } else {
5235            
5236             # get row length if row slice is Math::Matrix object
5237 0 0       0 $row_length = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix'));
  0         0  
5238            
5239             # flatten row slice
5240 0         0 $rows = ICC::Shared::flatten($rows);
5241            
5242             }
5243            
5244             } else {
5245            
5246             # use all rows
5247 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5248            
5249             }
5250              
5251             # get number of rows
5252 0         0 $m = @{$rows};
  0         0  
5253              
5254             # warn if invalid samples
5255 0 0 0     0 (@{$rows} == grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows})|| warn('row slice contains invalid samples');
  0 0       0  
  0         0  
  0         0  
5256              
5257             # if column slice defined
5258 0 0       0 if (defined($cols)) {
5259            
5260             # if column slice an empty array reference
5261 0 0 0     0 if (ref($cols) eq 'ARRAY' && @{$cols} == 0) {
  0         0  
5262            
5263             # use all columns
5264 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
5265            
5266             } else {
5267            
5268             # flatten column slice
5269 0         0 $cols = ICC::Shared::flatten($cols);
5270            
5271             }
5272            
5273             } else {
5274            
5275             # use all columns
5276 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
5277            
5278             }
5279              
5280             # map column slice, converting non-numeric values with 'test' method
5281 0 0       0 @{$cols} = map {Scalar::Util::looks_like_number($_) ? $_ : $self->test($_)} @{$cols};
  0         0  
  0         0  
  0         0  
5282              
5283             # get number of columns
5284 0         0 $n = @{$cols};
  0         0  
5285              
5286             # filter column slice
5287 0 0       0 @{$cols} = grep {$_ == int($_) && defined($self->[1][0][$_])} @{$cols};
  0         0  
  0         0  
  0         0  
5288              
5289             # warn if invalid fields
5290 0 0       0 ($n == @{$cols}) || warn('column slice contains invalid fields');
  0         0  
5291              
5292             # if 'sid' hash value defined
5293 0 0       0 if (defined($sid = $hash->{'sid'})) {
5294            
5295             # if array reference or Math::Matrix object
5296 0 0 0     0 if (ref($sid) eq 'ARRAY' || UNIVERSAL::isa($sid, 'Math::Matrix')) {
    0          
5297            
5298             # flatten 'sid' slice
5299 0         0 $sid = ICC::Shared::flatten($sid);
5300            
5301             # warn row slice and sid slice are different sizes
5302 0 0       0 ($m == @{$sid}) || warn('row slice and sid slice are different sizes');
  0         0  
5303            
5304             } elsif ($sid eq 'row') {
5305            
5306             # use sequential row list
5307 0         0 $sid = [1 .. $m];
5308            
5309             } else {
5310            
5311             # error
5312 0         0 croak('invalid \'sid\' hash value');
5313            
5314             }
5315            
5316             }
5317              
5318             # filter path
5319 0         0 ICC::Shared::filterPath($path);
5320              
5321             # open the file
5322 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
5323              
5324             # disable :crlf translation
5325 0         0 binmode($fh);
5326              
5327             # get the record separator
5328 0   0     0 $rs = $self->[0]{'write_rs'} || $self->[0]{'read_rs'} || "\n";
5329              
5330             # print the standard, if defined in hash
5331 0 0       0 print $fh $hash->{'standard'}, $rs if defined($hash->{'standard'});
5332              
5333             # print LGOROWLENGTH, if row length defined
5334 0 0       0 printf $fh "LGOROWLENGTH\t%d$rs", $row_length if (defined($row_length));
5335              
5336             # initialize color specification hash
5337             # so lines with 'FileInformation' source are printed
5338 0         0 %cspec = ('FileInformation' => 1);
5339              
5340             # add referenced sources to color specification hash
5341 0 0       0 for (@{$self->[2][5]}[@{$cols}]) {$cspec{$_}++ if defined()};
  0         0  
  0         0  
  0         0  
5342              
5343             # make hash of quoted keywords
5344 0         0 for (@{$self->[3]}) {$keys{"\"$_->[0]\""}++}
  0         0  
  0         0  
5345              
5346             # for each header line
5347 0         0 for (@{$self->[3]}) {
  0         0  
5348            
5349             # get keyword, value and source
5350 0         0 ($keyword, $value, $source) = @{$_};
  0         0  
5351            
5352             # if keyword defined and length > 0
5353 0 0 0     0 if (defined($keyword) && length($keyword)) {
5354            
5355             # make uppercase
5356 0         0 $keyword = uc($keyword);
5357            
5358             # skip certain keywords
5359 0 0       0 next if ($keyword =~ m/NUMBER_OF_FIELDS|NUMBER_OF_SETS/); # these are output later
5360 0 0 0     0 next if ($keyword =~ m/LGOROWLENGTH/ && defined($row_length)); # row length already output
5361 0 0 0     0 next if ($keyword eq 'KEYWORD' && ! exists($keys{$value})); # skip unused 'KEYWORD' entries
5362            
5363             # if no source or referenced source
5364 0 0 0     0 if (! defined($source) || $cspec{$source}) {
5365            
5366             # if value defined and length > 0
5367 0 0 0     0 if (defined($value) && length($value)) {
5368            
5369             # print keyword/value
5370 0         0 print $fh "$keyword\t$value$rs";
5371            
5372             } else {
5373            
5374             # print keyword only
5375 0         0 print $fh "$keyword$rs";
5376            
5377             }
5378            
5379             }
5380            
5381             } else {
5382            
5383             # print empty line
5384 0         0 print $fh "$rs";
5385            
5386             }
5387            
5388             }
5389              
5390             # get format fields
5391 0         0 @fields = @{$self->[1][0]}[@{$cols}];
  0         0  
  0         0  
5392              
5393             # remove any context, trim leading and trailing white space, and replace spaces with underscores
5394 0         0 for (@fields) {s/^.*\|//; s/^\s*(.*?)\s*$/$1/; s/ /_/g}
  0         0  
  0         0  
  0         0  
5395              
5396             # make standard format keyword regex (per ISO 28178 and common usage)
5397 0         0 $std_key = '^(?:' . join('|', qw(SAMPLE_ID SAMPLE_NO STRING RGB_[RGB] CMYK_[CMYK] [2-9A-F]CLR_[1-9A-F] PC\d+_\d+ SPOT_\d+
5398             (?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3} D_(?:RED|GREEN|BLUE|VIS|MAJOR_FILTER) XYZ_[XYZ] XYY_(?:X|Y|CAPY)
5399             LAB_[LABCH] LAB_DE LAB_DE_94 LAB_DE_CMC LAB_DE_2000 MEAN_DE STDDEV_[XYZ] STDDEV_[LAB] CHI_SQD_PAR)) . ')$';
5400              
5401             # for each format field
5402 0         0 for (@fields) {
5403            
5404             # if not a standard keyword
5405 0 0       0 if (! /$std_key/) {
5406            
5407             # print KEYWORD
5408 0         0 printf $fh "KEYWORD\t%s$rs", $_;
5409            
5410             }
5411            
5412             }
5413              
5414             # if 'sid' slice defined
5415 0 0       0 if (defined($sid)) {
5416            
5417             # if 'SAMPLE_ID' keyword(s)
5418 0 0       0 if (@s = grep {uc($fields[$_]) eq 'SAMPLE_ID'} (0 .. $#fields)) {
  0         0  
5419            
5420             # save index of first match
5421 0         0 $sidx = $s[0];
5422            
5423             } else {
5424            
5425             # insert 'SAMPLE_ID' keyword
5426 0         0 unshift(@fields, 'SAMPLE_ID');
5427            
5428             }
5429            
5430             }
5431              
5432             # print NUMBER_OF_FIELDS
5433 0         0 printf $fh "NUMBER_OF_FIELDS\t%d$rs", scalar(@fields);
5434              
5435             # print BEGIN_DATA_FORMAT
5436 0         0 print $fh 'BEGIN_DATA_FORMAT', $rs;
5437              
5438             # print format string (if any)
5439 0 0       0 print $fh join("\t", @fields), $rs if (@fields);
5440              
5441             # print END_DATA_FORMAT
5442 0         0 print $fh 'END_DATA_FORMAT', $rs;
5443              
5444             # print NUMBER_OF_SETS
5445 0         0 printf $fh "NUMBER_OF_SETS\t%d$rs", scalar(@{$rows});
  0         0  
5446              
5447             # print BEGIN_DATA
5448 0         0 print $fh 'BEGIN_DATA', $rs;
5449              
5450             # get null replacement value
5451 0   0     0 $null = $hash->{'null'} // 'null';
5452              
5453             # get undef replacement value
5454 0   0     0 $undef = $hash->{'undef'} // 'undef';
5455              
5456             # for each row
5457 0         0 for my $i (0 .. $#{$rows}) {
  0         0  
5458            
5459             # get data fields, replacing null and undefined values
5460 0 0       0 @fields = map {defined() ? length() ? $_ : $null : $undef} @{$self->[1][$rows->[$i]]}[@{$cols}];
  0 0       0  
  0         0  
  0         0  
5461            
5462             # trim leading and trailing white space, and replace spaces with underscores
5463 0         0 for (@fields) {s/^\s*(.*?)\s*$/$1/; s/ /_/g};
  0         0  
  0         0  
5464            
5465             # if 'sid' slice defined
5466 0 0       0 if (defined($sid)) {
5467            
5468             # if 'sid' index defined
5469 0 0       0 if (defined($sidx)) {
5470            
5471             # replace 'sid' value
5472 0         0 $fields[$sidx] = $sid->[$i];
5473            
5474             } else {
5475            
5476             # insert 'sid' value
5477 0         0 unshift(@fields, $sid->[$i]);
5478             }
5479            
5480             }
5481            
5482             # print the data record
5483 0         0 print $fh join("\t", @fields), $rs;
5484            
5485             }
5486              
5487             # print END_DATA
5488 0         0 print $fh 'END_DATA', $rs;
5489              
5490             # if 'append' hash value defined
5491 0 0       0 if (defined($append = $hash->{'append'})) {
5492            
5493             # replace line endings, if any
5494 0         0 $append =~ s/\n/$rs/g;
5495            
5496             # print appended data
5497 0         0 print $fh $append;
5498            
5499             }
5500              
5501             # close the file
5502 0         0 close($fh);
5503              
5504             }
5505              
5506             # write chart to CxF3 file
5507             # optional slice parameters are either scalars, array references or 'Math::Matrix' objects
5508             # optional hash parameter keys: 'cc:FileInformation'
5509             # parameters: (path_to_file, [row_slice, [column_slice]], [hash])
5510             sub writeCxF3 {
5511              
5512             # local variables
5513 0     0 1 0 my ($hash, $row_length, $n);
5514 0         0 my ($dom, $root, $ns, $nsURI, $xpc);
5515 0         0 my ($datetime, $id, $ops, $objcol);
5516 0         0 my ($prefix, $nid, $obj, $xpath, $node);
5517 0         0 my (%lookup, @data, $sub, $spot);
5518              
5519             # get optional hash parameter
5520 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
5521              
5522             # get remaining parameters
5523 0         0 my ($self, $path, $rows, $cols) = @_;
5524              
5525             # if row slice defined
5526 0 0       0 if (defined($rows)) {
5527            
5528             # if row slice an empty array reference
5529 0 0 0     0 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  0         0  
5530            
5531             # use all rows
5532 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5533            
5534             } else {
5535            
5536             # get row length if row slice is Math::Matrix object
5537 0 0       0 $row_length = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix'));
  0         0  
5538            
5539             # flatten row slice
5540 0         0 $rows = ICC::Shared::flatten($rows);
5541            
5542             }
5543            
5544             } else {
5545            
5546             # set array reference to all rows
5547 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5548            
5549             }
5550              
5551             # get number of rows
5552 0         0 $n = @{$rows};
  0         0  
5553              
5554             # filter row slice
5555 0 0 0     0 @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows};
  0         0  
  0         0  
  0         0  
5556              
5557             # warn if invalid samples
5558 0 0       0 ($n == @{$rows}) || warn('row slice contains invalid samples');
  0         0  
5559              
5560             # open CxF3 template
5561 0 0       0 eval {$dom = XML::LibXML->load_xml('location' => ICC::Shared::getICCPath('Templates/CxF3_template.xml'))} or croak('can\'t load CxF3 template');
  0         0  
5562              
5563             # get the root element
5564 0         0 $root = $dom->documentElement();
5565              
5566             # get the namespace prefix and URI
5567 0         0 $ns = $root->prefix();
5568 0         0 $nsURI = $root->namespaceURI();
5569              
5570             # make XPathContext object
5571 0         0 $xpc = XML::LibXML::XPathContext->new($root);
5572              
5573             # write 'FileInformation' nodes
5574 0         0 $datetime = _writeCxF3fileinfo($self, $xpc, $ns, $nsURI, $hash);
5575              
5576             # make write operations array from column slice
5577             # array structure: [[[class, prefix, XPath, [sub_paths], [columns], {attributes}, sort_order], ...], ...]
5578 0         0 $ops = _makeCxF3writeops($self, $ns, $cols);
5579              
5580             # write 'ColorSpecification' nodes
5581 0         0 _writeCxF3colorspec($self, $xpc, $ns, $nsURI, $ops);
5582              
5583             # get the 'ObjectCollection' node
5584 0         0 ($objcol) = $xpc->findnodes("$ns:Resources/$ns:ObjectCollection");
5585              
5586             # init object Id index
5587 0         0 $id = 1;
5588              
5589             # for each group of operations
5590 0         0 for my $i (0 .. $#{$ops}) {
  0         0  
5591            
5592             # get prefix (ObjectType)
5593 0         0 $prefix = $ops->[$i][0][1];
5594            
5595             # initialize name Id
5596 0         0 $nid = 0;
5597            
5598             # for each row in slice
5599 0         0 for my $j (@{$rows}) {
  0         0  
5600            
5601             # increment name Id
5602 0         0 $nid++;
5603            
5604             # add 'Object' node
5605 0         0 $obj = $objcol->appendChild(XML::LibXML::Element->new("$ns:Object"));
5606 0         0 $obj->setAttribute('ObjectType', $prefix);
5607 0         0 $obj->setAttribute('Name', "$prefix$nid");
5608 0         0 $obj->setAttribute('Id', "c$id");
5609 0         0 $obj->setNamespace($nsURI, $ns);
5610            
5611             # add 'CreationDate' node
5612 0         0 $node = $obj->appendChild(XML::LibXML::Element->new("$ns:CreationDate"));
5613 0         0 $node->appendText($datetime);
5614 0         0 $node->setNamespace($nsURI, $ns);
5615            
5616             # init XPath node hash
5617 0         0 %lookup = ();
5618            
5619             # for each operation in the group
5620 0         0 for my $k (0 .. $#{$ops->[$i]}) {
  0         0  
5621            
5622             # set current node to Object
5623 0         0 $node = $obj;
5624            
5625             # initialize XPath
5626 0         0 $xpath = undef;
5627            
5628             # for each XPath segment
5629 0         0 for (split(/\//, $ops->[$i][$k][2])) {
5630            
5631             # add segment to XPath
5632 0 0       0 $xpath = defined($xpath) ? "$xpath/$_" : $_;
5633            
5634             # if segment exists
5635 0 0       0 if (exists($lookup{$xpath})) {
5636            
5637             # use node
5638 0         0 $node = $lookup{$xpath};
5639            
5640             } else {
5641            
5642             # add node
5643 0         0 $node = $node->appendChild(XML::LibXML::Element->new($_));
5644 0         0 $node->setNamespace($nsURI, $ns);
5645            
5646             # add hash entry (except Tag elements)
5647 0 0       0 $lookup{$xpath} = $node if ($_ ne "$ns:Tag");
5648            
5649             }
5650            
5651             }
5652            
5653             # for each attribute key (if any)
5654 0         0 for (keys(%{$ops->[$i][$k][5]})) {
  0         0  
5655            
5656             # set node attribute using either data element or hash value
5657 0 0       0 $node->setAttribute($_, (ref($ops->[$i][$k][5]{$_}) eq 'ARRAY') ? $self->[1][$j][$ops->[$i][$k][5]{$_}[0]] : $ops->[$i][$k][5]{$_});
5658            
5659             }
5660            
5661             # get data
5662 0         0 @data = @{$self->[1][$j]}[@{$ops->[$i][$k][4]}];
  0         0  
  0         0  
5663            
5664             # warn on undefined data
5665 0 0       0 (@data == grep {defined()} @data) || warn("undefined data in sample $j when writing CxF3 file");
  0         0  
5666            
5667             # if subpaths
5668 0 0       0 if (@{$ops->[$i][$k][3]}) {
  0 0       0  
    0          
5669            
5670             # for each subpath
5671 0         0 for my $s (0 .. $#{$ops->[$i][$k][3]}) {
  0         0  
5672            
5673             # add node
5674             # CxF3 schema requires integer values for RGB data
5675 0         0 $sub = $node->appendChild(XML::LibXML::Element->new($ops->[$i][$k][3][$s]));
5676 0 0       0 $sub->appendText($ops->[$i][$k][0] eq 'RGB' ? int($data[$s] + 0.5) : $data[$s]);
5677 0         0 $sub->setNamespace($nsURI, $ns);
5678            
5679             }
5680            
5681             # if NCLR class
5682 0 0       0 if ($ops->[$i][$k][0] eq 'NCLR') {
5683            
5684             # for each spot color
5685 0         0 for my $s (4 .. $#data) {
5686            
5687             # add SpotColor elements
5688 0         0 $spot = $node->appendChild(XML::LibXML::Element->new("$ns:SpotColor"));
5689 0         0 $spot->setNamespace($nsURI, $ns);
5690 0         0 $sub = $spot->appendChild(XML::LibXML::Element->new("$ns:Name"));
5691 0         0 $sub->appendText('Spot' . ($s + 1));
5692 0         0 $sub->setNamespace($nsURI, $ns);
5693 0         0 $sub = $spot->appendChild(XML::LibXML::Element->new("$ns:Percentage"));
5694 0         0 $sub->appendText($data[$s]);
5695 0         0 $sub->setNamespace($nsURI, $ns);
5696            
5697             }
5698            
5699             }
5700            
5701             # no subpaths and one data value
5702             } elsif (@data == 1) {
5703            
5704             # add data as text content
5705 0         0 $node->appendText($data[0]);
5706            
5707             # no subpaths and multiple data values
5708             } elsif (@data > 1) {
5709            
5710             # if DENSITY class
5711 0 0       0 if ($ops->[$i][$k][0] eq 'DENSITY') {
5712            
5713             ##### to be done
5714            
5715             } else {
5716            
5717             # join data and add as text content
5718 0         0 $node->appendText(join(' ', @data));
5719            
5720             }
5721            
5722             }
5723            
5724             }
5725            
5726             # add Name attribute to TagCollection element
5727 0 0       0 $lookup{"$ns:TagCollection"}->setAttribute('Name', 'Location') if exists($lookup{"$ns:TagCollection"});
5728            
5729             # if nothing was added to Object
5730 0 0       0 if ($node->isSameNode($obj)) {
5731            
5732             # unbind the node
5733 0         0 $node->unbindNode();
5734            
5735             } else {
5736            
5737             # increment Object Id
5738 0         0 $id++;
5739            
5740             }
5741            
5742             }
5743            
5744             }
5745              
5746             # write CxF3 CustomResources nodes
5747 0         0 _writeCxF3customres($self, $xpc, $ns);
5748              
5749             # validate the CxF3 document
5750 0 0 0     0 _validateCxF3($dom) if (defined($hash->{'validate'}) && $hash->{'validate'});
5751              
5752             # filter path
5753 0         0 ICC::Shared::filterPath($path);
5754              
5755             # write CxF3 file
5756 0         0 $dom->toFile($path, 1);
5757              
5758             }
5759              
5760             # write chart data array as delimited ASCII file (for Excel, R, MATLAB, etc.)
5761             # optional slice parameters are either scalars, array references or 'Math::Matrix' objects
5762             # optional hash parameter keys: 'header', 'sep', 'eol', and 'undef'
5763             # parameters: (path_to_file, [row_slice, [column_slice]], [hash])
5764             sub writeASCII {
5765              
5766             # local variables
5767 0     0 1 0 my ($hash, $row_length, $n, $fh);
5768 0         0 my ($fs, $rs, $undef, $hdr, @fields);
5769              
5770             # get optional hash parameter
5771 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
5772              
5773             # get remaining parameters
5774 0         0 my ($self, $path, $rows, $cols) = @_;
5775              
5776             # if row slice defined
5777 0 0       0 if (defined($rows)) {
5778            
5779             # if row slice an empty array reference
5780 0 0 0     0 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  0         0  
5781            
5782             # use all rows
5783 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5784            
5785             } else {
5786            
5787             # get row length if row slice is Math::Matrix object
5788 0 0       0 $row_length = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix'));
  0         0  
5789            
5790             # flatten row slice
5791 0         0 $rows = ICC::Shared::flatten($rows);
5792            
5793             }
5794            
5795             } else {
5796            
5797             # use all rows
5798 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5799            
5800             }
5801              
5802             # get number of rows
5803 0         0 $n = @{$rows};
  0         0  
5804              
5805             # filter row slice
5806 0 0 0     0 @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows};
  0         0  
  0         0  
  0         0  
5807              
5808             # warn if invalid samples
5809 0 0       0 ($n == @{$rows}) || warn('row slice contains invalid samples');
  0         0  
5810              
5811             # if column slice defined
5812 0 0       0 if (defined($cols)) {
5813            
5814             # if column slice an empty array reference
5815 0 0 0     0 if (ref($cols) eq 'ARRAY' && @{$cols} == 0) {
  0         0  
5816            
5817             # use all columns
5818 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
5819            
5820             } else {
5821            
5822             # flatten column slice
5823 0         0 $cols = ICC::Shared::flatten($cols);
5824            
5825             }
5826            
5827             } else {
5828            
5829             # use all columns
5830 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
5831            
5832             }
5833              
5834             # map column slice, converting non-numeric values with 'test' method
5835 0 0       0 @{$cols} = map {Scalar::Util::looks_like_number($_) ? $_ : $self->test($_)} @{$cols};
  0         0  
  0         0  
  0         0  
5836              
5837             # get number of columns
5838 0         0 $n = @{$cols};
  0         0  
5839              
5840             # filter column slice
5841 0 0       0 @{$cols} = grep {$_ == int($_) && defined($self->[1][0][$_])} @{$cols};
  0         0  
  0         0  
  0         0  
5842              
5843             # warn if invalid fields
5844 0 0       0 ($n == @{$cols}) || warn('column slice contains invalid fields');
  0         0  
5845              
5846             # filter path
5847 0         0 ICC::Shared::filterPath($path);
5848              
5849             # open the file
5850 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
5851              
5852             # disable :crlf translation
5853 0         0 binmode($fh);
5854              
5855             # get header mode
5856 0   0     0 $hdr = $hash->{'header'} || 1;
5857              
5858             # get the field separator
5859 0   0     0 $fs = $hash->{'sep'} || "\t";
5860              
5861             # get the record separator
5862 0   0     0 $rs = $hash->{'eol'} || "\n";
5863              
5864             # get the undefined string
5865 0   0     0 $undef = $hash->{'undef'} || '';
5866              
5867             # if header enabled
5868 0 0       0 if ($hdr) {
5869            
5870             # if format fields, replacing undefined values
5871 0 0       0 if (@fields = map {defined() ? $_ : $undef} @{$self->[1][0]}[@{$cols}]) {
  0 0       0  
  0         0  
  0         0  
5872            
5873             # if header mode 2, remove contexts
5874 0 0       0 if ($hdr == 2) {for (@fields) {s/^.*\|//}};
  0         0  
  0         0  
5875            
5876             # trim leading and trailing white space, and replace spaces with underscores
5877 0         0 for (@fields) {s/^\s*(.*?)\s*$/$1/; s/ /_/g};
  0         0  
  0         0  
5878            
5879             # print format record
5880 0         0 print $fh join($fs, @fields), $rs;
5881            
5882             }
5883            
5884             }
5885              
5886             # for each row
5887 0         0 for my $i (@{$rows}) {
  0         0  
5888            
5889             # get data fields, replacing undefined values
5890 0 0       0 @fields = map {defined() ? $_ : $undef} @{$self->[1][$i]}[@{$cols}];
  0         0  
  0         0  
  0         0  
5891            
5892             # trim leading and trailing white space, and replace spaces with underscores
5893 0         0 for (@fields) {s/^\s*(.*?)\s*$/$1/; s/ /_/g};
  0         0  
  0         0  
5894            
5895             # print the data record
5896 0         0 print $fh join($fs, @fields), $rs;
5897            
5898             }
5899              
5900             # close the file
5901 0         0 close($fh);
5902              
5903             }
5904              
5905             # write TIFF file
5906             # RGB, CMYK, and CIE L*a*b* color spaces supported
5907             # 8-bit, 16-bit or 32-bit, Intel or Motorola byte order supported
5908             # alpha and spot channels in RGB and CMYK files supported
5909             # supported hash keys: 'width', 'height', 'gap', 'left', 'right', 'rows', 'bits', 'dither', 'endian', 'xres', 'yres', 'unit'
5910             # parameters: (path_to_file, [row_slice, [column_slice]], [hash])
5911             sub writeTIFF {
5912              
5913             # local variables
5914 0     0 1 0 my ($hash, $trows, $tcols, $n, $fh);
5915 0         0 my ($base, $cs, %fields, @alpha, $pi, $rcols, $fmt, $mult, $mab, $samples);
5916 0         0 my ($width, $height, $gap, $left, $right, $bits, $xres, $yres, $unit);
5917 0         0 my ($le, $short, $long, $fp, $max, $minab, $maxab);
5918 0         0 my ($tags, $imagewidth, $bytecount, $stripsize);
5919 0         0 my ($ifd, $data, @cmyk, @spot);
5920              
5921             # get optional hash
5922 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
5923              
5924             # get remaining parameters
5925 0         0 my ($self, $path, $rows, $cols) = @_;
5926              
5927             # if row slice defined
5928 0 0       0 if (defined($rows)) {
5929            
5930             # if row slice an empty array reference
5931 0 0 0     0 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  0         0  
5932            
5933             # use all rows
5934 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5935            
5936             } else {
5937            
5938             # get row length if row slice is Math::Matrix object
5939 0 0       0 $trows = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix'));
  0         0  
5940            
5941             # flatten row slice
5942 0         0 $rows = ICC::Shared::flatten($rows);
5943            
5944             }
5945            
5946             } else {
5947            
5948             # use all rows
5949 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
5950            
5951             }
5952              
5953             # get number of rows
5954 0         0 $n = @{$rows};
  0         0  
5955              
5956             # filter row slice
5957 0 0 0     0 @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows};
  0         0  
  0         0  
  0         0  
5958              
5959             # warn if invalid samples
5960 0 0       0 ($n == @{$rows}) || warn('row slice contains invalid samples');
  0         0  
5961              
5962             # get target row length, if not defined by row matrix
5963 0 0       0 $trows = _getRowLength($self, $hash) if (! defined($trows));
5964              
5965             # limit to number of samples
5966 0 0       0 $trows = $trows > $n ? $n : $trows;
5967              
5968             # verify row length
5969 0 0 0     0 ($trows == int($trows) && $trows > 0) or croak('invalid row length, stopped');
5970              
5971             # compute target columns
5972 0 0       0 $tcols = int($n/$trows) + ($n % $trows ? 1 : 0);
5973              
5974             # if column slice defined
5975 0 0       0 if (defined($cols)) {
5976            
5977             # if column slice an empty array reference
5978 0 0 0     0 if (ref($cols) eq 'ARRAY' && @{$cols} == 0) {
  0         0  
5979            
5980             # use all columns
5981 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
5982            
5983             } else {
5984            
5985             # flatten column slice
5986 0         0 $cols = ICC::Shared::flatten($cols);
5987            
5988             }
5989            
5990             } else {
5991            
5992             # use all columns
5993 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
5994            
5995             }
5996              
5997             # map column slice, converting non-numeric values with 'test' method
5998 0 0       0 @{$cols} = map {Scalar::Util::looks_like_number($_) ? $_ : $self->test($_)} @{$cols};
  0         0  
  0         0  
  0         0  
5999              
6000             # get number of columns
6001 0         0 $n = @{$cols};
  0         0  
6002              
6003             # filter column slice
6004 0 0       0 @{$cols} = grep {$_ == int($_) && defined($self->[1][0][$_])} @{$cols};
  0         0  
  0         0  
  0         0  
6005              
6006             # warn if invalid fields
6007 0 0       0 ($n == @{$cols}) || warn('column slice contains invalid fields');
  0         0  
6008              
6009             # for each column in slice
6010 0         0 for (@{$self->[1][0]}[@{$cols}]) {
  0         0  
  0         0  
6011            
6012             # if a supported color space
6013 0 0       0 if (m/^((?:.*\|)?(RGB|CMYK|[4-9A-F]CLR|LAB)_)/) {
6014            
6015             # set base and color space
6016 0         0 $base = $1;
6017 0         0 $cs = $2;
6018            
6019             # quit loop
6020 0         0 last();
6021            
6022             }
6023            
6024             }
6025              
6026             # verify color space
6027 0 0       0 (defined($cs)) or croak('column slice does not contain a supported color space, stopped');
6028              
6029             # get bits per sample and verify
6030 0 0       0 $bits = defined($hash->{'bits'}) ? $hash->{'bits'} : 16;
6031 0 0 0     0 ($bits == 8 || $bits == 16 || $bits == 32) or croak('invalid \'bits\' parameter, stopped');
      0        
6032              
6033             # set little-endian flag from system config
6034 0         0 $le = ($Config{'byteorder'} =~ m/1234/);
6035              
6036             # if endian parameter provided
6037 0 0       0 if (defined($hash->{'endian'})) {
6038              
6039             # if little-endian
6040 0 0       0 if ($hash->{'endian'} eq 'little') {
    0          
6041            
6042             # set flag
6043 0         0 $le = 1;
6044            
6045             # if big-endian
6046             } elsif ($hash->{'endian'} eq 'big') {
6047            
6048             # clear flag
6049 0         0 $le = 0;
6050            
6051             } else {
6052            
6053             # warn
6054 0         0 warn('invalid \'endian\' parameter');
6055            
6056             }
6057            
6058             }
6059              
6060             # if little-endian
6061 0 0       0 if ($le) {
6062            
6063             # set 'pack' formats
6064 0         0 $short = 'v';
6065 0         0 $long = 'V';
6066 0         0 $fp = 'f<';
6067            
6068             } else {
6069            
6070             # set 'pack' formats
6071 0         0 $short = 'n';
6072 0         0 $long = 'N';
6073 0         0 $fp = 'f>';
6074            
6075             }
6076              
6077             # make lookup hash of column slice fields
6078 0 0       0 %fields = map {defined($self->[1][0][$_]) ? ($self->[1][0][$_], $_) : ()} @{$cols};
  0         0  
  0         0  
6079              
6080             # if color space is RGB
6081 0 0 0     0 if ($cs eq 'RGB') {
    0 0        
    0 0        
    0          
6082            
6083             # set photometric interpretation
6084 0         0 $pi = 2;
6085            
6086             # get alpha channels (if any)
6087 0 0       0 @alpha = map {defined($fields{"$base$_"}) ? $fields{"$base$_"} : ()} ('A', 'A0' .. 'A9');
  0         0  
6088            
6089             # get refined column slice (including alpha channels)
6090 0         0 $rcols = [(map {$fields{"$base$_"}} qw(R G B)), @alpha];
  0         0  
6091            
6092             # set pack format (8, 16 or 32 bits)
6093 0 0       0 $fmt = ($bits == 8) ? 'C*' : ($bits == 16) ? "$short*" : "$fp*";
    0          
6094            
6095             # set multiplier (8, 16 or 32 bits)
6096 0 0       0 $mult = ($bits == 8) ? 1 : ($bits == 16) ? 257 : 1/255;
    0          
6097            
6098             # if color space is CMYK (8 or 16 bits)
6099             } elsif ($cs eq 'CMYK' && $bits != 32) {
6100            
6101             # set photometric interpretation
6102 0         0 $pi = 5;
6103            
6104             # get refined column slice
6105 0         0 $rcols = [map {$fields{"$base$_"}} qw(C M Y K)];
  0         0  
6106            
6107             # set pack format (8 or 16 bits)
6108 0 0       0 $fmt = ($bits == 8) ? 'C*' : "$short*";
6109            
6110             # set multiplier (8 or 16 bits)
6111 0 0       0 $mult = ($bits == 8) ? 2.55 : 655.35;
6112            
6113             # if color space is nCLR (8 or 16 bits)
6114             } elsif ($cs =~ m/^([4-9A-F])CLR$/ && $bits != 32) {
6115            
6116             # set photometric interpretation
6117 0         0 $pi = 5;
6118            
6119             # get refined column slice
6120 0         0 $rcols = [map {$fields{sprintf('%s%x', $base, $_)}} (1 .. CORE::hex($1))];
  0         0  
6121            
6122             # set pack format (8 or 16 bits)
6123 0 0       0 $fmt = ($bits == 8) ? 'C*' : "$short*";
6124            
6125             # set multiplier (8 or 16 bits)
6126 0 0       0 $mult = ($bits == 8) ? 2.55 : 655.35;
6127            
6128             # if color space if L*a*b* (8 or 16 bits)
6129             } elsif ($cs eq 'LAB' && $bits != 32) {
6130            
6131             # set photometric interpretation
6132 0         0 $pi = 8;
6133            
6134             # get refined column slice
6135 0         0 $rcols = [map {$fields{"$base$_"}} qw(L A B)];
  0         0  
6136            
6137             # set pack format (8 or 16 bits)
6138 0 0       0 $fmt = ($bits == 8) ? '(Ccc)*' : "$short*";
6139            
6140             # set multipliers (8 or 16 bits)
6141 0 0       0 $mult = ($bits == 8) ? 2.55 : 655.35; # L*
6142 0 0       0 $mab = ($bits == 8) ? 1 : 256; # a* and b*
6143            
6144             } else {
6145            
6146             # error
6147 0         0 croak('invalid TIFF format');
6148            
6149             }
6150              
6151             # verify all fields defined
6152 0 0       0 (@{$rcols} == grep {defined()} @{$rcols}) or croak('column slice has missing fields, stopped');
  0         0  
  0         0  
  0         0  
6153              
6154             # set number of samples
6155 0         0 $samples = @{$rcols};
  0         0  
6156              
6157             # get the sample patch width and verify
6158 0 0       0 $width = defined($hash->{'width'}) ? $hash->{'width'} : 1;
6159 0 0 0     0 ($width == int($width) && $width > 0) or croak('invalid \'width\' parameter, stopped');
6160              
6161             # get the sample patch height and verify
6162 0 0       0 $height = defined($hash->{'height'}) ? $hash->{'height'} : 1;
6163 0 0 0     0 ($height == int($height) && $height > 0) or croak('invalid \'height\' parameter, stopped');
6164              
6165             # get the sample patch gap and verify
6166 0 0       0 $gap = defined($hash->{'gap'}) ? $hash->{'gap'} : 0;
6167 0 0 0     0 ($gap == int($gap) && $gap >= 0) or croak('invalid \'gap\' parameter, stopped');
6168              
6169             # get the left edge width and verify
6170 0 0       0 $left = defined($hash->{'left'}) ? $hash->{'left'} : 0;
6171 0 0 0     0 ($left =~ m/^([0-9]+)(?:\.([0-9]+))?$/ && (! defined($2) || $1 >= $2)) or croak('invalid \'left\' parameter, stopped');
      0        
6172 0 0       0 $left = [$1, defined($2) ? $2 : 0];
6173              
6174             # get the right edge width and verify
6175 0 0       0 $right = defined($hash->{'right'}) ? $hash->{'right'} : 0;
6176 0 0 0     0 ($right =~ m/^([0-9]+)(?:\.([0-9]+))?$/ && (! defined($2) || $1 >= $2)) or croak('invalid \'right\' parameter, stopped');
      0        
6177 0 0       0 $right = [$1, defined($2) ? $2 : 0];
6178              
6179             # get the x-resolution and verify
6180 0 0       0 $xres = defined($hash->{'xres'}) ? $hash->{'xres'} : 72;
6181 0 0 0     0 ($xres > 0 && $xres <= 4E4) or croak('invalid \'xres\' parameter, stopped');
6182              
6183             # get the y-resolution and verify
6184 0 0       0 $yres = defined($hash->{'yres'}) ? $hash->{'yres'} : 72;
6185 0 0 0     0 ($yres > 0 && $yres <= 4E4) or croak('invalid \'yres\' parameter, stopped');
6186              
6187             # get the resolution unit and verify
6188 0 0       0 $unit = defined($hash->{'unit'}) ? $hash->{'unit'} : 2;
6189 0 0 0     0 ($unit == 1 || $unit == 2 || $unit == 3) or croak('invalid \'unit\' parameter, stopped');
      0        
6190              
6191             # compute image width
6192 0         0 $imagewidth = $tcols * $width + ($tcols - 1) * $gap + $left->[0] - $left->[1] + $right->[0] - $right->[1];
6193              
6194             # compute strip byte count
6195 0         0 $bytecount = $imagewidth * $height * $samples * $bits/8;
6196              
6197             # compute strip size (strips must begin on word boundary)
6198 0         0 $stripsize = $bytecount + $bytecount % 2;
6199              
6200             # set image tags [type, data]
6201 0         0 $tags->{'256'} = [3, $imagewidth]; # ImageWidth
6202 0         0 $tags->{'257'} = [3, $trows * $height]; # ImageLength
6203 0         0 $tags->{'258'} = [3, ($bits) x $samples]; # BitsPerSample
6204 0         0 $tags->{'259'} = [3, 1]; # Compression
6205 0         0 $tags->{'262'} = [3, $pi]; # PhotometricInterpretation
6206 0         0 $tags->{'273'} = [4, map {$_ * $stripsize + 8} (0 .. $trows - 1)]; # StripOffsets
  0         0  
6207 0         0 $tags->{'277'} = [3, $samples]; # SamplesPerPixel
6208 0         0 $tags->{'278'} = [3, $height]; # RowsPerStrip
6209 0         0 $tags->{'279'} = [4, ($bytecount) x $trows]; # StripByteCounts
6210 0         0 $tags->{'282'} = [5, $xres * 1E4, 1E4]; # XResolution
6211 0         0 $tags->{'283'} = [5, $yres * 1E4, 1E4]; # YResolution
6212 0         0 $tags->{'296'} = [3, $unit]; # ResolutionUnit
6213 0 0       0 $tags->{'339'} = [3, (3) x $samples] if ($bits == 32); # SampleFormat
6214              
6215             # filter path
6216 0         0 ICC::Shared::filterPath($path);
6217              
6218             # open the file
6219 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
6220              
6221             # set binary mode
6222 0         0 binmode($fh);
6223              
6224             # write TIFF header
6225 0 0       0 print $fh pack("A2$short$long", $le ? 'II' : 'MM', 42, $ifd = $trows * $stripsize + 8);
6226              
6227             # set min/max values
6228 0 0       0 $max = ($bits == 8) ? 255 : ($bits == 16) ? 65535 : 1;
    0          
6229 0 0       0 $minab = ($bits == 8) ? -128 : -32768;
6230 0 0       0 $maxab = ($bits == 8) ? 127 : 32767;
6231              
6232             # for each strip
6233 0         0 for my $i (0 .. $trows - 1) {
6234            
6235             # for each patch in strip
6236 0         0 for my $j (0 .. $tcols - 1) {
6237            
6238             # if patch in row slice
6239 0 0       0 if (defined($rows->[$trows * $j + $i])) {
6240            
6241             # if L*a*b* data
6242 0 0 0     0 if ($pi == 8) {
    0          
6243            
6244             # get the data
6245 0         0 $data->[$j][0] = $mult * $self->[1][$rows->[$trows * $j + $i]][$rcols->[0]];
6246 0         0 $data->[$j][1] = $mab * $self->[1][$rows->[$trows * $j + $i]][$rcols->[1]];
6247 0         0 $data->[$j][2] = $mab * $self->[1][$rows->[$trows * $j + $i]][$rcols->[2]];
6248            
6249             # limit the data
6250 0 0       0 $data->[$j][0] = $data->[$j][0] < 0 ? 0 : ($data->[$j][0] > $max ? $max : $data->[$j][0]);
    0          
6251 0 0       0 $data->[$j][1] = $data->[$j][1] < $minab ? $minab : ($data->[$j][1] > $maxab ? $maxab : $data->[$j][1]);
    0          
6252 0 0       0 $data->[$j][2] = $data->[$j][2] < $minab ? $minab : ($data->[$j][2] > $maxab ? $maxab : $data->[$j][2]);
    0          
6253            
6254             # if CMYK + spot data
6255 0         0 } elsif ($pi == 5 && @{$rcols} > 4) {
6256            
6257             # get CMYK values
6258 0         0 @cmyk = @{$self->[1][$rows->[$trows * $j + $i]]}[@{$rcols}[0 .. 3]];
  0         0  
  0         0  
6259            
6260             # get spot values
6261 0         0 @spot = @{$self->[1][$rows->[$trows * $j + $i]]}[@{$rcols}[4 .. $#{$rcols}]];
  0         0  
  0         0  
  0         0  
6262            
6263             # get the data (spot channels are inverted)
6264 0         0 $data->[$j] = [(map {$_ * $mult} @cmyk), (map {(100 - $_) * $mult} @spot)];
  0         0  
  0         0  
6265            
6266             # limit the data
6267 0 0       0 @{$data->[$j]} = map {$_ < 0 ? 0 : ($_ > $max ? $max : $_)} @{$data->[$j]};
  0 0       0  
  0         0  
  0         0  
6268            
6269             # RGB data
6270             } else {
6271            
6272             # get the data
6273 0         0 $data->[$j] = [map {$_ * $mult} @{$self->[1][$rows->[$trows * $j + $i]]}[@{$rcols}]];
  0         0  
  0         0  
  0         0  
6274            
6275             # limit the data (8 or 16 bits)
6276 0 0       0 @{$data->[$j]} = map {$_ < 0 ? 0 : ($_ > $max ? $max : $_)} @{$data->[$j]} if ($bits != 32);
  0 0       0  
  0 0       0  
  0         0  
6277            
6278             }
6279            
6280             # patch undefined
6281             } else {
6282            
6283             # if L*a*b* data
6284 0 0       0 if ($pi == 8) {
    0          
6285            
6286             # if last patch
6287 0 0 0     0 if ($i == ($trows - 1) && $j == ($tcols - 1)) {
6288            
6289             # set gray value
6290 0         0 $data->[$j] = [$max * 0.7, 0, 0];
6291            
6292             } else {
6293            
6294             # set white value
6295 0         0 $data->[$j] = [$max, 0, 0];
6296            
6297             }
6298            
6299             # if CMYK + spot data
6300             } elsif ($pi == 5) {
6301            
6302             # if last patch
6303 0 0 0     0 if ($i == ($trows - 1) && $j == ($tcols - 1)) {
6304            
6305             # set gray value
6306 0         0 $data->[$j] = [0, 0, 0, $max * 0.4, ($max) x ($samples - 4)];
6307            
6308             } else {
6309            
6310             # set white value
6311 0         0 $data->[$j] = [0, 0, 0, 0, ($max) x ($samples - 4)];
6312            
6313             }
6314            
6315             # RGB data
6316             } else {
6317            
6318             # if last patch
6319 0 0 0     0 if ($i == ($trows - 1) && $j == ($tcols - 1)) {
6320            
6321             # set gray value
6322 0         0 $data->[$j] = [($max * 0.7) x $samples];
6323            
6324             } else {
6325            
6326             # set white value
6327 0         0 $data->[$j] = [($max) x $samples];
6328            
6329             }
6330            
6331             }
6332            
6333             }
6334            
6335             }
6336            
6337             # write TIFF strip
6338 0         0 _writeTIFFstrip($fh, $tags, $width, $gap, $left, $right, $i, $data, $fmt, $hash->{'dither'});
6339            
6340             }
6341              
6342             # write TIFF IFD
6343 0         0 _writeTIFFdir($fh, $ifd, $short, $long, $tags);
6344              
6345             # close file
6346 0         0 close($fh);
6347              
6348             }
6349              
6350             # write chart to Adobe Swatch Exchange (.ase) file
6351             # column slice must be CMYK, RGB or L*a*b*
6352             # color type: 0 - global, 1 - spot, 2 - normal (default)
6353             # parameters: (path_to_file, row_slice, column_slice, [color_type])
6354             sub writeASE {
6355              
6356             # get parameters
6357 0     0 1 0 my ($self, $path, $rows, $cols, $type) = @_;
6358              
6359             # local variables
6360 0         0 my ($n, @fmt, $cs, $le, $sn, $fh);
6361 0         0 my ($name, $slen, $blen);
6362 0         0 my ($cmyk, $rgb, $Lab, $val);
6363              
6364             # verify row_slice and column_slice are supplied
6365 0 0 0     0 (defined($rows) && defined($cols)) or croak('missing parameters');
6366              
6367             # if row slice an empty array reference
6368 0 0 0     0 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  0         0  
6369            
6370             # use all rows
6371 0         0 $rows = [1 .. $#{$self->[1]}];
  0         0  
6372            
6373             } else {
6374            
6375             # flatten row slice
6376 0         0 $rows = ICC::Shared::flatten($rows);
6377            
6378             }
6379              
6380             # get number of rows
6381 0         0 $n = @{$rows};
  0         0  
6382              
6383             # filter row slice
6384 0 0 0     0 @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows};
  0         0  
  0         0  
  0         0  
6385              
6386             # warn if invalid samples
6387 0 0       0 ($n == @{$rows}) || warn('row slice contains invalid samples');
  0         0  
6388              
6389             # map column slice, converting non-numeric values with 'test' method
6390 0 0       0 @{$cols} = map {Scalar::Util::looks_like_number($_) ? $_ : $self->test($_)} @{$cols};
  0         0  
  0         0  
  0         0  
6391              
6392             # get format array
6393 0         0 @fmt = @{$self->[1][0]}[@{$cols}];
  0         0  
  0         0  
6394              
6395             # if column slice is CMYK
6396 0 0 0     0 if (4 == @fmt && 4 == grep {m/^(?:.*\|)?CMYK_[CMYK]$/} @fmt) {
  0 0 0     0  
    0 0        
6397            
6398             # set color space
6399 0         0 $cs = 'CMYK';
6400            
6401             # if column slice is RGB
6402 0         0 } elsif (3 == @fmt && 3 == grep {m/^(?:.*\|)?RGB_[RGB]$/} @fmt) {
6403            
6404             # set color space
6405 0         0 $cs = 'RGB ';
6406            
6407             # if column slice is L*a*b*
6408 0         0 } elsif (3 == @fmt && 3 == grep {m/^(?:.*\|)?LAB_[LAB]$/} @fmt) {
6409            
6410             # set color space
6411 0         0 $cs = 'LAB ';
6412            
6413             } else {
6414            
6415             # error
6416 0         0 croak('invalid column slice');
6417            
6418             }
6419              
6420             # if color type is undefined, set default (2 - normal)
6421 0 0       0 $type = 2 if (! defined($type));
6422              
6423             # verify color type
6424 0 0 0     0 ($type == int($type) && $type >= 0 && $type <= 2) or croak('invalid ASE color type');
      0        
6425              
6426             # get little-endian flag
6427 0         0 $le = ($Config{'byteorder'} =~ m/1234/);
6428              
6429             # get sample name slice (could be undefined)
6430 0         0 $sn = $self->name;
6431              
6432             # filter path
6433 0         0 ICC::Shared::filterPath($path);
6434              
6435             # open the file
6436 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
6437              
6438             # set binary mode
6439 0         0 binmode($fh);
6440              
6441             # print header (file signature, version, number of blocks)
6442 0         0 print $fh pack('A4nnN', 'ASEF', 1, 0, scalar(@{$rows}));
  0         0  
6443              
6444             # for each sample
6445 0         0 for my $s (@{$rows}) {
  0         0  
6446            
6447             # if color space is CMYK
6448 0 0       0 if ($cs eq 'CMYK') {
    0          
    0          
6449            
6450             # get the CMYK values
6451 0         0 $cmyk = $self->slice([$s], $cols);
6452            
6453             # if SAMPLE_NAME is defined
6454 0 0       0 if (defined($sn)) {
6455            
6456             # get color name
6457 0         0 $name = $self->[1][$s][$sn->[0]];
6458            
6459             # replace underscores with spaces
6460 0         0 $name =~ s/_/ /g;
6461            
6462             } else {
6463            
6464             # build color name from CMYK values
6465 0         0 $name = sprintf('C=%d M=%d Y=%d K=%d', @{$cmyk->[0]});
  0         0  
6466            
6467             }
6468            
6469             # compute string length
6470 0         0 $slen = length($name) + 1;
6471            
6472             # compute block length
6473 0         0 $blen = 2 * $slen + 24;
6474            
6475             # print block
6476 0         0 print $fh pack('nNn', 1, $blen, $slen);
6477 0         0 print $fh encode('UTF-16BE', $name . "\x00");
6478 0         0 print $fh pack('A4', 'CMYK');
6479            
6480             # for each CMYK value
6481 0         0 for my $i (0 .. 3) {
6482            
6483             # convert to floating point
6484 0         0 $val = pack('f', $cmyk->[0][$i]/100);
6485            
6486             # reverse if little-endian system
6487 0 0       0 $val = reverse($val) if ($le);
6488            
6489             # print value
6490 0         0 print $fh $val;
6491            
6492             }
6493            
6494             # print color type
6495 0         0 print $fh pack('n', $type);
6496            
6497             # if color space is RGB
6498             } elsif ($cs eq 'RGB ') {
6499            
6500             # get the RGB values
6501 0         0 $rgb = $self->slice([$s], $cols);
6502            
6503             # if SAMPLE_NAME is defined
6504 0 0       0 if (defined($sn)) {
6505            
6506             # get color name
6507 0         0 $name = $self->[1][$s][$sn->[0]];
6508            
6509             # replace underscores with spaces
6510 0         0 $name =~ s/_/ /g;
6511            
6512             } else {
6513            
6514             # build color name from RGB values
6515 0         0 $name = sprintf('R=%d G=%d B=%d', @{$rgb->[0]});
  0         0  
6516            
6517             }
6518            
6519             # compute string length
6520 0         0 $slen = length($name) + 1;
6521            
6522             # compute block length
6523 0         0 $blen = 2 * $slen + 20;
6524            
6525             # print block
6526 0         0 print $fh pack('nNn', 1, $blen, $slen);
6527 0         0 print $fh encode('UTF-16BE', $name . "\x00");
6528 0         0 print $fh pack('A4', 'RGB ');
6529            
6530             # for each RGB value
6531 0         0 for my $i (0 .. 2) {
6532            
6533             # convert to floating point
6534 0         0 $val = pack('f', $rgb->[0][$i]/255);
6535            
6536             # reverse if little-endian system
6537 0 0       0 $val = reverse($val) if ($le);
6538            
6539             # print value
6540 0         0 print $fh $val;
6541            
6542             }
6543            
6544             # print color type
6545 0         0 print $fh pack('n', $type);
6546            
6547             # if color space is L*a*b*
6548             } elsif ($cs eq 'LAB ') {
6549            
6550             # get the L*a*b* values
6551 0         0 $Lab = $self->slice([$s], $cols);
6552            
6553             # if SAMPLE_NAME is defined
6554 0 0       0 if (defined($sn)) {
6555            
6556             # get color name
6557 0         0 $name = $self->[1][$s][$sn->[0]];
6558            
6559             # replace underscores with spaces
6560 0         0 $name =~ s/_/ /g;
6561            
6562             } else {
6563            
6564             # build color name from L*a*b* values
6565 0         0 $name = sprintf('L=%d a=%d b=%d', @{$Lab->[0]});
  0         0  
6566            
6567             }
6568            
6569             # compute string length
6570 0         0 $slen = length($name) + 1;
6571            
6572             # compute block length
6573 0         0 $blen = 2 * $slen + 20;
6574            
6575             # print block
6576 0         0 print $fh pack('nNn', 1, $blen, $slen);
6577 0         0 print $fh encode('UTF-16BE', $name . "\x00");
6578 0         0 print $fh pack('A4', 'LAB ');
6579            
6580             # modify L* value
6581 0         0 $Lab->[0][0] /= 100;
6582            
6583             # for each L*a*b* value
6584 0         0 for my $i (0 .. 2) {
6585            
6586             # convert to floating point
6587 0         0 $val = pack('f', $Lab->[0][$i]);
6588            
6589             # reverse if little-endian system
6590 0 0       0 $val = reverse($val) if ($le);
6591            
6592             # print value
6593 0         0 print $fh $val;
6594            
6595             }
6596            
6597             # print color type
6598 0         0 print $fh pack('n', $type);
6599            
6600             }
6601            
6602             }
6603              
6604             # close file
6605 0         0 close($fh);
6606              
6607             }
6608              
6609             # print object contents to string
6610             # format is an array structure
6611             # parameter: ([format])
6612             # returns: (string)
6613             sub sdump {
6614              
6615             # get parameters
6616 0     0 1 0 my ($self, $p) = @_;
6617              
6618             # local variables
6619 0         0 my ($s, $fmt);
6620              
6621             # resolve parameter to an array reference
6622 0 0       0 $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
6623              
6624             # get format string
6625 0 0 0     0 $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef';
6626              
6627             # set string to object ID
6628 0         0 $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
6629              
6630             # return
6631 0         0 return($s);
6632              
6633             }
6634              
6635             # get column slice from DATA_FORMAT keys
6636             # format_keys is a list of keys with optional context
6637             # column_slice is reference to an array of column indices
6638             # note: returns 'undef' if any column is missing
6639             # parameters: (format_keys)
6640             # returns: (column_slice)
6641             sub _cols {
6642              
6643             # get object reference
6644 0     0   0 my $self = shift();
6645              
6646             # local variables
6647 0         0 my (%fmt, @cols);
6648              
6649             # make lookup hash of DATA_FORMAT keys
6650 0 0       0 %fmt = map {defined($self->[1][0][$_]) ? ($self->[1][0][$_], $_) : ()} (0 .. $#{$self->[1][0]});
  0         0  
  0         0  
6651              
6652             # lookup format keys in hash
6653 0         0 @cols = @fmt{@_};
6654              
6655             # return column slice or undef if any columns undefined
6656 0 0       0 return((grep {! defined()} @cols) ? undef : \@cols);
  0         0  
6657              
6658             }
6659              
6660             # get spectral fields array
6661             # array contains column indices and wavelength
6662             # and is sorted by wavelength (low to high)
6663             # parameters: (object_reference, [context])
6664             # returns: (array_reference)
6665             sub _spectral {
6666              
6667             # get parameters
6668 45     45   93 my ($self, $context) = @_;
6669              
6670             # local variables
6671 45         62 my (%fmt, @fields);
6672              
6673             # make lookup hash (context|wavelength -or- wavelength => column)
6674 45 100       54 %fmt = map {($self->[1][0][$_] =~ m/^(.*\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)(\d{3})$/) ? (defined($1) ? "$1$2" : $2, $_) : ()} (0 .. $#{$self->[1][0]});
  1679 100       6240  
  45         102  
6675              
6676             # if context defined
6677 45 100       175 if (defined($context)) {
6678            
6679             # make list of matching fields
6680 18 100       101 @fields = map {m/^$context\|(\d{3})$/ ? [$fmt{$_}, $1] : ()} keys(%fmt);
  540         1886  
6681            
6682             } else {
6683            
6684             # make list of matching fields
6685 27 100       141 @fields = map {m/^(\d{3})$/ ? [$fmt{$_}, $1] : ()} keys(%fmt);
  900         1822  
6686            
6687             # if no matching fields
6688 27 100       102 if (@fields == 0) {
6689            
6690             # make lookup hash (wavelength => column)
6691 14 100       29 %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)(\d{3})$/) ? ($1, $_) : ()} (0 .. $#{$self->[1][0]});
  503         1611  
  14         31  
6692            
6693             # make list of fields
6694 14         97 @fields = map {[$fmt{$_}, $_]} keys(%fmt);
  432         682  
6695            
6696             }
6697            
6698             }
6699              
6700             # return undef if no match
6701 45 100       143 return() if (@fields == 0);
6702              
6703             # sort by wavelength
6704 37         125 @fields = sort {$a->[1] <=> $b->[1]} @fields;
  5418         5758  
6705              
6706             # return array reference
6707 37         187 return(\@fields);
6708              
6709             }
6710              
6711             # fix incorrectly scaled (X-Rite) data
6712             # checks spectral, XYZ and xyY data
6713             # parameters: (object_ref)
6714             sub _scale_check {
6715              
6716             # get object reference
6717 19     19   32 my $self = shift();
6718              
6719             # local variables
6720 19         33 my (@s, $pct, $cie);
6721              
6722             # get indices of suspect spectral values
6723 19         27 @s = grep {$self->[1][0][$_] =~ /^SPECTRAL_(NM)?\d{3}$/} (0 .. $#{$self->[1][0]});
  410         516  
  19         43  
6724              
6725             # if format found
6726 19 50       44 if (@s) {
6727            
6728             # for each sample
6729 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
6730            
6731             # if average value > 2 (must be %-values)
6732 0 0       0 if (List::Util::sum(@{$self->[1][$i]}[@s])/@s > 2) {
  0         0  
6733            
6734             # set %-value flag
6735 0         0 $pct = 1;
6736            
6737 0         0 last;
6738            
6739             }
6740            
6741             }
6742            
6743             # if %-values
6744 0 0       0 if ($pct) {
6745            
6746             # for each sample
6747 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
6748            
6749             # for each spectral value
6750 0         0 for my $j (@s) {
6751            
6752             # fix value
6753 0         0 $self->[1][$i][$j] /= 100;
6754            
6755             }
6756            
6757             }
6758            
6759             }
6760            
6761             }
6762              
6763             # get indices of XYZ or XYY values
6764 19         24 @s = grep {$self->[1][0][$_] =~ /^(XYZ_[XYZ]|XYY_CAPY)$/} (0 .. $#{$self->[1][0]});
  410         669  
  19         80  
6765              
6766             # if format found
6767 19 100       43 if (@s) {
6768            
6769             # for each sample
6770 13         15 for my $i (1 .. $#{$self->[1]}) {
  13         32  
6771            
6772             # if average value > 2 (must be CIE values)
6773 13 50       23 if (List::Util::sum(@{$self->[1][$i]}[@s])/@s > 2) {
  13         77  
6774            
6775             # set CIE flag
6776 13         18 $cie = 1;
6777            
6778 13         28 last;
6779            
6780             }
6781            
6782             }
6783            
6784             # if not CIE values
6785 13 50       31 if (! $cie) {
6786            
6787             # for each sample
6788 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
6789            
6790             # for each value
6791 0         0 for my $j (@s) {
6792            
6793             # fix value
6794 0         0 $self->[1][$i][$j] *= 100;
6795            
6796             }
6797            
6798             }
6799            
6800             }
6801            
6802             }
6803            
6804             }
6805              
6806             # binary search
6807             # locates the interval containing or bounding the target value
6808             # returns an array of four index values, which indicate upper and lower transitions
6809             # parameters: (source_array, target_value, channel_index, low_index, high_index)
6810             # returns: (interval_index_array)
6811             sub _bin_search {
6812            
6813             # get parameters
6814 0     0   0 my ($source, $target, $channel, $low, $high) = @_;
6815            
6816             # local variables
6817 0         0 my ($k, $interval);
6818            
6819             # copy low and high indices
6820 0         0 $interval->[0] = $low;
6821 0         0 $interval->[1] = $high;
6822            
6823             # while interval is open
6824 0         0 while ($interval->[1] - $interval->[0] > 1) {
6825            
6826             # compute the midpoint
6827 0         0 $k = int(($interval->[1] + $interval->[0])/2);
6828            
6829             # if midpoint value >= target value
6830 0 0       0 if ($source->[$k][$channel] >= $target) {
6831            
6832             # set higher index to midpoint
6833 0         0 $interval->[1] = $k;
6834            
6835             } else {
6836            
6837             # set lower index to midpoint
6838 0         0 $interval->[0] = $k;
6839            
6840             }
6841            
6842             }
6843            
6844             # copy low and high indices
6845 0         0 $interval->[2] = $low;
6846 0         0 $interval->[3] = $high;
6847            
6848             # while interval is open
6849 0         0 while ($interval->[3] - $interval->[2] > 1) {
6850            
6851             # compute the midpoint
6852 0         0 $k = int(($interval->[3] + $interval->[2])/2);
6853            
6854             # if midpoint value > target value
6855 0 0       0 if ($source->[$k][$channel] > $target) {
6856            
6857             # set higher index to midpoint
6858 0         0 $interval->[3] = $k;
6859            
6860             } else {
6861            
6862             # set lower index to midpoint
6863 0         0 $interval->[2] = $k;
6864            
6865             }
6866            
6867             }
6868            
6869             # return interval array
6870 0         0 return($interval);
6871            
6872             }
6873              
6874             # linear search
6875             # locates the closest source sample based on Manhattan distance
6876             # parameters: (source_array, target_vector)
6877             # returns: (low_index, high_index)
6878             sub _lin_search {
6879            
6880             # get parameters
6881 0     0   0 my ($source, $target) = @_;
6882            
6883             # local variables
6884 0         0 my ($d0, $d1, $d2, $low, $high);
6885            
6886             # set initial difference
6887 0         0 $d0 = @{$target};
  0         0  
6888            
6889             # for each source sample
6890 0         0 for my $i (0 .. $#{$source}) {
  0         0  
6891            
6892             # clear differences
6893 0         0 $d1 = $d2 = 0;
6894            
6895             # for each channel
6896 0         0 for my $j (0 .. $#{$target}) {
  0         0  
6897            
6898             # add difference to target sample
6899 0         0 $d1 += abs($source->[$i][$j] - $target->[$j]);
6900            
6901             # add difference to previous sample
6902 0 0       0 $d2 += abs($source->[$i][$j] - $source->[$i - 1][$j]) if ($i > 0);
6903            
6904             }
6905            
6906             # if new difference less
6907 0 0       0 if ($d1 < $d0) {
6908            
6909             # save index
6910 0         0 $low = $high = $i;
6911            
6912             # update difference
6913 0         0 $d0 = $d1;
6914            
6915             }
6916            
6917             # if duplicate sample
6918 0 0 0     0 if ($d0 == $d1 && $d2 == 0) {
6919            
6920             # save index
6921 0         0 $high = $i;
6922            
6923             }
6924            
6925             }
6926            
6927             # return
6928 0         0 return($low, $high);
6929            
6930             }
6931              
6932             # add average sample
6933             # assumes device values (if any) are same for each sample
6934             # averages measurements values - spectral, XYZ, L*a*b*, or density
6935             # L*a*b* values are converted to xyz for averaging, then back to L*a*b*
6936             # density values are converted to reflectance for averaging, then back to density
6937             # parameters: (object_reference, row_slice, linear_slice, L*a*b*_slice, density_slice, id_slice, name_slice, hash)
6938             # returns: (average_sample_index)
6939             sub _add_avg {
6940              
6941             # get parameters
6942 0     0   0 my ($self, $rows, $c1, $c2, $c3, $id, $name, $hash) = @_;
6943              
6944             # local variables
6945 0         0 my ($n, $next, @xyz, $sid, $sn);
6946              
6947             # get number of samples
6948 0         0 $n = @{$rows};
  0         0  
6949              
6950             # get index of next data row
6951 0         0 $next = $#{$self->[1]} + 1;
  0         0  
6952              
6953             # copy first sample
6954 0         0 $self->[1][$next] = [@{$self->[1][shift(@{$rows})]}];
  0         0  
  0         0  
6955              
6956             # for each group of L*a*b* columns
6957 0         0 for (my $j = 0; $j < @{$c2}; $j += 3) {
  0         0  
6958            
6959             # convert to L*a*b* values to xyz
6960 0         0 @{$self->[1][$next]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_Lab2xyz(@{$self->[1][$next]}[@{$c2}[$j .. $j + 2]]);
  0         0  
  0         0  
  0         0  
  0         0  
6961            
6962             }
6963            
6964             # for each density column
6965 0         0 for my $j (@{$c3}) {
  0         0  
6966            
6967             # convert to density to reflectance
6968 0         0 $self->[1][$next][$j] = POSIX::pow(10, -$self->[1][$next][$j]);
6969            
6970             }
6971              
6972             # for remaining samples
6973 0         0 for my $i (@{$rows}) {
  0         0  
6974            
6975             # for each linear column
6976 0         0 for my $j (@{$c1}) {
  0         0  
6977            
6978             # add value
6979 0         0 $self->[1][$next][$j] += $self->[1][$i][$j];
6980            
6981             }
6982            
6983             # for each group of L*a*b* columns
6984 0         0 for (my $j = 0; $j < @{$c2}; $j += 3) {
  0         0  
6985            
6986             # get xyz values
6987 0         0 @xyz = ICC::Shared::_Lab2xyz(@{$self->[1][$i]}[@{$c2}[$j .. $j + 2]]);
  0         0  
  0         0  
6988            
6989             # add to self
6990 0         0 $self->[1][$next][$c2->[$j]] += $xyz[0];
6991 0         0 $self->[1][$next][$c2->[$j + 1]] += $xyz[1];
6992 0         0 $self->[1][$next][$c2->[$j + 2]] += $xyz[2];
6993            
6994             }
6995            
6996             # for each density column
6997 0         0 for my $j (@{$c3}) {
  0         0  
6998            
6999             # add temp reflectance
7000 0         0 $self->[1][$next][$j] += POSIX::pow(10, -$self->[1][$i][$j]);
7001            
7002             }
7003            
7004             }
7005              
7006             # for each measurement column
7007 0         0 for my $j (@{$c1}, @{$c2}, @{$c3}) {
  0         0  
  0         0  
  0         0  
7008            
7009             # divide by number of samples
7010 0         0 $self->[1][$next][$j] /= $n;
7011            
7012             }
7013              
7014             # for each group of L*a*b* columns
7015 0         0 for (my $j = 0; $j < @{$c2}; $j += 3) {
  0         0  
7016            
7017             # convert to xyz values to L*a*b*
7018 0         0 @{$self->[1][$next]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_xyz2Lab(@{$self->[1][$next]}[@{$c2}[$j .. $j + 2]]);
  0         0  
  0         0  
  0         0  
  0         0  
7019            
7020             }
7021              
7022             # for each density column
7023 0         0 for my $j (@{$c3}) {
  0         0  
7024            
7025             # convert to reflectance to density
7026 0         0 $self->[1][$next][$j] = -POSIX::log10($self->[1][$next][$j]);
7027            
7028             }
7029              
7030             # get SAMPLE_ID value from hash
7031 0         0 $sid = $hash->{'id'};
7032              
7033             # for each SAMPLE_ID column
7034 0         0 for my $i (@{$id}) {
  0         0  
7035            
7036             # if SAMPLE_ID defined
7037 0 0       0 if (defined($sid)) {
7038            
7039             # set to hash value
7040 0         0 $self->[1][$next][$i] = $sid;
7041            
7042             } else {
7043            
7044             # set to row index
7045 0         0 $self->[1][$next][$i] = $next;
7046            
7047             }
7048            
7049             }
7050              
7051             # get SAMPLE_NAME value from hash
7052 0         0 $sn = $hash->{'name'};
7053              
7054             # for each SAMPLE_NAME column
7055 0         0 for my $i (@{$name}) {
  0         0  
7056            
7057             # if SAMPLE_NAME defined
7058 0 0       0 if (defined($sn)) {
7059            
7060             # set to hash value
7061 0         0 $self->[1][$next][$i] = $sn;
7062            
7063             } else {
7064            
7065             # append '_AVG' to existing value
7066 0         0 $self->[1][$next][$i] .= '_AVG';
7067            
7068             }
7069            
7070             }
7071              
7072             # return row
7073 0         0 return($next);
7074              
7075             }
7076              
7077             # get averaging groups
7078             # returns column slices for each averaging method
7079             # parameters: (object_reference, hash)
7080             # returns: (linear_slice, L*a*b*_slice, density_slice)
7081             sub _avg_groups {
7082              
7083             # get parameters
7084 1     1   3 my ($self, $hash) = @_;
7085              
7086             # local variables
7087 1         1 my (@c1, @c2, @c3, @cs);
7088              
7089             # for each format field
7090 1         3 for my $i (0 .. $#{$self->[1][0]}) {
  1         3  
7091            
7092             # add column if XYZ or spectral field
7093 12 100       33 push(@c1, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?(?:XYZ_[XYZ]|(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3})$/);
7094            
7095             # add column if L*a*b* field
7096 12 100       25 push(@c2, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?LAB_[LAB]$/);
7097            
7098             # add column if density field
7099 12 50       21 push(@c3, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?D_(?:RED|GREEN|BLUE|VIS)$/);
7100            
7101             }
7102            
7103             # linear averaging method (L*a*b* values are converted to xyz, density values are converted to reflectance)
7104 1 50 33     5 if (! defined($hash->{'method'}) || $hash->{'method'} eq 'LINEAR') {
    0 0        
7105            
7106             # verify number of L*a*b* fields
7107 1 50       4 (@c2 % 3 == 0) or croak('wrong number of L*a*b* fields');
7108            
7109             # for each group of L*a*b* columns
7110 1         4 for (my $j = 0; $j < @c2; $j += 3) {
7111            
7112             # sort by field name
7113 1         6 @cs = sort {$self->[1][0][$a] cmp $self->[1][0][$b]} @c2[$j .. $j + 2];
  3         9  
7114            
7115             # verify field consistency
7116 1 50       4 (join('', map {substr($_, -1, 1)} @{$self->[1][0]}[@cs]) eq 'ABL') or croak('L*a*b* field inconsistency');
  3         9  
  1         3  
7117            
7118             # save columns in LAB order
7119 1         6 @c2[$j .. $j + 2] = @cs[2, 0, 1];
7120            
7121             }
7122            
7123             # if simple averaging method
7124             } elsif (defined($hash->{'method'}) && $hash->{'method'} eq 'SIMPLE') {
7125            
7126             # copy L*a*b* and density columns to XYZ or spectral array
7127 0         0 push(@c1, @c2, @c3);
7128            
7129             # clear L*a*b* and density arrays
7130 0         0 @c2 = ();
7131 0         0 @c3 = ();
7132            
7133             } else {
7134            
7135             # error
7136 0         0 croak('unsupported averaging method');
7137            
7138             }
7139              
7140             # return slices
7141 1         4 return(\@c1, \@c2, \@c3);
7142              
7143             }
7144              
7145             # add OBA effect to XYZ array
7146             # parameters: (chart_object, M1_slice, M2_slice, XYZ_array, oba_factor, hash)
7147             sub _add_oba {
7148              
7149             # get parameters
7150 0     0   0 my ($self, $spec1, $spec2, $xyz, $oba, $hash) = @_;
7151              
7152             # local variables
7153 0         0 my ($color, $illum, @m1, @m2, $spectral, $xyzoba);
7154              
7155             # save illuminant
7156 0         0 $illum = $hash->{'illuminant'};
7157              
7158             # if illuminant an array reference
7159 0 0 0     0 if (defined($hash->{'illuminant'}) && ref($hash->{'illuminant'}) eq 'ARRAY') {
7160            
7161             # set illuminant to CIE D50
7162 0         0 $hash->{'illuminant'} = ['CIE', 'D50'];
7163            
7164             } else {
7165            
7166             # set illuminant to ASTM D50
7167 0         0 $hash->{'illuminant'} = 'D50';
7168            
7169             }
7170              
7171             # make 'Color.pm' object (D50 illuminant)
7172 0         0 $color = ICC::Support::Color->new($hash);
7173              
7174             # restore illuminant
7175 0         0 $hash->{'illuminant'} = $illum;
7176              
7177             # for each sample
7178 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
7179            
7180             # get M1 spectral values
7181 0         0 @m1 = @{$self->[1][$i]}[@{$spec1}];
  0         0  
  0         0  
7182            
7183             # get M2 spectral values
7184 0         0 @m2 = @{$self->[1][$i]}[@{$spec2}];
  0         0  
  0         0  
7185            
7186             # compute (M1 - M2) spectral values
7187 0         0 $spectral->[$i - 1] = [map {$m1[$_] - $m2[$_]} (0 .. $#m1)];
  0         0  
7188            
7189             }
7190              
7191             # transform (M1 - M2) spectral to D50 XYZ (hash may contain 'encoding' key)
7192 0         0 $xyzoba = ICC::Support::Color::_trans2($color, $spectral, $hash);
7193              
7194             # for each sample
7195 0         0 for my $i (0 .. $#{$xyz}) {
  0         0  
7196            
7197             # for each XYZ
7198 0         0 for my $j (0 .. 2) {
7199            
7200             # add scaled OBA effect
7201 0         0 $xyz->[$i][$j] += $xyzoba->[$i][$j] * $oba;
7202            
7203             }
7204            
7205             }
7206            
7207             }
7208              
7209             # get/set data
7210             # common routine called by get/set methods
7211             # row_slice and column_slice may be either a scalar or array reference
7212             # an empty array reference indicates all samples or fields
7213             # replacement_data is reference to a 2-D array of replacement values
7214             # array dimensions must match size of row_slice and column_slice
7215             # data_slice is Math::Matrix object, defined by row_slice and column_slice
7216             # get_code_ref and set_code_ref transform the data when getting and setting
7217             # parameters: (object_ref, object_index, row_slice, column_slice, replacement_data, get_code_ref, set_code_ref)
7218             # if column_slice undefined, returns: ()
7219             # if row_slice undefined, returns: (column_slice)
7220             # if replacement_data undefined, returns: (data_slice)
7221             # otherwise, sets replacement data and returns: (column_slice)
7222             sub _getset {
7223              
7224             # get parameters
7225 403     403   731 my ($self, $ix, $rows, $cols, $data, $get, $set) = @_;
7226              
7227             # return empty if no column slice
7228 403 100       980 defined($cols) || return();
7229              
7230             # if column slice an empty array reference
7231 217 100 100     468 if (ref($cols) eq 'ARRAY' && @{$cols} == 0) {
  207         528  
7232            
7233             # use all columns
7234 2         3 $cols = [0 .. $#{$self->[$ix][0]}];
  2         6  
7235            
7236             } else {
7237            
7238             # flatten column slice
7239 215         427 $cols = ICC::Shared::flatten($cols);
7240            
7241             # verify column slice contents
7242 215 50 33     252 (@{$cols} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$cols}) or croak('invalid column slice');
  215 50       245  
  2017         5939  
  215         290  
7243            
7244             }
7245              
7246             # return columns slice if no row slice
7247 217 100       779 defined($rows) || return($cols);
7248              
7249             # if row slice an empty array reference
7250 96 100 66     181 if (ref($rows) eq 'ARRAY' && @{$rows} == 0) {
  96         214  
7251            
7252             # use all rows
7253 35         45 $rows = [1 .. $#{$self->[$ix]}];
  35         75  
7254            
7255             } else {
7256            
7257             # flatten row slice
7258 61         103 $rows = ICC::Shared::flatten($rows);
7259            
7260             # verify row slice contents
7261 61 50 33     67 (@{$rows} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$rows}) or croak('invalid row slice');
  61 50       69  
  201         662  
  61         86  
7262            
7263             }
7264              
7265             # no replacement data (get)
7266 96 100       175 if (! defined($data)) {
7267            
7268             # verify 'get' code ref, or use identity function
7269 71 100 66 339   237 $get = (defined($get) && ref($get) eq 'CODE') ? $get : sub {@_};
  339         936  
7270            
7271             # for each row
7272 71         89 for my $i (0 .. $#{$rows}) {
  71         143  
7273            
7274             # get transformed data row
7275 476         502 @{$data->[$i]} = &$get(@{$self->[$ix][$rows->[$i]]}[@{$cols}]);
  476         885  
  476         811  
  476         536  
7276            
7277             }
7278            
7279             # return data slice as a Math::Matrix object
7280 71         724 return(bless($data, 'Math::Matrix'));
7281            
7282             # with replacement data (set)
7283             } else {
7284            
7285             # verify replacement data is 2-D array or Math::Matrix object
7286 25 50 33     127 ((ref($data) eq 'ARRAY' || UNIVERSAL::isa($data, 'Math::Matrix')) && ref($data->[0]) eq 'ARRAY') or croak('replacement data not a 2-D array reference');
      33        
7287            
7288             # verify replacement data size
7289 25 50 33     32 ($#{$data} == $#{$rows} && $#{$data->[0]} == $#{$cols}) or croak('replacement data is wrong sized');
  25         52  
  25         52  
  25         73  
  25         53  
7290            
7291             # verify 'set' code ref, or use identity function
7292 25 100 66 48   97 $set = (defined($set) && ref($set) eq 'CODE') ? $set : sub {@_};
  48         107  
7293            
7294             # for each row
7295 25         30 for my $i (0 .. $#{$rows}) {
  25         51  
7296            
7297             # set transformed data row
7298 75         86 @{$self->[$ix][$rows->[$i]]}[@{$cols}] = &$set(@{$data->[$i]});
  75         218  
  75         93  
  75         118  
7299            
7300             }
7301            
7302             # return column slice
7303 25         132 return($cols);
7304            
7305             }
7306            
7307             }
7308              
7309             # get accumulated sample values
7310             # sample dimensions are in pixels
7311             # used by _readChartTIFF to extract samples from a data stripe
7312             # parameters: (reference_to_data, sample_offset, sample_width, number_channels)
7313             # returns: (accumulated_sample_values)
7314             sub _getSample {
7315            
7316             # get parameters
7317 0     0   0 my ($data, $so, $sx, $c) = @_;
7318            
7319             # initialize sample values
7320 0         0 my @sv = (0) x $c;
7321            
7322             # for each row
7323 0         0 for my $i (0 .. $#{$data}) {
  0         0  
7324            
7325             # for each pixel
7326 0         0 for my $j (0 .. $sx - 1) {
7327            
7328             # for each channel
7329 0         0 for my $k (0 .. $c - 1) {
7330            
7331             # accumulate sample value
7332 0         0 $sv[$k] += $data->[$i][($so + $j) * $c + $k];
7333            
7334             }
7335            
7336             }
7337            
7338             }
7339            
7340             # return sample values
7341 0         0 return(@sv);
7342            
7343             }
7344              
7345             # get row length
7346             # hash keys: 'except', 'rows', 'undef'
7347             # parameters: (object_reference, hash)
7348             # returns: (row_length)
7349             sub _getRowLength {
7350              
7351             # get parameters
7352 0     0   0 my ($self, $hash) = @_;
7353              
7354             # local variables
7355 0         0 my ($rows, $pages, $n, $square);
7356              
7357             # row length exceptions
7358 0         0 state $exc = {
7359             'a541a1dbe0ad9b9641fa14c1105426ee' => 4, # microP2P
7360             '911c1ff09e25eaa835a3d83292dddc4c' => 5, # miniP2P
7361             '751fb2709976713309acbd832a6c28ba' => 5, # miniP2P53
7362             '4dcb109fa2f8b2332c7d3860cccf0bbe' => 2, # G7 verifier
7363             '7af45f1bc56c2e46042c7ee524bca773' => 25, # P2P25
7364             '288da8bd79a209f3cb222ec6fd4eb195' => 12, # P2P51H
7365             'b7045af5a40dbe3f8d8d5a93f3a14f42' => 25, # P2P51
7366             '79c4aab9771a9eb40c69168b5bb3c619' => 12, # P2P53
7367             };
7368              
7369             # return if chart found in exception table
7370 0 0 0     0 return($rows) if ($hash->{'except'} && defined($rows = $exc->{signature($self)}));
7371              
7372             # if 'rows' hash key is defined
7373 0 0       0 if (defined($rows = $hash->{'rows'})) {
7374            
7375             # if valid row length
7376 0 0 0     0 if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) {
      0        
7377            
7378             # return
7379 0         0 return($rows);
7380            
7381             } else {
7382            
7383             # warn
7384 0         0 warn('invalid \'rows\' parameter');
7385            
7386             }
7387            
7388             }
7389              
7390             # if LGOROWLENGTH keyword (ProfileMaker notation)
7391 0 0       0 if (defined($rows = keyword($self, 'LGOROWLENGTH'))) {
7392            
7393             # if valid row length
7394 0 0 0     0 if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) {
      0        
7395            
7396             # return
7397 0         0 return($rows);
7398            
7399             } else {
7400            
7401             # warn
7402 0         0 warn('invalid \'LGOROWLENGTH\' value');
7403            
7404             }
7405            
7406             }
7407              
7408             # if NUMBER_OF_STRIPS keyword (EFI notation)
7409 0 0       0 if (defined($rows = keyword($self, 'NUMBER_OF_STRIPS'))) {
7410            
7411             # if valid row length
7412 0 0 0     0 if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) {
      0        
7413            
7414             # return
7415 0         0 return($rows);
7416            
7417             } else {
7418            
7419             # warn
7420 0         0 warn('invalid \'NUMBER_OF_STRIPS\' value');
7421            
7422             }
7423            
7424             }
7425              
7426             # if 'NumberPatchRows' key (i1profiler CxF3)
7427 0 0       0 if (defined($rows = $self->[0]{'xrp:CustomAttributes'}{'NumberPatchRows'})) {
7428            
7429             # get 'NumberPatchPages' value
7430 0   0     0 $pages = $self->[0]{'xrp:CustomAttributes'}{'NumberPatchPages'} // 1;
7431            
7432             # if valid row length
7433 0 0 0     0 if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) {
      0        
7434            
7435             # return
7436 0         0 return($rows * $pages);
7437            
7438             } else {
7439            
7440             # warn
7441 0         0 warn('invalid \'NumberPatchRows\' attribute');
7442            
7443             }
7444            
7445             }
7446              
7447             # return, if 'undef' hash key
7448 0 0       0 return(undef) if ($hash->{'undef'});
7449              
7450             # get number of samples
7451 0         0 $n = $#{$self->[1]};
  0         0  
7452              
7453             # return if 0
7454 0 0       0 return(0) if ($n == 0);
7455              
7456             # return if 1 or 2
7457 0 0       0 return(1) if ($n < 3);
7458              
7459             # compute size of square chart
7460 0         0 $square = POSIX::ceil(sqrt($n));
7461              
7462             # return if chart is square
7463 0 0       0 return($square) if ($n == $square**2);
7464              
7465             # set row length one less than square chart
7466 0         0 $rows = $square - 1;
7467              
7468             # while modulus is non-zero, decrement row length
7469 0         0 while ($n % $rows) {$rows--}
  0         0  
7470              
7471             # return row length, choosing full rectangle if possible
7472 0 0       0 return($rows > $square/2 ? $rows : $square);
7473              
7474             }
7475              
7476             # invert ink map
7477             # fills the ink map, then inverts it
7478             # parameter: (ink_map_vector)
7479             # returns: (inverted_ink_map, [filled_ink_map, missing_process_channels])
7480             sub _invert_ink_map {
7481              
7482             # get parameters
7483 0     0   0 my ($map) = @_;
7484              
7485             # local variables
7486 0         0 my ($ix, @ms, @mp, @all, @inv);
7487              
7488             # get upper index
7489 0         0 $ix = $#{$map};
  0         0  
7490              
7491             # get missing channels
7492 0         0 @ms = grep {my $i = $_; ! grep {$_ eq $i} @{$map}} (0 .. $ix);
  0         0  
  0         0  
  0         0  
  0         0  
7493              
7494             # filter missing CMYK channels
7495 0         0 @mp = grep {$_ < 4} @ms;
  0         0  
7496              
7497             # assign stationary missing channels
7498 0 0 0     0 @all = map {my $i = $_; ($map->[$i] !~ m/^\d+$/ && grep {$_ == $i} @ms) ? $i : $map->[$i]} (0 .. $ix);
  0         0  
  0         0  
7499              
7500             # get missing channels
7501 0         0 @ms = grep {my $i = $_; ! grep {$_ eq $i} @all} (0 .. $ix);
  0         0  
  0         0  
  0         0  
7502              
7503             # assign remaining missing channels
7504 0 0       0 @all = map {! m/^\d+$/ ? shift(@ms) : int($_)} @all if (@ms);
  0 0       0  
7505              
7506             # invert filled map
7507 0         0 @inv = map {my $i = $_; grep {$all[$_] == $i} (0 .. $ix)} (0 .. $ix);
  0         0  
  0         0  
  0         0  
7508              
7509             # return
7510 0 0       0 return(wantarray ? (\@inv, \@all, \@mp) : \@inv);
7511              
7512             }
7513              
7514             # map array values
7515             # returns input_array, if output_size is undefined
7516             # parameters: (output_size, input_slice, output_slice, input_array)
7517             # returns: (output_array)
7518             sub _map_array {
7519              
7520             # get parameters
7521 0     0   0 my ($size, $si, $so) = splice(@_, 0, 3);
7522              
7523             # return if size undefined
7524 0 0       0 return(@_) if (! defined($size));
7525              
7526             # make output array
7527 0         0 my @out = (0) x $size;
7528              
7529             # map values
7530 0         0 @out[@{$so}] = @_[@{$si}];
  0         0  
  0         0  
7531              
7532             # return output array
7533 0         0 return(@out);
7534              
7535             }
7536              
7537             # get illuminant white point
7538             # returns XYZ vector from colorimetry array
7539             # returns D50 if CAT or undefined
7540             # parameter: (object_reference, column_slice, [hash])
7541             # returns: (XYZ_vector)
7542             sub _illumWP {
7543              
7544             # get parameters
7545 0     0   0 my ($self, $cols, $hash) = @_;
7546              
7547             # if XYZ values are valid
7548 0 0 0     0 if (3 == grep {defined() && ! ref() && $_ > 0} @{$self->[2][2]}[@{$cols}]) {
  0 0       0  
  0         0  
  0         0  
7549            
7550             # return XYZ vector
7551 0         0 return([@{$self->[2][2]}[@{$cols}]]);
  0         0  
  0         0  
7552            
7553             } else {
7554            
7555             # return D50 vector
7556 0         0 return(ICC::Shared::D50);
7557            
7558             }
7559              
7560             }
7561              
7562             # compute media white point
7563             # multiple samples are averaged
7564             # result also stored in colorimetry array
7565             # parameter: (object_reference, column_slice, [hash])
7566             # returns: (XYZ_vector)
7567             sub _mediaWP {
7568              
7569             # get parameters
7570 0     0   0 my ($self, $cols, $hash) = @_;
7571              
7572             # local variables
7573 0         0 my ($WPxyz, $dev, $mwv, $n, @XYZ, @XYZs);
7574              
7575             # if column slice is L*a*b*
7576 0 0       0 if ((3 == grep {$self->[1][0][$_] =~ m/LAB_[LAB]$/} @{$cols})) {
  0 0       0  
  0         0  
7577            
7578             # get illuminant white point
7579 0 0       0 $WPxyz = defined($self->[2][2][$cols->[0]]) ? [@{$self->[2][2]}[@{$cols}]] : ICC::Shared::D50;
  0         0  
  0         0  
7580            
7581             # if column slice is not XYZ
7582 0         0 } elsif ((3 != grep {$self->[1][0][$_] =~ m/XYZ_[XYZ]$/} @{$cols})) {
  0         0  
7583            
7584             # warning
7585 0         0 warn('column slice not XYZ or L*a*b* data');
7586            
7587             # return empty
7588 0         0 return();
7589            
7590             }
7591              
7592             # if no device data (using 'device' context)
7593 0 0       0 if (! ($dev = device($self, {'context' => $hash->{'device'}}))) {
7594            
7595             # warning
7596 0         0 warn('no device data');
7597            
7598             # return empty
7599 0         0 return();
7600            
7601             }
7602              
7603             # set media white device value (255 if RGB, 0 otherwise)
7604 0 0       0 $mwv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 255 : 0;
7605              
7606             # for each sample
7607 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
7608            
7609             # if all device channels are white
7610 0 0       0 if (@{$dev} == grep {$_ == $mwv} @{$self->[1][$i]}[@{$dev}]) {
  0         0  
  0         0  
  0         0  
  0         0  
7611            
7612             # if L*a*b* data
7613 0 0       0 if ($WPxyz) {
7614            
7615             # convert L*a*b* values to XYZ
7616 0         0 @XYZs = ICC::Shared::_Lab2XYZ(@{$self->[1][$i]}[@{$cols}], $WPxyz);
  0         0  
  0         0  
7617            
7618             # accumulate XYZ values
7619 0         0 $XYZ[0] += $XYZs[0];
7620 0         0 $XYZ[1] += $XYZs[1];
7621 0         0 $XYZ[2] += $XYZs[2];
7622            
7623             # if XYZ data
7624             } else {
7625            
7626             # accumulate XYZ values
7627 0         0 $XYZ[0] += $self->[1][$i][$cols->[0]];
7628 0         0 $XYZ[1] += $self->[1][$i][$cols->[1]];
7629 0         0 $XYZ[2] += $self->[1][$i][$cols->[2]];
7630            
7631             }
7632            
7633             # increment count
7634 0         0 $n++;
7635            
7636             }
7637            
7638             }
7639              
7640             # if media white sample(s)
7641 0 0       0 if ($n) {
7642            
7643             # store average XYZ values in colorimetry array, and return XYZ vector
7644 0         0 return([@{$self->[2][3]}[@{$cols}] = map {$_/$n} @XYZ]);
  0         0  
  0         0  
  0         0  
7645            
7646             } else {
7647            
7648             # warning
7649 0         0 warn('no media white sample found');
7650            
7651             # return empty
7652 0         0 return();
7653            
7654             }
7655            
7656             }
7657              
7658             # compute media black point
7659             # multiple samples are averaged
7660             # result also stored in colorimetry array
7661             # parameter: (object_reference, column_slice, [hash])
7662             # returns: (XYZ_vector)
7663             sub _mediaBP {
7664              
7665             # get parameters
7666 0     0   0 my ($self, $cols, $hash) = @_;
7667              
7668             # local variables
7669 0         0 my ($WPxyz, $dev, $mbv, $n, @XYZ, @XYZs);
7670              
7671             # if column slice is L*a*b*
7672 0 0       0 if ((3 == grep {$self->[1][0][$_] =~ m/LAB_[LAB]$/} @{$cols})) {
  0 0       0  
  0         0  
7673            
7674             # get illuminant white point
7675 0 0       0 $WPxyz = defined($self->[2][2][$cols->[0]]) ? [@{$self->[2][2]}[@{$cols}]] : ICC::Shared::D50;
  0         0  
  0         0  
7676            
7677             # if column slice is not XYZ
7678 0         0 } elsif ((3 != grep {$self->[1][0][$_] =~ m/XYZ_[XYZ]$/} @{$cols})) {
  0         0  
7679            
7680             # warning
7681 0         0 warn('column slice not XYZ or L*a*b* data');
7682            
7683             # return empty
7684 0         0 return();
7685            
7686             }
7687              
7688             # if no device data (using 'device' context)
7689 0 0       0 if (! ($dev = device($self, {'context' => $hash->{'device'}}))) {
7690            
7691             # warning
7692 0         0 warn('no device data');
7693            
7694             # return empty
7695 0         0 return();
7696            
7697             }
7698              
7699             # set media black device value (0 if RGB, 100 otherwise)
7700 0 0       0 $mbv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 1 : 100;
7701              
7702             # for each sample
7703 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
7704            
7705             # if all device channels are black
7706 0 0       0 if (@{$dev} == grep {$_ == $mbv} @{$self->[1][$i]}[@{$dev}]) {
  0         0  
  0         0  
  0         0  
  0         0  
7707            
7708             # increment count
7709 0         0 $n++;
7710            
7711             # if L*a*b* data
7712 0 0       0 if ($WPxyz) {
7713            
7714             # convert L*a*b* values to XYZ
7715 0         0 @XYZs = ICC::Shared::_Lab2XYZ(@{$self->[1][$i]}[@{$cols}], $WPxyz);
  0         0  
  0         0  
7716            
7717             # accumulate XYZ values
7718 0         0 $XYZ[0] += $XYZs[0];
7719 0         0 $XYZ[1] += $XYZs[1];
7720 0         0 $XYZ[2] += $XYZs[2];
7721            
7722             # if XYZ data
7723             } else {
7724            
7725             # accumulate XYZ values
7726 0         0 $XYZ[0] += $self->[1][$i][$cols->[0]];
7727 0         0 $XYZ[1] += $self->[1][$i][$cols->[1]];
7728 0         0 $XYZ[2] += $self->[1][$i][$cols->[2]];
7729            
7730             }
7731            
7732             }
7733            
7734             }
7735              
7736             # if media black sample(s)
7737 0 0       0 if ($n) {
7738            
7739             # store average XYZ values in colorimetry array, and return XYZ vector
7740 0         0 return([@{$self->[2][4]}[@{$cols}] = map {$_/$n} @XYZ]);
  0         0  
  0         0  
  0         0  
7741            
7742             } else {
7743            
7744             # warning
7745 0         0 warn('no media black sample found');
7746            
7747             # return empty
7748 0         0 return();
7749            
7750             }
7751            
7752             }
7753              
7754             # make SAMPLE_ID hash
7755             # if no SAMPLE_ID field, hash is initialized
7756             # parameter: (object_reference)
7757             sub _makeSampleID {
7758              
7759             # get object reference
7760 24     24   35 my $self = shift();
7761              
7762             # if SAMPLE_ID column(s) exist
7763 24 100       41 if (my @id = grep {$self->[1][0][$_] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID)$/} (0 .. $#{$self->[1][0]})) {
  609         916  
  24         57  
7764            
7765             # make the SAMPLE_ID hash, omitting undefined ID values
7766 16 50       24 $self->[4] = {map {defined($self->[1][$_][$id[0]]) ? ($self->[1][$_][$id[0]], $_) : ()} (1 .. $#{$self->[1]})};
  169         361  
  16         29  
7767            
7768             } else {
7769            
7770             # initialize the hash
7771 8         20 $self->[4] = {};
7772            
7773             }
7774              
7775             }
7776              
7777             # add colorimetry metadata
7778             # called when creating a new object
7779             # parameter: (object_reference)
7780             sub _addColorMeta {
7781              
7782             # get object reference
7783 22     22   33 my $self = shift();
7784              
7785             # local variables
7786 22         36 my (@cols, $hash, $illum, $spec, $nm, $str, $color, $WPxyz, @values);
7787              
7788             # if object contains colorimetric data
7789 22 100       33 if (@cols = grep {$self->[1][0][$_] =~ m/^(?:(.*)\|)?(?:LAB_[LAB]|XYZ_[XYZ]|STDEV_[LABXYZ]|MEAN_DE|STDEV_DE|CHI_SQD_PAR)$/} (0 .. $#{$self->[1][0]})) {
  599         1451  
  22         54  
7790            
7791             # set default hash values
7792 9         43 $hash = {'illuminant' => 'D50', 'observer' => '2'};
7793            
7794             # if CxF3 'TristimulusSpec' node
7795 9 50 100     41 if (defined($self->[0]{'CxF3_dom'}) && 0) {
    100          
7796            
7797             ##### to be implemented #####
7798            
7799             # if 'WEIGHTING_FUNCTION' keyword(s)
7800             } elsif (@values = keyword($self, 'WEIGHTING_FUNCTION')) {
7801            
7802             # join values into string
7803 7         28 $str = join(';', @values);
7804            
7805             # match illuminant and save in hash
7806 7 50       22 $hash->{'illuminant'} = $1 if ($str =~ m/ILLUMINANT\s*,\s*(\w+)"/);
7807            
7808             # match observer and save in hash
7809 7 50       16 $hash->{'observer'} = $1 if ($str =~ m/OBSERVER\s*,\s*(\d+).*"/);
7810            
7811             }
7812            
7813             # if non-standard illuminant
7814 9 50 33     44 if ($hash->{'illuminant'} ne 'D50' || $hash->{'observer'} ne '2') {
7815            
7816             # make an empty 'Color.pm' object
7817 0         0 $color = ICC::Support::Color->new();
7818            
7819             # if illuminant is an ARRAY reference
7820 0 0       0 if (ref($hash->{'illuminant'}) eq 'ARRAY') {
7821            
7822             # initialize object for CIE method
7823 0         0 ICC::Support::Color::_cie($color, $hash);
7824            
7825             } else {
7826            
7827             # initialize object for ASTM method
7828 0         0 ICC::Support::Color::_astm($color, $hash);
7829            
7830             }
7831            
7832             # use computed white point
7833 0         0 $WPxyz = $color->iwtpt();
7834            
7835             } else {
7836            
7837             # use D50
7838 9         16 $WPxyz = ICC::Shared::D50;
7839            
7840             }
7841            
7842             # for each colorimetric field
7843 9         14 for my $i (@cols) {
7844            
7845             # if field name ends in L or X
7846 48 100       151 if ($self->[1][0][$i] =~ m/[LX]$/) {
    100          
    50          
7847            
7848             # save WP X-value
7849 16         33 $self->[2][2][$i] = $WPxyz->[0];
7850            
7851             # if field name ends in A or Y
7852             } elsif ($self->[1][0][$i] =~ m/[AY]$/) {
7853            
7854             # save WP Y-value
7855 16         28 $self->[2][2][$i] = $WPxyz->[1];
7856            
7857             # if field name ends in B or Z
7858             } elsif ($self->[1][0][$i] =~ m/[BZ]$/) {
7859            
7860             # save WP Z-value
7861 16         43 $self->[2][2][$i] = $WPxyz->[2];
7862            
7863             }
7864            
7865             }
7866            
7867             }
7868            
7869             }
7870              
7871             # read chart from list of data files
7872             # averages color measurement data (spectral, XYZ, L*a*b* or density)
7873             # files must have identical structure (rows and cols)
7874             # parameters: (object_reference, ref_to_file_list, hash)
7875             # returns: (number_of_files_averaged)
7876             sub _readChartAvg {
7877              
7878             # get parameters
7879 1     1   4 my ($self, $list, $hash) = @_;
7880              
7881             # local variables
7882 1         7 my ($n, $result, $c1, $c2, $c3, $keys, $temp, @xyz);
7883 1         0 my ($charts, $fstat, @ctx1, @ctx2, $add_hash);
7884              
7885             # initialize file count
7886 1         1 $n = 0;
7887              
7888             # if hash is defined
7889 1 50       3 if (defined($hash)) {
7890            
7891             # for each hash key
7892 1         1 for (keys(%{$hash})) {
  1         4  
7893            
7894             # if XYZ based stat requested
7895 0 0       0 if (m/^STDEV_XYZ$/) {
    0          
7896            
7897             # if value is a scalar
7898 0 0       0 if (! ref($hash->{$_})) {
    0          
7899            
7900             # save XYZ context
7901 0         0 push(@ctx1, $hash->{$_});
7902            
7903             } elsif (ref($hash->{$_}) eq 'ARRAY') {
7904            
7905             # save XYZ contexts
7906 0         0 push(@ctx1, @{$hash->{$_}});
  0         0  
7907            
7908             }
7909            
7910             # increment flag
7911 0         0 $fstat++;
7912            
7913             # if L*a*b* based stat requested
7914             } elsif (m/^(MEAN_DE|STDEV_LAB|CHI_SQD_PAR)$/) {
7915            
7916             # if value is a scalar
7917 0 0       0 if (! ref($hash->{$_})) {
    0          
7918            
7919             # save L*a*b* context
7920 0         0 push(@ctx2, $hash->{$_});
7921            
7922             } elsif (ref($hash->{$_}) eq 'ARRAY') {
7923            
7924             # save L*a*b* contexts
7925 0         0 push(@ctx2, @{$hash->{$_}});
  0         0  
7926            
7927             }
7928            
7929             # increment flag
7930 0         0 $fstat++;
7931            
7932             }
7933            
7934             }
7935            
7936             }
7937              
7938             # for each file
7939 1         3 for my $file (@{$list}) {
  1         2  
7940            
7941             # if first file
7942 4 100       8 if ($n == 0) {
7943            
7944             # if file read successfully
7945 1 50       2 if (! ($result = _readChart($self, $file, $hash))) {
7946            
7947             # add colorimetric metadata
7948 1         4 _addColorMeta($self);
7949            
7950             # make format key string
7951 1 50       4 $keys = join(':', map {defined() ? $_ : '-'} @{$self->[1][0]});
  12         19  
  1         3  
7952            
7953             # for each XYZ context
7954 1         4 for my $ctx (@ctx1) {
7955            
7956             # copy the hash
7957 0         0 $add_hash = Storable::dclone($hash);
7958            
7959             # set the context (undef for no context)
7960 0 0 0     0 $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef;
7961            
7962             # delete the 'added' context
7963 0         0 delete($add_hash->{'added'});
7964            
7965             # add the XYZ values
7966 0         0 add_xyz($self, $add_hash);
7967            
7968             }
7969            
7970             # for each L*a*b* context
7971 1         3 for my $ctx (@ctx2) {
7972            
7973             # copy the hash
7974 0         0 $add_hash = Storable::dclone($hash);
7975            
7976             # set the context (undef for no context)
7977 0 0 0     0 $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef;
7978            
7979             # delete the 'added' context
7980 0         0 delete($add_hash->{'added'});
7981            
7982             # add the L*a*b* values
7983 0         0 add_lab($self, $add_hash);
7984            
7985             }
7986            
7987             # save copy of chart data, if needed for stats
7988 1 50       3 $charts->[0] = Storable::dclone($self->[1]) if ($fstat);
7989            
7990             # get averaging groups
7991 1         5 ($c1, $c2, $c3) = _avg_groups($self, $hash);
7992            
7993             # if there are L*a*b* or density groups
7994 1 50 33     1 if (@{$c2} || @{$c3}) {
  1         5  
  0         0  
7995            
7996             # for each sample
7997 1         2 for my $i (1 .. $#{$self->[1]}) {
  1         3  
7998            
7999             # for each group of L*a*b* columns
8000 10         13 for (my $j = 0; $j < @{$c2}; $j += 3) {
  20         28  
8001            
8002             # convert to L*a*b* values to xyz
8003 10         12 @{$self->[1][$i]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_Lab2xyz(@{$self->[1][$i]}[@{$c2}[$j .. $j + 2]]);
  10         28  
  10         11  
  10         23  
  10         12  
8004            
8005             }
8006            
8007             # for each density column
8008 10         10 for my $j (@{$c3}) {
  10         18  
8009            
8010             # convert to density to reflectance
8011 0         0 $self->[1][$i][$j] = POSIX::pow(10, -$self->[1][$i][$j]);
8012            
8013             }
8014            
8015             }
8016            
8017             }
8018            
8019             # increment file count
8020 1         2 $n++;
8021            
8022             } else {
8023            
8024             # print warning
8025 0         0 warn("chart $file $result, ignored\n");
8026            
8027             }
8028            
8029             } else {
8030            
8031             # make temporary Chart object
8032 3         16 $temp = ICC::Support::Chart->new();
8033            
8034             # if file read successfully
8035 3 50       6 if (! ($result = _readChart($temp, $file, $hash))) {
8036            
8037             # if charts have same structure (rows and cols)
8038 3 50 33     5 if ($#{$self->[1]} == $#{$temp->[1]} && $keys eq join(':', map {defined() ? $_ : '-'} @{$temp->[1][0]})) {
  3 50       5  
  3         11  
  36         65  
  3         5  
8039            
8040             # for each XYZ context
8041 3         5 for my $ctx (@ctx1) {
8042            
8043             # copy the hash
8044 0         0 $add_hash = Storable::dclone($hash);
8045            
8046             # set the context (undef for no context)
8047 0 0 0     0 $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef;
8048            
8049             # delete the 'added' context
8050 0         0 delete($add_hash->{'added'});
8051            
8052             # add the XYZ values
8053 0         0 add_xyz($temp, $add_hash);
8054            
8055             }
8056            
8057             # for each L*a*b* context
8058 3         4 for my $ctx (@ctx2) {
8059            
8060             # copy the hash
8061 0         0 $add_hash = Storable::dclone($hash);
8062            
8063             # set the context (undef for no context)
8064 0 0 0     0 $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef;
8065            
8066             # delete the 'added' context
8067 0         0 delete($add_hash->{'added'});
8068            
8069             # add the L*a*b* values
8070 0         0 add_lab($temp, $add_hash);
8071            
8072             }
8073            
8074             # save copy of chart data, if needed for stats
8075 3 50       8 $charts->[$n] = $temp->[1] if ($fstat);
8076            
8077             # for each sample
8078 3         5 for my $i (1 .. $#{$self->[1]}) {
  3         7  
8079            
8080             # for each linear column
8081 30         33 for my $j (@{$c1}) {
  30         34  
8082            
8083             # add temp value
8084 90         147 $self->[1][$i][$j] += $temp->[1][$i][$j];
8085            
8086             }
8087            
8088             # for each group of L*a*b* columns
8089 30         35 for (my $j = 0; $j < @{$c2}; $j += 3) {
  60         97  
8090            
8091             # get temp xyz values
8092 30         37 @xyz = ICC::Shared::_Lab2xyz(@{$temp->[1][$i]}[@{$c2}[$j .. $j + 2]]);
  30         53  
  30         35  
8093            
8094             # add to self
8095 30         49 $self->[1][$i][$c2->[$j]] += $xyz[0];
8096 30         39 $self->[1][$i][$c2->[$j + 1]] += $xyz[1];
8097 30         37 $self->[1][$i][$c2->[$j + 2]] += $xyz[2];
8098            
8099             }
8100            
8101             # for each density column
8102 30         29 for my $j (@{$c3}) {
  30         45  
8103            
8104             # add temp reflectance
8105 0         0 $self->[1][$i][$j] += POSIX::pow(10, -$temp->[1][$i][$j]);
8106            
8107             }
8108            
8109             }
8110            
8111             # increment file count
8112 3         8 $n++;
8113            
8114             } else {
8115            
8116             # print warning
8117 0         0 warn("chart $file has different structure, ignored\n");
8118            
8119             }
8120            
8121             } else {
8122            
8123             # print warning
8124 0         0 warn("chart $file $result, ignored\n");
8125            
8126             }
8127            
8128             }
8129            
8130             }
8131              
8132             # if any files were read
8133 1 50       4 if ($n) {
8134            
8135             # if there are measurement values
8136 1 0 33     2 if (@{$c1} || @{$c2} || @{$c3}) {
  1   33     5  
  0         0  
  0         0  
8137            
8138             # for each sample
8139 1         3 for my $i (1 .. $#{$self->[1]}) {
  1         3  
8140            
8141             # for each measurement column
8142 10         11 for my $j (@{$c1}, @{$c2}, @{$c3}) {
  10         10  
  10         11  
  10         12  
8143            
8144             # divide by n
8145 60         71 $self->[1][$i][$j] /= $n;
8146            
8147             }
8148            
8149             # for each group of L*a*b* columns
8150 10         13 for (my $j = 0; $j < @{$c2}; $j += 3) {
  20         30  
8151            
8152             # convert to xyz values to L*a*b*
8153 10         13 @{$self->[1][$i]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_xyz2Lab(@{$self->[1][$i]}[@{$c2}[$j .. $j + 2]]);
  10         18  
  10         13  
  10         18  
  10         12  
8154            
8155             }
8156            
8157             # for each density column
8158 10         11 for my $j (@{$c3}) {
  10         16  
8159            
8160             # convert reflectance to density
8161 0         0 $self->[1][$i][$j] = -POSIX::log10($self->[1][$i][$j]);
8162            
8163             }
8164            
8165             }
8166            
8167             }
8168            
8169             # add ISO statistics, if requested
8170 1 50       3 _addStats($self, $charts, $hash) if ($fstat);
8171            
8172             # print message
8173 1         42 print "$n files read in directory $self->[0]{'file_path'}\n\n";
8174            
8175             # save number of files read
8176 1         5 $self->[0]{'files_read'} = $n;
8177            
8178             }
8179              
8180             # return
8181 1         17 return($n);
8182              
8183             }
8184              
8185             # add ISO statistics
8186             # the object_reference contains the mean values
8187             # the individual charts are in the array_of_chart_objects
8188             # parameters: (object_reference, array_of_chart_objects, hash)
8189             sub _addStats {
8190              
8191             # get parameters
8192 0     0   0 my ($self, $charts, $hash) = @_;
8193              
8194             # local variables
8195 0         0 my (@ctx, $cols, $scols);
8196              
8197             # for each hash key
8198 0         0 for (keys(%{$hash})) {
  0         0  
8199            
8200             # if value is a scalar
8201 0 0       0 if (! ref($hash->{$_})) {
    0          
8202            
8203             # save context value
8204 0         0 @ctx = ($hash->{$_});
8205            
8206             } elsif (ref($hash->{$_}) eq 'ARRAY') {
8207            
8208             # save context values
8209 0         0 @ctx = @{$hash->{$_}};
  0         0  
8210            
8211             }
8212            
8213             # if 'STDEV_XYZ'
8214 0 0       0 if (m/^STDEV_XYZ$/) {
    0          
    0          
8215            
8216             # for each context
8217 0         0 for my $context (@ctx) {
8218            
8219             # resolve context value
8220 0 0 0     0 $context = defined($context) && length($context) ? $context : undef;
8221            
8222             # if no STDEV_XYZ columns with context
8223 0 0       0 if (! test($self, 'STDEVXYZ', $context)) {
8224            
8225             # get XYZ columns
8226 0 0       0 $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z));
  0         0  
8227            
8228             # add STDEV_XYZ columns
8229 0 0       0 $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(STDEV_X STDEV_Y STDEV_Z));
  0         0  
8230            
8231             # for each XYZ
8232 0         0 for my $i (0 .. 2) {
8233            
8234             # add STDEV_XYZ values
8235 0         0 _addStdDevCol($self, $charts, $cols->[$i], $scols->[$i]);
8236            
8237             }
8238            
8239             }
8240            
8241             # set origin
8242 0         0 @{$self->[2][0]}[@{$scols}] = ($cols) x 3;
  0         0  
  0         0  
8243            
8244             # save illuminant white point
8245 0         0 @{$self->[2][2]}[@{$scols}] = @{$self->[2][2]}[@{$cols}];
  0         0  
  0         0  
  0         0  
  0         0  
8246            
8247             }
8248            
8249             # if 'STDEV_LAB' or 'CHI_SQD_PAR'
8250             } elsif (m/^(STDEV_LAB|CHI_SQD_PAR)$/) {
8251            
8252             # for each context
8253 0         0 for my $context (@ctx) {
8254            
8255             # resolve context value
8256 0 0 0     0 $context = defined($context) && length($context) ? $context : undef;
8257            
8258             # if no STDEV_LAB columns with context
8259 0 0       0 if (! test($self, 'STDEVLAB', $context)) {
8260            
8261             # get L*a*b* columns
8262 0 0       0 $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B));
  0         0  
8263            
8264             # add STDEV_LAB columns
8265 0 0       0 $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(STDEV_L STDEV_A STDEV_B));
  0         0  
8266            
8267             # for each L*a*b*
8268 0         0 for my $i (0 .. 2) {
8269            
8270             # add STDEV_LAB values
8271 0         0 _addStdDevCol($self, $charts, $cols->[$i], $scols->[$i]);
8272            
8273             }
8274            
8275             # set origin
8276 0         0 @{$self->[2][0]}[@{$scols}] = ($cols) x 3;
  0         0  
  0         0  
8277            
8278             # save illuminant white point
8279 0         0 @{$self->[2][2]}[@{$scols}] = @{$self->[2][2]}[@{$cols}];
  0         0  
  0         0  
  0         0  
  0         0  
8280            
8281             }
8282            
8283             # if 'CHI_SQD_PAR'
8284 0 0       0 if ($1 eq 'CHI_SQD_PAR') {
8285            
8286             # get STDEV_LAB columns
8287 0 0       0 $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(STDEV_L STDEV_A STDEV_B));
  0         0  
8288            
8289             # add CHI_SQD_PAR column
8290 0 0       0 $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(CHI_SQD_PAR));
  0         0  
8291            
8292             # for each sample
8293 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
8294            
8295             # set CHI_SQD_PAR value (average of L*a*b* standard deviations)
8296 0         0 $self->[1][$i][$scols->[0]] = List::Util::sum(@{$self->[1][$i]}[@{$cols}])/3;
  0         0  
  0         0  
8297            
8298             }
8299            
8300             # set origin
8301 0         0 $self->[2][0][$scols->[0]] = $cols;
8302            
8303             }
8304            
8305             }
8306            
8307             # if 'MEAN_DE'
8308             } elsif (m/^MEAN_DE$/) {
8309            
8310             # for each context
8311 0         0 for my $context (@ctx) {
8312            
8313             # resolve context value
8314 0 0 0     0 $context = defined($context) && length($context) ? $context : undef;
8315            
8316             # if no MEAN_DE columns with context
8317 0 0       0 if (! test($self, 'MEAN_DE', $context)) {
8318            
8319             # get L*a*b* columns
8320 0 0       0 $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B));
  0         0  
8321            
8322             # add MEAN_DE column
8323 0 0       0 $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(MEAN_DE));
  0         0  
8324            
8325             # add MEAN_DE values
8326 0         0 _addMeanDECol($self, $charts, $cols, $scols->[0]);
8327            
8328             # set origin
8329 0         0 $self->[2][0][$scols->[0]] = $cols;
8330            
8331             }
8332            
8333             }
8334            
8335             }
8336            
8337             }
8338            
8339             }
8340              
8341             # add standard deviation column
8342             # the object_reference contains the mean values
8343             # the individual charts are in the array_of_chart_objects
8344             # parameters: (object_reference, array_of_chart_objects, mean_column, std_dev_column)
8345             sub _addStdDevCol {
8346              
8347             # get parameters
8348 0     0   0 my ($self, $charts, $m, $s) = @_;
8349              
8350             # local variables
8351 0         0 my ($n);
8352              
8353             # get number of charts
8354 0         0 $n = @{$charts};
  0         0  
8355              
8356             # for each sample
8357 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
8358            
8359             # initialize value
8360 0         0 $self->[1][$i][$s] = 0;
8361            
8362             # if number of charts > 0
8363 0 0       0 if ($n) {
8364            
8365             # for each chart
8366 0         0 for my $j (0 .. $#{$charts}) {
  0         0  
8367            
8368             # add squared difference
8369 0         0 $self->[1][$i][$s] += ($charts->[$j][$i][$m] - $self->[1][$i][$m])**2;
8370            
8371             }
8372            
8373             # complete calculation
8374 0         0 $self->[1][$i][$s] = sqrt($self->[1][$i][$s]/$n);
8375            
8376             } else {
8377            
8378             # error
8379 0         0 croak('can\'t compute standard deviation with zero samples');
8380            
8381             }
8382            
8383             }
8384            
8385             }
8386              
8387             # add mean dEab column
8388             # the object_reference contains the mean values
8389             # the individual charts are in the array_of_chart_objects
8390             # parameters: (object_reference, array_of_chart_objects, mean_L*a*b*_columns, mean_dE_column)
8391             sub _addMeanDECol {
8392              
8393             # get parameters
8394 0     0   0 my ($self, $charts, $m, $s) = @_;
8395              
8396             # local variables
8397 0         0 my ($n, $dE);
8398              
8399             # get number of charts
8400 0         0 $n = @{$charts};
  0         0  
8401              
8402             # for each sample
8403 0         0 for my $i (1 .. $#{$self->[1]}) {
  0         0  
8404            
8405             # initialize value
8406 0         0 $self->[1][$i][$s] = 0;
8407            
8408             # if number of charts > 0
8409 0 0       0 if ($n) {
8410            
8411             # for each chart
8412 0         0 for my $j (0 .. $#{$charts}) {
  0         0  
8413            
8414             # initialize dE
8415 0         0 $dE = 0;
8416            
8417             # for each L*a*b*
8418 0         0 for my $k (0 .. 2) {
8419            
8420             # add squared difference
8421 0         0 $dE += ($self->[1][$i][$m->[$k]] - $charts->[$j][$i][$m->[$k]])**2;
8422            
8423             }
8424            
8425             # add dE for this chart
8426 0         0 $self->[1][$i][$s] += sqrt($dE);
8427            
8428             }
8429            
8430             # complete calculation
8431 0         0 $self->[1][$i][$s] /= $n;
8432            
8433             } else {
8434            
8435             # error
8436 0         0 croak('can\'t compute mean dE with zero samples');
8437            
8438             }
8439            
8440             }
8441            
8442             }
8443              
8444             # read chart from list of data files
8445             # reads first chart, then appends other charts
8446             # files must have identical structure (cols)
8447             # parameters: (object_reference, ref_to_file_list, hash)
8448             # returns: (number_of_files_appended)
8449             sub _readChartAppend {
8450              
8451             # get parameters
8452 1     1   4 my ($self, $list, $hash) = @_;
8453              
8454             # local variables
8455 1         2 my ($n, $result, $keys, $temp);
8456              
8457             # initialize file counter
8458 1         2 $n = 0;
8459              
8460             # for each file
8461 1         2 for my $file (@{$list}) {
  1         3  
8462            
8463             # if first file
8464 4 100       8 if ($n == 0) {
8465            
8466             # if file read successfully
8467 1 50       5 if (! ($result = _readChart($self, $file, $hash))) {
8468            
8469             # add colorimetric metadata
8470 1         10 _addColorMeta($self);
8471            
8472             # make format key string
8473 1 50       2 $keys = join(':', map {defined() ? $_ : '-'} @{$self->[1][0]});
  12         21  
  1         2  
8474            
8475             # increment counter
8476 1         3 $n++;
8477            
8478             } else {
8479            
8480             # print warning
8481 0         0 warn("chart $file $result, ignored\n");
8482            
8483             }
8484            
8485             } else {
8486            
8487             # make temporary Chart object
8488 3         14 $temp = ICC::Support::Chart->new();
8489            
8490             # if file read successfully
8491 3 50       6 if (! ($result = _readChart($temp, $file, $hash))) {
8492            
8493             # if charts have same structure (cols)
8494 3 50       5 if ($keys eq join(':', map {defined() ? $_ : '-'} @{$temp->[1][0]})) {
  36 50       56  
  3         7  
8495            
8496             # append temp samples
8497 3         5 push(@{$self->[1]}, @{$temp->[1]}[1 .. $#{$temp->[1]}]);
  3         6  
  3         9  
  3         5  
8498            
8499             # increment counter
8500 3         9 $n++;
8501            
8502             } else {
8503            
8504             # print warning
8505 0         0 warn("chart $file has different structure, ignored\n");
8506            
8507             }
8508            
8509             } else {
8510            
8511             # print warning
8512 0         0 warn("chart $file $result, ignored\n");
8513            
8514             }
8515            
8516             }
8517            
8518             }
8519              
8520             # print message if any files were read
8521 1 50       34 print "$n files read in directory $self->[0]{'file_path'}\n\n" if ($n);
8522              
8523             # save number of files read
8524 1         5 $self->[0]{'files_read'} = $n;
8525            
8526             # return
8527 1         10 return($n);
8528              
8529             }
8530              
8531             # read chart from list of data files
8532             # assumes charts are M0, M1, M2 or M3 measurement conditions
8533             # reads first chart, then merges other charts, adding contexts
8534             # files must have identical structure (rows and cols)
8535             # parameters: (object_reference, ref_to_file_list, hash)
8536             # returns: (number_of_files_merged)
8537             sub _readChartMerge {
8538              
8539             # get parameters
8540 0     0   0 my ($self, $list, $hash) = @_;
8541              
8542             # local variables
8543 0         0 my ($n, $ctx, $result, $keys, $sig, @cols, $temp);
8544              
8545             # initialize file counter
8546 0         0 $n = 0;
8547              
8548             # for each file
8549 0         0 for my $file (@{$list}) {
  0         0  
8550            
8551             # if first file
8552 0 0       0 if ($n == 0) {
8553            
8554             # if file read successfully
8555 0 0       0 if (! ($result = _readChart($self, $file, $hash))) {
8556            
8557             # add colorimetric metadata
8558 0         0 _addColorMeta($self);
8559            
8560             # make format key string (removing contexts)
8561 0 0       0 $keys = join(':', map {s/^.*\|// if defined(); defined() ? $_ : '-'} @{$self->[1][0]});
  0 0       0  
  0         0  
  0         0  
8562            
8563             # make signature
8564 0         0 $sig = signature($self);
8565            
8566             # get column slice (spectral, XYZ, or L*a*b* data)
8567 0 0       0 @cols = grep {defined($self->[1][0][$_]) && $self->[1][0][$_] =~ m/((?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3}|XYZ_[XYZ]|LAB_[LAB])$/} (0 .. $#{$self->[1][0]});
  0         0  
  0         0  
8568            
8569             # if format keys lack context (not a CxF3 file)
8570 0 0       0 if ($self->[1][0][$cols[0]] !~ m/^M[0-3]_Measurement\|/) {
8571            
8572             # get measurement condition from file name (very loose match)
8573 0 0       0 if ($file =~ m/.*(M[0-3])/) {
8574            
8575             # make context string
8576 0         0 $ctx = $1 . '_Measurement|';
8577            
8578             # for each column
8579 0         0 for my $i (@cols) {
8580            
8581             # remove current context (if any)
8582 0         0 $self->[1][0][$i] =~ s/^.*\|//;
8583            
8584             # add derived context
8585 0         0 $self->[1][0][$i] = $ctx . $self->[1][0][$i];
8586            
8587             }
8588            
8589             } else {
8590            
8591             # print warning
8592 0         0 warn("can't determine context of chart $file\n");
8593            
8594             }
8595            
8596             }
8597            
8598             # increment counter
8599 0         0 $n++;
8600            
8601             } else {
8602            
8603             # print warning
8604 0         0 warn("chart $file $result, ignored\n");
8605            
8606             }
8607            
8608             } else {
8609            
8610             # make temporary Chart object
8611 0         0 $temp = ICC::Support::Chart->new();
8612            
8613             # if file read successfully
8614 0 0       0 if (! ($result = _readChart($temp, $file, $hash))) {
8615            
8616             # if charts have same structure (rows and cols)
8617 0 0 0     0 if ((defined($sig) && $sig eq signature($temp) || $#{$self->[1]} == $#{$temp->[1]}) && $keys eq join(':', map {s/^.*\|// if defined(); defined() ? $_ : '-'} @{$temp->[1][0]})) {
  0 0 0     0  
  0 0       0  
  0         0  
8618            
8619             # if format keys lack context (not a CxF3 file)
8620 0 0       0 if ($temp->[1][0][$cols[0]] !~ m/^M[0-3]_Measurement\|/) {
8621            
8622             # get measurement condition from file name (very loose match)
8623 0 0       0 if ($file =~ m/.*(M[0-3])/) {
8624            
8625             # make context string
8626 0         0 $ctx = $1 . '_Measurement|';
8627            
8628             # for each column
8629 0         0 for my $i (@cols) {
8630            
8631             # remove current context (if any)
8632 0         0 $temp->[1][0][$i] =~ s/^.*\|//;
8633            
8634             # add derived context
8635 0         0 $temp->[1][0][$i] = $ctx . $temp->[1][0][$i];
8636            
8637             }
8638            
8639             } else {
8640            
8641             # print warning
8642 0         0 warn("can't determine context of chart $file\n");
8643            
8644             }
8645            
8646             }
8647            
8648             # for each row
8649 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
8650            
8651             # append temp data
8652 0         0 push(@{$self->[1][$i]}, @{$temp->[1][$i]}[@cols]);
  0         0  
  0         0  
8653            
8654             }
8655            
8656             # increment counter
8657 0         0 $n++;
8658            
8659             } else {
8660            
8661             # print warning
8662 0         0 warn("chart $file has different structure, ignored\n");
8663            
8664             }
8665            
8666             } else {
8667            
8668             # print warning
8669 0         0 warn("chart $file $result, ignored\n");
8670            
8671             }
8672            
8673             }
8674            
8675             }
8676              
8677             # print message if any files were read
8678 0 0       0 print "$n files read in directory $self->[0]{'file_path'}\n\n" if ($n);
8679              
8680             # save number of files read
8681 0         0 $self->[0]{'files_read'} = $n;
8682            
8683             # return
8684 0         0 return($n);
8685              
8686             }
8687              
8688             # read chart
8689             # parameters: (object_reference, path_to_file, hash)
8690             # returns: (result)
8691             sub _readChart {
8692              
8693             # get parameters
8694 27     27   60 my ($self, $path, $hash) = @_;
8695              
8696             # local variables
8697 27         35 my ($fh, $buf, $result);
8698              
8699             # open the file (read-only)
8700 27 50       1086 open($fh, '<', $path) || return("$! when opening file");
8701              
8702             # set binary mode
8703 27         84 binmode($fh);
8704              
8705             # read start of file
8706 27 50       737 read($fh, $buf, 1024) || return("is zero length");
8707              
8708             # reset file pointer
8709 27         249 seek($fh, 0, 0);
8710              
8711             # if an ASE file
8712 27 50 33     285 if ($buf =~ m/^ASEF/) {
    50          
    50          
    100          
    50          
8713            
8714             # save file type
8715 0         0 $self->[0]{'file_type'} = 'ASEF';
8716            
8717             # read ASE file
8718 0         0 $result = _readChartASE($self, $fh, $hash);
8719            
8720             # if a TIFF file
8721             } elsif ($buf =~ m/^(II\*\x00|MM\x00\*)/) {
8722            
8723             # save file type
8724 0         0 $self->[0]{'file_type'} = 'TIFF';
8725            
8726             # read TIFF file
8727 0         0 $result = _readChartTIFF($self, $fh, $hash);
8728            
8729             # if an ICC profile
8730             } elsif (substr($buf, 36, 4) eq 'acsp') {
8731            
8732             # save file type
8733 0         0 $self->[0]{'file_type'} = 'prof';
8734            
8735             # read ICC file
8736 0         0 $result = _readChartICC($self, $fh, $hash);
8737            
8738             # if a CxF3 file
8739             } elsif ($buf =~ m/http:\/\/colorexchangeformat.com\/CxF3-core/) {
8740            
8741             # save file type
8742 8         25 $self->[0]{'file_type'} = 'CXFX';
8743            
8744             # read CxF3 file
8745 8         22 $result = _readChartCxF3($self, $fh, $hash);
8746            
8747             # if an SS3 file
8748             } elsif (substr($buf, 0, 4) eq "\x00\x20\x00\x00" || substr($buf, 0, 4) eq "\x00\x32\x00\x00") {
8749            
8750             # save file type
8751 0         0 $self->[0]{'file_type'} = 'SS3';
8752            
8753             # read SS3 file
8754 0         0 $result = _readChartSS3($self, $fh, $hash);
8755            
8756             # must be a text file
8757             } else {
8758            
8759             # save file type
8760 19         54 $self->[0]{'file_type'} = 'TEXT';
8761            
8762             # read ASCII file
8763 19         47 $result = _readChartASCII($self, $fh, $hash);
8764            
8765             }
8766              
8767             # close the file
8768 27         1270 close($fh);
8769              
8770             # return
8771 27         170 return($result);
8772              
8773             }
8774              
8775             # read chart from ISO 28178 ASCII data file
8776             # parameters: (object_reference, file_handle, hash)
8777             # returns: (result)
8778             sub _readChartASCII {
8779              
8780             # get parameters
8781 19     19   35 my ($self, $fh, $hash) = @_;
8782              
8783             # local variables
8784 19         40 my ($buf, $state, $iflag, $eflag, $index);
8785 19         0 my (@fields, $illum, $append);
8786              
8787             # read start of file
8788 19 50       177 read($fh, $buf, 1024) || return("is zero length");
8789              
8790             # reset file pointer
8791 19         144 seek($fh, 0, 0);
8792              
8793             # check for CR-LF (DOS/Windows)
8794 19 100       114 if ($buf =~ m/\015\012/) {
    100          
    50          
8795            
8796             # set record separator
8797 1         8 $self->[0]{'read_rs'} = "\015\012";
8798            
8799             # check for LF (Unix/OSX)
8800             } elsif ($buf =~ m/\012/) {
8801            
8802             # set record separator
8803 17         44 $self->[0]{'read_rs'} = "\012";
8804            
8805             # check for CR (Mac)
8806             } elsif ($buf =~ m/\015/) {
8807            
8808             # set record separator
8809 1         4 $self->[0]{'read_rs'} = "\015";
8810            
8811             # not a text file
8812             } else {
8813            
8814             # close the file
8815 0         0 close($fh);
8816            
8817             # return
8818 0         0 return('unknown file type');
8819            
8820             }
8821              
8822             # localize input record separator
8823 19         79 local $/ = $self->[0]{'read_rs'};
8824              
8825             # localize loop variable
8826 19         31 local $_;
8827              
8828             # initialize variables
8829 19         47 $self->[1] = [[]];
8830 19         33 $illum = [[]];
8831 19         24 $index = 1;
8832 19         23 $state = 0;
8833 19         23 $iflag = 0;
8834              
8835             # read the file, line by line
8836 19         221 while (<$fh>) {
8837            
8838             # add appended text, as is
8839 497 50       849 $append .= $_ if ($state == 4);
8840            
8841             # remove leading spaces/tabs and trailing whitespace
8842 497         4190 s/^[\ \t]*(.*?)[\s,]*$/$1/;
8843            
8844             # if normal comment line (all comments are removed)
8845 497 100 66     1114 if (s/#\s*(.*)// && $state == 0) {
8846            
8847             # if remaining line blank
8848 32 100       63 if (length() == 0) {
8849            
8850             # add comment to header array
8851 26         26 push(@{$self->[3]}, ['#', $1]);
  26         74  
8852            
8853             } else {
8854            
8855             # restore comment to header line
8856             # preserves time in ProfileMaker 'CREATED' lines
8857 6         19 $_ .= "# $1";
8858            
8859             }
8860            
8861             }
8862            
8863             # skip blank lines
8864 497 100       739 next if (length() == 0);
8865            
8866             # begin data format
8867 471 100       1268 if (m/^BEGIN_DATA_FORMAT$/) {
    100          
    100          
    100          
    50          
    50          
8868            
8869             # set state
8870 19         49 $state = 1;
8871            
8872             # end data format
8873             } elsif (m/^END_DATA_FORMAT$/) {
8874            
8875             # set state
8876 19         48 $state = 2;
8877            
8878             # begin data
8879             } elsif (m/^BEGIN_DATA$/) {
8880            
8881             # set state
8882 19         43 $state = 3;
8883            
8884             # end data
8885             } elsif (m/^END_DATA$/) {
8886            
8887             # set state
8888 19         55 $state = 4;
8889            
8890             # begin ProfileMaker illuminant section
8891             } elsif (m/^BEGIN_DATA_EMISSION$/) {
8892            
8893             # set illuminant flag
8894 0         0 $iflag = 1;
8895            
8896             # reset index
8897 0         0 $index = 1;
8898            
8899             # end ProfileMaker illuminant section
8900             } elsif (m/^END_DATA_EMISSION$/) {
8901            
8902             # clear illuminant flag
8903 0         0 $iflag = 0;
8904            
8905             # reset appended data
8906 0         0 $append = '';
8907            
8908             # anything else
8909             } else {
8910            
8911             # format
8912 395 100 66     1640 if ($iflag == 0 && $state == 1) {
    100 66        
    50 33        
    50 33        
    50 66        
      33        
8913            
8914             # change 'SampleID' to 'SAMPLE_ID'
8915             # non-standard notation used by ProfileMaker
8916 19         46 s/SampleID/SAMPLE_ID/;
8917            
8918             # parse and save format keys
8919 19         25 push(@{$self->[1][0]}, split(/[\s,]+/));
  19         277  
8920            
8921             # data
8922             } elsif ($iflag == 0 && $state == 3) {
8923            
8924             # if Euro flag not defined
8925 190 100       265 if (! defined($eflag)) {
8926            
8927             # split data
8928 19         175 @fields = split(/[\s,]+/);
8929            
8930             # set flag for Euro decimal notation (e.g. 6,3 becomes 6.3)
8931 19   33     49 $eflag = m/,/ && @fields > (@{$self->[1][0]} || 0);
8932            
8933             }
8934            
8935             # fix Euro decimal notation (e.g. 6,3 becomes 6.3)
8936 190 50       258 s/(\d),(\d)/$1.$2/g if ($eflag);
8937            
8938             # parse and save data
8939 190         2582 $self->[1][$index++] = [split(/[\s,]+/)];
8940            
8941             # illuminant format
8942             # may be different from data format
8943             } elsif ($iflag == 1 && $state == 1) {
8944            
8945             # change 'SampleID' to 'SAMPLE_ID'
8946             # non-standard notation used by ProfileMaker
8947 0         0 s/SampleID/SAMPLE_ID/;
8948            
8949             # parse and save illuminant format keys
8950 0         0 push(@{$illum->[0]}, split(/[\s,]+/));
  0         0  
8951            
8952             # illuminant data
8953             } elsif ($iflag == 1 && $state == 3) {
8954            
8955             # fix Euro decimal notation (e.g. 6,3 becomes 6.3)
8956 0 0       0 s/(\d),(\d)/$1.$2/g if ($eflag);
8957            
8958             # parse and save illuminant data
8959 0         0 $illum->[$index++] = [split(/[\s,]+/)];
8960            
8961             # header lines
8962             } elsif ($iflag == 0 && ($state == 0 || $state == 2)) {
8963            
8964             # match keyword/value
8965 186         533 m/^([^\s,]*)[\s,]*(.*?)$/;
8966            
8967             # add to header array
8968 186 50       359 push(@{$self->[3]}, [$1, $2]) if (length($1));
  186         772  
8969            
8970             }
8971            
8972             }
8973            
8974             }
8975              
8976             # save illuminant data, if any
8977 19 50       48 $self->[0]{'illuminant'} = $illum if defined($illum->[1]);
8978              
8979             # save appended data, if any
8980 19 50       30 $self->[0]{'append'} = $append if (defined($append));
8981              
8982             # apply rotation/flip (special keywords)
8983 19         47 _rotateChartASCII($self);
8984              
8985             # check spectral data
8986 19         56 _scale_check($self);
8987              
8988             # return success flag
8989 19 50       110 return($state == 4 ? () : "ASCII read failed with state $state");
8990              
8991             }
8992              
8993             # apply rotation/flip to ASCII chart data
8994             # if LGOROWLENGTH and (DPLGROTATE or DPLGFLIP) keywords are present
8995             # parameter: (object_reference)
8996             sub _rotateChartASCII {
8997              
8998             # get object reference
8999 19     19   28 my $self = shift();
9000              
9001             # local variables
9002 19         32 my ($rot, $flip, $mat, $rows);
9003              
9004             # get the rotation and flip values
9005 19         42 $rot = keyword($self, 'DPLGROTATE');
9006 19         32 $flip = keyword($self, 'DPLGFLIP');
9007              
9008             # if LGOROWLENGTH and (DPLGROTATE or DPLGFLIP) keywords
9009 19 50 33     29 if (keyword($self, 'LGOROWLENGTH') && ($rot || $flip)) {
      33        
9010            
9011             # get selection matrix
9012 0         0 $mat = select_matrix($self)->rotate($rot)->flip($flip);
9013            
9014             # flatten matrix
9015 0         0 $rows = ICC::Shared::flatten($mat);
9016            
9017             # prepend DATA_FORMAT row index (0)
9018 0         0 unshift(@{$rows}, 0);
  0         0  
9019            
9020             # rearrange chart data
9021 0         0 $self->[1] = [@{$self->[1]}[@{$rows}]];
  0         0  
  0         0  
9022            
9023             # update LGOROWLENGTH
9024 0         0 keyword($self, 'LGOROWLENGTH', scalar(@{$mat->[0]}));
  0         0  
9025            
9026             }
9027            
9028             }
9029              
9030             # read chart from Adobe Swatch Exchange (.ase) file
9031             # optional hash key: 'colorspace'
9032             # 'colorspace' values: 'CMYK', 'LAB ', 'RGB ' or 'Gray'
9033             # 'Gray' swatches are mapped to CMYK values
9034             # parameters: (object_reference, file_handle, hash)
9035             # returns: (result)
9036             sub _readChartASE {
9037              
9038             # get parameters
9039 0     0   0 my ($self, $fh, $hash) = @_;
9040              
9041             # local variables
9042 0         0 my ($cs, $le, $buf, @header, $sn);
9043 0         0 my ($mark, $type, $blen, $slen);
9044 0         0 my ($name, $space, $cmyk, $rgb, $Lab, $dev);
9045              
9046             # set colorspace selector
9047 0 0       0 $cs = $hash->{'colorspace'} if defined($hash->{'colorspace'});
9048              
9049             # get little-endian flag
9050 0         0 $le = ($Config{'byteorder'} =~ m/1234/);
9051              
9052             # read header (file signature, version, number of blocks)
9053 0         0 read($fh, $buf, 12);
9054              
9055             # unpack buffer
9056 0         0 @header = unpack('A4nnN', $buf);
9057              
9058             # verify file signature
9059 0 0       0 ($header[0] eq 'ASEF') || return('not a valid ASE file');
9060              
9061             # add SAMPLE_NAME field
9062 0         0 $sn = add_fmt($self, 'SAMPLE_NAME');
9063              
9064             # set file pointer
9065 0         0 $mark = 12;
9066              
9067             # for each block
9068 0         0 for my $s (1 .. $header[3]) {
9069            
9070             # read block type, block length, and string length
9071 0         0 read($fh, $buf, 8);
9072            
9073             # unpack buffer
9074 0         0 ($type, $blen, $slen) = unpack('nNn', $buf);
9075            
9076             # if color entry type
9077 0 0       0 if ($type == 1) {
9078            
9079             # read color name
9080 0         0 read($fh, $buf, 2 * $slen);
9081            
9082             # decode color name
9083 0         0 $name = decode('UTF-16BE', $buf);
9084            
9085             # trim trailing '0'
9086 0         0 $name =~ s/\x00$//;
9087            
9088             # change spaces to underscores
9089 0         0 $name =~ s/\s/_/g;
9090            
9091             # read color space
9092 0         0 read($fh, $space, 4);
9093            
9094             # if colorspace is CMYK
9095 0 0 0     0 if (($space eq 'CMYK' && (! defined($cs)) || $cs eq 'CMYK')) {
    0 0        
    0 0        
    0 0        
      0        
      0        
      0        
      0        
9096            
9097             # store color as SAMPLE_NAME
9098 0         0 $self->[1][$s][$sn->[0]] = $name;
9099            
9100             # init device array
9101 0         0 $dev = [];
9102            
9103             # for each CMYK value
9104 0         0 for my $i (0 .. 3) {
9105            
9106             # read 32-bit floating point value
9107 0         0 read($fh, $buf, 4);
9108            
9109             # reverse bytes if long-endian system
9110 0 0       0 $buf = reverse($buf) if ($le);
9111            
9112             # unpack buffer
9113 0         0 $dev->[$i] = unpack('f', $buf);
9114            
9115             }
9116            
9117             # if CMYK slice undefined
9118 0 0       0 if (! defined($cmyk)) {
9119            
9120             # add CMYK slice
9121 0         0 $cmyk = add_fmt($self, qw(CMYK_C CMYK_M CMYK_Y CMYK_K));
9122            
9123             }
9124            
9125             # store CMYK values
9126 0         0 @{$self->[1][$s]}[@{$cmyk}] = map {100 * $_} @{$dev};
  0         0  
  0         0  
  0         0  
  0         0  
9127            
9128             # if colorspace is RGB
9129             } elsif (($space eq 'RGB ' && (! defined($cs)) || $cs eq 'RGB ')) {
9130            
9131             # store color as SAMPLE_NAME
9132 0         0 $self->[1][$s][$sn->[0]] = $name;
9133            
9134             # init device array
9135 0         0 $dev = [];
9136            
9137             # for each RGB value
9138 0         0 for my $i (0 .. 2) {
9139            
9140             # read 32-bit floating point value
9141 0         0 read($fh, $buf, 4);
9142            
9143             # reverse bytes if long-endian system
9144 0 0       0 $buf = reverse($buf) if ($le);
9145            
9146             # unpack buffer
9147 0         0 $dev->[$i] = unpack('f', $buf);
9148            
9149             }
9150            
9151             # if RGB slice undefined
9152 0 0       0 if (! defined($rgb)) {
9153            
9154             # add RGB slice
9155 0         0 $rgb = add_fmt($self, qw(RGB_R RGB_G RGB_B));
9156            
9157             }
9158            
9159             # store RGB values
9160 0         0 @{$self->[1][$s]}[@{$rgb}] = map {255 * $_} @{$dev};
  0         0  
  0         0  
  0         0  
  0         0  
9161            
9162             # if colorspace is L*a*b*
9163             } elsif (($space eq 'LAB ' && (! defined($cs)) || $cs eq 'LAB ')) {
9164            
9165             # store color as SAMPLE_NAME
9166 0         0 $self->[1][$s][$sn->[0]] = $name;
9167            
9168             # init device array
9169 0         0 $dev = [];
9170            
9171             # for each L*a*b* value
9172 0         0 for my $i (0 .. 2) {
9173            
9174             # read 32-bit floating point value
9175 0         0 read($fh, $buf, 4);
9176            
9177             # reverse bytes if long-endian system
9178 0 0       0 $buf = reverse($buf) if ($le);
9179            
9180             # unpack buffer
9181 0         0 $dev->[$i] = unpack('f', $buf);
9182            
9183             }
9184            
9185             # if L*a*b* slice undefined
9186 0 0       0 if (! defined($Lab)) {
9187            
9188             # add L*a*b* fields
9189 0         0 $Lab = add_fmt($self, qw(LAB_L LAB_A LAB_B));
9190            
9191             }
9192            
9193             # store L*a*b* values
9194 0         0 @{$self->[1][$s]}[@{$Lab}] = (100 * $dev->[0], $dev->[1], $dev->[2]);
  0         0  
  0         0  
9195            
9196             # if colorspace is Grayscale
9197             } elsif (($space eq 'Gray' && (! defined($cs)) || $cs eq 'Gray')) {
9198            
9199             # store color as SAMPLE_NAME
9200 0         0 $self->[1][$s][$sn->[0]] = $name;
9201            
9202             # read 32-bit floating point value
9203 0         0 read($fh, $buf, 4);
9204            
9205             # reverse bytes if long-endian system
9206 0 0       0 $buf = reverse($buf) if ($le);
9207            
9208             # unpack buffer
9209 0         0 $dev = [unpack('f', $buf)];
9210            
9211             # if CMYK slice is undefined
9212 0 0       0 if (! defined($cmyk)) {
9213            
9214             # add CMYK slice
9215 0         0 $cmyk = add_fmt($self, qw(CMYK_C CMYK_M CMYK_Y CMYK_K));
9216            
9217             }
9218            
9219             # store CMYK values
9220 0         0 @{$self->[1][$s]}[@{$cmyk}] = (0, 0, 0, 100 * (1 - $dev->[0]));
  0         0  
  0         0  
9221            
9222             }
9223            
9224             }
9225            
9226             # set file pointer to next block
9227 0         0 $mark += $blen + 6;
9228            
9229             # seek next block
9230 0         0 seek($fh, $mark, 0);
9231            
9232             }
9233              
9234             # return
9235 0         0 return();
9236              
9237             }
9238              
9239             # read chart from ICC profile
9240             # some profiles have tags containing chart data
9241             # parameters: (object_reference, file_handle, hash)
9242             # returns: (result)
9243             sub _readChartICC {
9244              
9245             # get parameters
9246 0     0   0 my ($self, $fh, $hash) = @_;
9247              
9248             # local variables
9249 0         0 my (@header, @tagtab, %offset, %tags, $type, $class);
9250 0         0 my ($temp, $data, $targ, $text, $result);
9251              
9252             # load ICC::Profile modules, if not already included
9253 0         0 require ICC::Profile;
9254              
9255             # read the profile header
9256 0 0       0 ICC::Profile::_readICCheader($fh, \@header) || return('failed reading ICC profile header');
9257              
9258             # read the profile tag table
9259 0 0       0 ICC::Profile::_readICCtagtable($fh, \@tagtab) || return('failed reading ICC profile tag table');
9260              
9261             # for each tag
9262 0         0 for my $tag (@tagtab) {
9263            
9264             # if tag contains measurement data
9265 0 0       0 if ($tag->[0] =~ m/^(?:CxF |DevD|CIED|DEVD|targ)$/) {
9266            
9267             # if a duplicate tag
9268 0 0       0 if (exists($offset{$tag->[1]})) {
9269            
9270             # use original tag
9271 0         0 $tags{$tag->[0]} = $offset{$tag->[1]};
9272            
9273             } else {
9274            
9275             # seek to start of tag
9276 0         0 seek($fh, $tag->[1], 0);
9277            
9278             # read tag type signature
9279 0         0 read($fh, $type, 4);
9280            
9281             # convert non-word characters to underscores
9282 0         0 $type =~ s|\W|_|g;
9283            
9284             # form class specifier
9285 0         0 $class = 'ICC::Profile::' . $type;
9286            
9287             # if 'class->new_fh' method exists
9288 0 0       0 if ($class->can('new_fh')) {
9289            
9290             # create specific tag object
9291 0         0 $tags{$tag->[0]} = $class->new_fh($self, $fh, $tag);
9292            
9293             } else {
9294            
9295             # create generic tag object
9296 0         0 $tags{$tag->[0]} = ICC::Profile::Generic->new_fh($self, $fh, $tag);
9297            
9298             }
9299            
9300             # save tag in hash
9301 0         0 $offset{$tag->[1]} = $tags{$tag->[0]};
9302            
9303             }
9304            
9305             }
9306            
9307             }
9308            
9309             # if creator is i1Profiler and 'CxF ' tag exists
9310 0 0 0     0 if ($header[23] eq 'XRCM' && exists($tags{'CxF '})) {
    0 0        
    0 0        
    0 0        
      0        
9311            
9312             # close file handle
9313 0         0 close($fh);
9314            
9315             # open file handle to CxF3 string
9316 0         0 open($fh, '<', \$tags{'CxF '}->text());
9317            
9318             # make chart from CxF3 string
9319 0         0 return(_readChartCxF3($self, $fh, $hash));
9320            
9321             # if creator is ProfileMaker and 'DevD' / 'CIED' tags exist
9322             } elsif ($header[23] eq 'LOGO' && exists($tags{'DevD'}) && exists($tags{'CIED'})) {
9323            
9324             # close file handle
9325 0         0 close($fh);
9326            
9327             # open file handle to 'DevD' text string
9328 0         0 open($fh, '<', \$tags{'DevD'}->text());
9329            
9330             # read chart from text
9331 0 0       0 ($result = _readChartASCII($self, $fh, $hash)) && return("failed reading ICC profile DEVD tag, $result");
9332            
9333             # close file handle
9334 0         0 close($fh);
9335            
9336             # make temporary chart object
9337 0         0 $temp = ICC::Support::Chart->new();
9338            
9339             # open file handle to 'CIED' text string
9340 0         0 open($fh, '<', \$tags{'CIED'}->text());
9341            
9342             # read chart from text
9343 0 0       0 ($result = _readChartASCII($temp, $fh, $hash)) && return("failed reading ICC profile CIED tag, $result");
9344            
9345             # get data slice (all rows, spectral, XYZ and L*a*b* columns)
9346 0         0 $data = slice($temp, [0 .. $#{$temp->[1]}], [grep {$temp->[1][0][$_] =~ m/^(nm\d{3}|XYZ_(X|Y|Z)|LAB_(L|A|B))$/} (0 .. $#{$temp->[1][0]})]);
  0         0  
  0         0  
  0         0  
9347            
9348             # append to chart
9349 0         0 add_cols($self, $data);
9350            
9351             # for each keyword
9352 0         0 for my $key (@{$temp->[3]}) {
  0         0  
9353            
9354             # if keyword not in main chart
9355 0 0       0 if (0 == grep {$key->[0] eq $_->[0]} @{$self->[3]}) {
  0         0  
  0         0  
9356            
9357             # append keyword/value
9358 0         0 push(@{$self->[3]}, $key);
  0         0  
9359            
9360             }
9361            
9362             }
9363            
9364             # return
9365 0         0 return();
9366            
9367             # if creator is Monaco and 'DEVD' tag exists (some old profiles are identified by preferred CMM)
9368             } elsif (($header[23] eq 'MONS' || $header[1] eq 'mnco') && exists($tags{'DEVD'})) {
9369            
9370             # read chart from Monaco 'DEVD' tag
9371 0         0 return(_readMonacoDEVD($self, $tags{'DEVD'}->data(), \@header));
9372            
9373             # if 'targ' tag exists
9374             } elsif (exists($tags{'targ'})) {
9375            
9376             # get the 'targ' tag
9377 0         0 $targ = $tags{'targ'};
9378            
9379             # if an 'ICC::Profile::text' object
9380 0 0       0 if (ref($targ) eq 'ICC::Profile::text') {
    0          
9381            
9382             # get 'targ' tag text string
9383 0         0 $text = $targ->text();
9384            
9385             } elsif (ref($targ) eq 'ICC::Profile::Generic') {
9386            
9387             # get 'targ' tag text string
9388 0         0 $text = $targ->data();
9389            
9390             } else {
9391            
9392             # return
9393 0         0 return("failed reading ICC profile 'targ' tag");
9394            
9395             }
9396            
9397             # if reference to ICC Characterization Data Registry
9398 0 0       0 if ($text =~ m/^ICCHDAT (.*)$/) {
9399            
9400             # return
9401 0         0 return("profile derived from $1 characterization data, available at www.color.org");
9402            
9403             } else {
9404            
9405             # close file handle
9406 0         0 close($fh);
9407            
9408             # open file handle to text string
9409 0         0 open($fh, '<', \$text);
9410            
9411             # read chart from text
9412 0         0 return(_readChartASCII($self, $fh, $hash));
9413            
9414             }
9415            
9416             }
9417              
9418             # return
9419 0         0 return('failed reading ICC profile characterization data');
9420              
9421             }
9422              
9423             # read chart from Monaco 'DEVD' tag
9424             # parameters: (object_reference, tag_data, profile_header)
9425             # returns: (result)
9426             sub _readMonacoDEVD {
9427              
9428             # get parameters
9429 0     0   0 my ($self, $data, $header) = @_;
9430              
9431             # local variables
9432 0         0 my ($big, %cshash, $cs, $nc, $fix, $ix, $tag, $tac, $limit, $mult, $dev, $lab);
9433 0         0 my ($ns, $sec, @devfix, @nd, $nt, @devstep, @dev, @cmy, @sum, @temp, $m, @dat);
9434              
9435             # get big-endian flag (true if our system is big-endian)
9436 0         0 $big = ($Config{'byteorder'} =~ m/4321/);
9437              
9438             # colorspace hash (colorspace => number_channels)
9439 0         0 %cshash = ('RGB ' => 3, 'CMYK' => 4, '5CLR' => 5, '6CLR' => 6, '7CLR' => 7, '8CLR' => 8);
9440              
9441             # initialize fixed value array
9442 0         0 @devfix = ();
9443              
9444             # get colorspace from profile header
9445 0         0 $cs = $header->[4];
9446              
9447             # lookup number of channels
9448 0         0 $nc = $cshash{$cs};
9449              
9450             # set number of fixed channels
9451 0         0 $fix = $nc - 3;
9452              
9453             # set index to start of first tag
9454 0         0 $ix = 28;
9455              
9456             # set tag value
9457 0         0 $tag = pack('N', 0x002D);
9458              
9459             # find TAC tag
9460 0   0     0 do {$ix = index($data, $tag, $ix)} while ($ix >= 0 && $ix % 4 && $ix++);
  0   0     0  
9461              
9462             # verify tag found
9463 0 0       0 ($ix >= 0) || return('failed reading TAC from Monaco DEVD tag');
9464              
9465             # get TAC value
9466 0 0       0 $tac = 100 * unpack('d', $big ? substr($data, $ix + 4, 8) : reverse(substr($data, $ix + 4, 8)));
9467              
9468             # if RGB colorspace
9469 0 0       0 if ($cs eq 'RGB ') {
    0          
9470            
9471             # add device fields
9472 0         0 $dev = add_fmt($self, qw(RGB_R RGB_G RGB_B));
9473            
9474             # set device multiplier
9475 0         0 $mult = 255;
9476            
9477             # if CMYK colorspace
9478             } elsif ($cs eq 'CMYK') {
9479            
9480             # add device fields
9481 0         0 $dev = add_fmt($self, qw(CMYK_C CMYK_M CMYK_Y CMYK_K));
9482            
9483             # set device multiplier
9484 0         0 $mult = 100;
9485            
9486             } else {
9487            
9488             # add device fields
9489 0         0 $dev = add_fmt($self, map {"$cs\_$_"} (1 .. $nc));
  0         0  
9490            
9491             # set device multiplier
9492 0         0 $mult = 100;
9493            
9494             }
9495              
9496             # add L*a*b* fields
9497 0         0 $lab = add_fmt($self, qw(LAB_L LAB_A LAB_B));
9498              
9499             # advance index
9500 0         0 $ix += 12;
9501              
9502             # set tag value
9503 0         0 $tag = pack('N', 0x0027);
9504              
9505             # find data group tag
9506 0   0     0 do {$ix = index($data, $tag, $ix)} while ($ix >= 0 && $ix % 4 && $ix++);
  0   0     0  
9507              
9508             # verify tag found
9509 0 0       0 ($ix >= 0) || return('failed reading data group from Monaco DEVD tag');
9510              
9511             # get number data sections in group
9512 0         0 $ns = unpack('N', substr($data, $ix + 4, 4));
9513              
9514             # advance index
9515 0         0 $ix += 8;
9516              
9517             # for data each section
9518 0         0 for my $s (0 .. $ns - 1) {
9519            
9520             # verify tag 0x0028
9521 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x0028)) || return(0);
9522            
9523             # get section index
9524 0         0 $sec = unpack('N', substr($data, $ix + 4, 4));
9525            
9526             # verify section index is correct
9527 0 0       0 ($sec == $s) || return('failed reading section index from Monaco DEVD tag');
9528            
9529             # advance index
9530 0         0 $ix += 8;
9531            
9532             # verify tag 0x002A (fixed device values)
9533 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x002A)) || return('failed reading fixed device values from Monaco DEVD tag');
9534            
9535             # if fixed device values (none for RGB)
9536 0 0       0 if ($fix) {
9537            
9538             # get fixed device values (black plus any extra colors, e.g. orange or green)
9539 0 0       0 @devfix = unpack("d$fix", $big ? substr($data, $ix + 4, 8 * $fix) : reverse(substr($data, $ix + 4, 8 * $fix)));
9540            
9541             # reverse array if little-endian
9542 0 0       0 @devfix = reverse(@devfix) if (! $big);
9543            
9544             # apply multiplier
9545 0         0 @devfix = map {$_ * $mult} @devfix;
  0         0  
9546            
9547             }
9548            
9549             # advance index
9550 0         0 $ix += 8 * $fix + 4;
9551            
9552             # verify tag 0x002B (step counts by color)
9553 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x002B)) || return('failed reading step counts from Monaco DEVD tag');
9554            
9555             # get device step counts
9556 0         0 @nd = unpack('N3', substr($data, $ix + 4, 12));
9557            
9558             # get total number of steps
9559 0         0 $nt = $nd[0] + $nd[1] + $nd[2];
9560            
9561             # advance index
9562 0         0 $ix += 16;
9563            
9564             # verify tag 0x002C (step values by color)
9565 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x002C)) || return('failed reading step values from Monaco DEVD tag');
9566            
9567             # get step values
9568 0 0       0 @devstep = unpack("d$nt", $big ? substr($data, $ix + 4, 8 * $nt) : reverse(substr($data, $ix + 4, 8 * $nt)));
9569            
9570             # reverse array if little-endian
9571 0 0       0 @devstep = reverse(@devstep) if (! $big);
9572            
9573             # apply multiplier
9574 0         0 @devstep = map {$_ * $mult} @devstep;
  0         0  
9575            
9576             # advance index
9577 0         0 $ix += 8 * $nt + 4;
9578            
9579             # initialize arrays
9580 0         0 @dev = ();
9581 0         0 @sum = ();
9582 0         0 @temp = ();
9583            
9584             # if RGB colorspace
9585 0 0       0 if ($cs eq 'RGB ') {
9586            
9587             # for each blue step
9588 0         0 for my $i (0 .. $nd[2] - 1) {
9589            
9590             # for each green step
9591 0         0 for my $j (0 .. $nd[1] - 1) {
9592            
9593             # for each red step
9594 0         0 for my $k (0 .. $nd[0] - 1) {
9595            
9596             # save RGB values
9597 0         0 push(@dev, $devstep[$k], $devstep[$j + $nd[0]], $devstep[$i + $nd[0] + $nd[1]]);
9598            
9599             }
9600            
9601             }
9602            
9603             }
9604            
9605             # if CMYK or NCLR colorspace
9606             } else {
9607            
9608             # for each yellow step
9609 0         0 for my $i (0 .. $nd[2] - 1) {
9610            
9611             # for each cyan step
9612 0         0 for my $j (0 .. $nd[0] - 1) {
9613            
9614             # for each magenta step
9615 0         0 for my $k (0 .. $nd[1] - 1) {
9616            
9617             # get CMY values
9618 0         0 @cmy = ($devstep[$j], $devstep[$k + $nd[0]], $devstep[$i + $nd[0] + $nd[1]]);
9619            
9620             # save CMY values
9621 0         0 push(@temp, [@cmy]);
9622            
9623             # save total ink value
9624 0         0 push(@sum, List::Util::sum(@cmy, @devfix));
9625            
9626             }
9627            
9628             }
9629            
9630             }
9631            
9632             # initialize actual ink limit
9633 0         0 $limit = $nc * 100;
9634            
9635             # find actual ink limit (smallest value greater than TAC)
9636 0 0 0     0 for (@sum) {$limit = $_ if ($_ > $tac && $_ < $limit)};
  0         0  
9637            
9638             # for each sample
9639 0         0 for my $i (0 .. $#sum) {
9640            
9641             # get cmy values
9642 0         0 @cmy = @{$temp[$i]};
  0         0  
9643            
9644             # if sample within ink limit, or a corner point
9645 0 0 0     0 if ($sum[$i] <= $limit || ((0 < grep {$_ == 0} @cmy) && (0 < grep {$_ == 100} @cmy))) {
  0   0     0  
  0         0  
9646            
9647             # copy cmy values
9648 0         0 push(@dev, @cmy);
9649            
9650             }
9651            
9652             }
9653            
9654             }
9655            
9656             # verify tag 0x0032 (L*a*b* sample data)
9657 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x0032)) || return('failed reading L*a*b* data from Monaco DEVD tag');
9658            
9659             # get number of values
9660 0         0 $m = unpack('N', substr($data, $ix + 4, 4)) * 3;
9661            
9662             # get L*a*b* color data
9663 0 0       0 @dat = unpack("d$m", $big ? substr($data, $ix + 8, 8 * $m) : reverse(substr($data, $ix + 8, 8 * $m)));
9664            
9665             # reverse array if little-endian
9666 0 0       0 @dat = reverse(@dat) if (! $big);
9667            
9668             # advance index
9669 0         0 $ix += 8 * $m + 8;
9670            
9671             # verify @dev and @dat are same size
9672 0 0       0 (scalar(@dev) == scalar(@dat)) || return('failed comparing data counts of Monaco DEVD tag');
9673            
9674             # for each sample (3 values per sample)
9675 0         0 for my $i (0 .. ($m/3 - 1)) {
9676            
9677             # add sample data to object
9678 0         0 push (@{$self->[1]}, [@dev[($i * 3) .. ($i * 3 + 2)], @devfix, @dat[($i * 3) .. ($i * 3 + 2)]]);
  0         0  
9679            
9680             }
9681            
9682             # verify tag 0x0029 (end of section)
9683 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x0029)) || return('failed reading section end from Monaco DEVD tag');
9684            
9685             # advance index
9686 0         0 $ix += 4;
9687            
9688             }
9689              
9690             # verify tag 0x0030 (end of data)
9691 0 0       0 (substr($data, $ix, 4) eq pack('N', 0x0030)) || return('failed reading data end from Monaco DEVD tag');
9692              
9693             # add 'CREATED' keyword/value from header date/time
9694 0         0 push(@{$self->[3]}, ['CREATED', sprintf('%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ', @{$header}[6 .. 11])]);
  0         0  
  0         0  
9695              
9696             # return
9697 0         0 return();
9698              
9699             }
9700              
9701             # read chart from SpectraShop (.ss3) file
9702             # parameters: (object_reference, file_handle, hash)
9703             sub _readChartSS3 {
9704              
9705             # get parameters
9706 0     0   0 my ($self, $fh, $hash) = @_;
9707              
9708             # local variables
9709 0         0 my (%fmt, $buf, @data, $notes);
9710 0         0 my ($meta, $measure, %tally, @keys, $value, $nm);
9711              
9712             # metadata format array (v32)
9713 0         0 $fmt{'32'} = [
9714             [qw(Identifier_1 SAMPLE_NAME P)],
9715             [qw(Identifier_2 SAMPLE_ID2 P)],
9716             [qw(Identifier_3 SAMPLE_ID3 P)],
9717             [qw(Material MATERIAL P)],
9718             [qw(Manufacturer MANUFACTURER P)],
9719             [qw(Model MODEL P)],
9720             [qw(Serial_Number SERIAL_NUMBER P)],
9721             [qw(Production_Date PROD_DATE P)],
9722             [qw(Surface SURFACE P)],
9723             [qw(Originator ORIGINATOR P)],
9724             [qw(Creation_Date CREATED P)],
9725             [qw(Comments NOTE P)],
9726             [qw(Instrument INSTRUMENTATION P)],
9727             [qw(Spectrum_Type SPECTRUM_TYPE n), [qw(Emissive-light Emissive-monitor Observer Reflective Transmissive)]],
9728             [qw(Filter MEASUREMENT_FILTER P)],
9729             [qw(Geometry MEASUREMENT_GEOMETRY P)],
9730             [qw(Aperture MEASUREMENT_APERTURE P)],
9731             [qw(Data_Reference DATA_REFERENCE P)],
9732             [qw(Illuminant MEASUREMENT_SOURCE P)],
9733             [qw(Backing SAMPLE_BACKING P)],
9734             [qw(Measurements NSAMPLES n)],
9735             [qw(Notes ACQUIRE_NOTE P)],
9736             ];
9737              
9738             # metadata format array (v50)
9739 0         0 $fmt{'50'} = [
9740             [qw(Identifier_1 SAMPLE_NAME P)],
9741             [qw(Identifier_2 SAMPLE_ID2 P)],
9742             [qw(Identifier_3 SAMPLE_ID3 P)],
9743             [qw(Material MATERIAL P)],
9744             [qw(Manufacturer MANUFACTURER P)],
9745             [qw(Model MODEL P)],
9746             [qw(Serial_Number SERIAL_NUMBER P)],
9747             [qw(Production_Date PROD_DATE P)],
9748             [qw(Surface SURFACE P)],
9749             [qw(Originator ORIGINATOR P)],
9750             [qw(Creation_Date CREATED P)],
9751             [qw(Comments NOTE P)],
9752             [qw(Instrument INSTRUMENTATION P)],
9753             [qw(Serial_Number INSTRUMENT_SERIAL_NUMBER P)],
9754             [qw(Spectrum_Type SPECTRUM_TYPE n), [qw(Emissive-light Emissive-monitor Observer Reflective Transmissive)]],
9755             [qw(Filter MEASUREMENT_FILTER P)],
9756             [qw(Geometry MEASUREMENT_GEOMETRY P)],
9757             [qw(Aperture MEASUREMENT_APERTURE P)],
9758             [qw(Data_Reference DATA_REFERENCE P)],
9759             [qw(Illuminant MEASUREMENT_SOURCE P)],
9760             [qw(Backing SAMPLE_BACKING P)],
9761             [qw(Measurements NSAMPLES n)],
9762             [qw(Notes ACQUIRE_NOTE P)],
9763             ];
9764              
9765             # read version, samples, Collection Notes length
9766 0         0 read($fh, $buf, 7);
9767              
9768             # unpack
9769 0         0 @data = unpack('nx2nC', $buf);
9770              
9771             # read Collection Notes string
9772 0         0 read($fh, $notes, $data[2]);
9773              
9774             # for each sample
9775 0         0 for my $i (0 .. ($data[1] - 1)) {
9776            
9777             # for each metadata field
9778 0         0 for my $j (0 .. $#{$fmt{$data[0]}}) {
  0         0  
9779            
9780             # if a Pascal string
9781 0 0       0 if ($fmt{$data[0]}[$j][2] eq 'P') {
    0          
9782            
9783             # read string length
9784 0         0 read($fh, $buf, 1);
9785            
9786             # read string
9787 0         0 read($fh, $meta->[$i][$j], unpack('C', $buf));
9788            
9789             # if an unsigned short integer
9790             } elsif ($fmt{$data[0]}[$j][2] eq 'n') {
9791            
9792             # read short integer
9793 0         0 read($fh, $buf, 2);
9794            
9795             # unpack
9796 0         0 $meta->[$i][$j] = unpack('n', $buf);
9797            
9798             }
9799            
9800             }
9801            
9802             # read wavelength parameters (start, end, increment, count)
9803 0         0 read($fh, $buf, 8);
9804            
9805             # unpack (unsigned short integer)
9806 0         0 $measure->[$i][0] = [unpack('n4', $buf)];
9807            
9808             # for each wavelength
9809 0         0 for my $j (1 .. $measure->[$i][0][3]) {
9810            
9811             # read measurements (avg, low, high, std_dev)
9812 0         0 read($fh, $buf, 16);
9813            
9814             # unpack (32-bit float, big endian)
9815 0         0 $measure->[$i][$j] = [unpack('(f4)>', $buf)];
9816            
9817             }
9818            
9819             }
9820              
9821             # add Collection Notes to header line array, if not null string
9822 0 0       0 push(@{$self->[3]}, ['FILE_DESCRIPTOR', "\"$notes\""]) if (length($notes));
  0         0  
9823              
9824             # for each metadata field
9825 0         0 for my $j (0 .. $#{$meta->[0]}) {
  0         0  
9826            
9827             # init hash
9828 0         0 %tally = ();
9829            
9830             # for each sample
9831 0         0 for my $i (0 .. $#{$meta}) {
  0         0  
9832            
9833             # increment hash value
9834 0         0 $tally{$meta->[$i][$j]}++;
9835            
9836             }
9837            
9838             # get hash keys
9839 0         0 @keys = keys(%tally);
9840            
9841             # if one hash key
9842 0 0       0 if (@keys == 1) {
9843            
9844             # if not the null string
9845 0 0       0 if (length($keys[0])) {
9846            
9847             # if value is string
9848 0 0       0 if ($fmt{$data[0]}[$j][2] eq 'P') {
9849            
9850             # wrap in quotes
9851 0         0 $value = "\"$keys[0]\"";
9852            
9853             } else {
9854            
9855             # if value is an enumeration
9856 0 0       0 if (defined($fmt{$data[0]}[$j][3])) {
9857            
9858             # look up enumerated value and wrap in quotes
9859 0         0 $value = "\"$fmt{$data[0]}[$j][3][$keys[0]]\"";
9860            
9861             } else {
9862            
9863             # use value as-is
9864 0         0 $value = $keys[0];
9865            
9866             }
9867            
9868             }
9869            
9870             # add KEYWORD/VALUE to header line array
9871 0         0 push(@{$self->[3]}, [$fmt{$data[0]}[$j][1], $value]);
  0         0  
9872            
9873             }
9874            
9875             } else {
9876            
9877             # add keyword to DATA_FORMAT array
9878 0         0 push(@{$self->[1][0]}, $fmt{$data[0]}[$j][1]);
  0         0  
9879            
9880             # for each sample
9881 0         0 for my $i (0 .. $#{$meta}) {
  0         0  
9882            
9883             # if value is an enumeration
9884 0 0       0 if (defined($fmt{$data[0]}[$j][3])) {
9885            
9886             # look up enumerated value
9887 0         0 $value = $fmt{$data[0]}[$j][3][$meta->[$i][$j]];
9888            
9889             } else {
9890            
9891             # use value as-is
9892 0         0 $value = $meta->[$i][$j];
9893            
9894             }
9895            
9896             # add value to DATA array
9897 0         0 push(@{$self->[1][$i + 1]}, $meta->[$i][$j]);
  0         0  
9898            
9899             }
9900            
9901             }
9902            
9903             }
9904              
9905             # for each wavelength parameter (start, end, increment, count)
9906 0         0 for my $j (0 .. 3) {
9907            
9908             # init hash
9909 0         0 %tally = ();
9910            
9911             # for each sample
9912 0         0 for my $i (0 .. $#{$measure}) {
  0         0  
9913            
9914             # increment hash value
9915 0         0 $tally{$measure->[$i][0][$j]}++;
9916            
9917             }
9918            
9919             # get hash keys
9920 0         0 @keys = keys(%tally);
9921            
9922             # verify all samples have same wavelength parameter value
9923 0 0       0 (@keys == 1) || return('samples have varied spectral range');
9924            
9925             }
9926              
9927             # for each wavelength
9928 0         0 for my $j (0 .. ($#{$measure->[0]} - 1)) {
  0         0  
9929            
9930             # compute wavelength from start and increment values
9931 0         0 $nm = $measure->[0][0][0] + $j * $measure->[0][0][2];
9932            
9933             # add keyword to DATA_FORMAT array
9934 0         0 push(@{$self->[1][0]}, "nm$nm");
  0         0  
9935            
9936             # for each sample
9937 0         0 for my $i (0 .. $#{$measure}) {
  0         0  
9938            
9939             # add average measurement to DATA array
9940 0         0 push(@{$self->[1][$i + 1]}, $measure->[$i][$j + 1][0]);
  0         0  
9941            
9942             }
9943            
9944             }
9945              
9946             # return
9947 0         0 return();
9948              
9949             }
9950              
9951             # read data from TIFF file
9952             # RGB, CMYK, and CIE L*a*b* color spaces supported
9953             # 8-bit, 16-bit or 32-bit, Intel or Motorola byte order supported
9954             # alpha and spot channels in RGB and CMYK files supported
9955             # optional hash keys: 'rows', 'columns', 'crop', 'ratio', 'aperture', 'udf', 'format'
9956             # default 'rows' and 'columns' are taken from image size, default 'ratio' is 0.5
9957             # 'crop' is an array containing the left, right, upper and lower crop values in pixels
9958             # 'ratio' is a value between 0 and 1, sample is a single pixel when 'ratio' is 0
9959             # 'aperture' is in millimeters, and take precedence over 'ratio'
9960             # 'udf' is a code reference to a pixel processing function
9961             # 'format' is an array reference containing the format fields
9962             # parameters: (object_reference, file_handle, hash)
9963             # returns: (result)
9964             sub _readChartTIFF {
9965              
9966             # get parameters
9967 0     0   0 my ($self, $fh, $hash) = @_;
9968              
9969             # local variables
9970 0         0 my ($buf, $short, $long, $fp, @header, $tags);
9971 0         0 my ($cols, $rows, $bits, $pi, $samples);
9972 0         0 my ($context, $fmt, $upf, $udf, $dev, $div, $dab);
9973 0         0 my ($trows, $tcols, $crop, $roff, $coff);
9974 0         0 my ($res, $size, $frac, $ratio, $rxo, $cxo, $pixels, $width);
9975 0         0 my ($lower, $upper, $left, $right, $band, $pval, @data, @pix);
9976              
9977             # read the header
9978 0         0 read($fh, $buf, 8);
9979              
9980             # if big-endian (Motorola)
9981 0 0       0 if (substr($buf, 0, 2) eq 'MM') {
    0          
9982            
9983             # set 'unpack' formats
9984 0         0 $short = 'n';
9985 0         0 $long = 'N';
9986 0         0 $fp = 'f>'; # might not be IEEE FP on some platforms
9987            
9988             # if little-endian (Intel)
9989             } elsif (substr($buf, 0, 2) eq 'II'){
9990            
9991             # set 'unpack' formats
9992 0         0 $short = 'v';
9993 0         0 $long = 'V';
9994 0         0 $fp = 'f<'; # might not be IEEE FP on some platforms
9995            
9996             } else {
9997            
9998             # error
9999 0         0 return('TIFF byte order incorrect');
10000            
10001             }
10002              
10003             # unpack the header
10004 0         0 @header = unpack("A2 $short $long", $buf);
10005              
10006             # verify file signature
10007 0 0       0 ($header[1] == 42) || return('TIFF file signature incorrect');
10008              
10009             # read TIFF image file directory (IFD)
10010 0         0 $tags = _readTIFFdir($fh, $header[2], $short, $long);
10011              
10012             # verify compression (1 = uncompressed)
10013 0 0       0 ($tags->{'259'}[0] == 1) || return('TIFF compression unsupported');
10014              
10015             # verify orientation (1 = normal)
10016 0 0 0     0 (! exists($tags->{'274'}) || $tags->{'274'}[0] == 1) || warn('TIFF orientation rotated and/or flipped');
10017              
10018             # verify planar configuration (1 = chunky)
10019 0 0 0     0 (! exists($tags->{'284'}) || $tags->{'284'}[0] == 1) || return('TIFF planar configuration unsupported');
10020              
10021             # verify not tiled
10022 0 0       0 (! exists($tags->{'322'})) || return('TIFF tiled layout unsupported');
10023              
10024             # get TIFF columns (width)
10025 0         0 $cols = $tags->{'256'}[0];
10026              
10027             # get TIFF rows (length)
10028 0         0 $rows = $tags->{'257'}[0];
10029              
10030             # get TIFF bits per sample
10031 0         0 $bits = $tags->{'258'}[0];
10032              
10033             # verify bits per sample
10034 0 0 0     0 ($bits == 8 || $bits == 16 || $bits == 32) || return('TIFF bits per sample unsupported');
      0        
10035              
10036             # get the photometric interpretation
10037 0         0 $pi = $tags->{'262'}[0];
10038              
10039             # if 32-bits per sample
10040 0 0       0 if ($bits == 32) {
10041            
10042             # verify 32-bit IEEE FP format, RGB image
10043 0 0 0     0 ($tags->{'339'}[0] == 3 && $pi == 2) || return('TIFF format unsupported');
10044            
10045             }
10046              
10047             # get TIFF samples per pixel
10048 0         0 $samples = $tags->{'277'}[0];
10049              
10050             # verify bits per sample array
10051 0 0       0 ($samples == grep {$_ == $bits} @{$tags->{'258'}}) || return('TIFF image structure unsupported');
  0         0  
  0         0  
10052              
10053             # get context (if any)
10054 0         0 $context = $hash->{'context'};
10055              
10056             # get user defined function (if any)
10057 0         0 $udf = $hash->{'udf'};
10058              
10059             # verify UDF is a code reference
10060 0 0 0     0 (ref($udf) eq 'CODE') || return('UDF not a code reference') if (defined($udf));
10061              
10062             # set device value divisor
10063 0 0       0 $dev = ($bits == 8) ? 255 : 65535;
10064              
10065             # add fields for udf (if any)
10066 0 0       0 $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} @{$hash->{'format'}}) if defined($hash->{'format'});
  0 0       0  
  0         0  
10067              
10068             # if RGB file
10069 0 0 0     0 if ($pi == 2 && $samples < 13) {
    0 0        
    0 0        
    0 0        
      0        
10070            
10071             # add RGB and ALPHA fields, if not already defined
10072 0 0       0 $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} (qw(RGB_R RGB_G RGB_B), map {"RGB_A$_"} (1 .. $samples - 3))) if (! defined($fmt));
  0 0       0  
  0         0  
10073            
10074             # set unpack format (8, 16 or 32 bits)
10075 0 0       0 $upf = ($bits == 8) ? 'C*' : ($bits == 16) ? "$short*" : "$fp*";
    0          
10076            
10077             # set divisor (8, 16 or 32 bits)
10078 0 0       0 $div = ($bits == 8) ? 1 : ($bits == 16) ? 257 : 1/255;
    0          
10079            
10080             # if CMYK file
10081             } elsif ($pi == 5 && $samples == 4) {
10082            
10083             # add CMYK fields, if not already defined
10084 0 0       0 $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(CMYK_C CMYK_M CMYK_Y CMYK_K)) if (! defined($fmt));
  0 0       0  
10085            
10086             # set unpack format (8 or 16 bits)
10087 0 0       0 $upf = ($bits == 8) ? 'C*' : "$short*";
10088            
10089             # set divisor (8 or 16 bits)
10090 0 0       0 $div = ($bits == 8) ? 2.55 : 655.35;
10091            
10092             # if nCLR file
10093             } elsif ($pi == 5 && $samples > 4 && $samples < 16) {
10094            
10095             # add nCLR fields, if not already defined
10096 0 0       0 $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} map {sprintf('%xCLR_%x', $samples, $_)} (1 .. $samples)) if (! defined($fmt));
  0 0       0  
  0         0  
10097            
10098             # set unpack format (8 or 16 bits)
10099 0 0       0 $upf = ($bits == 8) ? 'C*' : "$short*";
10100            
10101             # set divisor (8 or 16 bits)
10102 0 0       0 $div = ($bits == 8) ? 2.55 : 655.35;
10103            
10104             # if CIE L*a*b* file
10105             } elsif ($pi == 8 && $samples == 3) {
10106            
10107             # add L*a*b* fields, if not already defined
10108 0 0       0 $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)) if (! defined($fmt));
  0 0       0  
10109            
10110             # set unpack format (8 or 16 bits)
10111 0 0       0 $upf = ($bits == 8) ? '(Ccc)*' : "$short*";
10112            
10113             # set divisors (8 or 16 bits)
10114 0 0       0 $div = ($bits == 8) ? 2.55 : 655.35; # L*
10115 0 0       0 $dab = ($bits == 8) ? 1 : 256; # a* and b*
10116            
10117             } else {
10118            
10119             # return error
10120 0         0 return('TIFF color space unsupported');
10121            
10122             }
10123              
10124             # get target rows (could be undefined)
10125 0         0 $trows = $hash->{'rows'};
10126              
10127             # get target columns (could be undefined)
10128 0         0 $tcols = $hash->{'columns'};
10129              
10130             # if 'crop' parameter is defined
10131 0 0       0 if (defined($hash->{'crop'})) {
10132            
10133             # get crop parameter
10134 0         0 $crop = $hash->{'crop'};
10135            
10136             # verify array reference
10137 0 0       0 (ref($crop) eq 'ARRAY') || return('TIFF crop parameter not an array reference');
10138            
10139             # verify array contains four non-negative integers
10140 0 0 0     0 (4 == @{$crop} && 4 == grep {$_ == int($_) && $_ >= 0} @{$crop}) || return('TIFF crop parameter(s) invalid');
  0 0       0  
  0         0  
  0         0  
10141            
10142             # adjust rows and columns
10143 0         0 $rows -= $crop->[2] + $crop->[3];
10144 0         0 $cols -= $crop->[0] + $crop->[1];
10145            
10146             # verify cropped size
10147 0 0 0     0 ($rows > 0 && $cols > 0) || return('TIFF crop size too small');
10148            
10149             # set offset values
10150 0         0 $roff = $crop->[2];
10151 0         0 $coff = $crop->[0];
10152            
10153             } else {
10154            
10155             # set offset values
10156 0         0 $roff = 0;
10157 0         0 $coff = 0;
10158            
10159             }
10160              
10161             # if aperture is defined in hash
10162 0 0       0 if (defined($hash->{'aperture'})) {
10163            
10164             # compute image resolution
10165 0         0 $res = $tags->{'283'}[0]/$tags->{'283'}[1];
10166            
10167             # convert to lines/mm if resolution unit is inch
10168 0 0       0 $res /= 25.4 if ($tags->{'296'}[0] == 2);
10169            
10170             # convert to lines/mm if resolution unit is cm
10171 0 0       0 $res /= 10 if ($tags->{'296'}[0] == 3);
10172            
10173             # if target rows or target columns are defined
10174 0 0 0     0 if (defined($trows) || defined($tcols)) {
10175            
10176             # use image rows if target rows undefined
10177 0 0       0 $trows = $rows if (! defined($trows));
10178            
10179             # use image columns if target columns undefined
10180 0 0       0 $tcols = $cols if (! defined($tcols));
10181            
10182             # compute aperture size (diameter in pixels)
10183 0         0 ($frac, $size) = POSIX::modf(sqrt(ICC::Shared::PI/4) * $res * $hash->{'aperture'});
10184            
10185             # if fractional part < 0.25
10186 0 0       0 if ($frac < 0.25) {
    0          
10187            
10188             # set row and column index offsets
10189 0         0 $rxo = $cxo = $size - 1;
10190            
10191             # if fractional part < 0.75
10192             } elsif ($frac < 0.75) {
10193            
10194             # set row index offset
10195 0         0 $rxo = $size - 1;
10196            
10197             # set column index offset
10198 0         0 $cxo = $size;
10199            
10200             } else {
10201            
10202             # set row and column index offsets
10203 0         0 $rxo = $cxo = $size;
10204            
10205             }
10206            
10207             # verify aperture is within sample area
10208 0 0 0     0 ($rxo <= $rows/$trows && $cxo <= $cols/$tcols) or croak('TIFF aperture exceeds sample area')
10209            
10210             } else {
10211            
10212             # compute aperture area (in pixels)
10213 0         0 $size = ICC::Shared::PI * ($res * $hash->{'aperture'}/2)**2;
10214            
10215             # compute the target rows
10216 0         0 $trows = int(sqrt($size * $rows/$cols) + 0.5);
10217            
10218             # compute the target columns
10219 0         0 $tcols = int($size/$trows + 0.5);
10220            
10221             # set row and column indices (single pixel sample)
10222 0         0 $rxo = $cxo = 0;
10223            
10224             }
10225            
10226             } else {
10227            
10228             # use image rows if target rows undefined
10229 0 0       0 $trows = $rows if (! defined($trows));
10230            
10231             # use image columns if target columns undefined
10232 0 0       0 $tcols = $cols if (! defined($tcols));
10233            
10234             # get mask ratio (default 0.5)
10235 0 0       0 $ratio = defined($hash->{'ratio'}) ? $hash->{'ratio'} : 0.5;
10236            
10237             # verify mask ratio
10238 0 0 0     0 ($ratio >= 0 && $ratio <= 1) or croak('TIFF mask ratio < 0 or > 1');
10239            
10240             # compute row index offset
10241 0         0 $rxo = int($ratio * $rows/$trows - 0.5);
10242            
10243             # compute column index offset
10244 0         0 $cxo = int($ratio * $cols/$tcols - 0.5);
10245            
10246             }
10247              
10248             # warn if large target size
10249 0 0       0 ($trows * $tcols <= 10000) || warn('TIFF target size > 10000 samples');
10250              
10251             # compute number of pixels
10252 0         0 $pixels = ($rxo + 1) * ($cxo + 1);
10253              
10254             # compute row width (bytes)
10255 0         0 $width = $tags->{'256'}[0] * List::Util::sum(@{$tags->{'258'}})/8;
  0         0  
10256              
10257             # for each target row
10258 0         0 for my $i (0 .. $trows - 1) {
10259            
10260             # compute sample lower row
10261 0         0 $lower = int(($i + 0.5) * $rows/$trows - $rxo/2) + $roff;
10262            
10263             # compute sample upper row
10264 0         0 $upper = $lower + $rxo;
10265            
10266             # get sample band data
10267 0         0 $band = _readTIFFband($fh, $tags, $lower, $upper, $width, $upf);
10268            
10269             # for each target column
10270 0         0 for my $j (0 .. $tcols - 1) {
10271            
10272             # compute sample left column
10273 0         0 $left = int(($j + 0.5) * $cols/$tcols - $cxo/2) + $coff;
10274            
10275             # compute sample right column
10276 0         0 $right = $left + $cxo;
10277            
10278             # initialize data
10279 0         0 @data = ();
10280            
10281             # for each row (band)
10282 0         0 for my $m (0 .. $#{$band}) {
  0         0  
10283            
10284             # for each column
10285 0         0 for my $n ($left .. $right) {
10286            
10287             # get pixel value (all samples)
10288 0         0 @pix = @{$band->[$m]}[$n * $samples .. ($n + 1) * $samples - 1];
  0         0  
10289            
10290             # if 16-bit L*a*b*
10291 0 0 0     0 if ($pi == 8 && $bits == 16) {
10292            
10293             # adjust a* and b* if pixel value negative (signed 16-bit)
10294 0 0       0 $pix[1] += -65536 if ($pix[1] > 32767);
10295 0 0       0 $pix[2] += -65536 if ($pix[2] > 32767);
10296            
10297             }
10298            
10299             # if user defined function provided
10300 0 0       0 if (defined($udf)) {
10301            
10302             # if L*a*b* file
10303 0 0       0 if ($pi == 8) {
10304            
10305             # convert values
10306 0         0 $pix[0] /= $div;
10307 0         0 $pix[1] /= $dab;
10308 0         0 $pix[2] /= $dab;
10309            
10310             } else {
10311            
10312             # convert to device values
10313 0         0 @pix = map {$_/$dev} @pix;
  0         0  
10314            
10315             # if a CMYK file
10316 0 0       0 if ($pi == 5) {
10317            
10318             # for alpha/spot colors (if any)
10319 0         0 for my $s (4 .. $samples - 1) {
10320            
10321             # invert device value
10322 0         0 $pix[$s] = 1 - $pix[$s];
10323            
10324             }
10325            
10326             }
10327            
10328             }
10329            
10330             # call user defined function
10331 0         0 @pix = &$udf(@pix);
10332            
10333             }
10334            
10335             # for each channel (may be different from TIFF samples)
10336 0         0 for my $s (0 .. $#pix) {
10337            
10338             # accumulate pixel values
10339 0         0 $data[$s] += $pix[$s]
10340            
10341             }
10342            
10343             }
10344            
10345             }
10346            
10347             # if user defined function provided
10348 0 0       0 if (defined($udf)) {
    0          
10349            
10350             # save data in object
10351 0         0 @{$self->[1][$j * $trows + $i + 1]}[@{$fmt}] = map {$_/$pixels} @data;
  0         0  
  0         0  
  0         0  
10352            
10353             # if L*a*b* file
10354             } elsif ($pi == 8) {
10355            
10356             # save data in object
10357 0         0 $self->[1][$j * $trows + $i + 1][$fmt->[0]] = $data[0]/($pixels * $div);
10358 0         0 $self->[1][$j * $trows + $i + 1][$fmt->[1]] = $data[1]/($pixels * $dab);
10359 0         0 $self->[1][$j * $trows + $i + 1][$fmt->[2]] = $data[2]/($pixels * $dab);
10360            
10361             # all others
10362             } else {
10363            
10364             # normalize data values
10365 0         0 @data = map {$_/($pixels * $div)} @data;
  0         0  
10366            
10367             # if a CMYK file
10368 0 0       0 if ($pi == 5) {
10369            
10370             # for alpha/spot colors (if any)
10371 0         0 for my $s (4 .. $samples - 1) {
10372            
10373             # invert %-dot value
10374 0         0 $data[$s] = 100 - $data[$s];
10375            
10376             }
10377            
10378             }
10379            
10380             # save data in object
10381 0         0 @{$self->[1][$j * $trows + $i + 1]}[@{$fmt}] = @data;
  0         0  
  0         0  
10382            
10383             }
10384            
10385             }
10386            
10387             }
10388              
10389             # save the tag hash in object header
10390 0         0 $self->[0]{'TIFF_tag'} = $tags;
10391              
10392             # add LGOROWLENGTH keyword
10393 0         0 keyword($self, 'LGOROWLENGTH', $trows);
10394              
10395             # return
10396 0         0 return();
10397              
10398             }
10399              
10400             # read TIFF image file directory (IFD)
10401             # parameters: (file_handle, offset, short_format, long_format)
10402             # returns: (IFD_hash)
10403             sub _readTIFFdir {
10404              
10405             # get parameters
10406 0     0   0 my ($fh, $start, $short, $long) = @_;
10407              
10408             # local variables
10409 0         0 my (@ts, $buf, $id, $type, $count, $size, $mark, $offset, $num, $denom, $tags);
10410              
10411             # field type size (in bytes)
10412 0         0 @ts = (0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4);
10413              
10414             # seek start of IFD
10415 0         0 seek($fh, $start, 0);
10416              
10417             # read number entries
10418 0         0 read($fh, $buf, 2);
10419              
10420             # read the directory
10421 0         0 for (1 .. unpack($short, $buf)) {
10422            
10423             # read first part of IFD entry
10424 0         0 read($fh, $buf, 8);
10425            
10426             # unpack first three fields (ID, type, count)
10427 0         0 ($id, $type, $count) = unpack("$short$short$long", $buf);
10428            
10429             # read last part of IFD entry
10430 0         0 read($fh, $buf, 4);
10431            
10432             # determine value/offset size (size * count) + (1 if ASCII string)
10433 0 0       0 $size = $ts[$type] * $count + (($type == 2) ? 1 : 0);
10434            
10435             # if an offset
10436 0 0 0     0 if ($size > 4) {
    0          
    0          
    0          
    0          
10437            
10438             # mark file location
10439 0         0 $mark = tell($fh);
10440            
10441             # unpack offset
10442 0         0 $offset = unpack($long, $buf);
10443            
10444             # seek values
10445 0         0 seek($fh, $offset, 0);
10446            
10447             # if binary string
10448 0 0 0     0 if ($type == 1 || $type == 7) {
    0          
    0          
    0          
    0          
10449            
10450             # read binary string
10451 0         0 read($fh, $buf, $count);
10452            
10453             # unpack value
10454 0         0 $tags->{$id} = [unpack("a$count", $buf)];
10455            
10456             # if ASCII string
10457             } elsif ($type == 2) {
10458            
10459             # read ASCII string
10460 0         0 read($fh, $buf, $count);
10461            
10462             # unpack null-terminated ASCII string
10463 0         0 $tags->{$id} = [unpack("Z$count", $buf)];
10464            
10465             # if short values
10466             } elsif ($type == 3) {
10467            
10468             # read values
10469 0         0 read($fh, $buf, 2 * $count);
10470            
10471             # unpack values
10472 0         0 $tags->{$id} = [unpack("$short$count", $buf)];
10473            
10474             # if long values
10475             } elsif ($type == 4) {
10476            
10477             # read values
10478 0         0 read($fh, $buf, 4 * $count);
10479            
10480             # unpack values
10481 0         0 $tags->{$id} = [unpack("$long$count", $buf)];
10482            
10483             # if rational values
10484             } elsif ($type == 5) {
10485            
10486             # double count (one rational value is two long values)
10487 0         0 $count *= 2;
10488            
10489             # read values
10490 0         0 read($fh, $buf, 4 * $count);
10491            
10492             # unpack values
10493 0         0 $tags->{$id} = [unpack("$long$count", $buf)];
10494            
10495             }
10496            
10497             # reset file pointer
10498 0         0 seek($fh, $mark, 0);
10499            
10500             # if binary string
10501             } elsif ($type == 1 || $type == 7) {
10502            
10503             # unpack binary string
10504 0         0 $tags->{$id} = [unpack("a$count", $buf)];
10505            
10506             # if ASCII string
10507             } elsif ($type == 2) {
10508            
10509             # unpack null-terminated ASCII string
10510 0         0 $tags->{$id} = [unpack("Z$count", $buf)];
10511            
10512             # if short value(s)
10513             } elsif ($type == 3) {
10514            
10515             # unpack value(s)
10516 0         0 $tags->{$id} = [unpack("$short$count", $buf)];
10517            
10518             # if long value
10519             } elsif ($type == 4) {
10520            
10521             # unpack value
10522 0         0 $tags->{$id} = [unpack($long, $buf)];
10523            
10524             } else {
10525            
10526             # save packed value
10527 0         0 $tags->{$id} = [$buf];
10528            
10529             }
10530            
10531             }
10532              
10533             # return
10534 0         0 return($tags);
10535              
10536             }
10537              
10538             # read TIFF image band
10539             # row zero is top of image
10540             # parameters: (file_handle, IFD_hash, lower_row, upper_row, row_width, unpack_format)
10541             # returns: (2D_array)
10542             sub _readTIFFband {
10543              
10544             # get parameters
10545 0     0   0 my ($fh, $tags, $lower, $upper, $width, $upf) = @_;
10546              
10547             # local variables
10548 0         0 my ($offset, $rows, $buf, $band);
10549              
10550             # get strip offset array
10551 0         0 $offset = $tags->{'273'};
10552              
10553             # get rows per strip
10554 0         0 $rows = $tags->{'278'}[0];
10555              
10556             # for each row
10557 0         0 for my $i ($lower .. $upper) {
10558            
10559             # set file pointer
10560 0         0 seek($fh, $offset->[int($i/$rows)] + ($i % $rows) * $width, 0);
10561            
10562             # read row data
10563 0         0 read($fh, $buf, $width);
10564            
10565             # unpack data
10566 0         0 $band->[$i - $lower] = [unpack($upf, $buf)];
10567            
10568             }
10569              
10570             # return
10571 0         0 return($band);
10572              
10573             }
10574              
10575             # write TIFF image file directory (IFD)
10576             # parameters: (file_handle, offset, short_format, long_format, IFD_hash)
10577             sub _writeTIFFdir {
10578              
10579             # get parameters
10580 0     0   0 my ($fh, $ifd, $short, $long, $tags) = @_;
10581              
10582             # local variables
10583 0         0 my (@ts, @sid, $mark, $type, $count, $size, $fmt);
10584              
10585             # field type size (in bytes)
10586 0         0 @ts = (0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4);
10587              
10588             # make list of tag ids, sorted numerically
10589 0         0 @sid = sort {$a <=> $b} keys(%{$tags});
  0         0  
  0         0  
10590              
10591             # seek start of IFD
10592 0         0 seek($fh, $ifd, 0);
10593              
10594             # write number of tags
10595 0         0 print $fh pack($short, scalar(@sid));
10596              
10597             # set data pointer
10598 0         0 $mark = $ifd + 12 * @sid + 6;
10599              
10600             # for each tag
10601 0         0 for my $id (@sid) {
10602            
10603             # get data type
10604 0         0 $type = $tags->{$id}[0];
10605            
10606             # if a binary string
10607 0 0 0     0 if ($type == 1 || $type == 7) {
    0          
    0          
10608            
10609             # set count to string length
10610 0         0 $count = length($tags->{$id}[1]);
10611            
10612             # if an ASCII string
10613             } elsif ($type == 2) {
10614            
10615             # set count to string length + 1
10616 0         0 $count = length($tags->{$id}[1]) + 1;
10617            
10618             # if a rational value
10619             } elsif ($type == 5) {
10620            
10621             # set count to number of values/2
10622 0         0 $count = $#{$tags->{$id}}/2;
  0         0  
10623            
10624             } else {
10625            
10626             # set count to number of values
10627 0         0 $count = $#{$tags->{$id}};
  0         0  
10628            
10629             }
10630            
10631             # if size of value/offset > 4
10632 0 0       0 if (($size = $count * $ts[$type]) > 4) {
10633            
10634             # write directory entry with offset
10635 0         0 print $fh pack("$short$short$long$long", $id, $type, $count, $mark);
10636            
10637             # increment data pointer
10638 0         0 $mark += $size;
10639            
10640             # make a word boundary
10641 0         0 $mark += $mark % 2;
10642            
10643             } else {
10644            
10645             # if a binary string
10646 0 0 0     0 if ($type == 1 || $type == 7) {
    0          
    0          
    0          
10647            
10648             # set pack format
10649 0         0 $fmt = 'a4';
10650            
10651             # if an ASCII string
10652             } elsif ($type == 2) {
10653            
10654             # set pack format
10655 0         0 $fmt = 'Z4';
10656            
10657             # if a short value
10658             } elsif ($type == 3) {
10659            
10660             # set pack format (one or two values)
10661 0 0       0 $fmt = $count == 1 ? $short . 'x2' : $short . '2';
10662            
10663             # if a long value
10664             } elsif ($type == 4) {
10665            
10666             # set pack format
10667 0         0 $fmt = $long;
10668            
10669             } else {
10670            
10671             # error
10672 0         0 croak('unsupported TIFF data type, stopped');
10673            
10674             }
10675            
10676             # write directory entry (12 bytes) with value(s)
10677 0         0 print $fh pack("$short$short$long$fmt", $id, $type, $count, @{$tags->{$id}}[1 .. $#{$tags->{$id}}]);
  0         0  
  0         0  
10678            
10679             }
10680            
10681             }
10682              
10683             # set data pointer
10684 0         0 $mark = $ifd + 12 * @sid + 6;
10685              
10686             # for each tag
10687 0         0 for my $id (@sid) {
10688            
10689             # get data type
10690 0         0 $type = $tags->{$id}[0];
10691            
10692             # if a binary string
10693 0 0 0     0 if ($type == 1 || $type == 7) {
    0          
    0          
10694            
10695             # set count to string length
10696 0         0 $count = length($tags->{$id}[1]);
10697            
10698             # if an ASCII string
10699             } elsif ($type == 2) {
10700            
10701             # set count to string length + 1
10702 0         0 $count = length($tags->{$id}[1]) + 1;
10703            
10704             # if a rational value
10705             } elsif ($type == 5) {
10706            
10707             # set count to number of values/2
10708 0         0 $count = $#{$tags->{$id}}/2;
  0         0  
10709            
10710             } else {
10711            
10712             # set count to number of values
10713 0         0 $count = $#{$tags->{$id}};
  0         0  
10714            
10715             }
10716            
10717             # if size of value/offset > 4
10718 0 0       0 if (($size = $count * $ts[$type]) > 4) {
10719            
10720             # if a binary string
10721 0 0 0     0 if ($type == 1 || $type == 7) {
    0          
    0          
    0          
    0          
10722            
10723             # set pack format
10724 0         0 $fmt = "a$count";
10725            
10726             # if an ASCII string
10727             } elsif ($type == 2) {
10728            
10729             # set pack format
10730 0         0 $fmt = "Z$count";
10731            
10732             # if a short value
10733             } elsif ($type == 3) {
10734            
10735             # set pack format
10736 0         0 $fmt = "$short$count";
10737            
10738             # if a long value
10739             } elsif ($type == 4) {
10740            
10741             # set pack format
10742 0         0 $fmt = "$long$count";
10743            
10744             # if a rational value
10745             } elsif ($type == 5) {
10746            
10747             # set pack format
10748 0         0 $fmt = "$long$#{$tags->{$id}}";
  0         0  
10749            
10750             } else {
10751            
10752             # error
10753 0         0 croak('unsupported TIFF data type, stopped');
10754             }
10755            
10756             # set file pointer
10757 0         0 seek($fh, $mark, 0);
10758            
10759             # write the data value(s)
10760 0         0 print $fh pack($fmt, @{$tags->{$id}}[1 .. $#{$tags->{$id}}]);
  0         0  
  0         0  
10761            
10762             # increment data pointer
10763 0         0 $mark += $size;
10764            
10765             # make a word boundary
10766 0         0 $mark += $mark % 2;
10767            
10768             }
10769            
10770             }
10771            
10772             }
10773              
10774             # write TIFF data strip
10775             # parameters: (file_handle, IFD_hash, patch_width, gap_width, left_edge_width, right_edge_width, strip_index, strip_data_array, pack_format, dither_value)
10776             sub _writeTIFFstrip {
10777              
10778             # get parameters
10779 0     0   0 my ($fh, $tags, $width, $gap, $left, $right, $sx, $data, $fmt, $dither) = @_;
10780              
10781             # local variables
10782 0         0 my ($pi, $samples, $bits, $max, $diff, $edge, $w, @spot, $rms, $gdata, @row, $strip);
10783              
10784             # get photometric interpretation
10785 0         0 $pi = $tags->{'262'}[1];
10786              
10787             # get number of samples (channels)
10788 0         0 $samples = $tags->{'277'}[1];
10789              
10790             # get bits per sample
10791 0         0 $bits = $tags->{'258'}[1];
10792              
10793             # max binary value (8, 16 or 32 bits)
10794 0 0       0 $max = ($bits == 8) ? 255 : ($bits == 16) ? 65535 : 1;
    0          
10795              
10796             # make list of spot channel indices
10797 0         0 @spot = (4 .. $tags->{'277'}[1] - 1);
10798              
10799             # for each patch
10800 0         0 for my $i (0 .. $#{$data}) {
  0         0  
10801            
10802             # if RGB data
10803 0 0       0 if ($pi == 2) {
    0          
10804            
10805             # compute white and black differences
10806 0         0 $diff->[$i][0] = sqrt(($max - $data->[$i][0])**2 + ($max - $data->[$i][1])**2 + ($max - $data->[$i][2])**2);
10807 0         0 $diff->[$i][1] = sqrt($data->[$i][0]**2 + $data->[$i][1]**2 + $data->[$i][2]**2);
10808            
10809             # if CMYK data
10810             } elsif ($pi == 5) {
10811            
10812             # compute rms value of CMY + spot channels (CMY weighted and spot channels inverted)
10813 0         0 $rms = sqrt(List::Util::sum(0.25 * $data->[$i][0]**2, 3 * $data->[$i][1]**2, 0.25 * $data->[$i][2]**2, (map {($max - $data->[$i][$_])**2} @spot))/(3 + @spot));
  0         0  
10814            
10815             # compute white and black differences (black * color)
10816 0         0 $diff->[$i][0] = $max - ($max - $data->[$i][3]) * ($max - $rms)/$max;
10817 0         0 $diff->[$i][1] = ($max - $data->[$i][3]) * ($max - $rms)/$max;
10818            
10819             # L*a*b* data
10820             } else {
10821            
10822             # compute white and black differences (approx dEab)
10823 0         0 $diff->[$i][0] = sqrt(($max - $data->[$i][0])**2 + 6.55 * $data->[$i][1]**2 + 6.55 * $data->[$i][2]**2);
10824 0         0 $diff->[$i][1] = sqrt($data->[$i][0]**2 + 6.55 * $data->[$i][1]**2 + 6.55 * $data->[$i][2]**2);
10825            
10826             }
10827            
10828             # skip first patch
10829 0 0       0 if ($i > 0) {
10830            
10831             # if RGB data
10832 0 0       0 if ($pi == 2) {
    0          
10833            
10834             # if max white difference > max black difference
10835 0 0       0 if (($diff->[$i - 1][0] > $diff->[$i][0] ? $diff->[$i - 1][0] : $diff->[$i][0]) > ($diff->[$i - 1][1] > $diff->[$i][1] ? $diff->[$i - 1][1] : $diff->[$i][1])) {
    0          
    0          
10836            
10837             # gap is white
10838 0         0 $gdata->[$i - 1] = [($max) x 3];
10839            
10840             } else {
10841            
10842             # gap is black
10843 0         0 $gdata->[$i - 1] = [0, 0, 0];
10844            
10845             }
10846            
10847             # if CMYK data
10848             } elsif ($pi == 5) {
10849            
10850             # if max white difference > max black difference
10851 0 0       0 if (($diff->[$i - 1][0] > $diff->[$i][0] ? $diff->[$i - 1][0] : $diff->[$i][0]) > ($diff->[$i - 1][1] > $diff->[$i][1] ? $diff->[$i - 1][1] : $diff->[$i][1])) {
    0          
    0          
10852            
10853             # gap is white
10854 0         0 $gdata->[$i - 1] = [0, 0, 0, 0, ($max) x ($samples - 4)];
10855            
10856             } else {
10857            
10858             # gap is black
10859 0         0 $gdata->[$i - 1] = [0, 0, 0, ($max) x ($samples - 3)];
10860            
10861             }
10862            
10863             # L*a*b* data
10864             } else {
10865            
10866             # if max white difference > max black difference
10867 0 0       0 if (($diff->[$i - 1][0] > $diff->[$i][0] ? $diff->[$i - 1][0] : $diff->[$i][0]) > ($diff->[$i - 1][1] > $diff->[$i][1] ? $diff->[$i - 1][1] : $diff->[$i][1])) {
    0          
    0          
10868            
10869             # gap is white
10870 0         0 $gdata->[$i - 1] = [$max, 0, 0];
10871            
10872             } else {
10873            
10874             # gap is black
10875 0         0 $gdata->[$i - 1] = [0, 0, 0];
10876            
10877             }
10878            
10879             }
10880            
10881             }
10882            
10883             }
10884              
10885             # compute edge pixel values (black)
10886 0 0       0 $edge = ($pi == 5) ? [0, 0, 0, ($max) x ($samples - 3)] : [0, 0, 0];
10887              
10888             # for each patch
10889 0         0 for my $i (0 .. $#{$data}) {
  0         0  
10890            
10891             # if first patch
10892 0 0       0 if ($i == 0) {
    0          
10893            
10894             # add left edge data
10895 0         0 push(@row, (@{$edge}) x $left->[0]);
  0         0  
10896            
10897             # set patch width
10898 0         0 $w = $width - $left->[1];
10899            
10900             # if last patch
10901 0         0 } elsif ($i == $#{$data}) {
10902            
10903             # set patch width
10904 0         0 $w = $width - $right->[1];
10905            
10906             # others
10907             } else {
10908            
10909             # set patch width
10910 0         0 $w = $width;
10911            
10912             }
10913            
10914             # if dither enabled or 32-bits
10915 0 0 0     0 if (defined($dither) || $bits == 32) {
10916            
10917             # add patch data
10918 0         0 push(@row, (@{$data->[$i]}) x $w);
  0         0  
10919            
10920             } else {
10921            
10922             # add patch data, adding/subtracting 0.5 to round to the nearest integer (by 'pack', below)
10923 0 0       0 push(@row, (map {$_ < 0 ? $_ - 0.5 : $_ + 0.5} @{$data->[$i]}) x $w);
  0         0  
  0         0  
10924            
10925             }
10926            
10927             # if last patch
10928 0 0       0 if ($i == $#{$data}) {
  0         0  
10929            
10930             # add right edge data
10931 0         0 push(@row, (@{$edge}) x $right->[0]);
  0         0  
10932            
10933             } else {
10934            
10935             # add gap data
10936 0         0 push(@row, (@{$gdata->[$i]}) x $gap);
  0         0  
10937            
10938             }
10939            
10940             }
10941              
10942             # set file pointer to strip offset
10943 0         0 seek($fh, $tags->{'273'}[$sx + 1], 0);
10944            
10945             # if dither enabled and 8-bit
10946 0 0 0     0 if (defined($dither) && $bits == 8) {
10947            
10948             # for each strip row
10949 0         0 for my $i (0 .. $tags->{'278'}[1] - 1) {
10950            
10951             # write packed data with dithering
10952 0 0       0 print $fh pack($fmt, map {$_ < 0 ? $_ - rand() : $_ + rand()} @row);
  0         0  
10953            
10954             }
10955            
10956             } else {
10957            
10958             # for each strip row
10959 0         0 for my $i (0 .. $tags->{'278'}[1] - 1) {
10960            
10961             # write packed data
10962 0         0 print $fh pack($fmt, @row);
10963            
10964             }
10965            
10966             }
10967            
10968             }
10969              
10970             # read chart from CxF3 data file
10971             # parameters: (object_reference, file_handle, hash)
10972             # returns: (result)
10973             sub _readChartCxF3 {
10974              
10975             # get parameters
10976 8     8   19 my ($self, $fh, $hash) = @_;
10977              
10978             # local variables
10979 8         22 my ($dom, $root, @rns, $core, $ns, $xpc);
10980 8         0 my (@obj, $ops_hash, $ix, $nx, $ops, $type, $name, $mode, $name_ix, $node, @data);
10981              
10982             # parse CxF3 document
10983 8 50       12 eval{$dom = XML::LibXML->load_xml('IO' => $fh)} || return('failed parsing CxF3 document');
  8         50  
10984              
10985             # get root element
10986 8         7531 $root = $dom->documentElement();
10987              
10988             # get root namespaces
10989 8         53 @rns = $root->getNamespaces();
10990              
10991             # get CxF3-core namespace object, verify a CxF3 document
10992 8 50       17 (($core) = grep {$_->value eq 'http://colorexchangeformat.com/CxF3-core'} @rns) || return('CxF3 document has wrong URI');
  16         66  
10993              
10994             # validate the CxF3 document
10995 8 0 33     20 _validateCxF3($dom) if (defined($hash->{'validate'}) && $hash->{'validate'});
10996              
10997             # if CxF3-core namespace has prefix
10998 8 50       28 if ($ns = $core->declaredPrefix) {
10999            
11000             # append ':' to namespace prefix
11001 8         17 $ns .= ':';
11002            
11003             } else {
11004            
11005             # register default namespace
11006 0         0 $xpc->registerNs('cc' => 'http://colorexchangeformat.com/CxF3-core');
11007            
11008             # set default namespace prefix
11009 0         0 $ns = 'cc:';
11010            
11011             }
11012              
11013             # save document object model in header
11014 8         18 $self->[0]{'CxF3_dom'} = $dom;
11015              
11016             # save record separator in header
11017             # note: XML files might not have record separators
11018             # so we use Perl's input record separator instead
11019 8         22 $self->[0]{'read_rs'} = $/;
11020              
11021             # make XPathContext object
11022 8         157 $xpc = XML::LibXML::XPathContext->new($root);
11023              
11024             # read CxF3 FileInformation nodes
11025 8         29 _readCxF3fileinfo($self, $xpc, $ns);
11026              
11027             # get cc:Object nodes
11028 8         380 @obj = $xpc->findnodes("${ns}Resources/${ns}ObjectCollection/${ns}Object");
11029              
11030             # make the operations hash and add format fields
11031 8         327 $ops_hash = _makeCxF3readops($self, $xpc, $ns, \@obj, $hash);
11032              
11033             # get the ObjectType attribute of first object
11034 8         147 $type = $obj[0]->getAttribute('ObjectType');
11035              
11036             # get the Name attribute of first object
11037 8         77 $name = $obj[0]->getAttribute('Name');
11038              
11039             # determine the object linking mode
11040 8         105 $mode = $name =~ m/^$type(\d+)/;
11041              
11042             # initialize next sample index
11043 8         14 $nx = 1;
11044              
11045             # for each cc:Object element
11046 8         15 for my $s (@obj) {
11047            
11048             # get the ObjectType attribute
11049 160         319 $type = $s->getAttribute('ObjectType');
11050            
11051             # get the Name attribute
11052 160         1301 $name = $s->getAttribute('Name');
11053            
11054             # if Name is ObjectType with index appended (X-Rite i1Profiler)
11055 160 50       1034 if ($mode) {
    0          
11056            
11057             # match row index
11058 160 50       885 if ($name =~ m/^$type(\d+)/) {
11059            
11060             # set row index
11061 160         401 $ix = $1;
11062            
11063             } else {
11064            
11065             # print message
11066 0         0 print "invalid CxF3 Object node\n";
11067            
11068             # next object
11069 0         0 next;
11070            
11071             }
11072            
11073             # if Name found in hash (objects related by Name attribute)
11074             } elsif (exists($name_ix->{$name})) {
11075            
11076             # set row index
11077 0         0 $ix = $name_ix->{$name};
11078            
11079             } else {
11080            
11081             # add Name to hash and increment next sample index
11082 0         0 $ix = $name_ix->{$name} = $nx++;
11083            
11084             }
11085            
11086             # get operation list for this ObjectType
11087 160         277 $ops = $ops_hash->{$type};
11088            
11089             # for each operation
11090 160         176 for my $i (0 .. $#{$ops}) {
  160         335  
11091            
11092             # get main Xpath node
11093 160         395 ($node) = $xpc->findnodes($ops->[$i][1], $s);
11094            
11095             # if subpaths
11096 160 100       6229 if (@{$ops->[$i][2]}) {
  160 50       289  
    50          
11097            
11098             # if data class is NCLR
11099 100 100       159 if ($ops->[$i][0] eq 'NCLR') {
11100            
11101             # get the CMYK values
11102 20         22 @data = map {$xpc->findvalue($_, $node)} @{$ops->[$i][2]};
  80         3440  
  20         39  
11103            
11104             # for each SpotColor
11105 20         1113 for my $spotcolor ($xpc->findnodes("${ns}SpotColor", $node)) {
11106            
11107             # push SpotColor value
11108 40         1693 push(@data, $xpc->findvalue("${ns}Percentage", $spotcolor));
11109            
11110             }
11111            
11112             # set chart data (CMYK + SPOT values)
11113 20         1126 @{$self->[1][$ix]}[@{$ops->[$i][3]}] = @data;
  20         74  
  20         278  
11114            
11115             } else {
11116            
11117             # set chart data using subpaths
11118 80         97 @{$self->[1][$ix]}[@{$ops->[$i][3]}] = map {$xpc->findvalue($_, $node)} @{$ops->[$i][2]};
  80         294  
  80         4371  
  280         11375  
  80         146  
11119            
11120             }
11121            
11122             # if no subpaths and one field
11123 60         106 } elsif (@{$ops->[$i][3]} == 1) {
11124            
11125             # set chart data to text content
11126 0         0 $self->[1][$ix][$ops->[$i][3][0]] = $node->textContent();
11127            
11128             # if no subpaths and multiple fields (e.g. spectral data)
11129 60         113 } elsif (@{$ops->[$i][3]} > 1) {
11130            
11131             # set chart data splitting text content
11132 60         603 @{$self->[1][$ix]}[@{$ops->[$i][3]}] = split(' ', $node->textContent());
  60         468  
  60         102  
11133            
11134             }
11135            
11136             }
11137            
11138             }
11139              
11140             # read CxF3 ColorSpecification nodes
11141 8         26 _readCxF3colorspec($self, $xpc, $ns);
11142              
11143             # read CxF3 CustomResources nodes
11144 8         213 _readCxF3customres($self, $xpc, $ns, \@rns);
11145              
11146             # clean-up contexts
11147 8         163 _readCxF3cleanup($self);
11148              
11149             # save XPath context object in header
11150             # note: all CustomResources namespaces are registered
11151 8         13 $self->[0]{'CxF3_XPathContext'} = $xpc;
11152              
11153             # return
11154 8         30 return();
11155              
11156             }
11157              
11158             # make CxF3 read operations hash
11159             # adds the format fields to object
11160             # parameters: (object_reference, XPath_object, CxF3_prefix, CxF3_object_array_reference, hash)
11161             # returns: (hash_ref)
11162             sub _makeCxF3readops {
11163              
11164             # get parameters
11165 8     8   18 my ($self, $xpc, $ns, $obj, $hash) = @_;
11166              
11167             # local variables
11168 8         21 my (@attr, @tags, %keys, $table, $k, $m, $n, $t, $type, $entry, $ops_hash);
11169 8         0 my (@format, @nodes, $node, @data, $name, $colorspec, $start, $inc);
11170              
11171             # if cc:Object filter parameter provided
11172 8 50 33     26 if (defined($hash->{'cc:Object'}) && ref($hash->{'cc:Object'}) eq 'ARRAY') {
11173            
11174             # for each entry
11175 0         0 for (@{$hash->{'cc:Object'}}) {
  0         0  
11176            
11177             # match type/attribute
11178 0         0 m/^([^\s\/]*?)\/?([^\s\/]*)$/;
11179            
11180             # save matched values
11181 0         0 $entry = [$1, $2];
11182            
11183             # if a valid attribute (see CxF3_Core.xsd)
11184 0 0       0 if ($2 =~ m/^(?:|ObjectType|Name|Id|GUID|\*)$/) {
11185            
11186             # push on array
11187 0         0 push(@attr, $entry);
11188            
11189             } else {
11190            
11191             # print warning
11192 0         0 warn('invalid cc:Object attribute');
11193            
11194             }
11195            
11196             }
11197            
11198             }
11199              
11200             # if cc:Tag filter parameter provided
11201 8 50 33     18 if (defined($hash->{'cc:Tag'}) && ref($hash->{'cc:Tag'}) eq 'ARRAY') {
11202            
11203             # for each entry
11204 0         0 for (@{$hash->{'cc:Tag'}}) {
  0         0  
11205            
11206             # match type/key
11207 0         0 m/^([^\s\/]*?)\/?([^\s\/]*)$/;
11208            
11209             # push on array
11210 0         0 push(@tags, [$1, $2]);
11211            
11212             }
11213            
11214             }
11215              
11216             # make hash for sort order of certain keys
11217 8         29 %keys = ('SampleID' => -2, 'SampleName' => -1, 'Id' => -2, 'Name' => -1);
11218              
11219             # table [data_class, CxF3_main_path, [CxF3_sub_paths], [CGATS/ASCII field names]]
11220             # some mappings have no sub-paths, which is indicated by an empty sub_path array
11221             # the 'NCLR', 'SPECTRAL' and 'DENSITY' data classes are special cases
11222 8         316 $table = [
11223             ['RGB', "${ns}DeviceColorValues/${ns}ColorRGB", ["${ns}R", "${ns}G", "${ns}B"], [qw(RGB_R RGB_G RGB_B)]],
11224             ['CMYK', "${ns}DeviceColorValues/${ns}ColorCMYK", ["${ns}Cyan", "${ns}Magenta", "${ns}Yellow", "${ns}Black"], [qw(CMYK_C CMYK_M CMYK_Y CMYK_K)]],
11225             ['NCLR', "${ns}DeviceColorValues/${ns}ColorCMYKPlusN", ["${ns}Cyan", "${ns}Magenta", "${ns}Yellow", "${ns}Black"], [qw(nCLR)]],
11226             ['SPECTRAL', "${ns}ColorValues/${ns}ReflectanceSpectrum", [], [qw(nm)]],
11227             ['DENSITY', "${ns}ColorValues/${ns}ColorDensity/${ns}Density", [], [qw(D_RED D_GREEN D_BLUE D_VIS)]],
11228             ['XYZ', "${ns}ColorValues/${ns}ColorCIEXYZ", ["${ns}X", "${ns}Y", "${ns}Z"], [qw(XYZ_X XYZ_Y XYZ_Z)]],
11229             ['XYY', "${ns}ColorValues/${ns}ColorCIExyY", ["${ns}x", "${ns}y", "${ns}Y"], [qw(XYY_X XYY_Y XYY_YCAP)]],
11230             ['LAB', "${ns}ColorValues/${ns}ColorCIELab", ["${ns}L", "${ns}A", "${ns}B"], [qw(LAB_L LAB_A LAB_B)]],
11231             ['LCH', "${ns}ColorValues/${ns}ColorCIELCh", ["${ns}L", "${ns}C", "${ns}H"], [qw(LAB_L LAB_C LAB_H)]],
11232             ['SRGB', "${ns}ColorValues/${ns}ColorSRGB", ["${ns}R", "${ns}G", "${ns}B"], [qw(SRGB_R SRGB_G SRGB_B)]],
11233             ['DE', "${ns}ColorDifferenceValues/${ns}DeltaCIELab/${ns}dE", [], [qw(LAB_DE)]],
11234             ['DE94', "${ns}ColorDifferenceValues/${ns}DeltaCIELab/${ns}dE94", [], [qw(LAB_DE94)]],
11235             ['DECMC', "${ns}ColorDifferenceValues/${ns}DeltaCIELab/${ns}dEcmc", [], [qw(LAB_CMC)]],
11236             ['DE2000', "${ns}ColorDifferenceValues/${ns}DeltaCIELab/${ns}dE2000", [], [qw(LAB_2000)]],
11237             ];
11238              
11239             # set next table index
11240 8         44 $m = $#{$table} + 1;
  8         15  
11241              
11242             # for each CxF3 'Object'
11243 8         10 for my $s (@{$obj}) {
  8         14  
11244            
11245             # get the ObjectType attribute
11246 160         235 $type = $s->getAttribute('ObjectType');
11247            
11248             # get the Name attribute
11249 160         1126 $name = $s->getAttribute('Name');
11250            
11251             # if 'ObjectType' not in hash
11252 160 100       1100 if (! defined($ops_hash->{$type})) {
11253            
11254             # add 'ObjectType' to hash
11255 16         55 $ops_hash->{$type} = [];
11256            
11257             # if 'Object' attributes are mapped
11258 16 50       31 if (@attr) {
11259            
11260             # for each 'Object' attribute (GUID is optional)
11261 0         0 for my $t (qw(ObjectType Name Id GUID)) {
11262            
11263             # if attribute exists and is mapped
11264 0 0 0     0 if ($xpc->exists("\@$t", $s) && grep {($_->[0] eq $type || $_->[0] =~ m/^\*?$/) && ($_->[1] eq $t || $_->[1] =~ m/^\*?$/)} @attr) {
  0 0 0     0  
      0        
11265            
11266             # get sort order
11267 0 0       0 $k = defined($keys{$t}) ? $keys{$t} : $m++;
11268            
11269             # push table entry on hash array (note: attribute XPaths begin with @)
11270 0         0 push(@{$ops_hash->{$type}}, $entry = ["ATTR:$t", "\@$t", [], [$t], $type, $k]);
  0         0  
11271            
11272             # push table entry on format array
11273 0         0 push(@format, $entry);
11274            
11275             }
11276            
11277             }
11278            
11279             } else {
11280            
11281             # if Name is not ObjectType with appended number, and type is not 'Measurement'
11282 16 50 33     309 if ($name !~ m/^$type(\d+)/ && $type ne 'Measurement') {
11283            
11284             # push table entry on hash array (note: attribute XPaths begin with @)
11285 0         0 push(@{$ops_hash->{$type}}, $entry = ['NAME', '@Name', [], ['SAMPLE_NAME'], $type, -1]);
  0         0  
11286            
11287             # push table entry on format array
11288 0         0 push(@format, $entry);
11289            
11290             }
11291            
11292             }
11293            
11294             # for each table entry
11295 16         31 for my $i (0 .. $#{$table}) {
  16         39  
11296            
11297             # get table entry
11298 224         4865 $t = $table->[$i];
11299            
11300             # if main XPath exists
11301 224 100       364 if ($xpc->exists($t->[1], $s)) {
11302            
11303             # get ColorSpecification attribute (if any)
11304 16         424 $colorspec = $xpc->findvalue("$t->[1]/\@ColorSpecification", $s);
11305            
11306             # push table entry on hash array
11307 16         978 push(@{$ops_hash->{$type}}, $entry = [@{$t}, $type, $i, $colorspec]);
  16         34  
  16         41  
11308            
11309             # push table entry on format array
11310 16         24 push(@format, $entry);
11311            
11312             # if an 'NCLR' entry
11313 16 100       48 if ($entry->[0] eq 'NCLR') {
    100          
11314            
11315             # get cc:SpotColor nodes
11316 2         8 @nodes = $xpc->findnodes(".//${ns}SpotColor", $s);
11317            
11318             # get number of colors
11319 2         60 $n = @nodes + 4;
11320            
11321             # add format fields
11322 2         6 $entry->[3] = [map {sprintf('%xCLR_%x', $n, $_)} (1 .. $n)];
  12         29  
11323            
11324             # if a 'SPECTRAL' entry
11325             } elsif ($entry->[0] eq 'SPECTRAL') {
11326            
11327             # get the ReflectanceSpectrum data
11328 6         14 @data = split(' ', $xpc->findvalue($t->[1], $s));
11329            
11330             # get the ColorSpecification node (linked by the ColorSpecification attribute)
11331 6         353 ($node) = $xpc->findnodes("${ns}Resources/${ns}ColorSpecificationCollection/${ns}ColorSpecification[\@Id='$colorspec']");
11332            
11333             # get the StartWL attribute
11334 6         300 $start = $xpc->findvalue("${ns}MeasurementSpec/${ns}WavelengthRange/\@StartWL", $node);
11335            
11336             # get the Increment attribute
11337 6         316 $inc = $xpc->findvalue("${ns}MeasurementSpec/${ns}WavelengthRange/\@Increment", $node);
11338            
11339             # add format fields
11340 6         291 $entry->[3] = [map {'nm' . ($start + $_ * $inc)} (0 .. $#data)];
  216         358  
11341            
11342             }
11343            
11344             }
11345            
11346             }
11347            
11348             # if Tags are mapped
11349 16 50       410 if (@tags) {
11350            
11351             # for each Tag
11352 0         0 for my $t ($xpc->findnodes("${ns}TagCollection/${ns}Tag", $s)) {
11353            
11354             # get Tag Name attribute
11355 0         0 $name = $t->getAttribute('Name');
11356            
11357             # if this Tag is mapped
11358 0 0 0     0 if (grep {($_->[0] eq $type || $_->[0] =~ m/^\*?$/) && ($_->[1] eq $name || $_->[1] =~ m/^\*?$/)} @tags) {
  0 0 0     0  
11359            
11360             # get sort order
11361 0 0       0 $k = defined($keys{$name}) ? $keys{$name} : $m++;
11362            
11363             # push table entry on hash array (note: attribute XPaths begin with @)
11364 0         0 push(@{$ops_hash->{$type}}, $entry = ["TAG:$name", "${ns}TagCollection/${ns}Tag[\@Name = '$name']/\@Value", [], [$name], $type, $k]);
  0         0  
11365            
11366             # push table entry on format array
11367 0         0 push(@format, $entry);
11368            
11369             }
11370            
11371             }
11372            
11373             }
11374            
11375             }
11376            
11377             }
11378              
11379             # sort format array by table index
11380 8         34 @format = sort {$a->[5] <=> $b->[5]} @format;
  8         29  
11381            
11382             # for each format entry
11383 8         14 for my $fmt (@format) {
11384            
11385             # add format fields to data array and replace keys with column indices
11386 16         18 $fmt->[3] = add_fmt($self, map {"$fmt->[4]|$_"} @{$fmt->[3]});
  256         482  
  16         26  
11387            
11388             # if entry has ColorSpecification
11389 16 50       46 if (defined($fmt->[6])) {
11390            
11391             # add ColorSpecification attribute to colorimetry array
11392 16         17 for (@{$fmt->[3]}) {$self->[2][5][$_] = $fmt->[6]}
  16         28  
  256         326  
11393            
11394             }
11395            
11396             }
11397              
11398             # return
11399 8         48 return($ops_hash);
11400              
11401             }
11402              
11403             # read CxF3 FileInformation nodes
11404             # parameters: (object_reference, XPath_object, CxF3_prefix)
11405             sub _readCxF3fileinfo {
11406              
11407             # get parameters
11408 8     8   17 my ($self, $xpc, $ns) = @_;
11409            
11410             # local variables
11411 8         11 my (@info, %keys, $name, $value);
11412              
11413             # get cc:FileInformation nodes (optional)
11414 8         63 @info = $xpc->findnodes("${ns}FileInformation/*");
11415              
11416             # make CxF3 => ASCII mapping table (from ISO 17972-1, Annex A)
11417 8         355 %keys = ('Creator' => 'ORIGINATOR', 'Description' => 'FILE_DESCRIPTOR', 'CreationDate' => 'CREATED', 'Comment' => 'CXF3_COMMENT');
11418              
11419             # for each cc:FileInformation element
11420 8         17 for my $s (@info) {
11421            
11422             # if 'Tag' node
11423 40 100       104 if ($s->localname() eq 'Tag') {
11424            
11425             # get name attribute
11426 16         47 $name = $s->getAttribute('Name');
11427            
11428             # get value attribute
11429 16         131 $value = $s->getAttribute('Value');
11430            
11431             } else {
11432            
11433             # get node name (no prefix)
11434 24         48 $name = $s->localname();
11435            
11436             # lookup name in hash
11437 24 50       60 $name = defined($keys{$name}) ? $keys{$name} : $name;
11438            
11439             # get node value
11440 24         68 $value = $s->textContent();
11441            
11442             }
11443            
11444             # add name/value to header array
11445 40         164 push(@{$self->[3]}, [$name, "\"$value\"", 'FileInformation']);
  40         148  
11446            
11447             }
11448            
11449             }
11450              
11451             # read CxF3 ColorSpecification nodes
11452             # parameters: (object_reference, XPath_object, CxF3_prefix)
11453             sub _readCxF3colorspec {
11454              
11455             # get parameters
11456 8     8   24 my ($self, $xpc, $ns) = @_;
11457              
11458             # local variables
11459 8         15 my (@keys, @cspec, $id, $node, $child, $value);
11460              
11461             # make CxF3 => ASCII mapping table (from ISO 17972-1, Annex A)
11462 8         174 @keys = (
11463             ["${ns}MeasurementSpec/${ns}GeometryChoice" => 'MEASUREMENT_GEOMETRY'],
11464             ["${ns}MeasurementSpec/${ns}CalibrationStandard" => 'DEVCALSTD'],
11465             ["${ns}MeasurementSpec/${ns}Device/${ns}Manufacturer" => 'MANUFACTURER'],
11466             ["${ns}MeasurementSpec/${ns}Device/${ns}Model" => 'MODEL'],
11467             ["${ns}MeasurementSpec/${ns}Device/${ns}SerialNumber" => 'SERIAL_NUMBER'],
11468             ["${ns}MeasurementSpec/${ns}Device/${ns}DeviceClass" => 'DEVICE_CLASS'],
11469             ["${ns}MeasurementSpec/${ns}Device/${ns}DeviceFilter" => 'FILTER'],
11470             ["${ns}MeasurementSpec/${ns}Device/${ns}DeviceIllumination" => 'MEASUREMENT_SOURCE'],
11471             ["${ns}MeasurementSpec/${ns}Device/${ns}DevicePolarization" => 'POLARIZATION'],
11472             );
11473              
11474             # find the ColorSpecification nodes
11475 8         44 @cspec = $xpc->findnodes("${ns}Resources/${ns}ColorSpecificationCollection/${ns}ColorSpecification");
11476              
11477             # for each ColorSpecification node
11478 8         247 for my $s (@cspec) {
11479            
11480             # get the Id attribute and skip if 'Unknown'
11481 16 100       319 next if (($id = $s->getAttribute('Id')) eq 'Unknown');
11482            
11483             # for each entry in mapping table
11484 8         83 for my $i (0 .. $#keys) {
11485            
11486             # if XPath is found
11487 72 100       1294 if (($node) = $xpc->findnodes($keys[$i][0], $s)) {
11488            
11489             # get the first non-blank child node
11490 24 50       735 if (($child) = $node->nonBlankChildNodes()) {
11491            
11492             # if child is an element node
11493 24 100       317 if ($child->nodeType() == 1) {
    50          
11494            
11495             # serialize node
11496 8         204 $value = $node->toString(1);
11497            
11498             # remove tabs and endlines
11499 8         119 $value =~ s/[\t\n]+//g;
11500            
11501             # remove namespace prefix
11502 8         156 $value =~ s/([<\/])${ns}/$1/g;
11503            
11504             # if child is a text node
11505             } elsif ($child->nodeType() == 3) {
11506            
11507             # get the value
11508 16         58 $value = $node->textContent();
11509            
11510             }
11511            
11512             # save in header line array
11513 24         36 push(@{$self->[3]}, [$keys[$i][1], "\"$value\"", $id]);
  24         108  
11514            
11515             }
11516            
11517             }
11518            
11519             }
11520            
11521             }
11522            
11523             }
11524              
11525             # read CxF3 CustomResources nodes
11526             # parameters: (object_reference, XPath_object, CxF3_prefix, root_namespace_array_reference)
11527             sub _readCxF3customres {
11528              
11529             # get parameters
11530 8     8   16 my ($self, $xpc, $ns, $rns) = @_;
11531              
11532             # local variables
11533 8         20 my (@crnodes, $name, $cr, @crns, $nsobj, $uri, $nsd);
11534 8         0 my (@nodes, @nodes2, @attr, $bg, $tint, $objref, $rsnr);
11535              
11536             # initialize default namespace prefix
11537 8         12 $nsd = 'ns00';
11538              
11539             # find the CustomResources nodes
11540 8         26 @crnodes = $xpc->findnodes("${ns}CustomResources/*");
11541              
11542             # for each CustomResources node
11543 8         207 for my $s (@crnodes) {
11544            
11545             # get node properties
11546 8         27 $cr = $s->prefix;
11547 8         25 @crns = $s->getNamespaces();
11548            
11549             # if node has prefix
11550 8 50       17 if (defined($cr)) {
11551            
11552             # find corresponding namespace object
11553 8 50       13 ($nsobj) = grep {defined($_->declaredPrefix) && $_->declaredPrefix eq $cr} (@crns, @{$rns});
  24         120  
  8         13  
11554            
11555             } else {
11556            
11557             # use the default node namespace (no declared prefix)
11558 0         0 ($nsobj) = grep {! defined($_->declaredPrefix)} @crns;
  0         0  
11559            
11560             # make a unique prefix
11561 0         0 $cr = $nsd++;
11562            
11563             }
11564            
11565             # get node URI
11566 8         20 $uri = $nsobj->value;
11567            
11568             # register custom resource namespace, if necessary
11569 8 50       51 $xpc->registerNs($cr, $uri) if (! $xpc->lookupNs($cr));
11570            
11571             # append ':' to namespace prefix
11572 8         14 $cr .= ':';
11573            
11574             # if spot ink characterization (CxF/X4)
11575 8 50       29 if ($uri eq 'http://colorexchangeformat.com/CxF3-SpotInkCharacterisation') {
    50          
    50          
11576            
11577             # get the attribute list
11578 0         0 @attr = $s->attributes();
11579            
11580             # add sic:SpotInkCharacterisation hash
11581 0         0 $self->[0]{'sic:SpotInkCharacterisation'} = {map {$_->nodeName, $_->getValue()} @attr};
  0         0  
11582            
11583             # get the sic:MeasurementSet nodes
11584 0 0       0 if (@nodes = $xpc->findnodes("${cr}MeasurementSet", $s)) {
11585            
11586             # for each MeasurementSet node
11587 0         0 for my $t (@nodes) {
11588            
11589             # get the Background attribute
11590 0         0 ($bg) = grep {$_->name eq 'Background'} $t->attributes();
  0         0  
11591            
11592             # get Measurement nodes
11593 0         0 @nodes2 = $xpc->findnodes("${cr}Measurement", $t);
11594            
11595             # for each Measurement node
11596 0         0 for my $i (0 .. $#nodes2) {
11597            
11598             # get Measurement node attributes
11599 0         0 @attr = $nodes2[$i]->attributes();
11600            
11601             # get the attribute list
11602 0         0 @attr = $nodes2[$i]->attributes();
11603            
11604             # add Measurement hash
11605 0         0 $self->[0]{'sic:MeasurementSet'}{$bg->value}[$i] = {map {$_->nodeName, $_->getValue()} @attr};
  0         0  
11606            
11607             }
11608            
11609             }
11610            
11611             }
11612            
11613             # if quality control
11614             } elsif ($uri eq 'http://colorexchangeformat.com/CxF3-qualitycontrol') {
11615            
11616             # to be added someday
11617            
11618             # if prism (X-Rite i1Profiler)
11619             } elsif ($uri = 'http://www.xrite.com/products/prism') {
11620            
11621             # get the attribute list, excluding namespace
11622 8         25 @attr = grep {$_->getValue() ne 'http://www.xrite.com/products/prism'} $s->attributes();
  16         127  
11623            
11624             # add xrp:Prism hash
11625 8         29 $self->[0]{'xrp:Prism'} = {map {$_->nodeName, $_->getValue()} @attr};
  8         79  
11626            
11627             # get the xrp:CustomAttributes node
11628 8 50       37 if (@nodes = $xpc->findnodes("${cr}CustomAttributes", $s)) {
11629            
11630             # get the attribute list
11631 8         279 @attr = $nodes[0]->attributes();
11632            
11633             # add xrp:CustomAttributes hash
11634 8         172 $self->[0]{'xrp:CustomAttributes'} = {map {$_->nodeName, $_->getValue()} @attr};
  400         1325  
11635            
11636             }
11637            
11638             } else {
11639            
11640             # print message
11641 0         0 print "unsupported custom resource '$uri' encountered when reading CxF3 file\n\n";
11642            
11643             }
11644            
11645             }
11646            
11647             }
11648              
11649             # clean-up CxF3 contexts
11650             # adds measurement condition when object type has none
11651             # parameters: (object_reference)
11652             sub _readCxF3cleanup {
11653              
11654             # get parameters
11655 8     8   12 my ($self) = @_;
11656              
11657             # local variables
11658 8         13 my ($id, $ms, $cond);
11659              
11660             # for each data format field
11661 8         10 for my $i (0 .. $#{$self->[1][0]}) {
  8         24  
11662            
11663             # if context is 'Measurement'
11664 256 50       380 if ($self->[1][0][$i] =~ m/^Measurement\|/) {
11665            
11666             # get ColorSpecification Id for this format field
11667 0         0 $id = $self->[2][5][$i];
11668            
11669             # if a 'MEASUREMENT_SOURCE' record with this ColorSpecification Id
11670 0 0       0 if (($ms) = grep {$_->[0] eq 'MEASUREMENT_SOURCE' && $_->[2] eq $id} @{$self->[3]}) {
  0 0       0  
  0         0  
11671            
11672             # if measurement source contains an M-value
11673 0 0       0 if ($ms->[1] =~ m/(M[0-3])/) {
11674            
11675             # use matched measurement condition
11676 0         0 $cond = $1;
11677            
11678             } else {
11679            
11680             # use measurement condition M0
11681 0         0 $cond = 'M0';
11682            
11683             }
11684            
11685             } else {
11686            
11687             # use measurement condition M0
11688 0         0 $cond = 'M0';
11689            
11690             }
11691            
11692             # prepend data format with measurement condition
11693 0         0 $self->[1][0][$i] = "$cond\_$self->[1][0][$i]";
11694            
11695             }
11696            
11697             }
11698            
11699             }
11700              
11701             # make CxF3 write operations array
11702             # parameters: (object_reference, CxF3_prefix, column_slice)
11703             # returns: (array_ref)
11704             sub _makeCxF3writeops {
11705              
11706             # get parameters
11707 0     0   0 my ($self, $ns, $cols) = @_;
11708              
11709             # local variables
11710 0         0 my ($n, %keys, $table, $class, $prefix, $key, $ops, $groups, $sort);
11711              
11712             # if column slice defined
11713 0 0       0 if (defined($cols)) {
11714            
11715             # if column slice an empty array reference
11716 0 0 0     0 if (ref($cols) eq 'ARRAY' && @{$cols} == 0) {
  0         0  
11717            
11718             # use all columns
11719 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
11720            
11721             } else {
11722            
11723             # flatten column slice
11724 0         0 $cols = ICC::Shared::flatten($cols);
11725            
11726             }
11727            
11728             } else {
11729            
11730             # use all columns
11731 0         0 $cols = [0 .. $#{$self->[1][0]}];
  0         0  
11732            
11733             }
11734              
11735             # map column slice, converting non-numeric values with 'test' method
11736 0 0       0 @{$cols} = map {Scalar::Util::looks_like_number($_) ? $_ : $self->test($_)} @{$cols};
  0         0  
  0         0  
  0         0  
11737              
11738             # get number of fields
11739 0         0 $n = @{$cols};
  0         0  
11740              
11741             # remove undefined keys
11742 0         0 @{$cols} = grep {defined($self->[1][0][$_])} @{$cols};
  0         0  
  0         0  
  0         0  
11743              
11744             # warn if undefined keys
11745 0 0       0 ($n == @{$cols}) || warn('undefined keys in column slice');
  0         0  
11746              
11747             # get number of fields
11748 0         0 $n = @{$cols};
  0         0  
11749              
11750             # remove duplicate keys
11751 0         0 @{$cols} = grep {! $keys{$self->[1][0][$_]}++} @{$cols};
  0         0  
  0         0  
  0         0  
11752              
11753             # warn if duplicate keys
11754 0 0       0 ($n == @{$cols}) || warn('duplicate keys in column slice');
  0         0  
11755              
11756             # table structure: [data_class, CxF3_main_path, [CxF3_sub_paths], regex, sort_order]
11757             # some mappings have no sub-paths, which is indicated by an empty sub_path array
11758             # sort_order array contains the last character(s) of the format keys, and is optional
11759             # the 'NCLR', 'SPECTRAL' and 'DENSITY' data classes are special cases
11760 0         0 $table = [
11761             ['RGB', "$ns:DeviceColorValues/$ns:ColorRGB", ["$ns:R", "$ns:G", "$ns:B"], qr/^(?:(.*)\|)?RGB_[RGB]$/, [qw(R G B)]],
11762             ['CMYK', "$ns:DeviceColorValues/$ns:ColorCMYK", ["$ns:Cyan", "$ns:Magenta", "$ns:Yellow", "$ns:Black"], qr/^(?:(.*)\|)?CMYK_[CMYK]$/, [qw(C M Y K)]],
11763             ['NCLR', "$ns:DeviceColorValues/$ns:ColorCMYKPlusN", ["$ns:Cyan", "$ns:Magenta", "$ns:Yellow", "$ns:Black"], qr/^(?:(.*)\|)?(?:[2-9A-F]CLR_[1-9A-F]|PC[2-9A-F]_[1-9A-F])$/],
11764             ['SPECTRAL', "$ns:ColorValues/$ns:ReflectanceSpectrum", [], qr/^(?:(.*)\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3}$/],
11765             ['DENSITY', "$ns:ColorValues/$ns:ColorDensity/$ns:Density", [], qr/^(?:(.*)\|)?D_(?:RED|GREEN|BLUE|VIS)$/, [qw(RED GREEN BLUE VIS)]],
11766             ['XYZ', "$ns:ColorValues/$ns:ColorCIEXYZ", ["$ns:X", "$ns:Y", "$ns:Z"], qr/^(?:(.*)\|)?XYZ_[XYZ]$/, [qw(X Y Z)]],
11767             ['XYY', "$ns:ColorValues/$ns:ColorCIExyY", ["$ns:x", "$ns:y", "$ns:Y"], qr/^(?:(.*)\|)?XYY_(?:X|Y|CAPY)$/, [qw(_X _Y _CAPY)]],
11768             ['LAB', "$ns:ColorValues/$ns:ColorCIELab", ["$ns:L", "$ns:A", "$ns:B"], qr/^(?:(.*)\|)?LAB_[LAB]$/, [qw(L A B)]],
11769             ['LCH', "$ns:ColorValues/$ns:ColorCIELCh", ["$ns:L", "$ns:C", "$ns:H"], qr/^(?:(.*)\|)?LAB_[LCH]$/, [qw(L C H)]],
11770             ['SRGB', "$ns:ColorValues/$ns:ColorSRGB", ["$ns:R", "$ns:G", "$ns:B"], qr/^(?:(.*)\|)?SRGB_[RGB]$/, [qw(R G B)]],
11771             ['DE', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE", [], qr/^(?:(.*)\|)?LAB_DE$/],
11772             ['DE94', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE94", [], qr/^(?:(.*)\|)?LAB_DE94$/],
11773             ['DECMC', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dEcmc", [], qr/^(?:(.*)\|)?LAB_CMC$/],
11774             ['DE2000', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE2000", [], qr/^(?:(.*)\|)?LAB_2000$/],
11775             ];
11776              
11777             # following section builds operations array from column slice
11778             #
11779             # sort keys alphabetically
11780 0         0 @{$cols} = sort {$self->[1][0][$a] cmp $self->[1][0][$b]} @{$cols};
  0         0  
  0         0  
  0         0  
11781              
11782             # for each field
11783 0         0 for my $i (@{$cols}) {
  0         0  
11784            
11785             # if key matches current class and prefix (prefix could be undefined)
11786 0 0 0     0 if (defined($class) && $self->[1][0][$i] =~ /$table->[$class][3]/ && (defined($prefix) ? $prefix : "\n") eq (defined($1) ? $1 : "\n")) {
    0 0        
    0          
11787            
11788             # add index to current operation
11789 0         0 push(@{$ops->[-1][4]}, $i);
  0         0  
11790            
11791             } else {
11792            
11793             # for each data class
11794 0         0 for my $j (0 .. $#{$table}) {
  0         0  
11795            
11796             # if key matches class
11797 0 0       0 if ($self->[1][0][$i] =~ /$table->[$j][3]/) {
    0          
11798            
11799             # save current prefix
11800 0         0 $prefix = $1;
11801            
11802             # save current class
11803 0         0 $class = $j;
11804            
11805             # add new operation
11806 0         0 push(@{$ops}, [$table->[$j][0], $prefix, $table->[$j][1], $table->[$j][2], [$i], {}, $j]);
  0         0  
11807            
11808             # quit loop
11809 0         0 last;
11810            
11811             # if no match found in table
11812 0         0 } elsif ($j == $#{$table}) {
11813            
11814             # match prefix/key
11815 0         0 $self->[1][0][$i] =~ m/^(?:(.*)\|)?(.*)/;
11816            
11817             # save matched values
11818 0         0 $prefix = $1;
11819 0         0 $key = $2;
11820            
11821             # set current class
11822 0         0 $class = undef;
11823            
11824             # if prefix defined, and not Target or ...Measurement, and key is SAMPLE_NAME
11825 0 0 0     0 if (defined($prefix) && $prefix !~ m/^Target$|Measurement$/ && $key =~ m/^SAMPLE_NAME$|^SampleName$/) {
      0        
11826            
11827             # add special operation to set 'Object' 'Name' attribute to SAMPLE_NAME
11828 0         0 push(@{$ops}, ['TAG', $prefix, '', [], [], {'Name' => [$i]}, -1]);
  0         0  
11829            
11830             } else {
11831            
11832             # add Tag operation
11833 0         0 push(@{$ops}, ['TAG', $prefix, "$ns:TagCollection/$ns:Tag", [], [], {'Name' => $key, 'Value' => [$i]}, 100]);
  0         0  
11834            
11835             }
11836            
11837             }
11838            
11839             }
11840            
11841             }
11842            
11843             }
11844              
11845             # following section sorts and verifies column slices, sets default prefixes and checks for multiple elements
11846             #
11847             # init loop variable
11848 0         0 %keys = ();
11849            
11850             # for each array entry
11851 0         0 for my $t (@{$ops}) {
  0         0  
11852            
11853             # if sort order is defined
11854 0 0       0 if (defined($table->[$t->[6]][4])) {
11855            
11856             # arrange column indices in sort order
11857 0         0 @{$t->[4]} = map {my $end = $_; grep {$self->[1][0][$_] =~ m/$end$/} @{$t->[4]}} @{$table->[$t->[6]][4]};
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
11858            
11859             }
11860            
11861             # if class is SPECTRAL
11862 0 0       0 if ($t->[0] eq 'SPECTRAL') {
    0          
    0          
11863            
11864             # verify spectral slice
11865 0 0       0 (@{$t->[4]} == @{_spectral($self, $t->[1])}) || warn("invalid column slice - SPECTRAL class");
  0         0  
  0         0  
11866            
11867             # if class is DENSITY
11868             } elsif ($t->[0] eq 'DENSITY') {
11869            
11870             # to be done
11871            
11872             # if class is NCLR
11873             } elsif ($t->[0] eq 'NCLR') {
11874            
11875             # match first key to get number of channels
11876 0         0 $self->[1][0][$t->[4][0]] =~ m/([2-9A-F])(?:CLR_|_)[1-9A-F]$/;
11877            
11878             # verify nCLR slice
11879 0 0       0 (@{$t->[4]} == CORE::hex($1)) || warn("invalid column slice - NCLR class");
  0         0  
11880            
11881             # all others
11882             } else {
11883            
11884             # verify subpaths match column slice
11885 0 0 0     0 (@{$t->[4]} == @{$t->[3]} || (@{$t->[4]} == 1 && @{$t->[3]} == 0)) || warn("invalid column slice - $t->[0] class");
  0   0     0  
  0         0  
  0         0  
  0         0  
11886            
11887             }
11888            
11889             # if prefix undefined
11890 0 0       0 if (! defined($t->[1])) {
11891            
11892             # if XPath contains 'ColorValues' or 'ColorDifferenceValues'
11893 0 0       0 if ($t->[2] =~ m/^$ns:(?:ColorValues|ColorDifferenceValues)\//) {
    0          
11894            
11895             # set prefix to M0_Measurement
11896 0         0 $t->[1] = 'M0_Measurement';
11897            
11898             # if XPath contains 'DeviceColorValues'
11899             } elsif ($t->[2] =~ m/^$ns:DeviceColorValues\//) {
11900            
11901             # set prefix to Target
11902 0         0 $t->[1] = 'Target';
11903            
11904             # all others
11905             } else {
11906            
11907             # set prefix to '~~'
11908 0         0 $t->[1] = '~~';
11909            
11910             }
11911            
11912             }
11913            
11914             # for 'ColorValues' or 'DeviceColorValues'
11915 0 0       0 if ($t->[2] =~ m/^$ns:(ColorValues|DeviceColorValues)\//) {
11916            
11917             # warn on multiple elements (not allowed by i1Profiler)
11918 0 0       0 print "warning: multiple $1 elements in CxF3 $t->[1] object\n" if ($keys{"$1/$t->[1]"}++ == 1);
11919            
11920             }
11921            
11922             }
11923              
11924             # following section groups operations by prefix
11925             #
11926             # sort by prefix, then by table index
11927 0 0       0 @{$ops} = sort {($a->[1] cmp $b->[1]) or ($a->[6] <=> $b->[6])} @{$ops};
  0         0  
  0         0  
  0         0  
11928              
11929             # init loop variable
11930 0         0 $prefix = undef;
11931              
11932             # for each operation
11933 0         0 for my $t (@{$ops}) {
  0         0  
11934            
11935             # if same prefix as last operation
11936 0 0 0     0 if (defined($prefix) && $prefix eq $t->[1]) {
    0 0        
11937            
11938             # add operation to last group
11939 0         0 push(@{$groups->[-1]}, $t);
  0         0  
11940            
11941             # if class is TAG and prefix is '~~'
11942             } elsif ($t->[0] eq 'TAG' && $t->[1] eq '~~') {
11943            
11944             # for each group
11945 0         0 for my $g (@{$groups}) {
  0         0  
11946            
11947             # add operation
11948 0         0 push(@{$g}, $t);
  0         0  
11949            
11950             }
11951            
11952             # set prefix
11953 0         0 $prefix = undef;
11954            
11955             # others
11956             } else {
11957            
11958             # add new group
11959 0         0 push(@{$groups}, [$t]);
  0         0  
11960            
11961             # set prefix
11962 0         0 $prefix = $t->[1];
11963            
11964             }
11965            
11966             }
11967              
11968             # return
11969 0         0 return($groups);
11970              
11971             }
11972              
11973             # write CxF3 FileInformation nodes
11974             # optional hash parameter contains 'cc:FileInformation' filter array
11975             # parameters: (object_reference, XPath_object, CxF3_prefix, CxF3_namespace_URI, hash)
11976             # returns: (datetime)
11977             sub _writeCxF3fileinfo {
11978              
11979             # get parameters
11980 0     0   0 my ($self, $xpc, $ns, $nsURI, $hash) = @_;
11981              
11982             # local variables
11983 0         0 my (@filter, $t, $datetime, $info, %keys);
11984 0         0 my ($keyword, $value, $source, $node, $child);
11985              
11986             # get filter array (if any)
11987 0 0       0 @filter = @{ICC::Shared::flatten($hash->{'cc:FileInformation'})} if (defined($hash->{'cc:FileInformation'}));
  0         0  
11988              
11989             # make Time::Piece object
11990 0         0 $t = localtime();
11991              
11992             # get the 'FileInformation' node
11993 0         0 ($info) = $xpc->findnodes("$ns:FileInformation");
11994              
11995             # make ASCII => CxF3 mapping table (from ISO 17972-1, Annex A)
11996 0         0 %keys = ('ORIGINATOR' => "$ns:Creator", 'FILE_DESCRIPTOR' => "$ns:Description", 'CXF3_COMMENT' => "$ns:Comment");
11997              
11998             # for each file header line
11999 0         0 for (@{$self->[3]}) {
  0         0  
12000            
12001             # get keyword, value and source
12002 0         0 ($keyword, $value, $source) = @{$_};
  0         0  
12003            
12004             # strip quotes from value
12005 0         0 $value =~ s/^\"(.*)\"$/$1/;
12006            
12007             # if keyword is 'CREATED'
12008 0 0 0     0 if ($keyword eq 'CREATED') {
    0 0        
12009            
12010             # make Time::Piece object from 'CREATED' value
12011 0         0 $t = _makeTimePiece($value);
12012            
12013             # if source is 'FileInformation' or keyword is in filter array
12014 0         0 } elsif ((defined($source) && $source eq 'FileInformation') || grep {$_ eq $keyword} @filter) {
12015            
12016             # if keyword in mapping table
12017 0 0       0 if (exists($keys{$keyword})) {
12018            
12019             # if XPath exists in FileInformation element
12020 0 0       0 if (($node) = $xpc->findnodes($keys{$keyword}, $info)) {
12021            
12022             # if text content exists
12023 0 0 0     0 if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) {
12024            
12025             # update text content
12026 0         0 $child->setData($value);
12027            
12028             }
12029            
12030             }
12031            
12032             # must be a 'Tag' element
12033             } else {
12034            
12035             # if XPath exists in FileInformation element
12036 0 0       0 if (($node) = $xpc->findnodes("$ns:Tag[\@Name='$keyword']", $info)) {
12037            
12038             # update the Value attribute
12039 0         0 $node->setAttribute('Value', $value);
12040            
12041             } else {
12042            
12043             # add new Tag element
12044 0         0 $node = $info->appendChild(XML::LibXML::Element->new('Tag'));
12045 0         0 $node->setAttribute('Name', $keyword);
12046 0         0 $node->setAttribute('Value', $value);
12047 0         0 $node->setNamespace($nsURI, $ns);
12048            
12049             }
12050            
12051             }
12052            
12053             }
12054            
12055             }
12056              
12057             # make ISO 8601 datetime string from Time::Piece object
12058 0         0 $datetime = sprintf("%s%+03d:00", $t->datetime, $t->tzoffset->hours);
12059              
12060             # get the 'CreationDate' node
12061 0         0 ($node) = $xpc->findnodes("$ns:CreationDate", $info);
12062              
12063             # if text content exists
12064 0 0 0     0 if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) {
12065            
12066             # update text content
12067 0         0 $child->setData($datetime);
12068            
12069             }
12070              
12071             # return datetime
12072 0         0 return($datetime);
12073              
12074             }
12075              
12076             # write CxF3 ColorSpecification nodes
12077             # parameters: (object_reference, XPath_object, CxF3_prefix, CxF3_namespace_URI, operations_array)
12078             sub _writeCxF3colorspec {
12079              
12080             # get parameters
12081 0     0   0 my ($self, $xpc, $ns, $nsURI, $ops) = @_;
12082              
12083             # local variables
12084 0         0 my (@illum, @filter, $cscol, $template, $unknown);
12085 0         0 my (%table, %cspec, %hash, $keyword, $value, $source);
12086 0         0 my ($Id, $cs, @nodes, $node, $node2, $child, @wav);
12087 0         0 my ($parser, $frag, $std, $xpath);
12088              
12089             # illumination types
12090 0         0 @illum = qw(M0_Incandescent M1_Daylight M2_UVExcluded M3_Polarized);
12091              
12092             # filter types
12093 0         0 @filter = qw(Filter_None Filter_None Filter_UVExcluded Filter_None);
12094              
12095             # get the 'ColorSpecificationCollection' node
12096 0         0 ($cscol) = $xpc->findnodes("$ns:Resources/$ns:ColorSpecificationCollection");
12097              
12098             # get the 'ColorSpecification' node with Id = 'template'
12099 0         0 ($template) = $xpc->findnodes("$ns:ColorSpecification[\@Id='template']", $cscol);
12100              
12101             # get the 'ColorSpecification' node with Id = 'Unknown'
12102 0         0 ($unknown) = $xpc->findnodes("$ns:ColorSpecification[\@Id='Unknown']", $cscol);
12103              
12104             # make ASCII => CxF3 mapping table (from ISO 17972-1, Annex A)
12105 0         0 %table = (
12106             'MEASUREMENT_GEOMETRY' => "$ns:MeasurementSpec/$ns:GeometryChoice",
12107             'DEVCALSTD' => "$ns:MeasurementSpec/$ns:CalibrationStandard",
12108             'MANUFACTURER' => "$ns:MeasurementSpec/$ns:Device/$ns:Manufacturer",
12109             'MODEL' => "$ns:MeasurementSpec/$ns:Device/$ns:Model",
12110             'SERIAL_NUMBER' => "$ns:MeasurementSpec/$ns:Device/$ns:SerialNumber",
12111             'DEVICE_CLASS' => "$ns:MeasurementSpec/$ns:Device/$ns:DeviceClass",
12112             'FILTER' => "$ns:MeasurementSpec/$ns:Device/$ns:DeviceFilter",
12113             'MEASUREMENT_SOURCE' => "$ns:MeasurementSpec/$ns:Device/$ns:DeviceIllumination",
12114             'POLARIZATION' => "$ns:MeasurementSpec/$ns:Device/$ns:DevicePolarization",
12115             'SAMPLE_BACKING' => "$ns:MeasurementSpec/$ns:Backing",
12116             );
12117              
12118             # for each group
12119 0         0 for my $group (@{$ops}) {
  0         0  
12120            
12121             # for each operation
12122 0         0 for my $t (@{$group}) {
  0         0  
12123            
12124             # if ColorValues (only ColorValues reference a ColorSpecification)
12125 0 0       0 if ($t->[2] =~ m/^$ns:ColorValues\//) {
    0          
12126            
12127             # set Id to saved value, if defined, or add '_spec' to prefix
12128             # the ColorSpecification Id is saved in the Colorimetry array when reading a CxF3 file
12129 0 0       0 $Id = defined($self->[2][5][$t->[4][0]]) ? $self->[2][5][$t->[4][0]] : "$t->[1]\_spec";
12130            
12131             # set attribute hash
12132 0         0 $t->[5]{'ColorSpecification'} = $Id;
12133            
12134             # if 'ColorSpecification' undefined
12135 0 0       0 if (! $cspec{$Id}++) {
12136            
12137             # initialize keyword hash
12138 0         0 %hash = ();
12139            
12140             # add cloned 'ColorSpecification' element to 'ColorSpecificationCollection'
12141 0         0 $cs = $cscol->appendChild($template->cloneNode(1));
12142            
12143             # set the Id
12144 0         0 $cs->setAttribute('Id', $Id);
12145            
12146             # if spectral data
12147             # there are three types of spectral data, reflective, transmissive and emissive
12148             # spectral data has a WavelengthRange node which contains the starting wavelength and increment
12149 0 0       0 if ($t->[2] =~ m/(Reflectance|Transmittance|Emissive)Spectrum$/) {
12150            
12151             # get the 'MeasurementType' node
12152 0         0 ($node) = $xpc->findnodes("$ns:MeasurementSpec/$ns:MeasurementType", $cs);
12153            
12154             # if text content exists
12155 0 0 0     0 if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) {
12156            
12157             # update text content
12158 0         0 $child->setData("Spectrum_$1");
12159            
12160             }
12161            
12162             # for first two data columns
12163 0         0 for ($t->[4][0], $t->[4][1]) {
12164            
12165             # match wavelength in format key
12166 0         0 $self->[1][0][$_] =~ m/(\d{3})$/;
12167            
12168             # push to array
12169 0         0 push(@wav, $1);
12170            
12171             }
12172            
12173             # find the 'WavelengthRange' node
12174 0         0 ($node) = $xpc->findnodes("$ns:MeasurementSpec/$ns:WavelengthRange", $cs);
12175            
12176             # set the 'StartWL' attribute
12177 0         0 $node->setAttribute('StartWL', $wav[0]);
12178            
12179             # set the 'Increment' attribute
12180 0         0 $node->setAttribute('Increment', $wav[1] - $wav[0]);
12181            
12182             # set operation 'StartWL' attribute
12183 0         0 $t->[5]{'StartWL'} = $wav[0];
12184            
12185             } else {
12186            
12187             # find the 'WavelengthRange' node
12188 0         0 ($node) = $xpc->findnodes("$ns:MeasurementSpec/$ns:WavelengthRange", $cs);
12189            
12190             # unbind the node (used only with spectral data)
12191 0         0 $node->unbindNode();
12192            
12193             }
12194            
12195             # for each file header entry
12196 0         0 for (@{$self->[3]}) {
  0         0  
12197            
12198             # get keyword, value and source
12199 0         0 ($keyword, $value, $source) = @{$_};
  0         0  
12200            
12201             # strip quotes from value
12202 0         0 $value =~ s/^\"(.*)\"$/$1/;
12203            
12204             # if source is ColorSpecification Id
12205 0 0 0     0 if (defined($source) && $source eq $Id) {
12206            
12207             # add keyword to hash
12208 0         0 $hash{$keyword}++;
12209            
12210             # if keyword in table
12211 0 0       0 if (exists($table{$keyword})) {
12212            
12213             # if XPath does not exist in ColorSpecification element
12214 0 0       0 if (! (($node) = $xpc->findnodes($table{$keyword}, $cs))) {
12215            
12216             # set node
12217 0         0 $node = $cs;
12218            
12219             # initialize XPath
12220 0         0 $xpath = undef;
12221            
12222             # for each segment
12223 0         0 for (split(/\//, $table{$keyword})) {
12224            
12225             # add segment to XPath
12226 0 0       0 $xpath = defined($xpath) ? "$xpath/$_" : $_;
12227            
12228             # if XPath exists in ColorSpecification element
12229 0 0       0 if (($node2) = $xpc->findnodes($xpath, $cs)) {
12230            
12231             # use existing node
12232 0         0 $node = $node2;
12233            
12234             } else {
12235            
12236             # add element for XPath segment
12237 0         0 $node = $node->appendChild(XML::LibXML::Element->new($_));
12238 0         0 $node->setNamespace($nsURI, $ns);
12239            
12240             }
12241            
12242             }
12243            
12244             }
12245            
12246             # get the first non-blank child node
12247 0         0 ($child) = $node->nonBlankChildNodes();
12248            
12249             # make a parser object
12250 0         0 $parser = XML::LibXML->new();
12251            
12252             # if value is an XML balanced chunk
12253 0 0 0     0 if ($value =~ m/parse_balanced_chunk($value)}) {
  0 0       0  
    0          
12254            
12255             # get all element nodes
12256 0         0 @nodes = $frag->findnodes('//*');
12257            
12258             # replace existing node
12259 0         0 $node->replaceNode($frag);
12260            
12261             # set namespace of each element
12262 0         0 for (@nodes) {$_->setNamespace($nsURI, $ns)};
  0         0  
12263            
12264             # if no child node
12265             } elsif (! defined($child)) {
12266            
12267             # set text content to value
12268 0         0 $node->appendText($value);
12269            
12270             # if child node is text type
12271             } elsif ($child->nodeType == 3) {
12272            
12273             # modify existing text content
12274 0         0 $child->setData($value);
12275            
12276             }
12277            
12278             }
12279            
12280             }
12281            
12282             }
12283            
12284             # match illumination standard in prefix (M0, M1, M2, M3)
12285 0 0       0 $std = ($t->[1] =~ m/^M([0-3])/) ? $1 : 0;
12286            
12287             # if 'FILTER' not a keyword -and- M0 or M2 standard
12288 0 0 0     0 if (! exists($hash{'FILTER'}) && ($std == 0 || $std == 2)) {
      0        
12289            
12290             # find the 'Device' node
12291 0         0 ($node) = $xpc->findnodes("$ns:MeasurementSpec/$ns:Device", $cs);
12292            
12293             # add 'DeviceFilter' node
12294 0         0 $child = $node->appendChild(XML::LibXML::Element->new("$ns:DeviceFilter"));
12295 0         0 $child->setNamespace($nsURI, $ns);
12296            
12297             # set filter type
12298 0         0 $child->appendText($filter[$std]);
12299            
12300             }
12301            
12302             # if 'MEASUREMENT_SOURCE' not a keyword
12303 0 0       0 if (! exists($hash{'MEASUREMENT_SOURCE'})) {
12304            
12305             # find the 'Device' node
12306 0         0 ($node) = $xpc->findnodes("$ns:MeasurementSpec/$ns:Device", $cs);
12307            
12308             # add 'DeviceIllumination' node
12309 0         0 $child = $node->appendChild(XML::LibXML::Element->new("$ns:DeviceIllumination"));
12310 0         0 $child->setNamespace($nsURI, $ns);
12311            
12312             # set illumination type
12313 0         0 $child->appendText($illum[$std]);
12314            
12315             }
12316            
12317             }
12318            
12319             # if 'DeviceColorValues'
12320             } elsif ($t->[2] =~ m/^$ns:DeviceColorValues\//) {
12321            
12322             # set attributes hash
12323 0         0 $t->[5]{'ColorSpecification'} = 'Unknown';
12324            
12325             # increment 'ColorSpecification' hash
12326 0         0 $cspec{'Unknown'}++;
12327            
12328             }
12329            
12330             }
12331            
12332             }
12333              
12334             # unbind 'template' node
12335 0         0 $template->unbindNode();
12336              
12337             # unbind 'Unknown' node, if not referenced
12338 0 0       0 $unknown->unbindNode() if (! $cspec{'Unknown'});
12339              
12340             }
12341              
12342             # write CxF3 CustomResources nodes
12343             # parameters: (object_reference, XPath_object, CxF3_prefix)
12344             sub _writeCxF3customres {
12345              
12346             # get parameters
12347 0     0   0 my ($self, $xpc, $ns) = @_;
12348              
12349             # local variables
12350 0         0 my ($nsURI, $sic, $ms, $xrp, $ca);
12351 0         0 my ($cnode, $snode, $mnode, $pnode);
12352              
12353             # find the CustomResources node
12354 0         0 ($cnode) = $xpc->findnodes("$ns:CustomResources");
12355              
12356             # if object header has 'sic:SpotInkCharacterisation' key
12357 0 0       0 if (defined($sic = $self->[0]{'sic:SpotInkCharacterisation'})) {
12358            
12359             # set namespace URI
12360 0         0 $nsURI = 'http://colorexchangeformat.com/CxF3-SpotInkCharacterisation';
12361            
12362             # add sic:SpotInkCharacterisation node
12363 0         0 $snode = _writeCxF3node($cnode, $xpc, 'sic:SpotInkCharacterisation', undef, $sic, $nsURI, 'sic');
12364            
12365             # if object header has 'sic:MeasurementSet' key
12366 0 0       0 if (defined($ms = $self->[0]{'sic:MeasurementSet'})) {
12367            
12368             # for each measurement set
12369 0         0 for my $key (keys(%{$ms})) {
  0         0  
12370            
12371             # add sic:MeasurementSet node
12372 0         0 $mnode = _writeCxF3node($snode, $xpc, 'sic:MeasurementSet', undef, {'Background' => $key});
12373            
12374             # for each measurement
12375 0         0 for my $m (@{$ms->{$key}}) {
  0         0  
12376            
12377             # add sic:Measurement node
12378 0         0 _writeCxF3node($mnode, $xpc, 'sic:Measurement', undef, $m);
12379            
12380             }
12381            
12382             }
12383            
12384             }
12385            
12386             }
12387              
12388             # if object header has 'xrp:Prism' key
12389 0 0       0 if (defined($xrp = $self->[0]{'xrp:Prism'})) {
12390            
12391             # set namespace URI
12392 0         0 $nsURI = 'http://www.xrite.com/products/prism';
12393            
12394             # add xrp:Prism node
12395 0         0 $pnode = _writeCxF3node($cnode, $xpc, 'xrp:Prism', undef, $xrp, $nsURI, 'xrp');
12396            
12397             # if object header has 'xrp:CustomAttributes' key
12398 0 0       0 if (defined($ca = $self->[0]{'xrp:CustomAttributes'})) {
12399            
12400             # add xrp:CustomAttributes node
12401 0         0 _writeCxF3node($pnode, $xpc, 'xrp:CustomAttributes', undef, $ca);
12402            
12403             }
12404            
12405             }
12406            
12407             }
12408              
12409             # write CxF3 node
12410             # adds nodes to complete the XPath, as necessary
12411             # text and attributes, if defined, are added to the last node
12412             # the namespace of the added nodes may be specified, if different from the base node
12413             # parameters: (base_node, XPath_object, XPath, text, attribute_hash, [namespace_URI, namespace_prefix])
12414             # returns: (last_node)
12415             sub _writeCxF3node {
12416              
12417             # get parameters
12418 0     0   0 my ($base, $xpc, $xpath, $text, $attr, $nsURI, $ns) = @_;
12419              
12420             # local variables
12421 0         0 my (@seg, $node);
12422              
12423             # if the namespace URI and prefix are supplied
12424 0 0 0     0 if (defined($nsURI) && defined($ns)) {
12425            
12426             # register supplied namespace
12427 0         0 $xpc->registerNs($ns, $nsURI);
12428            
12429             } else {
12430            
12431             # use the base namespace prefix and URI
12432 0         0 $ns = $base->prefix();
12433 0         0 $nsURI = $base->namespaceURI();
12434            
12435             }
12436              
12437             # split XPath into segments
12438 0         0 @seg = split('/', $xpath);
12439              
12440             # for each XPath segment
12441 0         0 for my $i (0 .. $#seg) {
12442            
12443             # get next node in Xpath
12444 0         0 ($node) = $xpc->findnodes($seg[$i], $base);
12445            
12446             # if node not found or last segment
12447 0 0 0     0 if (! $node || $i == $#seg) {
12448            
12449             # add node
12450 0         0 $node = $base->appendChild(XML::LibXML::Element->new($seg[$i]));
12451            
12452             # set node namespace
12453 0         0 $node->setNamespace($nsURI, $ns);
12454            
12455             }
12456            
12457             # update base node
12458 0         0 $base = $node;
12459            
12460             }
12461              
12462             # add text, if defined
12463 0 0       0 $node->appendText($text) if (defined($text));
12464              
12465             # if attributes defined
12466 0 0       0 if (defined($attr)) {
12467            
12468             # for each attribute key
12469 0         0 for (sort(keys(%{$attr}))) {
  0         0  
12470            
12471             # add attribute
12472 0         0 $node->setAttribute($_, $attr->{$_});
12473            
12474             }
12475            
12476             }
12477              
12478             # return added node
12479 0         0 return($node);
12480              
12481             }
12482              
12483             # validate CxF3 document
12484             # prints warning and error info
12485             # parameters: (document_reference)
12486             sub _validateCxF3 {
12487              
12488             # get document reference
12489 0     0   0 my $doc = shift();
12490              
12491             # load CxF3 schema
12492 0         0 state $xmlschema = XML::LibXML::Schema->new('location' => ICC::Shared::getICCPath('Templates/CxF3_Core.xsd'));
12493              
12494             # validate the document
12495 0 0       0 if (! defined(eval {$xmlschema->validate($doc)})) {
  0         0  
12496            
12497             # print warning on failure
12498 0         0 print "warning: invalid CxF3 document\n$@\n";
12499            
12500             }
12501            
12502             }
12503              
12504             # make patch set
12505             # supported hash keys: 'colorspace', 'template', 'sort', 'limit'
12506             # parameters: (object_reference, hash)
12507             # returns: (result)
12508             sub _makePatchSet {
12509              
12510             # get parameters
12511 0     0   0 my ($self, $hash) = @_;
12512              
12513             # local variables
12514 0         0 my ($cs, $template, $sort, $tac, $n, $data, $eps);
12515 0         0 my (@fields, $loop, $limit, @inc, $init, $s, $code);
12516              
12517             # get the colorspace parameter
12518 0 0       0 (defined($cs = $hash->{'colorspace'})) || return('colorspace parameter missing');
12519              
12520             # get the template parameter
12521 0 0       0 (defined($template = $hash->{'template'})) || return('template parameter missing');
12522              
12523             # get the sort parameter (optional)
12524 0         0 $sort = $hash->{'sort'};
12525              
12526             # get the ink limit parameter (optional)
12527 0         0 $tac = $hash->{'limit'};
12528              
12529             # get number of elements in first group
12530 0         0 $n = @{$template->[0]};
  0         0  
12531              
12532             # for each group
12533 0         0 for my $i (0 .. $#{$template}) {
  0         0  
12534            
12535             # verify number of elements
12536 0 0       0 ($n == @{$template->[$i]}) || return("wrong number of elements in template group $i");
  0         0  
12537            
12538             # verify number of array references
12539 0 0       0 ($n == grep {ref() eq 'ARRAY'} @{$template->[$i]}) || return("non-array element(s) in template group $i");
  0         0  
  0         0  
12540            
12541             # for each element
12542 0         0 for my $j (0 .. $#{$template->[$i]}) {
  0         0  
12543            
12544             # verify element contains only numeric scalars
12545 0 0 0     0 (@{$template->[$i][$j]} > 0 && @{$template->[$i][$j]} == grep {! ref() && Scalar::Util::looks_like_number($_)} @{$template->[$i][$j]}) || return("non-numeric element in template group $i, $j");
  0 0       0  
  0         0  
  0         0  
  0         0  
12546            
12547             }
12548            
12549             }
12550              
12551             # if RGB colorspace
12552 0 0       0 if ($cs eq 'RGB') {
    0          
    0          
    0          
12553            
12554             # verify number of channels
12555 0 0       0 ($n == 3) || return('wrong number of template elements for RGB colorspace');
12556            
12557             # set fields
12558 0         0 @fields = qw(RGB_R RGB_G RGB_B);
12559            
12560             # if CMYK colorspace
12561             } elsif ($cs eq 'CMYK') {
12562            
12563             # verify number of channels
12564 0 0       0 ($n == 4) || return('wrong number of template elements for CMYK colorspace');
12565            
12566             # set fields
12567 0         0 @fields = qw(CMYK_C CMYK_M CMYK_Y CMYK_K);
12568            
12569             # if nCLR colorspace
12570             } elsif ($cs eq 'nCLR') {
12571            
12572             # verify number of channels
12573 0 0 0     0 ($n > 0 && $n < 16) || return('wrong number of template elements for nCLR colorspace');
12574            
12575             # set fields
12576 0         0 @fields = map {$n . "CLR_$_"} (1 .. $n);
  0         0  
12577            
12578             # if L*a*b* colorspace
12579             } elsif ($cs eq 'Lab') {
12580            
12581             # verify number of channels
12582 0 0       0 ($n == 3) || return('wrong number of template elements for L*a*b* colorspace');
12583            
12584             # set fields
12585 0         0 @fields = qw(LAB_L LAB_A LAB_B);
12586            
12587             } else {
12588            
12589             # error
12590 0         0 return('invalid colorspace parameter');
12591            
12592             }
12593              
12594             # make loop variable list
12595 0         0 $loop = join(', ', map {"\$i$_"} (0 .. $n - 1));
  0         0  
12596              
12597             # make initial code fragment
12598 0         0 $init = "\$data->[\$s++] = [$loop]";
12599              
12600             # initialize index
12601 0         0 $s = 0;
12602              
12603             # for each group
12604 0         0 for my $i (0 .. $#{$template}) {
  0         0  
12605            
12606             # copy initial code fragment
12607 0         0 $code = $init;
12608            
12609             # for each device channel (in reverse order)
12610 0         0 for my $j (reverse(0 .. $#{$template->[$i]})) {
  0         0  
12611            
12612             # add loop code to fragment
12613 0         0 $code = "for my \$i$j (" . join(', ', @{$template->[$i][$j]}) . ") {$code}";
  0         0  
12614            
12615             }
12616            
12617             # evaluate code fragment
12618 0         0 eval($code);
12619            
12620             }
12621              
12622             # if ink limit defined and color space is CMYK or nCLR
12623 0 0 0     0 if (defined($tac) && ($cs eq 'CMYK' || $cs eq 'nCLR')) {
      0        
12624            
12625             # compute max comparison error
12626 0         0 $eps = 1E-12;
12627            
12628             # verify ink limit is a number
12629 0 0 0     0 if (! ref($tac) && Scalar::Util::looks_like_number($tac)) {
12630            
12631             # for each patch
12632 0         0 for (@{$data}) {
  0         0  
12633            
12634             # add the total ink value
12635 0         0 push(@{$_}, List::Util::sum(@{$_}));
  0         0  
  0         0  
12636            
12637             }
12638            
12639             # make sort code fragment (sorts in ascending order by columns K ... total_ink_value)
12640 0         0 $code = '@{$data} = sort {' . join(' || ', map {"\$a->[$_] <=> \$b->[$_]"} (3 .. $n)) . '} @{$data}';
  0         0  
12641            
12642             # sort data
12643 0         0 eval($code);
12644            
12645             # for each patch
12646 0         0 for my $i (0 .. $#{$data}) {
  0         0  
12647            
12648             # undefine limit if new group (different black or spot values)
12649 0 0       0 undef($limit) if (grep {$data->[$i][$_] != $data->[$i ? $i - 1 : 0][$_]} (3 .. $n - 1));
  0 0       0  
12650            
12651             # select patch if limit undefined or total ink <= limit or a CMY corner point
12652 0 0 0     0 push(@inc, [@{$data->[$i]}[0 .. $n - 1]]) if (! defined($limit) || ($data->[$i][-1] - $limit <= $eps) || ((grep {$data->[$i][$_] == 0} (0 .. 2)) && (grep {$data->[$i][$_] == 100} (0 .. 2))));
  0   0     0  
  0   0     0  
  0         0  
12653            
12654             # set limit if undefined and total ink > TAC
12655 0 0 0     0 $limit = $data->[$i][-1] if (! defined($limit) && $data->[$i][-1] - $tac > $eps);
12656            
12657             }
12658            
12659             # set data to selected patches
12660 0         0 $data = \@inc;
12661            
12662             } else {
12663            
12664             # display warning
12665 0         0 carp("invalid ink limit parameter, ink limiting failed\n");
12666            
12667             }
12668            
12669             }
12670              
12671             # if sort parameter defined
12672 0 0       0 if (defined($sort)) {
12673            
12674             # verify sort parameter
12675 0 0 0     0 if (ICC::Shared::is_num_vector($sort) && @{$sort} == grep {$_ && $_ == int($_) && abs($_) <= @{$data->[0]}} @{$sort}) {
  0 0 0     0  
  0         0  
  0         0  
  0         0  
12676            
12677             # make sort code fragment
12678 0 0       0 $code = '@{$data} = sort {' . join(' || ', map {my $dir = m/-/; my $col = abs($_) - 1; $dir ? "\$b->[$col] <=> \$a->[$col]" : "\$a->[$col] <=> \$b->[$col]"} @{$sort}) . '} @{$data}';
  0         0  
  0         0  
  0         0  
  0         0  
12679            
12680             # evaluate code fragment
12681 0         0 eval($code);
12682            
12683             } else {
12684            
12685             # display warning
12686 0         0 carp("invalid sort parameter, sorting failed\n");
12687            
12688             }
12689            
12690             }
12691              
12692             # add format fields
12693 0         0 unshift(@{$data}, [@fields]);
  0         0  
12694              
12695             # set object reference
12696 0         0 $self->[1] = $data;
12697              
12698             # return
12699 0         0 return();
12700              
12701             }
12702              
12703             # make Time::Piece object from text string
12704             # parses most common date/time notations
12705             # no object returned if parsing fails
12706             # parameter: (string -or- value)
12707             # returns: (object)
12708             sub _makeTimePiece {
12709              
12710             # get parameter
12711 0     0   0 my $str = shift();
12712              
12713             # local variables
12714 0         0 my ($parse, $fmt, $hr, $sec, $month);
12715              
12716             # if a numeric value (Unix time)
12717 0 0       0 if (Scalar::Util::looks_like_number($str)) {
12718            
12719             # return Time::Piece object from Unix time
12720 0         0 return(scalar(localtime($str)));
12721            
12722             } else {
12723            
12724             # if UTC offset matched (time string ends in '+/-hh:mm', '+/-hhmm', or '+/-hh')
12725 0 0       0 if ($str =~ s/(T[\d:]+)([+-]\d{2}):?(\d{2})?/$1/) {
    0          
12726            
12727             # set UTC offset to matched value
12728 0 0       0 $parse = $2 . (defined($3) ? $3 : '00');
12729            
12730             # set UTC format
12731 0         0 $fmt = '%z';
12732            
12733             # if Zulu time (time string ends in 'Z')
12734             } elsif ($str =~ s/(T[\d:]+)Z/$1/) {
12735            
12736             # set UTC offset to 0
12737 0         0 $parse = '+0000';
12738            
12739             # set UTC format
12740 0         0 $fmt = '%z';
12741            
12742             } else {
12743            
12744             # initialize strings
12745 0         0 $parse = $fmt = '';
12746            
12747             }
12748            
12749             # if time matched (time string 'hh:mm' or 'hh:mm:ss', 'AM' or 'PM' optional)
12750 0 0       0 if ($str =~ s/(\d{1,2})(:\d{1,2})(:\d{1,2})?\s*(AM|PM)?//) {
12751            
12752             # if 12 AM
12753 0 0 0     0 if (defined($4) && $4 eq 'AM' && $1 == 12) {
    0 0        
      0        
      0        
      0        
12754            
12755             # set hour
12756 0         0 $hr = 0;
12757            
12758             # if 1 PM - 11 PM
12759             } elsif (defined($4) && $4 eq 'PM' && $1 > 0 && $1 < 12) {
12760            
12761             # set hour
12762 0         0 $hr = $1 + 12;
12763            
12764             } else {
12765            
12766             # set hour
12767 0         0 $hr = $1;
12768            
12769             }
12770            
12771             # set seconds
12772 0 0       0 $sec = defined($3) ? $3 : ':00';
12773            
12774             # add time string
12775 0         0 $parse = "T$hr$2$sec$parse";
12776            
12777             # add time format
12778 0         0 $fmt = "T%T$fmt";
12779            
12780             }
12781            
12782             # if three number date matched
12783 0 0       0 if ($str =~ m/(\d{1,4})[\/-](\d{1,2})[\/-](\d{1,4})/) {
    0          
12784            
12785             # if first value > 99
12786 0 0       0 if ($1 > 99) {
    0          
12787            
12788             # add date string
12789 0         0 $parse = "$1-$2-$3$parse";
12790            
12791             # if last value > 99
12792             } elsif ($3 > 99) {
12793            
12794             # add date string
12795 0         0 $parse = "$3-$1-$2$parse";
12796            
12797             # last value is two digit year
12798             } else {
12799            
12800             # add date string
12801 0 0       0 $parse = ($3 > 68 ? 1900 + $3 : 2000 + $3) . "-$1-$2$parse";
12802            
12803             }
12804            
12805             # add date format
12806 0         0 $fmt = "%Y-%m-%d$fmt";
12807            
12808             # if text month matched
12809             } elsif (uc($str) =~ m/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/) {
12810            
12811             # save month
12812 0         0 $month = $1;
12813            
12814             # if two numbers matched
12815 0 0       0 if ($str =~ m/(\d{1,4})[^\d]+(\d{1,4})/) {
    0          
12816            
12817             # if first value > 99
12818 0 0       0 if ($1 > 99) {
    0          
12819            
12820             # add date string
12821 0         0 $parse = "$1-$month-$2$parse";
12822            
12823             # if last value > 99
12824             } elsif ($2 > 99) {
12825            
12826             # add date string
12827 0         0 $parse = "$2-$month-$1$parse";
12828            
12829             # last value is two digit year
12830             } else {
12831            
12832             # add date string
12833 0 0       0 $parse = ($2 > 68 ? 1900 + $2 : 2000 + $2) . "/$month/$1$parse";
12834            
12835             }
12836            
12837             # if one number matched
12838             } elsif ($str =~ m/(\d{1,4})/) {
12839            
12840             # if value > 99
12841 0 0       0 if ($1 > 99) {
12842            
12843             # add date string
12844 0         0 $parse = "$1-$month-1$parse";
12845            
12846             } else {
12847            
12848             # add date string
12849 0 0       0 $parse = ($1 > 68 ? 1900 + $1 : 2000 + $1) . "/$month/1$parse";
12850            
12851             }
12852            
12853             }
12854            
12855             # add date format
12856 0         0 $fmt = "%Y-%b-%d$fmt";
12857            
12858             }
12859            
12860             # return Time::Piece object, if string parsed successfully
12861 0 0       0 return(Time::Piece->strptime($parse, $fmt)) if (length($parse));
12862            
12863             }
12864            
12865             }
12866              
12867             # get file list
12868             # uses Perl 'bsd_glob' function
12869             # parameter: (path)
12870             # returns: (ref_to_file_list)
12871             sub _files {
12872              
12873             # get path
12874 21     21   34 my $path = shift();
12875              
12876             # replace '~~' with 'ICC' directory path
12877 21         38 $path =~ s/^~~/ICC::Shared::getICCPath()/e;
  0         0  
12878              
12879             # get list of files and/or directories
12880 21         485 my @files = File::Glob::bsd_glob($path);
12881              
12882             # if list is just one directory
12883 21 100 66     256 if (@files == 1 && -d $files[0]) {
12884            
12885             # get files in that directory
12886 2         192 @files = grep {-f} File::Glob::bsd_glob("$path/*");
  8         81  
12887            
12888             } else {
12889            
12890             # filter the files
12891 19         44 @files = grep {-f} @files;
  19         193  
12892            
12893             }
12894              
12895             # return file list
12896 21         68 return(\@files);
12897              
12898             }
12899              
12900             # compute Mahalanobis distance
12901             # assumes parameters are valid
12902             # parameters: (vector1, vector2, inverse_covariance_matrix)
12903             # returns: (distance)
12904             sub _mahal {
12905              
12906             # get parameters
12907 0     0   0 my ($x, $y, $sinv) = @_;
12908              
12909             # local variables
12910 0         0 my ($d, $dT);
12911              
12912             # for each dimension
12913 0         0 for my $i (0 .. $#{$x}) {
  0         0  
12914            
12915             # save difference
12916 0         0 $d->[0][$i] = $dT->[$i][0] = $x->[$i] - $y->[$i];
12917            
12918             }
12919              
12920             # bless matrices
12921 0         0 bless($d, 'Math::Matrix');
12922 0         0 bless($dT, 'Math::Matrix');
12923              
12924             # return Mahalanobis distance
12925 0         0 return(sqrt(($d * $sinv * $dT)->[0][0]));
12926              
12927             }
12928              
12929             # get L*a*b* encoding code refs
12930             # parameter: (object_reference, hash)
12931             # returns: (get_code_ref, set_code_ref)
12932             sub _lab_encoding {
12933              
12934             # get object reference
12935 29     29   47 my ($self, $hash) = @_;
12936              
12937             # local variable
12938 29         38 my ($encode);
12939              
12940             # get encoding parameter from hash
12941 29         40 $encode = $hash->{'encoding'};
12942              
12943             # if encoding parameter undefined
12944 29 50       44 if (! defined($encode)) {
    0          
    0          
    0          
    0          
    0          
    0          
12945            
12946             # return code refs (identity)
12947 29     41   139 return(sub {@_}, sub {@_});
  41         76  
  6         10  
12948            
12949             # if encoding is 8/16-bit ICC CIELAB
12950             } elsif ($encode == 0) {
12951            
12952             # return code refs
12953 0 0   0   0 return(sub {defined($_[0]) ? $_[0] / 100 : $_[0], defined($_[1]) ? ($_[1] + 128)/255 : $_[1], defined($_[2]) ? ($_[2] + 128)/255 : $_[2]},
    0          
    0          
12954 0 0   0   0 sub {defined($_[0]) ? $_[0] * 100 : $_[0], defined($_[1]) ? $_[1] * 255 - 128 : $_[1], defined($_[2]) ? $_[2] * 255 - 128 : $_[2]});
  0 0       0  
    0          
12955            
12956             # if encoding is 16-bit ICC legacy L*a*b*
12957             } elsif ($encode == 1) {
12958            
12959             # return code refs
12960 0 0   0   0 return(sub {defined($_[0]) ? $_[0] * 256/25700 : $_[0], defined($_[1]) ? ($_[1] + 128) * 256/65535 : $_[1], defined($_[2]) ? ($_[2] + 128) * 256/65535 : $_[2]},
    0          
    0          
12961 0 0   0   0 sub {defined($_[0]) ? $_[0] * 25700/256 : $_[0], defined($_[1]) ? $_[1] * 65535/256 - 128 : $_[1], defined($_[2]) ? $_[2] * 65535/256 - 128 : $_[2]});
  0 0       0  
    0          
12962            
12963             # if encoding is 16-bit EFI/Monaco L*a*b*
12964             } elsif ($encode == 2) {
12965            
12966             # return code refs
12967 0 0   0   0 return(sub {defined($_[0]) ? $_[0]/100 : $_[0], defined($_[1]) ? ($_[1] + 128) * 256/65535 : $_[1], defined($_[2]) ? ($_[2] + 128) * 256/65535 : $_[2]},
    0          
    0          
12968 0 0   0   0 sub {defined($_[0]) ? $_[0] * 100 : $_[0], defined($_[1]) ? $_[1] * 65535/256 - 128 : $_[1], defined($_[2]) ? $_[2] * 65535/256 - 128 : $_[2]});
  0 0       0  
    0          
12969            
12970             # if encoding is L*a*b*
12971             } elsif ($encode == 3) {
12972            
12973             # return code refs (identity)
12974 0     0   0 return(sub {@_}, sub {@_});
  0         0  
  0         0  
12975            
12976             # if encoding is LxLyLz
12977             } elsif ($encode == 4) {
12978            
12979             # return code refs
12980 0 0 0 0   0 return(sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {$_[0] + 116 * $_[1]/500, $_[0], $_[0] - 116 * $_[2]/200} else {@_}},
  0   0     0  
  0         0  
12981 0 0 0 0   0 sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {$_[1], 500 * ($_[0] - $_[1])/116, 200 * ($_[1] - $_[2])/116} else {@_}});
  0   0     0  
  0         0  
  0         0  
12982            
12983             # if encoding is unit LxLyLz
12984             } elsif ($encode == 5) {
12985            
12986             # return code refs
12987 0 0 0 0   0 return(sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {map {$_/100} ($_[0] + 116 * $_[1]/500, $_[0], $_[0] - 116 * $_[2]/200)} else {@_}},
  0   0     0  
  0         0  
  0         0  
12988 0 0 0 0   0 sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {map {$_ * 100} ($_[1], 500 * ($_[0] - $_[1])/116, 200 * ($_[1] - $_[2])/116)} else {@_}});
  0   0     0  
  0         0  
  0         0  
  0         0  
12989            
12990             } else {
12991            
12992             # error
12993 0         0 croak('invalid L*a*b* encoding');
12994            
12995             }
12996              
12997             }
12998              
12999             # get XYZ encoding code refs
13000             # assumes there are XYZ columns
13001             # parameter: (object_reference, column_slice, [hash])
13002             # returns: (get_code_ref, set_code_ref)
13003             sub _xyz_encoding {
13004              
13005             # get object reference
13006 23     23   37 my ($self, $cols, $hash) = @_;
13007              
13008             # local variable
13009 23         29 my ($encode, $wtpt);
13010              
13011             # get encoding parameter from hash
13012 23         41 $encode = $hash->{'encoding'};
13013              
13014             # if encoding parameter undefined
13015 23 50 0     30 if (! defined($encode)) {
    0          
    0          
    0          
    0          
    0          
    0          
13016            
13017             # return code refs (identity)
13018 23     3   108 return(sub {@_}, sub {@_});
  18         27  
  3         6  
13019            
13020             # if encoding is L*
13021             } elsif ($encode eq 'L*' || $encode == 4) {
13022            
13023             # get illuminant white point
13024 0 0       0 ($wtpt = _illumWP($self, $cols, $hash)) or croak('illuminant white point required for LxLyLz encoding');
13025            
13026             # return code refs
13027 0 0   0   0 return(sub {defined($_[0]) ? ICC::Shared::x2L($_[0] / $wtpt->[0]) : $_[0], defined($_[1]) ? ICC::Shared::x2L($_[1] / $wtpt->[1]) : $_[1], defined($_[2]) ? ICC::Shared::x2L($_[2] / $wtpt->[2]) : $_[2]},
    0          
    0          
13028 0 0   0   0 sub {defined($_[0]) ? ICC::Shared::L2x($_[0]) * $wtpt->[0] : $_[0], defined($_[1]) ? ICC::Shared::L2x($_[1]) * $wtpt->[1] : $_[1], defined($_[2]) ? ICC::Shared::L2x($_[2]) * $wtpt->[2] : $_[2]});
  0 0       0  
    0          
13029            
13030             # if encoding is 16-bit ICC XYZ
13031             } elsif ($encode == 7) {
13032            
13033             # return code refs
13034 0 0   0   0 return(sub {map {defined() ? $_ / 199.9969482421875 : $_} @_}, sub {map {defined() ? $_ * 199.9969482421875 : $_} @_});
  0 0       0  
  0         0  
  0         0  
  0         0  
13035            
13036             # if encoding is 32-bit ICC XYZNumber
13037             } elsif ($encode == 8) {
13038            
13039             # return code refs
13040 0 0   0   0 return(sub {map {defined() ? $_ / 100 : $_} @_}, sub {map {defined() ? $_ * 100 : $_} @_});
  0 0       0  
  0         0  
  0         0  
  0         0  
13041            
13042             # if encoding is xyz
13043             } elsif ($encode == 9) {
13044            
13045             # get illuminant white point
13046 0 0       0 ($wtpt = _illumWP($self, $cols, $hash)) or croak('illuminant white point required for xyz encoding');
13047            
13048             # return code refs
13049 0 0   0   0 return(sub {defined($_[0]) ? $_[0] / $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] / $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] / $wtpt->[2] : $_[2]},
    0          
    0          
13050 0 0   0   0 sub {defined($_[0]) ? $_[0] * $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] * $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] * $wtpt->[2] : $_[2]});
  0 0       0  
    0          
13051            
13052             # if encoding is XYZ
13053             } elsif ($encode == 10) {
13054            
13055             # return code refs (identity)
13056 0     0   0 return(sub {@_}, sub {@_});
  0         0  
  0         0  
13057            
13058             # if encoding is media relative xyz
13059             } elsif ($encode == 11) {
13060            
13061             # get media white point
13062 0 0       0 ($wtpt = _mediaWP($self, $cols, $hash)) or croak('media white point required for media relative xyz encoding');
13063            
13064             # return code refs
13065 0 0   0   0 return(sub {defined($_[0]) ? $_[0] / $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] / $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] / $wtpt->[2] : $_[2]},
    0          
    0          
13066 0 0   0   0 sub {defined($_[0]) ? $_[0] * $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] * $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] * $wtpt->[2] : $_[2]});
  0 0       0  
    0          
13067            
13068             } else {
13069            
13070             # error
13071 0         0 croak('invalid XYZ encoding');
13072            
13073             }
13074              
13075             }
13076              
13077             # get density encoding code refs
13078             # parameter: (object_reference, hash)
13079             # returns: (get_code_ref, set_code_ref)
13080             sub _density_encoding {
13081              
13082             # get object reference
13083 17     17   27 my ($self, $hash) = @_;
13084              
13085             # get encoding parameter from hash
13086 17         28 my $encode = $hash->{'encoding'};
13087              
13088             # if encoding parameter undefined or density
13089 17 50 33     43 if (! defined($encode) || $encode eq 'density') {
    0          
    0          
    0          
13090            
13091             # return code refs (identity)
13092 17     0   75 return(sub {@_}, sub {@_});
  0            
  0            
13093            
13094             # if encoding is linear (RGBV)
13095             } elsif ($encode eq 'linear') {
13096            
13097             # return code refs
13098 0 0   0     return(sub {map {defined() ? 100 * POSIX::pow(10, -$_) : $_} @_}, sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10($_/100)} else {warn("log of $_"); 99}} else {$_}} @_});
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
  0            
13099            
13100             # if encoding is unit
13101             } elsif ($encode eq 'unit') {
13102            
13103             # return code refs
13104 0 0   0     return(sub {map {defined() ? POSIX::pow(10, -$_) : $_} @_}, sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10($_)} else {warn("log of $_"); 99}} else {$_}} @_});
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
  0            
13105            
13106             # if encoding is L*
13107             } elsif ($encode eq 'L*') {
13108            
13109             # return code refs
13110 0 0   0     return(sub {map {defined() ? ICC::Shared::x2L(POSIX::pow(10, -$_)) : $_} @_}, sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10(ICC::Shared::L2x($_))} else {warn("log of $_"); 99}} else {$_}} @_});
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
  0            
13111            
13112             } else {
13113            
13114             # error
13115 0           croak('invalid density encoding');
13116            
13117             }
13118              
13119             }
13120              
13121             # get rgbv encoding code refs
13122             # parameter: (object_reference, hash)
13123             # returns: (get_code_ref, set_code_ref)
13124             sub _rgbv_encoding {
13125              
13126             # get object reference
13127 0     0     my ($self, $hash) = @_;
13128              
13129             # get encoding parameter from hash
13130 0           my $encode = $hash->{'encoding'};
13131              
13132             # if encoding parameter undefined or linear
13133 0 0 0       if (! defined($encode) || $encode eq 'linear'|| $encode eq 'RGBV') {
    0 0        
    0          
    0          
13134            
13135             # return code refs (identity)
13136 0     0     return(sub {@_}, sub {@_});
  0            
  0            
13137            
13138             # if encoding is unit
13139             } elsif ($encode eq 'unit') {
13140            
13141             # return code refs
13142 0     0     return(sub {map {$_/100} @_}, sub {map {$_ * 100} @_});
  0            
  0            
  0            
  0            
13143            
13144             # if encoding is density
13145             } elsif ($encode eq 'density') {
13146            
13147             # return code refs
13148 0 0   0     return(sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10($_/100)} else {warn("log of $_"); 99}} else {$_}} @_}, sub {map {defined() ? 100 * POSIX::pow(10, -$_) : $_} @_});
  0 0          
  0 0          
  0            
  0            
  0            
  0            
  0            
  0            
  0            
13149            
13150             # if encoding is L*
13151             } elsif ($encode eq 'L*') {
13152            
13153             # return code refs
13154 0     0     return(sub {map {ICC::Shared::x2L($_/100)} @_}, sub {map {ICC::Shared::L2x($_) * 100} @_});
  0            
  0            
  0            
  0            
13155            
13156             } else {
13157            
13158             # error
13159 0           croak('invalid rgbv encoding');
13160            
13161             }
13162              
13163             }
13164              
13165             #--------- additional Math::Matrix methods ---------
13166              
13167             package Math::Matrix;
13168              
13169             # rotate matrix
13170             # rotation: 0 = None, 1 = 90° CW, 2 = 180°, 3 = 90° CCW
13171             # note: rotation describes appearance in MeasureTool
13172             # parameter: (rotation)
13173             # returns: (rotated_matrix)
13174             sub rotate {
13175              
13176             # get parameters
13177 0     0 0   my ($self, $rot) = @_;
13178              
13179             # local variables
13180 0           my ($rows, $cols, $replace);
13181              
13182             # return if rotation undefined
13183 0 0         return($self) if (! defined($rot));
13184              
13185             # resolve rotation parameter
13186 0           $rot = int($rot) % 4;
13187              
13188             # get upper row index
13189 0           $rows = $#{$self};
  0            
13190              
13191             # get upper column index
13192 0           $cols = $#{$self->[0]};
  0            
13193              
13194             # if rotation = 0 (none)
13195 0 0         if ($rot == 0) {
    0          
    0          
    0          
13196            
13197             # for each row
13198 0           for my $i (0 .. $rows) {
13199            
13200             # for each column
13201 0           for my $j (0 .. $cols) {
13202            
13203             # copy matrix element
13204 0           $replace->[$i][$j] = $self->[$i][$j];
13205            
13206             }
13207            
13208             }
13209            
13210             # if rotation = 1 (90° CW)
13211             } elsif ($rot == 1) {
13212            
13213             # for each row
13214 0           for my $i (0 .. $rows) {
13215            
13216             # for each column
13217 0           for my $j (0 .. $cols) {
13218            
13219             # copy matrix element
13220 0           $replace->[$j][$i] = $self->[$i][$cols - $j];
13221            
13222             }
13223            
13224             }
13225            
13226             # if rotation = 2 (180°)
13227             } elsif ($rot == 2) {
13228            
13229             # for each row
13230 0           for my $i (0 .. $rows) {
13231            
13232             # for each column
13233 0           for my $j (0 .. $cols) {
13234            
13235             # copy matrix element
13236 0           $replace->[$i][$j] = $self->[$rows - $i][$cols - $j];
13237            
13238             }
13239            
13240             }
13241            
13242             # if rotation = 3 (90° CCW)
13243             } elsif ($rot == 3) {
13244            
13245             # for each row
13246 0           for my $i (0 .. $rows) {
13247            
13248             # for each column
13249 0           for my $j (0 .. $cols) {
13250            
13251             # copy matrix element
13252 0           $replace->[$j][$i] = $self->[$rows - $i][$j];
13253            
13254             }
13255            
13256             }
13257            
13258             }
13259              
13260             # return new object
13261 0           return(bless($replace, 'Math::Matrix'));
13262              
13263             }
13264              
13265             # flip matrix
13266             # flip: 0 = transpose, 1 = horizontal, 2 = cross transpose, 3 = vertical
13267             # note: flip describes appearance in MeasureTool
13268             # parameter: (flip)
13269             # returns: (flipped_matrix)
13270             sub flip {
13271              
13272             # get parameters
13273 0     0 0   my ($self, $flip) = @_;
13274              
13275             # local variables
13276 0           my ($rows, $cols, $replace);
13277              
13278             # return if flip undefined
13279 0 0         return($self) if (! defined($flip));
13280              
13281             # resolve flip parameter
13282 0           $flip = int($flip) % 4;
13283              
13284             # get upper row index
13285 0           $rows = $#{$self};
  0            
13286              
13287             # get upper column index
13288 0           $cols = $#{$self->[0]};
  0            
13289              
13290             # if flip = 0 (transpose)
13291 0 0         if ($flip == 0) {
    0          
    0          
    0          
13292            
13293             # for each row
13294 0           for my $i (0 .. $rows) {
13295            
13296             # for each column
13297 0           for my $j (0 .. $cols) {
13298            
13299             # copy matrix element
13300 0           $replace->[$j][$i] = $self->[$i][$j];
13301            
13302             }
13303            
13304             }
13305            
13306             # if flip = 1 (horizontal)
13307             } elsif ($flip == 1) {
13308            
13309             # for each row
13310 0           for my $i (0 .. $rows) {
13311            
13312             # for each column
13313 0           for my $j (0 .. $cols) {
13314            
13315             # copy matrix element
13316 0           $replace->[$i][$j] = $self->[$rows - $i][$j];
13317            
13318             }
13319            
13320             }
13321            
13322             # if flip = 2 (cross transpose)
13323             } elsif ($flip == 2) {
13324            
13325             # for each row
13326 0           for my $i (0 .. $rows) {
13327            
13328             # for each column
13329 0           for my $j (0 .. $cols) {
13330            
13331             # copy matrix element
13332 0           $replace->[$j][$i] = $self->[$rows - $i][$cols - $j];
13333            
13334             }
13335            
13336             }
13337            
13338             # if flip = 3 (vertical)
13339             } elsif ($flip == 3) {
13340            
13341             # for each row
13342 0           for my $i (0 .. $rows) {
13343            
13344             # for each column
13345 0           for my $j (0 .. $cols) {
13346            
13347             # copy matrix element
13348 0           $replace->[$i][$j] = $self->[$i][$cols - $j];
13349            
13350             }
13351            
13352             }
13353            
13354             }
13355              
13356             # return new object
13357 0           return(bless($replace, 'Math::Matrix'));
13358              
13359             }
13360              
13361             # randomize matrix
13362             # returns: (randomized_matrix)
13363             sub randomize {
13364              
13365             # get object reference
13366 0     0 0   my $self = shift();
13367              
13368             # local variables
13369 0           my (@ix, $rows, $cols, $replace);
13370              
13371             # flatten and randomize matrix
13372 0           @ix = List::Util::shuffle(@{ICC::Shared::flatten($self)});
  0            
13373              
13374             # get upper row index
13375 0           $rows = $#{$self};
  0            
13376              
13377             # get upper column index
13378 0           $cols = $#{$self->[0]};
  0            
13379              
13380             # for each row
13381 0           for my $i (0 .. $rows) {
13382            
13383             # for each column
13384 0           for my $j (0 .. $cols) {
13385            
13386             # set element
13387 0           $replace->[$i][$j] = $ix[$i * ($cols + 1) + $j];
13388            
13389             }
13390            
13391             }
13392              
13393             # return new object
13394 0           return(bless($replace, 'Math::Matrix'));
13395              
13396             }
13397              
13398             1;
13399