File Coverage

blib/lib/ICC/Profile/cvst.pm
Criterion Covered Total %
statement 45 1013 4.4
branch 6 394 1.5
condition 2 212 0.9
subroutine 13 62 20.9
pod 1 35 2.8
total 67 1716 3.9


line stmt bran cond sub pod time code
1             package ICC::Profile::cvst;
2              
3 7     7   103168 use strict;
  7         22  
  7         201  
4 7     7   32 use Carp;
  7         12  
  7         478  
5              
6             our $VERSION = 0.48;
7              
8             # revised 2019-09-28
9             #
10             # Copyright © 2004-2019 by William B. Birkett
11              
12             # add development directory
13 7     7   503 use lib 'lib';
  7         665  
  7         34  
14              
15             # inherit from Shared
16 7     7   1305 use parent qw(ICC::Shared);
  7         296  
  7         34  
17              
18             # support modules
19 7     7   4133 use Template;
  7         131508  
  7         222  
20 7     7   3960 use Time::Piece;
  7         64119  
  7         30  
21 7     7   4987 use XML::LibXML;
  7         337180  
  7         48  
22              
23             # enable static variables
24 7     7   1073 use feature 'state';
  7         15  
  7         103251  
25              
26             # create new cvst object
27             # array contains curve objects for each channel
28             # file path to 'iso_18620', 'store', or 'text' format curves
29             # curve objects must have 'transform' and 'derivative' methods
30             # parameters: ([ref_to_array])
31             # parameters: ([file_path])
32             # returns: (ref_to_object)
33             sub new {
34              
35             # get object class
36 11     11 0 2161 my $class = shift;
37              
38             # create empty cvst object
39 11         30 my $self = [
40             {}, # object header
41             [], # curve object array
42             ];
43              
44             # if there are parameters
45 11 100       35 if (@_) {
46            
47             # if one parameter, an array reference
48 4 50 33     34 if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
    0 0        
49            
50             # make new cvst object from array
51 4         19 _new_from_array($self, shift());
52            
53             # if one parameter, a scalar
54             } elsif (@_ == 1 && ! ref($_[0])) {
55            
56             # make new cvst object from curve file
57 0         0 _new_from_file($self, shift());
58            
59             } else {
60            
61             # error
62 0         0 croak('\'cvst\' invalid parameter');
63            
64             }
65            
66             }
67              
68             # bless object
69 11         24 bless($self, $class);
70              
71             # return object reference
72 11         31 return($self);
73              
74             }
75              
76             # create inverse 'cvst' object
77             # returns: (ref_to_object)
78             sub inv {
79              
80             # get object
81 0     0 0 0 my $self = shift();
82              
83             # local variables
84 0         0 my ($array);
85              
86             # for each curve object
87 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
88            
89             # verify curve object has 'inv' method
90 0 0       0 ($self->[1][$i]->can('inv')) or croak('curve element lacks \'inv\' method');
91            
92             # make inverse curve object
93 0         0 $array->[$i] = $self->[1][$i]->inv();
94            
95             }
96              
97             # return
98 0         0 return(ICC::Profile::cvst->new($array));
99              
100             }
101              
102             # create cvst object from ICC profile
103             # assumes file handle is positioned at start of cvst data
104             # header information must be read separately by the calling function
105             # parameters: (ref_to_parent_object, file_handle, input_channels, output_channels)
106             # returns: (ref_to_object)
107             sub new_fh {
108              
109             # get object class
110 0     0 0 0 my $class = shift();
111              
112             # create empty cvst object
113 0         0 my $self = [
114             {}, # object header
115             [], # curve object array
116             ];
117              
118             # verify 3 parameters
119 0 0       0 (@_ == 3) or croak('wrong number of parameters');
120              
121             # read cvst data from profile
122 0         0 _readICCcvst($self, @_);
123              
124             # bless object
125 0         0 bless($self, $class);
126            
127             # return object reference
128 0         0 return($self);
129              
130             }
131              
132             # writes cvst tag object to ICC profile
133             # parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
134             sub write_fh {
135              
136             # verify 4 parameters
137 0 0   0 0 0 (@_ == 4) or croak('wrong number of parameters');
138              
139             # write cvst data to profile
140 0         0 goto &_writeICCcvst;
141              
142             }
143              
144             # get cvst size (for writing to profile)
145             # returns: (cvst_size)
146             sub size {
147              
148             # get parameter
149 0     0 0 0 my $self = shift();
150              
151             # get size of header and table
152 0         0 my $size = 12 + 8 * @{$self->[1]};
  0         0  
153              
154             # for each curve object
155 0         0 for my $crv (@{$self->[1]}) {
  0         0  
156            
157             # add size
158 0         0 $size += $crv->size();
159            
160             # adjust to 4-byte boundary
161 0         0 $size += -$size % 4;
162            
163             }
164              
165             # return size
166 0         0 return($size);
167              
168             }
169              
170             # get number of input channels
171             # returns: (number)
172             sub cin {
173              
174             # get object reference
175 22     22 0 30 my $self = shift();
176              
177             # return
178 22         28 return(scalar(@{$self->[1]}));
  22         51  
179              
180             }
181              
182             # get number of output channels
183             # returns: (number)
184             sub cout {
185              
186             # get object reference
187 10     10 0 18 my $self = shift();
188              
189             # return
190 10         12 return(scalar(@{$self->[1]}));
  10         23  
191              
192             }
193              
194             # transform data
195             # hash key: 'clip'
196             # supported input types:
197             # parameters: (list, [hash])
198             # parameters: (vector, [hash])
199             # parameters: (matrix, [hash])
200             # parameters: (Math::Matrix_object, [hash])
201             # parameters: (structure, [hash])
202             # returns: (same_type_as_input)
203             sub transform {
204              
205             # set hash value (0 or 1)
206 0 0   0 0 0 my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
207              
208             # if input a 'Math::Matrix' object
209 0 0 0     0 if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
210            
211             # call matrix transform
212 0         0 &_trans2;
213            
214             # if input an array reference
215             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
216            
217             # if array contains numbers (vector)
218 0 0 0     0 if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0     0  
  0         0  
  0         0  
219            
220             # call vector transform
221 0         0 &_trans1;
222            
223             # if array contains vectors (2-D array)
224 0 0       0 } elsif (ref($_[1][0]) eq 'ARRAY' && @{$_[1]} == grep {ref($_) eq 'ARRAY' && Scalar::Util::looks_like_number($_->[0])} @{$_[1]}) {
  0         0  
  0         0  
225            
226             # call matrix transform
227 0         0 &_trans2;
228            
229             } else {
230            
231             # call structure transform
232 0         0 &_trans3;
233            
234             }
235            
236             # if input a list (of numbers)
237 0         0 } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
238            
239             # call list transform
240 0         0 &_trans0;
241            
242             } else {
243            
244             # error
245 0         0 croak('invalid transform input');
246            
247             }
248              
249             }
250              
251             # invert data
252             # hash key: 'clip'
253             # supported input types:
254             # parameters: (list, [hash])
255             # parameters: (vector, [hash])
256             # parameters: (matrix, [hash])
257             # parameters: (Math::Matrix_object, [hash])
258             # parameters: (structure, [hash])
259             # returns: (same_type_as_input)
260             sub inverse {
261              
262             # set hash value (0 or 1)
263 0 0   0 0 0 my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
264              
265             # if input a 'Math::Matrix' object
266 0 0 0     0 if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
267            
268             # call matrix transform
269 0         0 &_inv2;
270            
271             # if input an array reference
272             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
273            
274             # if array contains numbers (vector)
275 0 0 0     0 if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0     0  
  0         0  
  0         0  
276            
277             # call vector transform
278 0         0 &_inv1;
279            
280             # if array contains vectors (2-D array)
281 0 0       0 } elsif (ref($_[1][0]) eq 'ARRAY' && @{$_[1]} == grep {ref($_) eq 'ARRAY' && Scalar::Util::looks_like_number($_->[0])} @{$_[1]}) {
  0         0  
  0         0  
282            
283             # call matrix transform
284 0         0 &_inv2;
285            
286             } else {
287            
288             # call structure transform
289 0         0 &_inv3;
290            
291             }
292            
293             # if input a list (of numbers)
294 0         0 } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
295            
296             # call list transform
297 0         0 &_inv0;
298            
299             } else {
300            
301             # error
302 0         0 croak('invalid transform input');
303            
304             }
305              
306             }
307              
308             # compute Jacobian matrix
309             # hash key 'diag' for diagonal vector
310             # parameters: (input_vector, [hash])
311             # returns: (Jacobian_matrix, [output_vector])
312             sub jacobian {
313              
314             # get parameters
315 0     0 0 0 my ($self, $in, $hash) = @_;
316              
317             # local variables
318 0         0 my (@drv, $out, $jac);
319              
320             # for each channel
321 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
322            
323             # compute derivative
324 0         0 $drv[$i] = $self->[1][$i]->derivative($in->[$i]);
325            
326             # compute transform
327 0 0       0 $out->[$i] = $self->[1][$i]->transform($in->[$i]) if wantarray;
328            
329             }
330              
331             # if 'diag' enabled
332 0 0       0 if ($hash->{'diag'}) {
333            
334             # make diagonal vector
335 0         0 $jac = [@drv];
336            
337             } else {
338            
339             # make diagonal matrix
340 0         0 $jac = Math::Matrix->diagonal(@drv);
341            
342             }
343              
344             # if output values wanted
345 0 0       0 if (wantarray) {
346            
347             # return Jacobian matrix and output vector
348 0         0 return($jac, $out);
349            
350             } else {
351            
352             # return Jacobian matrix only
353 0         0 return($jac);
354            
355             }
356            
357             }
358              
359             # compute parametric Jacobian matrix
360             # parameters are selected by the 'slice' array -or- matrix
361             # note: see 'cvst_parajac_matrix.plx' for explanation
362             # parameters: (input_vector)
363             # returns: (parametric_jacobian_matrix)
364             sub parajac {
365              
366             # get parameters
367 0     0 0 0 my ($self, $in) = @_;
368              
369             # local variables
370 0         0 my ($s, $type, @pj, $jac);
371              
372             # verify curve object has 'parametric' method
373 0 0       0 ($self->[1][0]->can('parametric')) or croak("curve object has no 'parametric' method");
374              
375             # get 'slice' value
376 0         0 $s = $self->[0]{'slice'};
377              
378             # determine 'slice' type (0 is undef, 1 is vector, 2 is matrix)
379 0 0 0     0 $type = ! defined($s) ? 0 : ICC::Shared::is_num_vector($s) ? 1 : ICC::Shared::is_num_matrix($s) && @{$s} == @{$self->[1]} ? 2 : croak("invalid slice for 'parajac' method");
    0          
    0          
380              
381             # initialize matrix
382 0         0 $jac = [map {[]} 0 .. $#{$self->[1]}];
  0         0  
  0         0  
383              
384             # for each channel
385 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
386            
387             # skip if slice empty
388 0 0 0     0 next if (($type == 1 && ! @{$s}) || ($type == 2 && ! @{$s->[$i]}));
  0   0     0  
  0   0     0  
389            
390             # get parametric partial derivatives
391 0         0 @pj = $self->[1][$i]->parametric($in->[$i]);
392            
393             # for each channel
394 0         0 for my $j (0 .. $#{$self->[1]}) {
  0         0  
395            
396             # if current channel
397 0 0       0 if ($j == $i) {
398            
399             # if vector slice
400 0 0       0 if ($type == 1) {
    0          
401            
402             # push slice parameters on matrix row
403 0         0 push(@{$jac->[$j]}, @pj[@{$s}]);
  0         0  
  0         0  
404            
405             # if matrix slice
406             } elsif ($type == 2) {
407            
408             # push slice parameters on matrix row
409 0         0 push(@{$jac->[$j]}, @pj[@{$s->[$i]}]);
  0         0  
  0         0  
410            
411             } else {
412            
413             # push all parameters on matrix row
414 0         0 push(@{$jac->[$j]}, @pj);
  0         0  
415            
416             }
417            
418             } else {
419            
420             # if vector slice
421 0 0       0 if ($type == 1) {
    0          
422            
423             # push zeros on matrix row
424 0         0 push(@{$jac->[$j]}, (0) x @{$s});
  0         0  
  0         0  
425            
426             # if matrix slice
427             } elsif ($type == 2) {
428            
429             # push zeros on matrix row
430 0         0 push(@{$jac->[$j]}, (0) x @{$s->[$i]});
  0         0  
  0         0  
431            
432             } else {
433            
434             # push zeros on matrix row
435 0         0 push(@{$jac->[$j]}, (0) x @pj);
  0         0  
436            
437             }
438            
439             }
440            
441             }
442            
443             }
444              
445             # return Jacobian matrix
446 0         0 return(bless($jac, 'Math::Matrix'));
447              
448             }
449              
450             # get/set reference to header hash
451             # parameters: ([ref_to_new_hash])
452             # returns: (ref_to_hash)
453             sub header {
454            
455             # get object reference
456 0     0 0 0 my $self = shift();
457            
458             # if there are parameters
459 0 0       0 if (@_) {
460            
461             # if one parameter, a hash reference
462 0 0 0     0 if (@_ == 1 && ref($_[0]) eq 'HASH') {
463            
464             # set header to new hash
465 0         0 $self->[0] = {%{shift()}};
  0         0  
466            
467             } else {
468            
469             # error
470 0         0 croak('parameter must be a hash reference');
471            
472             }
473            
474             }
475            
476             # return reference
477 0         0 return($self->[0]);
478            
479             }
480              
481             # get/set array reference
482             # parameters: ([ref_to_new_array])
483             # returns: (ref_to_array)
484             sub array {
485              
486             # get object reference
487 65     65 0 98 my $self = shift();
488              
489             # if one parameter supplied
490 65 50       169 if (@_ == 1) {
    50          
491            
492             # verify array reference
493 0 0       0 (ref($_[0]) eq 'ARRAY') or croak('not an array reference');
494              
495             # get array reference
496 0         0 my $array = shift();
497            
498             # for each curve element
499 0         0 for my $i (0 .. $#{$array}) {
  0         0  
500            
501             # verify object has processing methods
502 0 0 0     0 ($array->[$i]->can('transform') && $array->[$i]->can('derivative')) or croak('curve element lacks \'transform\' or \'derivative\' method');
503            
504             # add curve element
505 0         0 $self->[1][$i] = $array->[$i];
506            
507             }
508            
509             } elsif (@_) {
510            
511             # error
512 0         0 croak("too many parameters\n");
513            
514             }
515            
516             # return array reference
517 65         188 return($self->[1]);
518            
519             }
520              
521             # get 'para' or 'parf' curve parameters
522             # returns: (ref_to_array)
523             sub pars {
524              
525             # get object reference
526 0     0 0 0 my $self = shift();
527              
528             # local variables
529 0         0 my ($pars);
530              
531             # for each curve
532 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
533            
534             # verify curve is a 'para' or 'parf' object
535 0 0 0     0 (UNIVERSAL::isa($self->[1][$i], 'ICC::Profile::para') || UNIVERSAL::isa($self->[1][$i], 'ICC::Profile::parf')) or croak('curve is not a \'para\' or \'parf\' object');
536            
537             # copy parameters
538 0         0 $pars->[$i] = [@{$self->[1][$i]->array}];
  0         0  
539            
540             }
541              
542             # return parameter array
543 0         0 return($pars);
544              
545             }
546              
547             # make new 'cvst' object containing 'curv' objects
548             # assumes curve domain/range is (0 - 1)
549             # direction: 0 - normal, 1 - inverse
550             # parameters: (number_of_table_entries, [direction])
551             # returns: (cvst_object)
552             sub curv {
553              
554             # get parameters
555 0     0 0 0 my ($self, $n, $dir) = @_;
556              
557             # local variables
558 0         0 my ($curv);
559              
560             # for each channel
561 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
562            
563             # create table array
564 0         0 $curv->[$i] = $self->[1][$i]->curv($n, $dir);
565            
566             }
567              
568             # return 'cvst' object
569 0         0 return(ICC::Profile::cvst->new($curv));
570              
571             }
572              
573             # write Agfa Apogee tone curve file
574             # assumes curve domain/range is (0 - 1)
575             # options parameter may be a hash reference or direction flag
576             # hash keys: 'dir', 'steps'
577             # direction: 0 - normal, 1 - inverse
578             # parameters: (file_path, [options])
579             sub apogee {
580              
581             # get parameters
582 0     0 0 0 my ($self, $path, $opts) = @_;
583              
584             # local variables
585 0         0 my ($dir, $steps, %ink);
586 0         0 my ($dom, $root, @obj);
587 0         0 my ($i, @out);
588              
589             # process options
590 0         0 ($dir, $steps) = _options($opts);
591              
592             # set ink hash
593 0         0 %ink = ('Cyan', 0, 'Magenta', 1, 'Yellow', 2, 'Black', 3);
594              
595             # filter path
596 0         0 ICC::Shared::filterPath($path);
597              
598             # open curve set template
599 0 0       0 eval {$dom = XML::LibXML->load_xml('location' => ICC::Shared::getICCPath('Templates/Apogee_template.xml'))} or croak('can\'t load Apogee curve template');
  0         0  
600              
601             # get the root element
602 0         0 $root = $dom->documentElement();
603              
604             # get the 'Curve' nodes
605 0         0 @obj = $root->findnodes('Curve');
606              
607             # for each 'Curve' node
608 0         0 for my $n (@obj) {
609            
610             # look-up the color index (0 - 3)
611 0         0 $i = $ink{$n->getAttribute('Name')};
612            
613             # set the 'Stimuli' values
614 0         0 $n->setAttribute('Stimuli', join(' ', @{$steps}));
  0         0  
615            
616             # set the 'Measured' values
617 0         0 $n->setAttribute('Measured', join(' ', @{$steps}));
  0         0  
618            
619             # compute and set the 'Wanted' values
620 0         0 $n->setAttribute('Wanted', join(' ', map {sprintf("%f", 100 * ($self->[1][$i]->_transform($dir, $_/100)))} @{$steps}));
  0         0  
  0         0  
621            
622             # compute and set the 'TransferCurve' values
623 0         0 $n->setAttribute('TransferCurve', join(' ', map {sprintf("%f", 100 * ($self->[1][$i]->_transform($dir, $_/255)))} (0 .. 255)));
  0         0  
624            
625             }
626              
627             # add namespace attribute
628 0         0 $root->setAttribute('xmlns', 'file:///procres');
629              
630             # write XML file
631 0         0 $dom->toFile($path, 1);
632              
633             }
634              
635             # write CGATS tone curve file
636             # assumes curve domain/range is (0 - 1)
637             # options parameter may be a hash reference or direction flag
638             # hash keys: 'dir', 'steps'
639             # direction: 0 - normal, 1 - inverse
640             # parameters: (file_path, [options])
641             sub cgats {
642              
643             # get parameters
644 0     0 0 0 my ($self, $path, $opts) = @_;
645              
646             # local variables
647 0         0 my ($dir, $steps, $mat, $fmt, $chart);
648              
649             # process options
650 0         0 ($dir, $steps) = _options($opts);
651              
652             # filter path
653 0         0 ICC::Shared::filterPath($path);
654              
655             # for each step
656 0         0 for my $i (0 .. $#{$steps}) {
  0         0  
657            
658             # add SampleID
659 0         0 $mat->[$i][0] = "A$i";
660            
661             # add input step value
662 0         0 $mat->[$i][1] = sprintf("\"%.2f\"", $steps->[$i]);
663            
664             # for each curve
665 0         0 for my $j (0 .. $#{$self->[1]}) {
  0         0  
666            
667             # add output step value
668 0         0 $mat->[$i][$j + 2] = sprintf("%.2f", 100 * ($self->[1][$j]->_transform($dir, $steps->[$i]/100)));
669            
670             }
671            
672             }
673              
674             # make format string
675 0         0 $fmt = [qw(SampleID SAMPLE_NAME CMYK_C CMYK_M CMYK_Y CMYK_K), map {"SPOT_$_"} 1 .. ($#{$self->[1]} - 3)];
  0         0  
  0         0  
676              
677             # make Chart object
678 0         0 $chart = ICC::Support::Chart->new($mat, {'format' => $fmt});
679              
680             # add keywords
681 0         0 $chart->keyword('ORIGINATOR', '"PressCal"');
682 0         0 $chart->created(time);
683 0         0 $chart->keyword('LGOROWLENGTH', 5);
684              
685             # write chart object
686 0         0 $chart->write($path);
687              
688             }
689              
690             # write device link profile containing tone curves
691             # assumes curve domain/range is (0 - 1)
692             # options parameter may be a hash reference or direction flag
693             # hash key: 'dir'
694             # direction: 0 - normal, 1 - inverse
695             # parameters: (file_path, [options])
696             sub device_link {
697              
698             # get parameters
699 0     0 0 0 my ($self, $path, $opts) = @_;
700              
701             # local variables
702 0         0 my ($dir, $n, $sig, $clrt, $profile, $b);
703              
704             # process options
705 0         0 ($dir) = _options($opts);
706              
707             # get number of channels
708 0         0 $n = @{$self->[1]};
  0         0  
709              
710             # filter path
711 0         0 ICC::Shared::filterPath($path);
712              
713             # if grayscale
714 0 0       0 if ($n == 1) {
    0          
    0          
715            
716             # make signature
717 0         0 $sig = 'GRAY';
718            
719             } elsif ($n == 3) {
720            
721             # make signature
722 0         0 $sig = 'RGB ';
723            
724             } elsif ($n == 4) {
725            
726             # make signature
727 0         0 $sig = 'CMYK';
728            
729             } else {
730            
731             # make signature
732 0         0 $sig = sprintf("%XCLR", $n);
733            
734             # make colorant tag (could be developed further)
735 0         0 $clrt = ICC::Profile::clrt->new();
736            
737             }
738              
739             # make device link profile object
740 0         0 $profile = ICC::Profile->new({'class' => 'link', 'data' => $sig, 'PCS' => $sig, 'version' => '04200000'});
741              
742             # add copyright tag
743 0         0 $profile->tag({'cprt' => ICC::Profile::mluc->new('en', 'US', 'Copyright (c) 2004-2019 by William B. Birkett')});
744              
745             # add description tag
746 0         0 $profile->tag({'desc' => ICC::Profile::mluc->new('en', 'US', 'tone curves')});
747              
748             # add profile sequence tag
749 0         0 $profile->tag({'pseq' => ICC::Profile::pseq->new()});
750              
751             # for each curve
752 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
753            
754             # if direction is forward and curve is an ICC::Profile object
755 0 0 0     0 if ($dir == 0 && (UNIVERSAL::isa($self->[1][$i], 'ICC::Profile::curv') || UNIVERSAL::isa($self->[1][$i], 'ICC::Profile::para'))) {
      0        
756            
757             # use curve object as-is
758 0         0 $b->[$i] = $self->[1][$i];
759            
760             } else {
761            
762             # use ICC::Profile::curv equivalent
763 0         0 $b->[$i] = $self->[1][$i]->curv(1285, $dir);
764            
765             }
766            
767             }
768              
769             # add A2B0 tag (B-curves only)
770 0         0 $profile->tag({'A2B0' => ICC::Profile::mAB_->new({'b_curves' => ICC::Profile::cvst->new($b)})});
771              
772             # add colorant tags, if nCLR
773 0 0       0 $profile->tag({'clrt' => $clrt, 'clot' => $clrt}) if (defined($clrt));
774              
775             # write profile
776 0         0 $profile->write($path);
777              
778             }
779              
780             # write EFI (.vpc/.vcc) tone curve file
781             # assumes curve domain/range is (0 - 1)
782             # options parameter may be a hash reference or direction flag
783             # hash keys: 'dir', 'steps'
784             # direction: 0 - normal, 1 - inverse
785             # parameters: (file_path, [options])
786             sub efi {
787              
788             # get parameters
789 0     0 0 0 my ($self, $path, $opts) = @_;
790              
791             # local variables
792 0         0 my ($dir, $steps, @ch, $include, $tt, $t, $fh, $str, $vars);
793              
794             # process options
795 0         0 ($dir, $steps) = _options($opts);
796              
797             # filter path
798 0         0 ICC::Shared::filterPath($path);
799              
800             # channel lookup (EFI ink sequence is YMCK)
801 0         0 @ch = (2, 1, 0, 3, 4, 5, 6, 7);
802              
803             # if ICC::Templates folder is found in @INC (may be relative)
804 0 0       0 if (($include) = grep {-d} map {File::Spec->catdir($_, 'ICC', 'Templates')} @INC) {
  0         0  
  0         0  
805            
806             # make a template processing object
807 0         0 $tt = Template->new({'INCLUDE_PATH' => $include});
808            
809             # for each curve
810 0         0 for my $i (0 .. 7) {
811            
812             # open file handle to string
813 0         0 open($fh, '>', \$str);
814            
815             # print header
816 0         0 print $fh "BEGIN\n";
817            
818             # if curve object is defined
819 0 0       0 if (defined($self->[1][$i])) {
820            
821             # print number of points
822 0         0 printf $fh "%d\n", scalar(@{$steps});
  0         0  
823            
824             # for each curve input
825 0         0 for my $t (@{$steps}) {
  0         0  
826            
827             # print output and input device values
828 0         0 printf $fh "%.5f %.5f\n", $self->[1][$i]->_transform($dir, $t/100), $t/100;
829            
830             }
831            
832             # for each integer byte value
833 0         0 for my $t (0 .. 255) {
834            
835             # print input and output values
836 0         0 printf $fh "%d %.0f\n", $t, 255 * $self->[1][$i]->_transform($dir, $t/255);
837            
838             }
839            
840             } else {
841            
842             # print identity curve
843 0         0 print $fh "2\n0.00000 0.00000\n1.00000 1.00000\n";
844            
845             # for each integer byte value
846 0         0 for my $t (0 .. 255) {
847            
848             # print input and output values
849 0         0 printf $fh "%d %d\n", $t, $t;
850            
851             }
852            
853             }
854            
855             # print footer
856 0         0 print $fh "END";
857            
858             # add string to template hash
859 0         0 $vars->{"curve$ch[$i]"} = $str;
860            
861             # close file handle
862 0         0 close($fh);
863            
864             }
865            
866             # make Time::Piece object
867 0         0 $t = localtime;
868            
869             # add date to template hash
870 0         0 $vars->{'date'} = $t->strftime('%m-%d-%y');
871            
872             # process the template
873 0 0       0 $tt->process('cvst_efi_vcc.tt2', $vars, $path) || CORE::die $tt->error();
874            
875             }
876            
877             }
878              
879             # write Fuji XMF tone curve file
880             # assumes curve domain/range is (0 - 1)
881             # options parameter may be a hash reference or direction flag
882             # hash key: 'dir'
883             # direction: 0 - normal, 1 - inverse
884             # parameters: (file_path, [options])
885             sub fuji_xmf {
886              
887             # get parameters
888 0     0 0 0 my ($self, $path, $opts) = @_;
889              
890             # local variables
891 0         0 my ($dir, $steps, $fh, $rs, @colors, @Tdot);
892              
893             # process options
894 0         0 ($dir, $steps) = _options($opts);
895              
896             # filter path
897 0         0 ICC::Shared::filterPath($path);
898              
899             # open the file
900 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
901              
902             # disable :crlf translation
903 0         0 binmode($fh);
904              
905             # set output record separator (Windows CR-LF)
906 0         0 $rs = "\015\012";
907              
908             # set color list
909 0         0 @colors = qw(Cyan Magenta Yellow Black);
910              
911             # print colors
912 0         0 print $fh join(';', @colors), $rs;
913              
914             # for each step
915 0         0 for my $j (0 .. 100) {
916            
917             # if a valid dot value
918 0 0       0 if (grep {$j == $_} @{$steps}) {
  0         0  
  0         0  
919            
920             # for each channel
921 0         0 for my $i (0 .. 3) {
922            
923             # compute transformed dot value
924 0         0 $Tdot[$i] = sprintf("%.2f", 100 * ($self->[1][$i]->_transform($dir, $j/100)));
925            
926             }
927            
928             # print transformed values
929 0         0 print $fh join(';', @Tdot), $rs;
930            
931             } else {
932            
933             # print empty line
934 0         0 print $fh '‐;‐;‐;‐', $rs;
935            
936             }
937            
938             }
939              
940             # close the file
941 0         0 close($fh);
942              
943             }
944              
945             # write Harlequin tone curve file
946             # assumes curve domain/range is (0 - 1)
947             # options parameter may be a hash reference or direction flag
948             # hash key: 'dir'
949             # direction: 0 - normal, 1 - inverse
950             # note: values must be entered manually in RIP
951             # use 'navigator' method to make Postscript curves
952             # parameters: (file_path, [options])
953             sub harlequin {
954              
955             # get parameters
956 0     0 0 0 my ($self, $path, $opts) = @_;
957              
958             # local variables
959 0         0 my ($dir, $steps, @files, $fh, $rs, @colors);
960              
961             # process options
962 0         0 ($dir, $steps) = _options($opts);
963              
964             # filter path
965 0         0 ICC::Shared::filterPath($path);
966              
967             # open the file
968 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
969              
970             # disable :crlf translation
971 0         0 binmode($fh);
972              
973             # set output record separator (Windows CR-LF)
974 0         0 $rs = "\015\012";
975            
976             # set color list
977 0         0 @colors = qw(Cyan Magenta Yellow Black);
978              
979             # for each channel
980 0         0 for my $i (0 .. 3) {
981            
982             # print color
983 0         0 print $fh "$colors[$i]$rs";
984            
985             # for each step
986 0         0 for my $j (0 .. $#{$steps}) {
  0         0  
987            
988             # print input and transformed values
989 0         0 printf $fh "%7.2f %7.2f$rs", $steps->[$j], 100 * ($self->[1][$i]->_transform($dir, $steps->[$j]/100));
990            
991             }
992            
993             # print space
994 0         0 print $fh "$rs$rs";
995            
996             }
997              
998             # close the file
999 0         0 close($fh);
1000              
1001             }
1002              
1003             # write HP Indigo tone curve file set
1004             # assumes curve domain/range is (0 - 1)
1005             # options parameter may be a hash reference or direction flag
1006             # hash key: 'dir'
1007             # direction: 0 - normal, 1 - inverse
1008             # parameters: (folder_path, [options])
1009             sub indigo {
1010              
1011             # get parameters
1012 0     0 0 0 my ($self, $path, $opts) = @_;
1013              
1014             # local variables
1015 0         0 my ($dir, $steps, $rs, $fh, $file);
1016 0         0 my (@CMYK, $dotr, $dotp);
1017              
1018             # process options
1019 0         0 ($dir, $steps) = _options($opts);
1020              
1021             # set output record separator (Windows CR-LF)
1022 0         0 $rs = "\015\012";
1023              
1024             # filter path
1025 0         0 ICC::Shared::filterPath($path);
1026              
1027             # make the folder
1028 0         0 File::Path::make_path($path);
1029              
1030             # ink color array (for building file names)
1031 0         0 @CMYK = qw(Cyan Magenta Yellow Black);
1032              
1033             # for each color
1034 0         0 for my $i (0 .. 3) {
1035            
1036             # build the file path
1037 0 0       0 $file = $^O eq 'MSWin32' ? "$path\\tone_curve-$CMYK[$i].lut" : "$path/tone_curve-$CMYK[$i].lut";
1038            
1039             # create the file
1040 0 0       0 open($fh, '>', $file) or croak("can't open $file: $!");
1041            
1042             # disable :crlf translation
1043 0         0 binmode($fh);
1044              
1045             # for each step
1046 0         0 for my $j (0 .. $#{$steps}) {
  0         0  
1047            
1048             # get reference device value
1049 0         0 $dotr = $steps->[$j]/100;
1050            
1051             # get press device value
1052 0         0 $dotp = $self->[1][$i]->_transform($dir, $dotr);
1053            
1054             # limit %-dot (0 - 100)
1055 0 0       0 $dotr = ($dotr < 0) ? 0 : $dotr;
1056 0 0       0 $dotp = ($dotp < 0) ? 0 : $dotp;
1057 0 0       0 $dotr = ($dotr > 1) ? 1 : $dotr;
1058 0 0       0 $dotp = ($dotp > 1) ? 1 : $dotp;
1059            
1060             # print step info
1061 0         0 printf $fh "%4.2f\t%6.4f$rs", $dotr, $dotp;
1062            
1063             }
1064            
1065             # close file
1066 0         0 close($fh);
1067            
1068             }
1069            
1070             }
1071              
1072             # write ISO 18620 (TED) tone curve file
1073             # assumes curve domain/range is (0 - 1)
1074             # options parameter may be a hash reference or direction flag
1075             # hash keys: 'dir', 'steps', 'inks', 'origin',
1076             # 'Creator', 'OperatorName', 'PressName', 'MediaName',
1077             # 'TransferCurveSetID', 'Side'
1078             # direction: 0 - normal, 1 - inverse
1079             # parameters: (file_path, [options])
1080             sub iso_18620 {
1081              
1082             # get parameters
1083 0     0 0 0 my ($self, $path, $opts) = @_;
1084              
1085             # local variables
1086 0         0 my ($dir, $steps, @inks, $zflag);
1087 0         0 my ($doc, $root, $t, $datetime, $curve, @out);
1088              
1089             # process options
1090 0         0 ($dir, $steps) = _options($opts);
1091              
1092             # set ink colors
1093 0 0       0 @inks = $#{$self->[1]} ? qw(Cyan Magenta Yellow Black) : qw(Black);
  0         0  
1094              
1095             # for each curve
1096 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1097            
1098             # set ink value, defaults to 'inkN'
1099 0   0     0 $inks[$i] = $opts->{'inks'}[$i] // $self->[0]{'inks'}[$i] // $inks[$i] // sprintf("ink%d", $i + 1);
      0        
      0        
1100            
1101             }
1102              
1103             # filter path
1104 0         0 ICC::Shared::filterPath($path);
1105              
1106             # create XML document
1107 0         0 $doc = XML::LibXML->createDocument('1.0', 'UTF-8');
1108              
1109             # create root element
1110 0         0 $root = $doc->createElement('TransferCurveSet');
1111              
1112             # add root node
1113 0         0 $doc->setDocumentElement($root);
1114              
1115             # make Time::Piece object
1116 0         0 $t = localtime;
1117              
1118             # set 'CreationDate' attribute
1119 0         0 $root->setAttribute('CreationDate', sprintf("%s%+03d:00", $t->datetime, $t->tzoffset->hours));
1120              
1121             # verify 'Side' attribute
1122 0 0 0     0 (! defined($opts->{'Side'}) || $opts->{'Side'} eq 'Front' || $opts->{'Side'} eq 'Back') or croak('invalid \'Side\' attribute');
      0        
1123              
1124             # for each optional TransferCurveSet attribute
1125 0         0 for my $key (qw(Creator OperatorName PressName MediaName TransferCurveSetID Side)) {
1126            
1127             # if attribute contained in hash
1128 0 0       0 if (defined($opts->{$key})) {
1129            
1130             # set attribute value
1131 0         0 $root->setAttribute($key, $opts->{$key});
1132            
1133             }
1134            
1135             }
1136              
1137             # set 'Creator' attribute, if undefined
1138 0 0       0 $root->setAttribute('Creator', 'ICC-Profile Toolkit') if (! defined($opts->{'Creator'}));
1139              
1140             # get 'origin' flag
1141 0   0     0 $zflag = $opts->{'origin'} // 0;
1142              
1143             # for each curve
1144 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1145            
1146             # create curve element
1147 0         0 $curve = $doc->createElement('TransferCurve');
1148            
1149             # set the 'Separation' attribute
1150 0         0 $curve->setAttribute('Separation', $inks[$i]);
1151            
1152             # compute and set the 'Curve' values
1153 0 0 0     0 $curve->setAttribute('Curve', join(' ', map {sprintf("%f %f", $_/100, ($_ == 0 && $zflag) ? 0 : $self->[1][$i]->_transform($dir, $_/100))} @{$steps}));
  0         0  
  0         0  
1154            
1155             # add curve node
1156 0         0 $root->addChild($curve);
1157            
1158             }
1159              
1160             # add namespace attribute
1161 0         0 $root->setAttribute('xmlns', 'http://www.npes.org/schema/ISO18620/');
1162              
1163             # write XML file
1164 0         0 $doc->toFile($path, 1);
1165              
1166             }
1167              
1168             # write Xitron Navigator tone curve file
1169             # assumes curve domain/range is (0 - 1)
1170             # options parameter may be a hash reference or direction flag
1171             # hash key: 'dir', 'inks', 'name', 'colorspace'
1172             # direction: 0 - normal, 1 - inverse
1173             # note: makes a Postscript file for 'push calibration'
1174             # see Harlequin technical note Hqn081
1175             # parameters: (file_path, [options])
1176             sub navigator {
1177              
1178             # get parameters
1179 0     0 0 0 my ($self, $path, $opts) = @_;
1180              
1181             # local variables
1182 0         0 my ($dir, $steps, @inks, $tt, $include, $vars, $fh, $str);
1183              
1184             # process options
1185 0         0 ($dir, $steps) = _options($opts);
1186              
1187             # set ink colors
1188 0 0       0 @inks = $#{$self->[1]} ? qw(Cyan Magenta Yellow Black) : qw(Black);
  0         0  
1189              
1190             # for each curve
1191 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1192            
1193             # set ink value, defaults to 'inkN'
1194 0   0     0 $inks[$i] = $opts->{'inks'}[$i] // $self->[0]{'inks'}[$i] // $inks[$i] // sprintf("ink%d", $i + 1);
      0        
      0        
1195            
1196             }
1197              
1198             # filter path
1199 0         0 ICC::Shared::filterPath($path);
1200              
1201             # if ICC::Templates folder is found in @INC (may be relative)
1202 0 0       0 if (($include) = grep {-d} map {File::Spec->catdir($_, 'ICC', 'Templates')} @INC) {
  0         0  
  0         0  
1203            
1204             # make a template processing object
1205 0         0 $tt = Template->new({'INCLUDE_PATH' => $include});
1206            
1207             # set channels
1208 0         0 $vars->{'channels'} = join(' ', map {"/$_"} @inks);
  0         0  
1209            
1210             # set channel colors
1211 0         0 $vars->{'channelcolors'} = join(' ', map {"($_)"} @inks);
  0         0  
1212            
1213             # set number of channels
1214 0         0 $vars->{'number'} = @inks;
1215            
1216             # set name
1217 0   0     0 $vars->{'name'} = $opts->{'name'} // 'PressCal Calset ' . time();
1218            
1219             # set colorspace
1220 0   0     0 $vars->{'colorspace'} = $opts->{'colorspace'} // 'DeviceCMYK';
1221            
1222             # open file handle to string
1223 0         0 open($fh, '>', \$str);
1224            
1225             # for each channel
1226 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1227            
1228             # start curve
1229 0         0 printf $fh " /%s [\n", $inks[$i];
1230            
1231             # for each step
1232 0         0 for my $j (0 .. $#{$steps}) {
  0         0  
1233            
1234             # print input and transformed values
1235 0         0 printf $fh " %.2f %% C%s\n", 100 * ($self->[1][$i]->_transform($dir, $steps->[$j]/100)), $steps->[$j];
1236            
1237             }
1238            
1239             # end curve
1240 0         0 print $fh " ]\n";
1241            
1242             }
1243            
1244             # add string to template hash
1245 0         0 $vars->{'curves'} = $str;
1246            
1247             # close file handle
1248 0         0 close($fh);
1249            
1250             # process the template
1251 0 0       0 $tt->process('cvst_navigator.tt2', $vars, $path) || CORE::die $tt->error();
1252            
1253             }
1254              
1255             }
1256              
1257             # write Photoshop tone curve file
1258             # assumes curve domain/range is (0 - 1)
1259             # options parameter may be a hash reference or direction flag
1260             # hash keys: 'dir', 'steps'
1261             # direction: 0 - normal, 1 - inverse
1262             # note: Photoshop curves must have between 2 and 16 steps
1263             # parameters: (file_path, [options])
1264             sub photoshop {
1265              
1266             # get parameters
1267 0     0 0 0 my ($self, $path, $opts) = @_;
1268              
1269             # local variables
1270 0         0 my ($dir, $steps, $xval, $n, $fh, $x, $y, $xmin, $xmax, $xp, @yx);
1271              
1272             # process options
1273 0         0 ($dir, $steps) = _options($opts);
1274              
1275             # if 'steps' array supplied
1276 0 0       0 if (@{$steps}) {
  0 0       0  
    0          
1277            
1278             # copy step values
1279 0         0 $xval = [map {$_/100} @{$steps}];
  0         0  
  0         0  
1280            
1281             # verify maximum number of curve points
1282 0 0       0 ($#{$xval} < 16) or croak('photoshop curve steps array has more than 16 points');
  0         0  
1283            
1284             # verify minimum number of curve points
1285 0 0       0 ($#{$xval} > 0) or croak('photoshop curve steps array has less than 2 points');
  0         0  
1286            
1287             # if 'bern' curve objects
1288             } elsif (UNIVERSAL::isa($self->[1][0], 'ICC::Support::bern')) {
1289            
1290             # get maximum upper index of Bernstein coefficient arrays
1291 0 0       0 $n = ($#{$self->[1][0]->input} > $#{$self->[1][0]->output}) ? $#{$self->[1][0]->input} : $#{$self->[1][0]->output};
  0         0  
  0         0  
  0         0  
  0         0  
1292            
1293             # compute upper index
1294 0 0       0 $n = 2 * $n < 16 ? 2 * $n : 15;
1295            
1296             # make x-value array
1297 0         0 $xval = [map {$_/$n} (0 .. $n)];
  0         0  
1298            
1299             # if 'spline' curve objects
1300             } elsif (UNIVERSAL::isa($self->[1][0], 'ICC::Support::spline')) {
1301            
1302             # compute upper index
1303 0 0       0 $n = 2 * $#{$self->[1][0]->output} < 16 ? 2 * $#{$self->[1][0]->output} : 15;
  0         0  
  0         0  
1304            
1305             # make x-value array
1306 0         0 $xval = [map {$_/$n} (0 .. $n)];
  0         0  
1307            
1308             } else {
1309            
1310             # use default array (5 points)
1311 0         0 $xval = [map {$_/4} (0 .. 4)];
  0         0  
1312            
1313             }
1314              
1315             # sort the x-values from low to high
1316 0         0 @{$xval} = sort {$a <=> $b} @{$xval};
  0         0  
  0         0  
  0         0  
1317              
1318             # filter path
1319 0         0 ICC::Shared::filterPath($path);
1320              
1321             # open the file
1322 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
1323              
1324             # set binary mode
1325 0         0 binmode($fh);
1326              
1327             # print the version and number of curves (including master curve)
1328 0         0 print $fh pack('n2', 4, scalar(@{$self->[1]}) + 1);
  0         0  
1329              
1330             # print null master curve
1331 0         0 print $fh pack('n5', 2, 0, 0, 255, 255);
1332              
1333             # for each channel
1334 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1335            
1336             # compute min and max x-values (correspond to y-values of 0 and 1)
1337 0         0 $xmin = $self->[1][$i]->_transform((1 - $dir), 0);
1338 0         0 $xmax = $self->[1][$i]->_transform((1 - $dir), 1);
1339            
1340             # swap min and max if negative curve
1341 0 0       0 ($xmax, $xmin) = ($xmin, $xmax) if ($xmin > $xmax);
1342            
1343             # initialize point array
1344 0         0 @yx = ();
1345            
1346             # initialize previous x-value
1347 0         0 $xp = -1;
1348            
1349             # for each point
1350 0         0 for my $j (0 .. $#{$xval}) {
  0         0  
1351            
1352             # get x-value
1353 0         0 $x = $xval->[$j];
1354            
1355             # limit x-value (previously limited domain 0 - 1)
1356 0 0       0 $x = $x > $xmax ? $xmax : ($x < $xmin ? $xmin : $x);
    0          
1357            
1358             # skip if x-value same as previous
1359 0 0       0 next if ($x == $xp);
1360            
1361             # set previous x-value
1362 0         0 $xp = $x;
1363            
1364             # get y-value
1365 0         0 $y = $self->[1][$i]->_transform($dir, $x);
1366            
1367             # limit y-value
1368 0 0       0 $y = $y > 1 ? 1 : ($y < 0 ? 0 : $y);
    0          
1369            
1370             # push y-x pair on array (Photoshop curve points are [output, input])
1371 0         0 push(@yx, [$y, $x]);
1372            
1373             }
1374            
1375             # print number of points
1376 0         0 print $fh pack('n', scalar(@yx));
1377            
1378             # if 3 channels (RGB)
1379 0 0       0 if (@{$self->[1]} == 3) {
  0         0  
1380            
1381             # for each point
1382 0         0 for (@yx) {
1383            
1384             # print point value (y, x), normal for RGB
1385 0         0 print $fh pack('n2', map {255 * $_ + 0.5} @{$_});
  0         0  
  0         0  
1386            
1387             }
1388            
1389             } else {
1390            
1391             # for each point (in reverse order)
1392 0         0 for (reverse(@yx)) {
1393            
1394             # print point value (y, x), complemented for Grayscale, CMYK, Multichannel
1395 0         0 print $fh pack('n2', map {255 * (1 - $_) + 0.5} @{$_});
  0         0  
  0         0  
1396            
1397             }
1398            
1399             }
1400            
1401             }
1402              
1403             # close the file
1404 0         0 close($fh);
1405              
1406             # set file creator and type (OS X only)
1407 0         0 ICC::Shared::setFile($path, '8BIM', '8BSC');
1408              
1409             }
1410              
1411             # write Prinergy (Harmony) tone curve file
1412             # assumes curve domain/range is (0 - 1)
1413             # options parameter may be a hash reference or direction flag
1414             # hash keys: 'dir', 'Comments', 'CurveSet', 'DefaultFrequency', 'DefaultMedium',
1415             # 'DefaultResolution', 'DefaultSpotFunction', 'Enabled', 'FirstName', 'FreqFrom', 'FreqTo',
1416             # 'ID', 'Medium', 'Resolution', 'ScreeningType', 'SpotFunction', 'SpotFunctionMode'
1417             # direction: 0 - normal, 1 - inverse
1418             # parameters: (file_path, [options])
1419             sub prinergy {
1420              
1421             # get parameters
1422 0     0 0 0 my ($self, $path, $opts) = @_;
1423              
1424             # local variables
1425 0         0 my ($dir, $steps, @inks, $tt, $include, $vars, @time, @month, $fh, $rs, @map, $str);
1426              
1427             # process options
1428 0         0 ($dir, $steps) = _options($opts);
1429              
1430             # set ink colors
1431 0 0       0 @inks = $#{$self->[1]} ? qw(Cyan Magenta Yellow Black) : qw(Black);
  0         0  
1432              
1433             # for each curve
1434 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1435            
1436             # set ink value, defaults to 'inkN'
1437 0   0     0 $inks[$i] = $opts->{'inks'}[$i] // $self->[0]{'inks'}[$i] // $inks[$i] // sprintf("ink%d", $i + 1);
      0        
      0        
1438            
1439             }
1440              
1441             # filter path
1442 0         0 ICC::Shared::filterPath($path);
1443              
1444             # if ICC::Templates folder is found in @INC (may be relative)
1445 0 0       0 if (($include) = grep {-d} map {File::Spec->catdir($_, 'ICC', 'Templates')} @INC) {
  0         0  
  0         0  
1446            
1447             # make a template processing object
1448 0         0 $tt = Template->new({'INCLUDE_PATH' => $include});
1449            
1450             # copy options hash
1451 0         0 $vars = Storable::dclone($opts);
1452            
1453             # set time
1454 0         0 $vars->{'Time'} = time();
1455            
1456             # get localtime
1457 0         0 @time = localtime($vars->{'Time'});
1458            
1459             # set date as string
1460 0         0 $vars->{'date'} = sprintf "%d/%d/%d %2.2d:%2.2d:%2.2d", $time[4] + 1, $time[3], $time[5] + 1900, $time[2], $time[1], $time[0];
1461              
1462             # make array of months
1463 0         0 @month = qw(January February March April May June July August September October November December);
1464              
1465             # set DateTime as string
1466 0         0 $vars->{'DateTime'} = sprintf "%2.2d %s %d %2.2d:%2.2d:%2.2d", $time[3], $month[$time[4]], $time[5] + 1900, $time[2], $time[1], $time[0];
1467            
1468             # set defaults
1469 0   0     0 $vars->{'FirstName'} = $vars->{'FirstName'} // 'PressCal';
1470 0   0     0 $vars->{'ID'} = $vars->{'ID'} // '0001';
1471 0   0     0 $vars->{'Enabled'} = $vars->{'Enabled'} // 'FALSE';
1472 0   0     0 $vars->{'CurveSet'} = $vars->{'CurveSet'} // 'CmykCurves';
1473 0   0     0 $vars->{'SpotFunctionMode'} = $vars->{'SpotFunctionMode'} // 'UserDefined';
1474            
1475             # set true or false
1476 0 0       0 $vars->{'MediumUsed'} = defined($vars->{'Medium'}) ? 'TRUE' : 'FALSE';
1477 0 0       0 $vars->{'ScreeningTypeUsed'} = defined($vars->{'ScreeningType'}) ? 'TRUE' : 'FALSE';
1478 0 0       0 $vars->{'ResolutionUsed'} = defined($vars->{'Resolution'}) ? 'TRUE' : 'FALSE';
1479 0 0 0     0 $vars->{'FrequencyUsed'} = (defined($vars->{'FreqFrom'}) && defined($vars->{'FreqFrom'})) ? 'TRUE' : 'FALSE';
1480 0 0       0 $vars->{'SpotFunctionUsed'} = defined($vars->{'SpotFunction'}) ? 'TRUE' : 'FALSE';
1481            
1482             # set combined description
1483 0         0 $vars->{'description'} = join(' ', grep {$_} @{$vars}{qw(FirstName Medium CurveSet FreqFrom Resolution)});
  0         0  
  0         0  
1484              
1485             # open file handle to string
1486 0         0 open($fh, '>', \$str);
1487            
1488             # disable :crlf translation
1489 0         0 binmode($fh);
1490            
1491             # set output record separator (Windows CR-LF)
1492 0         0 $rs = "\015\012";
1493              
1494             # set color map (KCMY + spot)
1495 0         0 @map = (3, 0, 1, 2, 4 .. 15);
1496            
1497             # for each channel
1498 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1499            
1500             # print curve dropoff
1501 0         0 printf $fh "Curve%d DropOff = %d$rs", $i + 1, 0;
1502            
1503             # print curve color
1504 0         0 printf $fh "Curve%d Color = %s$rs", $i + 1, $inks[$map[$i]];
1505            
1506             # print curve start
1507 0         0 printf $fh "Curve%d = ", $i + 1;
1508            
1509             # print curve points
1510 0         0 for my $t (@{$steps}) {
  0         0  
1511            
1512             # print curve values
1513 0         0 printf $fh "%d %d ", 1E7 * $t/100 + 0.5, 1E7 * $self->[1][$map[$i]]->_transform($dir, $t/100) + 0.5;
1514            
1515             }
1516            
1517             # print curve end
1518 0         0 print $fh "$rs";
1519            
1520             }
1521            
1522             # add string to template hash
1523 0         0 $vars->{'curves'} = $str;
1524            
1525             # close the file
1526 0         0 close($fh);
1527            
1528             # process the template
1529 0 0       0 $tt->process('cvst_prinergy.tt2', $vars, $path) || CORE::die $tt->error();
1530            
1531             }
1532              
1533             }
1534              
1535             # write Rampage tone curve file set
1536             # assumes curve domain/range is (0 - 1)
1537             # options parameter may be a hash reference or direction flag
1538             # hash key: 'dir'
1539             # direction: 0 - normal, 1 - inverse
1540             # parameters: (folder_path, [options])
1541             sub rampage {
1542              
1543             # get parameters
1544 0     0 0 0 my ($self, $path, $opts) = @_;
1545              
1546             # local variables
1547 0         0 my ($dir, $steps, $name, $rs, $fh0, $fh1, $file);
1548 0         0 my (@CMYK, $dotr, $dotp);
1549              
1550             # process options
1551 0         0 ($dir, $steps) = _options($opts);
1552              
1553             # filter path
1554 0         0 ICC::Shared::filterPath($path);
1555              
1556             # make the folder, if needed
1557 0         0 File::Path::make_path($path);
1558              
1559             # get the folder name
1560 0         0 $name = (File::Spec->splitdir($path))[-1];
1561              
1562             # set output record separator (Windows CR-LF)
1563 0         0 $rs = "\015\012";
1564              
1565             # ink color array (for building file names)
1566 0         0 @CMYK = qw(C M Y K);
1567              
1568             # for each color
1569 0         0 for my $i (0 .. 3) {
1570            
1571             # build the DESIRED file path
1572 0         0 $file = $path . '/' . $name . '_DESIRED_' . $CMYK[$i];
1573            
1574             # create the DESIRED file
1575 0 0       0 open($fh0, '>', $file) or croak("can't open $file: $!");
1576            
1577             # disable :crlf translation
1578 0         0 binmode($fh0);
1579              
1580             # set file creator and type
1581 0         0 ICC::Shared::setFile($file, 'RamC', 'Clst');
1582            
1583             # build the ACT file path
1584 0         0 $file = $path . '/' . $name . '_ACT_' . $CMYK[$i];
1585            
1586             # create the ACT file
1587 0 0       0 open($fh1, '>', $file) or croak("can't open $file: $!");
1588            
1589             # disable :crlf translation
1590 0         0 binmode($fh1);
1591              
1592             # set file creator and type
1593 0         0 ICC::Shared::setFile($file, 'RamC', 'Clst');
1594            
1595             # print DESIRED header
1596 0         0 print $fh0 "2$rs";
1597 0         0 print $fh0 "0.0000000000$rs";
1598 0         0 print $fh0 "0.0000000000$rs";
1599 0         0 printf $fh0 "%2d$rs", $steps + 1;
1600            
1601             # print ACT header
1602 0         0 print $fh1 "2$rs";
1603 0         0 print $fh1 "0.0000000000$rs";
1604 0         0 print $fh1 "0.0000000000$rs";
1605 0         0 printf $fh1 "%2d$rs", $steps + 1;
1606            
1607             # for each step
1608 0         0 for my $j (0 .. $#{$steps}) {
  0         0  
1609            
1610             # get reference %-dot
1611 0         0 $dotr = $steps->[$j];
1612            
1613             # get press %-dot
1614 0         0 $dotp = 100 * $self->[1][$i]->_transform($dir, $dotr/100);
1615            
1616             # limit %-dot (0 - 100)
1617 0 0       0 $dotr = ($dotr < 0) ? 0 : $dotr;
1618 0 0       0 $dotp = ($dotp < 0) ? 0 : $dotp;
1619 0 0       0 $dotr = ($dotr > 100) ? 100 : $dotr;
1620 0 0       0 $dotp = ($dotp > 100) ? 100 : $dotp;
1621            
1622             # print DESIRED step info
1623 0         0 printf $fh0 "%3.1f %3.1f$rs", $dotr, $dotp;
1624            
1625             # print ACT step info
1626 0         0 printf $fh1 "%3.1f %3.1f$rs", $dotr, $dotr;
1627            
1628             }
1629            
1630             # print DESIRED footer
1631 0         0 print $fh0 "Version: 2.0$rs";
1632            
1633             # print ACT footer
1634 0         0 print $fh1 "Version: 2.0$rs";
1635            
1636             # close the DESIRED file
1637 0         0 close($fh0);
1638            
1639             # close the ACT file
1640 0         0 close($fh1);
1641            
1642             }
1643            
1644             }
1645              
1646             # write Xitron Sierra tone curve file
1647             # assumes curve domain/range is (0 - 1)
1648             # options parameter may be a hash reference or direction flag
1649             # hash key: 'dir'
1650             # direction: 0 - normal, 1 - inverse
1651             # parameters: (file_path, [options])
1652             sub sierra {
1653              
1654             # get parameters
1655 0     0 0 0 my ($self, $path, $opts) = @_;
1656              
1657             # local variables
1658 0         0 my ($dir, $steps, $fh, $rs, @colors, @Tdot);
1659              
1660             # process options
1661 0         0 ($dir, $steps) = _options($opts);
1662              
1663             # filter path
1664 0         0 ICC::Shared::filterPath($path);
1665              
1666             # open the file
1667 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
1668              
1669             # disable :crlf translation
1670 0         0 binmode($fh);
1671              
1672             # set output record separator (Windows CR-LF)
1673 0         0 $rs = "\015\012";
1674              
1675             # set color list
1676 0         0 @colors = qw(Cyan Magenta Yellow Black);
1677              
1678             # print colors
1679 0         0 print $fh join(';', @colors), $rs;
1680              
1681             # for each step
1682 0         0 for my $j (0 .. $#{$steps}) {
  0         0  
1683            
1684             # for each channel
1685 0         0 for my $i (0 .. 3) {
1686            
1687             # compute transformed dot value
1688 0         0 $Tdot[$i] = sprintf("%.4f", 100 * ($self->[1][$i]->_transform($dir, $steps->[$j]/100)));
1689            
1690             }
1691            
1692             # print transformed values
1693 0         0 print $fh join(';', @Tdot), $rs;
1694            
1695             }
1696              
1697             # close the file
1698 0         0 close($fh);
1699              
1700             }
1701              
1702             # write Trueflow tone curve file
1703             # assumes curve domain/range is (0 - 1)
1704             # options parameter may be a hash reference or direction flag
1705             # hash key: 'dir'
1706             # direction: 0 - normal, 1 - inverse
1707             # parameters: (file_path, [options])
1708             sub trueflow {
1709              
1710             # get parameters
1711 0     0 0 0 my ($self, $path, $opts) = @_;
1712              
1713             # local variables
1714 0         0 my ($dir, $steps, @names, @colors, @map);
1715 0         0 my ($fh, $in, $out, $dg, @lut, $float);
1716              
1717             # process options
1718 0         0 ($dir, $steps) = _options($opts);
1719              
1720             # set curve names
1721 0         0 @names = qw(Y M C K);
1722              
1723             # set curve display colors (YMCK)
1724 0         0 @colors = (0x00ffff, 0xff00ff, 0xffff00, 0x000000);
1725              
1726             # set color map (YMCK)
1727 0         0 @map = (2, 1, 0, 3);
1728              
1729             # filter path
1730 0         0 ICC::Shared::filterPath($path);
1731              
1732             # open the file
1733 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
1734              
1735             # set binary mode
1736 0         0 binmode($fh);
1737              
1738             # print the header
1739 0         0 print $fh pack('C4a4', 4, 3, 2, 1, 'DGT'); # file signature
1740 0         0 print $fh pack('V', 256); # offset to first curve
1741 0         0 print $fh pack('V', 100); #
1742 0         0 print $fh pack('V', 4); # number of curves
1743 0         0 print $fh pack('V4', 640, 640, 640, 640); # curve block sizes
1744              
1745             # seek start of first curve
1746 0         0 seek($fh, 256, 0);
1747              
1748             # loop thru colors (0-3) (YMCK)
1749 0         0 for my $i (0 .. 3) {
1750            
1751             # print curve name
1752 0         0 print $fh pack('a128', $names[$i]);
1753            
1754             # print display color
1755 0         0 print $fh pack('V', $colors[$i]);
1756            
1757             # print curve parameters (LUT_size, dot_gain_steps, dot_gain_table_size)
1758 0         0 print $fh pack('V3', 256, 15, 240);
1759            
1760             # print binary LUT
1761             #
1762             # for each step
1763 0         0 for my $j (0 .. 255) {
1764            
1765             # compute output value
1766 0         0 $out = $self->[1][$map[$i]]->_transform($dir, $j/255);
1767            
1768             # print LUT value (limited and rounded)
1769 0 0       0 print $fh pack('C', 255 * ($out < 0 ? 0 : ($out > 1 ? 1 : $out)) + 0.5);
    0          
1770            
1771             }
1772            
1773             # print dot gain table
1774             #
1775             # for each tone curve step
1776 0         0 for my $j (0 .. $#{$steps}) {
  0         0  
1777            
1778             # compute input value
1779 0         0 $in = $steps->[$j]/100;
1780            
1781             # compute output value
1782 0         0 $out = $self->[1][$map[$i]]->_transform($dir, $in);
1783            
1784             # compute dot gain (rounded to 0.1%)
1785 0         0 $dg = POSIX::floor(1000 * ($out - $in) + 0.5)/10;
1786            
1787             # print dot gain value (little-endian double)
1788 0         0 print $fh pack('C2 x6 d<', $steps->[$j], 1, $dg);
1789            
1790             }
1791            
1792             }
1793              
1794             # close the file
1795 0         0 close($fh);
1796              
1797             }
1798              
1799             # write tab delimited text tone curve file
1800             # assumes curve domain/range is (0 - 1)
1801             # options parameter may be a hash reference or direction flag
1802             # hash keys: 'dir', 'steps'
1803             # direction: 0 - normal, 1 - inverse
1804             # parameters: (file_path, [options])
1805             sub text {
1806              
1807             # get parameters
1808 0     0 0 0 my ($self, $path, $opts) = @_;
1809              
1810             # local variables
1811 0         0 my ($dir, $steps, $fp, $fh, $rs, @Tdot);
1812              
1813             # process options
1814 0         0 ($dir, $steps) = _options($opts);
1815              
1816             # check for non-integer values
1817 0         0 $fp = grep {$_ != int($_)} @{$steps};
  0         0  
  0         0  
1818              
1819             # filter path
1820 0         0 ICC::Shared::filterPath($path);
1821              
1822             # open the file
1823 0 0       0 open($fh, '>', $path) or croak("can't open $path: $!");
1824              
1825             # disable :crlf translation
1826 0         0 binmode($fh);
1827              
1828             # set output record separator (Windows CR-LF)
1829 0         0 $rs = "\015\012";
1830              
1831             # for each step
1832 0         0 for my $t (@{$steps}) {
  0         0  
1833            
1834             # format input value
1835 0 0       0 $Tdot[0] = $fp ? sprintf("%.2f", $t) : $t;
1836            
1837             # for each channel
1838 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1839            
1840             # compute transformed dot value
1841 0         0 $Tdot[$i + 1] = sprintf("%.2f", 100 * ($self->[1][$i]->_transform($dir, $t/100)));
1842            
1843             }
1844            
1845             # print step values
1846 0         0 print $fh join("\t", @Tdot), $rs;
1847            
1848             }
1849              
1850             # close the file
1851 0         0 close($fh);
1852              
1853             }
1854              
1855             # graph tone curves
1856             # assumes curve domain/range is (0 - 1)
1857             # options parameter may be a hash reference or direction flag
1858             # hash keys: 'dir', 'lib', 'composite', 'titles', 'inks', 'files', 'open'
1859             # direction: 0 - normal, 1 - inverse
1860             # parameters: (folder_path, [options])
1861             # returns: (graph_path_list)
1862             sub graph {
1863              
1864             # get parameters
1865 0     0 0 0 my ($self, $path, $opts) = @_;
1866              
1867             # local variables
1868 0         0 my ($dir, $include, $tt, $vars, $min, $max, @inks, %exc, @colors, @data, @tooltips, $file, $s, @html);
1869              
1870             # process options
1871 0         0 ($dir) = _options($opts);
1872              
1873             # if ICC::Templates folder is found in @INC (may be relative)
1874 0 0       0 if (($include) = grep {-d} map {File::Spec->catdir($_, 'ICC', 'Templates')} @INC) {
  0         0  
  0         0  
1875            
1876             # purify folder path
1877 0         0 ICC::Shared::filterPath($path);
1878            
1879             # make a template processing object
1880 0         0 $tt = Template->new({'INCLUDE_PATH' => $include, 'OUTPUT_PATH' => $path});
1881            
1882             # if gray scale curve
1883 0 0       0 if ($#{$self->[1]} == 0) {
  0 0       0  
1884            
1885             # set default ink color
1886 0         0 @inks = qw(gray);
1887            
1888             # if RGB curves
1889 0         0 } elsif ($#{$self->[1]} == 2) {
1890            
1891             # set default ink colors
1892 0         0 @inks = qw(red green blue);
1893            
1894             # if CMYK+ curves
1895             } else {
1896            
1897             # set default ink colors
1898 0         0 @inks = qw(cyan magenta yellow black);
1899            
1900             }
1901            
1902             # for each curve
1903 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1904            
1905             # set ink value, defaults to 'inkN'
1906 0   0     0 $inks[$i] = $opts->{'inks'}[$i] // $self->[0]{'inks'}[$i] // $inks[$i] // sprintf("ink%d", $i + 1);
      0        
      0        
1907            
1908             }
1909            
1910             # graph color exceptions
1911 0         0 %exc = ('yellow' => '#ee0', 'orange' => '#f80', 'violet' => '#80f', 'gray' => '#777');
1912            
1913             # get graph colors, mapping exceptions
1914 0 0 0     0 @colors = map {$exc{$_} // $_} map {m/^ink_/ ? 'gray' : $_} @inks;
  0         0  
  0         0  
1915            
1916             # set RGraph library folder
1917 0   0     0 $vars->{'libjs'} = $opts->{'lib'} // 'lib';
1918            
1919             # set yaxis scale
1920 0   0     0 $vars->{'yscalemin'} = $min = $opts->{'yscalemin'} // 0.0;
1921 0   0     0 $vars->{'yscalemax'} = $max = $opts->{'yscalemax'} // 1.0;
1922            
1923             # if 'composite' curve
1924 0 0       0 if ($opts->{'composite'}) {
1925            
1926             # for each curve
1927 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1928            
1929             # compute curve data
1930 0         0 @data = map {sprintf("%.3f", $self->[1][$i]->_transform($dir, $_/100))} (0 .. 100);
  0         0  
1931            
1932             # clip the data
1933 0 0       0 @data = map {$_ < $min ? $min : $_ > $max ? $max : $_} @data;
  0 0       0  
1934            
1935             # make javascript string of curve data
1936 0         0 $s->[$i] = '[' . join(', ', @data) . ']';
1937            
1938             }
1939            
1940             # make composite javascript string of curve data
1941 0         0 $vars->{'data'} = '[' . join(', ', @{$s}) . ']';
  0         0  
1942            
1943             # disable tooltips
1944 0         0 $vars->{'tooltips'} = '[]';
1945            
1946             # set graph title
1947 0   0     0 $vars->{'title'} = $opts->{'titles'}[0] // "composite tone curves";
1948            
1949             # set graph colors
1950 0         0 $vars->{'colors'} = '[' . join(', ', map {"'$_'"} @colors) . ']';
  0         0  
1951            
1952             # get file name
1953 0   0     0 $file = $opts->{'files'}[0] // 'composite';
1954            
1955             # process the template
1956 0 0       0 $tt->process('cvst_graph_svg.tt2', $vars, "$file.html") || CORE::die $tt->error();
1957            
1958             } else {
1959            
1960             # for each curve
1961 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
1962            
1963             # compute curve data
1964 0         0 @data = map {sprintf("%.3f", $self->[1][$i]->_transform($dir, $_/100))} (0 .. 100);
  0         0  
1965            
1966             # clip the data
1967 0 0       0 @data = map {$_ < $min ? $min : $_ > $max ? $max : $_} @data;
  0 0       0  
1968            
1969             # make javascript string of curve data
1970 0         0 $vars->{'data'} = '[[' . join(', ', @data) . ']]';
1971            
1972             # compute tooltips array
1973 0 0       0 @tooltips = map {$_ % 5 ? 'null' : sprintf("'%d%% ➔ %.1f%%'", $_, 100 * $data[$_])} (0 .. 100);
  0         0  
1974            
1975             # make tooltips
1976 0         0 $vars->{'tooltips'} = '[' . join(', ', @tooltips) . ']';
1977            
1978             # set graph title
1979 0   0     0 $vars->{'title'} = $opts->{'titles'}[$i] // "$inks[$i] tone curve";
1980            
1981             # set graph color
1982 0         0 $vars->{'colors'} = "['$colors[$i]']";
1983            
1984             # get file name
1985 0   0     0 $file = $opts->{'files'}[$i] // $inks[$i];
1986            
1987             # process the template
1988 0 0       0 $tt->process('cvst_graph_svg.tt2', $vars, "$file.html") || CORE::die $tt->error();
1989            
1990             }
1991            
1992             }
1993            
1994             # if 'composite' curve
1995 0 0       0 if ($opts->{'composite'}) {
1996            
1997             # if Windows OS
1998 0 0       0 if ($^O eq 'MSWin32') {
1999            
2000             # set file list
2001 0         0 @html = ("$path\\$file.html");
2002            
2003             } else {
2004            
2005             # set file list
2006 0         0 @html = ("$path/$file.html");
2007            
2008             }
2009            
2010             } else {
2011            
2012             # for each curve
2013 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2014            
2015             # get file name
2016 0   0     0 $file = $opts->{'files'}[$i] // $inks[$i];
2017            
2018             # if Windows OS
2019 0 0       0 if ($^O eq 'MSWin32') {
2020            
2021             # add path to file list
2022 0         0 push(@html, "$path\\$file.html");
2023            
2024             } else {
2025            
2026             # add path to file list
2027 0         0 push(@html, "$path/$file.html");
2028            
2029             }
2030            
2031             }
2032            
2033             }
2034            
2035             }
2036              
2037             # open files, if enabled
2038 0 0 0     0 open_files(\@html) if ($opts->{'open'} // 1);
2039              
2040             # return
2041 0         0 return(@html);
2042              
2043             }
2044              
2045             # display graphs in web browser
2046             # parameters: (ref_to_file_list)
2047             sub open_files {
2048              
2049             # get file list
2050 0     0 0 0 my $files = shift();
2051              
2052             # local parameters
2053 0         0 my ($RGraph, $vol, $dir, $file, $lib, $app, @fox, @pid, @esc, $esc0, $flag, $timeout);
2054              
2055             # find RGraph folder path in @INC (may be relative)
2056 0         0 ($RGraph) = grep {-d} map {File::Spec->catdir($_, 'ICC', 'JavaScripts', 'RGraph')} @INC;
  0         0  
  0         0  
2057              
2058             # if valid file list and RGraph folder found
2059 0 0 0     0 if (ref($files) eq 'ARRAY' && defined($files->[0]) && -f $files->[0] && defined($RGraph)) {
      0        
      0        
2060            
2061             # split first file path
2062 0         0 ($vol, $dir, $file) = File::Spec->splitpath($files->[0]);
2063            
2064             # make 'lib' folder path
2065 0         0 $lib = File::Spec->catdir($vol, $dir, 'lib');
2066            
2067             # if macOS
2068 0 0       0 if ($^O eq 'darwin') {
    0          
2069            
2070             # copy RGraph JavaScripts to 'lib' folder
2071 0         0 qx(cp -Rp '$RGraph/' '$lib');
2072            
2073             # escape the file paths
2074 0         0 @esc = map {quotemeta()} @{$files};
  0         0  
  0         0  
2075            
2076             # get default app (using JXA)
2077 0         0 $app = qx(osascript -l JavaScript -e "Application('System Events').files.byName('$files->[0]').defaultApplication.name()");
2078            
2079             # remove endline
2080 0         0 chomp($app);
2081            
2082             # if default app is Firefox
2083 0 0       0 if ($app eq 'Firefox.app') {
2084            
2085             # get first file path
2086 0         0 $esc0 = shift(@esc);
2087            
2088             # open first graph
2089 0         0 qx(open $esc0);
2090            
2091             # if more graphs
2092 0 0       0 if (@esc) {
2093            
2094             # set timeout (5 secs)
2095 0         0 $timeout = time() + 5;
2096            
2097             # loop until we get Firefox pid -or- timeout
2098 0   0     0 while (! @fox && time() < $timeout) {
2099            
2100             # get Firefox pid
2101 0         0 @fox = split(/\s+/, qx(pgrep firefox));
2102            
2103             }
2104            
2105             # set flag
2106 0         0 $flag = 1;
2107            
2108             # loop until flag is cleared -or- timeout
2109 0   0     0 while ($flag && time() < $timeout) {
2110            
2111             # if 4 or more child processes
2112 0 0       0 if ((@pid = split(/\s+/, qx(pgrep -P $fox[0]))) > 3) {
2113            
2114             # for each child process
2115 0         0 for (@pid) {
2116            
2117             # clear flag if ps command contains '-sbAllowFileAccess'
2118 0 0       0 $flag = 0 if (qx(ps -p $_ -o command) =~ m/-sbAllowFileAccess/m);
2119            
2120             }
2121            
2122             }
2123            
2124             }
2125            
2126             # open remaining graphs
2127 0         0 qx(open @esc);
2128            
2129             }
2130            
2131             } else {
2132            
2133             # open all graphs
2134 0         0 qx(open @esc);
2135            
2136             }
2137            
2138             # if Windows OS
2139             } elsif ($^O eq 'MSWin32') {
2140            
2141             # copy RGraph JavaScripts to 'lib' folder
2142 0         0 qx(xcopy /I "$RGraph\\" "$lib\\");
2143            
2144 0         0 print "to be implemented\n\n"; ###########
2145            
2146             } else {
2147            
2148 0         0 print "unsupported OS\n\n";
2149            
2150             }
2151            
2152             }
2153              
2154             # return
2155 0         0 return();
2156              
2157             }
2158              
2159             # normalize all curve objects
2160             # sets the domain and range of curves
2161             # parameters: (as_appropriate_for_curve_objects)
2162             sub normalize {
2163              
2164             # get object reference
2165 0     0 0 0 my $self = shift();
2166              
2167             # for each channel
2168 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2169            
2170             # if curve object has 'normalize' method
2171 0 0       0 if ($self->[1][$i]->can('normalize')) {
2172            
2173             # call 'normalize' method
2174 0         0 $self->[1][$i]->normalize(@_);
2175            
2176             } else {
2177            
2178             # warning
2179 0         0 carp('\'normalize\' method not supported by ' . ref($self->[1][$i]) . ' object');
2180            
2181             }
2182            
2183             }
2184            
2185             }
2186              
2187             # update all curve objects
2188             # update internal object elements
2189             # this method used primarily when optimizing
2190             # parameters: (as_appropriate_for_curve_objects)
2191             sub update {
2192              
2193             # get object reference
2194 0     0 0 0 my $self = shift();
2195              
2196             # for each channel
2197 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2198            
2199             # if curve object has 'update' method
2200 0 0       0 if ($self->[1][$i]->can('update')) {
2201            
2202             # call 'update' method
2203 0         0 $self->[1][$i]->update(@_);
2204            
2205             } else {
2206            
2207             # warning
2208 0         0 carp('\'update\' method not supported by ' . ref($self->[1][$i]) . ' object');
2209            
2210             }
2211            
2212             }
2213            
2214             }
2215              
2216             # print object contents to string
2217             # format is an array structure
2218             # parameter: ([format])
2219             # returns: (string)
2220             sub sdump {
2221              
2222             # get parameters
2223 0     0 1 0 my ($self, $p) = @_;
2224              
2225             # local variables
2226 0         0 my ($element, $fmt, $s, $pt, $st);
2227              
2228             # resolve parameter to an array reference
2229 0 0       0 $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
2230              
2231             # get format string
2232 0 0 0     0 $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 's';
2233              
2234             # set string to object ID
2235 0         0 $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
2236              
2237             # if format contains 'o'
2238 0 0       0 if ($fmt =~ m/s/) {
2239            
2240             # get default parameter
2241 0         0 $pt = $p->[-1];
2242            
2243             # for each processing element
2244 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2245            
2246             # get element reference
2247 0         0 $element = $self->[1][$i];
2248            
2249             # if processing element is undefined
2250 0 0       0 if (! defined($element)) {
    0          
    0          
2251            
2252             # append message
2253 0         0 $s .= "\tprocessing element is undefined\n";
2254            
2255             # if processing element is not a blessed object
2256             } elsif (! Scalar::Util::blessed($element)) {
2257            
2258             # append message
2259 0         0 $s .= "\tprocessing element is not a blessed object\n";
2260            
2261             # if processing element has an 'sdump' method
2262             } elsif ($element->can('sdump')) {
2263            
2264             # get 'sdump' string
2265 0 0       0 $st = $element->sdump(defined($p->[$i + 1]) ? $p->[$i + 1] : $pt);
2266            
2267             # prepend tabs to each line
2268 0         0 $st =~ s/^/\t/mg;
2269            
2270             # append 'sdump' string
2271 0         0 $s .= $st;
2272            
2273             # processing element is object without an 'sdump' method
2274             } else {
2275            
2276             # append object info
2277 0         0 $s .= sprintf("\t'%s' object, (0x%x)\n", ref($element), $element);
2278            
2279             }
2280            
2281             }
2282            
2283             }
2284              
2285             # return
2286 0         0 return($s);
2287              
2288             }
2289              
2290             # transform list
2291             # parameters: (ref_to_object, list, [hash])
2292             # returns: (list)
2293             sub _trans0 {
2294              
2295             # local variables
2296 0     0   0 my ($self, @out, $hash);
2297              
2298             # get object reference
2299 0         0 $self = shift();
2300              
2301             # get optional hash
2302 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
2303              
2304             # for each channel
2305 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2306            
2307             # compute transform
2308 0         0 $out[$i] = $self->[1][$i]->transform($_[$i]);
2309            
2310             }
2311              
2312             # clip, if enabled
2313 0 0 0     0 ICC::Shared::clip_struct(\@out) if ($self->[0]{'clip'} || $hash->{'clip'});
2314              
2315             # return output array
2316 0         0 return(@out);
2317              
2318             }
2319              
2320             # transform vector
2321             # parameters: (ref_to_object, vector, [hash])
2322             # returns: (vector)
2323             sub _trans1 {
2324              
2325             # get parameters
2326 0     0   0 my ($self, $in, $hash) = @_;
2327              
2328             # local variable
2329 0         0 my ($out);
2330              
2331             # for each channel
2332 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2333            
2334             # compute transform
2335 0         0 $out->[$i] = $self->[1][$i]->transform($in->[$i]);
2336            
2337             }
2338              
2339             # clip, if enabled
2340 0 0 0     0 ICC::Shared::clip_struct($out) if ($self->[0]{'clip'} || $hash->{'clip'});
2341              
2342             # return
2343 0         0 return($out);
2344              
2345             }
2346              
2347             # transform matrix (2-D array -or- Math::Matrix object)
2348             # parameters: (ref_to_object, matrix, [hash])
2349             # returns: (matrix)
2350             sub _trans2 {
2351              
2352             # get parameters
2353 0     0   0 my ($self, $in, $hash) = @_;
2354              
2355             # local variable
2356 0         0 my ($out);
2357              
2358             # for each input vector
2359 0         0 for my $i (0 .. $#{$in}) {
  0         0  
2360            
2361             # for each channel
2362 0         0 for my $j (0 .. $#{$self->[1]}) {
  0         0  
2363            
2364             # compute transform
2365 0         0 $out->[$i][$j] = $self->[1][$j]->transform($in->[$i][$j]);
2366            
2367             }
2368            
2369             }
2370              
2371             # clip, if enabled
2372 0 0 0     0 ICC::Shared::clip_struct($out) if ($self->[0]{'clip'} || $hash->{'clip'});
2373              
2374             # return
2375 0 0       0 return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
2376              
2377             }
2378              
2379             # transform structure
2380             # parameters: (ref_to_object, structure, [hash])
2381             # returns: (structure)
2382             sub _trans3 {
2383              
2384             # get parameters
2385 0     0   0 my ($self, $in, $hash) = @_;
2386              
2387             # transform the array structure
2388 0         0 _crawl($self, $in, my $out = [], $hash);
2389              
2390             # clip, if enabled
2391 0 0 0     0 ICC::Shared::clip_struct($out) if ($self->[0]{'clip'} || $hash->{'clip'});
2392              
2393             # return
2394 0         0 return($out);
2395              
2396             }
2397              
2398             # recursive transform
2399             # array structure is traversed until scalar arrays are found and transformed
2400             # parameters: (ref_to_object, input_array_reference, output_array_reference, hash)
2401             sub _crawl {
2402              
2403             # get parameters
2404 0     0   0 my ($self, $in, $out, $hash) = @_;
2405              
2406             # if input is a vector (reference to a scalar array)
2407 0 0       0 if (@{$in} == grep {! ref()} @{$in}) {
  0         0  
  0         0  
  0         0  
2408            
2409             # transform input vector and copy to output
2410 0         0 @{$out} = @{_trans1($self, $in, $hash)};
  0         0  
  0         0  
2411            
2412             } else {
2413            
2414             # for each input element
2415 0         0 for my $i (0 .. $#{$in}) {
  0         0  
2416            
2417             # if an array reference
2418 0 0       0 if (ref($in->[$i]) eq 'ARRAY') {
2419            
2420             # transform next level
2421 0         0 _crawl($self, $in->[$i], $out->[$i] = [], $hash);
2422            
2423             } else {
2424            
2425             # error
2426 0         0 croak('invalid transform input');
2427            
2428             }
2429            
2430             }
2431            
2432             }
2433            
2434             }
2435              
2436             # invert list
2437             # parameters: (ref_to_object, list, [hash])
2438             # returns: (list)
2439             sub _inv0 {
2440              
2441             # local variables
2442 0     0   0 my ($self, $hash, @out);
2443              
2444             # get object reference
2445 0         0 $self = shift();
2446              
2447             # get optional hash
2448 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
2449              
2450             # for each channel
2451 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2452            
2453             # compute invert
2454 0         0 $out[$i] = $self->[1][$i]->inverse($_[$i]);
2455            
2456             }
2457              
2458             # clip, if enabled
2459 0 0 0     0 ICC::Shared::clip_struct(\@out) if ($self->[0]{'clip'} || $hash->{'clip'});
2460              
2461             # return output array
2462 0         0 return(@out);
2463              
2464             }
2465              
2466             # invert vector
2467             # parameters: (ref_to_object, vector, [hash])
2468             # returns: (vector)
2469             sub _inv1 {
2470              
2471             # get parameters
2472 0     0   0 my ($self, $in, $hash) = @_;
2473              
2474             # local variable
2475 0         0 my ($out);
2476              
2477             # for each channel
2478 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
2479            
2480             # compute invert
2481 0         0 $out->[$i] = $self->[1][$i]->inverse($in->[$i]);
2482            
2483             }
2484              
2485             # clip, if enabled
2486 0 0 0     0 ICC::Shared::clip_struct($out) if ($self->[0]{'clip'} || $hash->{'clip'});
2487              
2488             # return
2489 0         0 return($out);
2490              
2491             }
2492              
2493             # invert matrix (2-D array -or- Math::Matrix object)
2494             # parameters: (ref_to_object, matrix, [hash])
2495             # returns: (matrix)
2496             sub _inv2 {
2497              
2498             # get parameters
2499 0     0   0 my ($self, $in, $hash) = @_;
2500              
2501             # local variable
2502 0         0 my ($out);
2503              
2504             # for each input vector
2505 0         0 for my $i (0 .. $#{$in}) {
  0         0  
2506            
2507             # for each channel
2508 0         0 for my $j (0 .. $#{$self->[1]}) {
  0         0  
2509            
2510             # compute invert
2511 0         0 $out->[$i][$j] = $self->[1][$j]->inverse($in->[$i][$j]);
2512            
2513             }
2514            
2515             }
2516              
2517             # clip, if enabled
2518 0 0 0     0 ICC::Shared::clip_struct($out) if ($self->[0]{'clip'} || $hash->{'clip'});
2519              
2520             # return
2521 0 0       0 return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
2522              
2523             }
2524              
2525             # invert structure
2526             # parameters: (ref_to_object, structure, [hash])
2527             # returns: (structure)
2528             sub _inv3 {
2529              
2530             # get parameters
2531 0     0   0 my ($self, $in, $hash) = @_;
2532              
2533             # recursive inverse
2534 0         0 _crawl2($self, $in, my $out = []);
2535              
2536             # clip, if enabled
2537 0 0 0     0 ICC::Shared::clip_struct($out) if ($self->[0]{'clip'} || $hash->{'clip'});
2538              
2539             # return
2540 0         0 return($out);
2541              
2542             }
2543              
2544             # recursive inverse
2545             # array structure is traversed until scalar arrays are found and inverted
2546             # parameters: (ref_to_object, input_array_reference, output_array_reference, hash)
2547             sub _crawl2 {
2548            
2549             # get parameters
2550 0     0   0 my ($self, $in, $out, $hash) = @_;
2551            
2552             # if input is a vector (reference to a scalar array)
2553 0 0       0 if (@{$in} == grep {! ref()} @{$in}) {
  0         0  
  0         0  
  0         0  
2554            
2555             # invert input vector and copy to output
2556 0         0 @{$out} = @{_inv1($self, $in, $hash)};
  0         0  
  0         0  
2557            
2558             } else {
2559            
2560             # for each input element
2561 0         0 for my $i (0 .. $#{$in}) {
  0         0  
2562            
2563             # if an array reference
2564 0 0       0 if (ref($in->[$i]) eq 'ARRAY') {
2565            
2566             # invert next level
2567 0         0 _crawl2($self, $in->[$i], $out->[$i] = []);
2568            
2569             } else {
2570            
2571             # error
2572 0         0 croak('invalid inverse input');
2573            
2574             }
2575            
2576             }
2577            
2578             }
2579            
2580             }
2581              
2582             # process the curve output options parameter
2583             # the parameter may be a scalar or hash reference
2584             # output is based on the name of the calling method
2585             # parameter: ([options])
2586             # returns: (direction_flag, steps)
2587             sub _options {
2588              
2589             # get options
2590 0     0   0 my $opts = $_[0];
2591              
2592             # local variable
2593 0         0 my ($dir, $steps, @ctx, $caller, $n);
2594              
2595             # make hash of standard step ramps (method_name => [ref_to_steps_array, custom_flag])
2596             # steps array contains device values (%), flag indicate values may be custom
2597             state $std = {
2598 0         0 'apogee' => [[0 .. 6, (map {5 * $_} 2 .. 18), 94 .. 100], 1],
2599             'device_link' => [[], 0],
2600 0         0 'cgats' => [[0, 2, 4, 6, 8, (map {5 * $_} 2 .. 19), 98, 100], 1], # P2P51 ramp
2601 0         0 'efi' => [[0 .. 3, map {5 * $_} 1 .. 20], 0],
2602             'fuji_xmf' => [[0 .. 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95 .. 100], 0],
2603             'harlequin' => [[100, 95, 90, 85, 80, 70, 60, 50, 40, 30, 20, 15, 10, 8, 6, 4, 2, 0], 0],
2604 0         0 'indigo' => [[map {5 * $_} 0 .. 20], 0],
2605 0         0 'iso_18620' => [[0, 1, 2, 5, (map {10 * $_} (1 .. 9)), 95, 100], 1],
2606             'navigator' => [[100, 95, 90, 85, 80, 70, 60, 50, 40, 30, 20, 15, 10, 8, 6, 4, 2, 0], 0],
2607             'photoshop' => [[], 1],
2608             'prinergy' => [[0 .. 100], 0],
2609 0         0 'rampage' => [[0, 1, 3, (map {5 * $_} 1 .. 19), 97, 99, 100], 0],
2610             'sierra' => [[0 .. 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95 .. 100], 0],
2611 0         0 'trueflow' => [[0, 2, 5, (map {10 * $_} 1 .. 9), 95, 98, 100], 0],
2612 0         0 'text' => [[map {$_ * 5} 0 .. 20], 1],
  0         0  
2613             'graph' => [[], 0],
2614             };
2615              
2616             # match caller method name
2617 0 0       0 $ctx[3] =~ m/::(\w+)$/ if (@ctx = caller(1));
2618              
2619             # set caller, default is 'text'
2620 0 0 0     0 $caller = defined($1) && exists($std->{$1}) ? $1 : 'text';
2621              
2622             # set default direction (forward)
2623 0         0 $dir = 0;
2624              
2625             # set default steps value for caller
2626 0         0 $steps = $std->{$caller}[0];
2627              
2628             # return if options undefined
2629 0 0       0 return($dir, $steps) if (! defined($opts));
2630              
2631             # if options is a scalar
2632 0 0       0 if (! ref($opts)) {
    0          
2633            
2634             # set direction
2635 0 0       0 $dir = $opts ? 1 : 0;
2636            
2637             # undefine options (for caller, $_[0] is an alias)
2638 0         0 undef($_[0]);
2639            
2640             # if options is a hash ref
2641             } elsif (ref($opts) eq 'HASH') {
2642            
2643             # use 'dir' hash value, if any
2644 0   0     0 $dir = $opts->{'dir'} // 0;
2645            
2646             # if 'steps' defined in hash
2647 0 0       0 if (defined($opts->{'steps'})) {
2648            
2649             # if custom step values allowed
2650 0 0       0 if ($std->{$caller}[1]) {
2651            
2652             # set steps to hash value
2653 0         0 $steps = $opts->{'steps'};
2654            
2655             # if 'steps' value is a numeric vector
2656 0 0       0 if (ICC::Shared::is_num_vector($steps)) {
    0          
    0          
2657            
2658             # warn if values out of range (0 - 100)
2659 0 0       0 (0 == grep {$_ < 0 || $_ > 100} @{$steps}) or carp("'steps' value(s) out of range\n");
  0 0       0  
  0         0  
2660            
2661             # if 'steps' value is a number
2662             } elsif (Scalar::Util::looks_like_number($steps)) {
2663            
2664             # set upper range
2665 0         0 $n = int($steps) - 1;
2666            
2667             # limit number of steps (1 - 255)
2668 0 0       0 $n = $n < 1 ? 1 : $n > 255 ? 255 : $n;
    0          
2669            
2670             # set steps
2671 0         0 $steps = [map {100 * $_/$n} (0 .. $n)];
  0         0  
2672            
2673             # if 'steps' value is a string
2674             } elsif (! ref($steps)) {
2675            
2676             # if string a valid key
2677 0 0       0 if (exists($std->{$steps})) {
2678            
2679             # set steps
2680 0         0 $steps = $std->{$steps}[0];
2681            
2682             } else {
2683            
2684             # print warning
2685 0         0 carp("'steps' value '$steps' is invalid, using default steps\n");
2686            
2687             # set steps
2688 0         0 $steps = $std->{$caller}[0];
2689            
2690             }
2691            
2692             } else {
2693            
2694             # print warning
2695 0         0 carp("'steps' value must be a scalar or an array reference\n");
2696            
2697             }
2698            
2699             } else {
2700            
2701             # print warning
2702 0         0 carp("custom step values not allowed in $caller curves\n");
2703            
2704             }
2705            
2706             }
2707            
2708             } else {
2709            
2710             # print warning
2711 0         0 carp("options parameter must be a scalar or hash reference\n");
2712            
2713             }
2714              
2715             # return
2716 0         0 return($dir, $steps);
2717              
2718             }
2719              
2720             # read curves from text file
2721             # returns true if successful
2722             # parameters: (ref_to_object, file_handle)
2723             # returns: (flag)
2724             sub _read_text {
2725              
2726             # get parameters
2727 0     0   0 my ($self, $fh) = @_;
2728              
2729             # local variables
2730 0         0 my (@data, @cnt, $n, $last, $f, $mat);
2731              
2732             # localize input record separator
2733 0         0 local $/ = $self->[0]{'read_rs'};
2734              
2735             # localize loop variable
2736 0         0 local $_;
2737              
2738             # read the file, line by line
2739 0         0 while (<$fh>) {
2740            
2741             # split the line, and filter numeric values
2742 0         0 push(@data, [grep {Scalar::Util::looks_like_number($_)} split('[\s"]')]);
  0         0  
2743            
2744             }
2745              
2746             # for each line
2747 0         0 for my $line (@data) {
2748            
2749             # increment count
2750 0         0 $cnt[@{$line}]++
  0         0  
2751            
2752             }
2753              
2754             # get index with max count
2755 0 0 0 0   0 $n = List::Util::reduce {($cnt[$a] // 0) > ($cnt[$b] // 0) ? $a : $b} (1 .. $#cnt);
  0   0     0  
2756              
2757             # filter out extraneous lines
2758 0         0 @data = grep {$n == @{$_}} @data;
  0         0  
  0         0  
2759              
2760             # verify data table size
2761 0 0 0     0 (@data > 1 && @{$data[0]} > 1) or return(0);
  0         0  
2762              
2763             # sort by first value in each line
2764 0         0 @data = sort {$a->[0] <=> $b->[0]} @data;
  0         0  
2765              
2766             # filter any duplicates
2767 0 0       0 @data = grep {$f = (defined($last) ? $last->[0] != $_->[0] : 1); $last = $_; $f} @data;
  0         0  
  0         0  
  0         0  
2768              
2769             # convert to device values
2770 0         0 @data = map {[map {$_/100} @{$_}]} @data;
  0         0  
  0         0  
  0         0  
2771              
2772             # make a transposed matrix of the data
2773 0         0 $mat = Math::Matrix->new(@data)->transpose();
2774              
2775             # for each channel
2776 0         0 for my $i (1 .. $#{$mat}) {
  0         0  
2777            
2778             # it is assumed the first column of numbers are the input values
2779             # and the remaining columns are output values for each channel
2780            
2781             # add a 'spline' curve
2782 0         0 $self->[1][$i - 1] = ICC::Support::spline->new({'input' => $mat->[0], 'output' => $mat->[$i], 'type' => 'akima'});
2783            
2784             }
2785              
2786             # return
2787 0         0 return(1);
2788              
2789             }
2790              
2791             # read curves from ISO 18620 file
2792             # returns true if successful
2793             # parameters: (ref_to_object, file_handle)
2794             # returns: (flag)
2795             sub _read_iso_18620 {
2796              
2797             # get parameters
2798 0     0   0 my ($self, $fh) = @_;
2799              
2800             # local variables
2801 0         0 my ($dom, $root, @obj, $k, @sep, $curve, @xy, @x, @y);
2802              
2803             # parse ISO 18620 document
2804 0 0       0 eval{$dom = XML::LibXML->load_xml('IO' => $fh)} or return(0);
  0         0  
2805              
2806             # get root element
2807 0         0 $root = $dom->documentElement();
2808              
2809             # get all nodes (we select later)
2810 0         0 @obj = $root->findnodes('*');
2811              
2812             # init curve counter
2813 0         0 $k = 0;
2814              
2815             # for each element
2816 0         0 for my $s (@obj) {
2817            
2818             # if a 'TransferCurve' node
2819 0 0       0 if ($s->nodeName() eq 'TransferCurve') {
2820            
2821             # get the Separation attribute
2822 0         0 $sep[$k] = $s->getAttribute('Separation');
2823            
2824             # get the Curve attribute
2825 0         0 $curve = $s->getAttribute('Curve');
2826            
2827             # split the Curve data
2828 0         0 @xy = split('\s', $curve);
2829            
2830             # init value arrays
2831 0         0 @x = @y = ();
2832            
2833             # for each value
2834 0         0 for my $i (0 .. $#xy) {
2835            
2836             # if index is odd
2837 0 0       0 if ($i % 2) {
2838            
2839             # save as y-value
2840 0         0 $y[int($i/2)] = $xy[$i];
2841            
2842             } else {
2843            
2844             # save as x-value
2845 0         0 $x[int($i/2)] = $xy[$i];
2846            
2847             }
2848            
2849             }
2850            
2851             # add a 'spline' curve to object
2852 0         0 $self->[1][$k++] = ICC::Support::spline->new({'input' => \@x, 'output' => \@y, 'type' => 'akima'});
2853            
2854             }
2855            
2856             }
2857              
2858             # add ink sequence
2859 0         0 $self->[0]{'inks'} = \@sep;
2860              
2861             # return
2862 0         0 return($k);
2863              
2864             }
2865              
2866             # read curves from Esko .icpro/.dgc file set
2867             # returns true if successful
2868             # parameters: (ref_to_object, file_handle, path)
2869             # returns: (flag)
2870             sub _read_icpro {
2871              
2872             # get parameters
2873 0     0   0 my ($self, $fh, $path) = @_;
2874              
2875             # local variables
2876 0         0 my ($dom, $root, @obj, $k, @sep, $dgc, $curve);
2877 0         0 my ($vol, $dir, $file, $fh2, $buf, $ptr, $n, $max, @data);
2878              
2879             # split .icpro path
2880 0         0 ($vol, $dir, $file) = File::Spec->splitpath($path);
2881              
2882             # parse .icpro document
2883 0 0       0 eval{$dom = XML::LibXML->load_xml('IO' => $fh)} or return(0);
  0         0  
2884              
2885             # get root element
2886 0         0 $root = $dom->documentElement();
2887              
2888             # get all 'ink' nodes
2889 0         0 @obj = $root->findnodes('*/ink');
2890              
2891             # init curve counter
2892 0         0 $k = 0;
2893              
2894             # for each 'ink' node
2895 0         0 for my $s (@obj) {
2896            
2897             # if there is a 'dgc' node
2898 0 0       0 if (($dgc) = $s->findnodes('dgc')) {
2899            
2900             # get the 'fileName' attribute
2901 0         0 $file = $dgc->getAttribute('fileName');
2902            
2903             # concatenate file path
2904 0         0 $path = File::Spec->catfile($vol, $dir, $file);
2905            
2906             # open the file (read-only)
2907 0 0       0 open($fh2, '<', $path) or croak("$! when opening file $path");
2908            
2909             # seek table 4 index
2910 0         0 seek($fh2, 0x0000020C, 0);
2911            
2912             # read index to table 4
2913 0         0 read($fh2, $buf, 12);
2914            
2915             # unpack table pointer, number of points
2916 0         0 ($ptr, $n, $max) = unpack('N*', $buf);
2917            
2918             # seek table 4
2919 0         0 seek($fh2, $ptr, 0);
2920            
2921             # read table data
2922 0         0 read($fh2, $buf, $n * 4);
2923            
2924             # unpack table data
2925 0         0 @data = map {$_/$max} unpack('N*', $buf);
  0         0  
2926            
2927             # close file
2928 0         0 close($fh2);
2929            
2930             # make 'curv' object
2931 0         0 $self->[1][$k] = ICC::Profile::curv->new(\@data);
2932            
2933             # get the 'inkName' attribute
2934 0         0 $sep[$k++] = $s->getAttribute('inkName');
2935            
2936             }
2937            
2938             }
2939              
2940             # add ink sequence
2941 0         0 $self->[0]{'inks'} = \@sep;
2942              
2943             # return
2944 0         0 return($k);
2945              
2946             }
2947              
2948             # read curves from file
2949             # file path to 'iso_18620', 'icpro', 'text' or 'store' format curves
2950             # parameters: (ref_to_object, file_path)
2951             sub _new_from_file {
2952              
2953             # get parameters
2954 0     0   0 my ($self, $path) = @_;
2955              
2956             # local variables
2957 0         0 my ($fh, $buf, $result, $obj);
2958              
2959             # filter path name
2960 0         0 ICC::Shared::filterPath($path);
2961              
2962             # open the file (read-only)
2963 0 0       0 open($fh, '<', $path) or croak("$! when opening file $path");
2964              
2965             # set binary mode
2966 0         0 binmode($fh);
2967              
2968             # read start of file
2969 0 0       0 read($fh, $buf, 1024) or croak("file $path is zero length");
2970              
2971             # reset file pointer
2972 0         0 seek($fh, 0, 0);
2973              
2974             # if an ISO 18620 (.ted) file
2975 0 0 0     0 if ($buf =~ m/<\?xml.*\?>/ && $buf =~ m/ISO18620/) {
    0 0        
    0          
2976            
2977             # read ISO 18620 file
2978 0 0       0 _read_iso_18620($self, $fh) or croak("failed parsing ISO 18620 (XML) file $path");
2979            
2980             # save file type
2981 0         0 $self->[0]{'file_type'} = 'ISO_18620';
2982            
2983             # if an Esko .icpro file
2984             } elsif ($buf =~ m/<\?xml.*\?>/ && $buf =~ m/colDgc_xml/) {
2985            
2986             # read .icpro/.dgc file set
2987 0 0       0 _read_icpro($self, $fh, $path) or croak("failed parsing .icpro/.dgc file set $path");
2988            
2989             # save file type
2990 0         0 $self->[0]{'file_type'} = 'ESKO_ICPRO';
2991            
2992             # if a Storable file
2993             } elsif ($buf =~ m/ICC::Profile::cvst/) {
2994            
2995             # retrieve 'cvst' object from Storable file
2996 0 0       0 ($obj = Storable::fd_retrieve($fh)) or croak("failed retrieving Storable object $path");
2997            
2998             # verify a cvst object
2999 0 0       0 (UNIVERSAL::isa($obj, 'ICC::Profile::cvst')) or croak("not a 'cvst' object, retrieved from $path");
3000            
3001             # copy object elements
3002 0         0 @{$self} = @{$obj};
  0         0  
  0         0  
3003            
3004             # save file type
3005 0         0 $self->[0]{'file_type'} = 'STORABLE';
3006            
3007             } else {
3008            
3009             # check for CR-LF (DOS/Windows)
3010 0 0       0 if ($buf =~ m/\015\012/) {
    0          
    0          
3011            
3012             # set record separator
3013 0         0 $self->[0]{'read_rs'} = "\015\012";
3014            
3015             # check for LF (Unix/OSX)
3016             } elsif ($buf =~ m/\012/) {
3017            
3018             # set record separator
3019 0         0 $self->[0]{'read_rs'} = "\012";
3020            
3021             # check for CR (Mac)
3022             } elsif ($buf =~ m/\015/) {
3023            
3024             # set record separator
3025 0         0 $self->[0]{'read_rs'} = "\015";
3026            
3027             # not a text file
3028             } else {
3029            
3030             # close the file
3031 0         0 close($fh);
3032            
3033             # error
3034 0         0 croak('unknown file type');
3035            
3036             }
3037            
3038             # read text file
3039 0 0       0 _read_text($self, $fh) or croak("failed parsing text file $path");
3040            
3041             # save file type
3042 0         0 $self->[0]{'file_type'} = 'TEXT';
3043            
3044             }
3045              
3046             # close the file
3047 0         0 close($fh);
3048              
3049             }
3050              
3051             # make new cvst object from array
3052             # parameters: (ref_to_object, ref_to_array)
3053             sub _new_from_array {
3054              
3055             # get parameters
3056 4     4   17 my ($self, $array) = @_;
3057              
3058             # for each curve element
3059 4         7 for my $i (0 .. $#{$array}) {
  4         14  
3060            
3061             # verify object has processing methods
3062 11 50 33     84 ($array->[$i]->can('transform') && $array->[$i]->can('derivative')) or croak('curve element lacks \'transform\' or \'derivative\' method');
3063            
3064             # add curve element
3065 11         26 $self->[1][$i] = $array->[$i];
3066            
3067             }
3068              
3069             }
3070              
3071             # read cvst tag from ICC profile
3072             # parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
3073             sub _readICCcvst {
3074              
3075             # get parameters
3076 0     0     my ($self, $parent, $fh, $tag) = @_;
3077              
3078             # local variables
3079 0           my ($buf, @mft, $table, $tag2, $type, $class, %hash);
3080              
3081             # set tag signature
3082 0           $self->[0]{'signature'} = $tag->[0];
3083              
3084             # seek start of tag
3085 0           seek($fh, $tag->[1], 0);
3086              
3087             # read tag header
3088 0           read($fh, $buf, 12);
3089              
3090             # unpack header
3091 0           @mft = unpack('a4 x4 n2', $buf);
3092              
3093             # verify tag signature
3094 0 0         ($mft[0] eq 'cvst') or croak('wrong tag type');
3095              
3096             # for each curve set element
3097 0           for my $i (0 .. $mft[1] - 1) {
3098            
3099             # read positionNumber
3100 0           read($fh, $buf, 8);
3101            
3102             # unpack to processing element tag table
3103 0           $table->[$i] = ['cvst', unpack('N2', $buf)];
3104            
3105             }
3106              
3107             # for each curve set element
3108 0           for my $i (0 .. $mft[1] - 1) {
3109            
3110             # get tag table entry
3111 0           $tag2 = $table->[$i];
3112            
3113             # make offset absolute
3114 0           $tag2->[1] += $tag->[1];
3115            
3116             # if a duplicate tag
3117 0 0         if (exists($hash{$tag2->[1]})) {
3118            
3119             # use original tag object
3120 0           $self->[1][$i] = $hash{$tag2->[1]};
3121            
3122             } else {
3123            
3124             # seek to start of tag
3125 0           seek($fh, $tag2->[1], 0);
3126            
3127             # read tag type signature
3128 0           read($fh, $type, 4);
3129            
3130             # convert non-word characters to underscores
3131 0           $type =~ s|\W|_|g;
3132            
3133             # form class specifier
3134 0           $class = "ICC::Profile::$type";
3135            
3136             # if 'class->new_fh' method exists
3137 0 0         if ($class->can('new_fh')) {
3138            
3139             # create specific tag object
3140 0           $self->[1][$i] = $class->new_fh($self, $fh, $tag2);
3141            
3142             } else {
3143            
3144             # create generic tag object
3145 0           $self->[1][$i] = ICC::Profile::Generic->new_fh($self, $fh, $tag2);
3146            
3147             # print warning
3148 0           print "curve set element $type opened as generic\n";
3149            
3150             }
3151            
3152             # save tag object in hash
3153 0           $hash{$tag2->[1]} = $self->[1][$i];
3154            
3155             }
3156            
3157             }
3158            
3159             }
3160              
3161             # write cvst tag to ICC profile
3162             # parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
3163             sub _writeICCcvst {
3164              
3165             # get parameters
3166 0     0     my ($self, $parent, $fh, $tag) = @_;
3167              
3168             # local variables
3169 0           my ($n, $offset, $size, @cept, %hash);
3170              
3171             # get number of curve elements
3172 0           $n = @{$self->[1]};
  0            
3173              
3174             # verify number of channels (1 to 15)
3175 0 0 0       ($n > 0 && $n < 16) or croak('unsupported number of channels');
3176              
3177             # seek start of tag
3178 0           seek($fh, $tag->[1], 0);
3179              
3180             # write tag type signature and number channels
3181 0           print $fh pack('a4 x4 n2', 'cvst', $n, $n);
3182              
3183             # initialize tag offset
3184 0           $offset = 12 + 8 * $n;
3185              
3186             # for each curve element
3187 0           for my $i (0 .. $#{$self->[1]}) {
  0            
3188            
3189             # verify curve element is 'curf' object
3190 0 0         (UNIVERSAL::isa($self->[1][$i], 'ICC::Profile::curf')) or croak('curve element must a \'curf\' object');
3191            
3192             # if tag not in hash
3193 0 0         if (! exists($hash{$self->[1][$i]})) {
3194            
3195             # get size
3196 0           $size = $self->[1][$i]->size();
3197            
3198             # set table entry and add to hash
3199 0           $cept[$i] = $hash{$self->[1][$i]} = [$offset, $size];
3200            
3201             # update offset
3202 0           $offset += $size;
3203            
3204             # adjust to 4-byte boundary
3205 0           $offset += -$offset % 4;
3206            
3207             } else {
3208            
3209             # set table entry
3210 0           $cept[$i] = $hash{$self->[1][$i]};
3211            
3212             }
3213            
3214             # write curve element position entry
3215 0           print $fh pack('N2', @{$cept[$i]});
  0            
3216            
3217             }
3218              
3219             # initialize hash
3220 0           %hash = ();
3221              
3222             # for each curve element
3223 0           for my $i (0 .. $#{$self->[1]}) {
  0            
3224            
3225             # if tag not in hash
3226 0 0         if (! exists($hash{$self->[1][$i]})) {
3227            
3228             # make offset absolute
3229 0           $cept[$i][0] += $tag->[1];
3230            
3231             # write tag
3232 0           $self->[1][$i]->write_fh($self, $fh, ['cvst', $cept[$i][0], $cept[$i][1]]);
3233            
3234             # add key to hash
3235 0           $hash{$self->[1][$i]}++;
3236            
3237             }
3238            
3239             }
3240            
3241             }
3242              
3243             1;