File Coverage

blib/lib/ICC/Profile/matf.pm
Criterion Covered Total %
statement 53 452 11.7
branch 9 198 4.5
condition 1 108 0.9
subroutine 12 39 30.7
pod 1 20 5.0
total 76 817 9.3


line stmt bran cond sub pod time code
1             package ICC::Profile::matf;
2              
3 7     7   206901 use strict;
  7         51  
  7         195  
4 7     7   33 use Carp;
  7         16  
  7         475  
5              
6             our $VERSION = 0.33;
7              
8             # revised 2018-08-07
9             #
10             # Copyright © 2004-2019 by William B. Birkett
11              
12             # add development directory
13 7     7   2765 use lib 'lib';
  7         3882  
  7         40  
14              
15             # inherit from Shared
16 7     7   1786 use parent qw(ICC::Shared);
  7         579  
  7         35  
17              
18             # enable static variables
19 7     7   412 use feature 'state';
  7         20  
  7         45333  
20              
21             # create new matf object
22             # hash keys are: ('header', 'matrix', 'offset')
23             # 'header' value is a hash reference
24             # 'matrix' value is a 2D array reference -or- Math::Matrix object -or- positive integer
25             # 'offset' value is a 1D array reference -or- numeric value
26             # when the 'matrix' value is a positive integer, an identity matrix of that size is created
27             # when the 'offset' value is a numeric value, an array containing that value is created
28             # when the parameters are input and output arrays, the 'fit' method is called on the object
29             # parameters: ([ref_to_attribute_hash])
30             # parameters: (ref_to_input_array, ref_to_output_array, [offset_flag])
31             # returns: (ref_to_object)
32             sub new {
33              
34             # get object class
35 5     5 0 1057 my $class = shift();
36              
37             # create empty matf object
38 5         18 my $self = [
39             {}, # header
40             [], # matrix
41             [] # offset
42             ];
43            
44             # local parameter
45 5         20 my ($info);
46              
47             # if there are parameters
48 5 50       18 if (@_) {
49            
50             # if one parameter, a hash reference
51 0 0 0     0 if (@_ == 1 && ref($_[0]) eq 'HASH') {
    0 0        
52            
53             # make new matf object from attribute hash
54 0         0 _new_from_hash($self, shift());
55            
56             # if two or three parameters
57             } elsif (@_ == 2 || @_ == 3) {
58            
59             # fit the object to data
60 0 0       0 ($info = fit($self, @_)) && croak("'matf' fit operation failed with error $info");
61            
62             } else {
63            
64             # error
65 0         0 croak('\'matf\' invalid parameter(s)');
66            
67             }
68            
69             }
70              
71             # bless object
72 5         13 bless($self, $class);
73              
74             # return object reference
75 5         17 return($self);
76              
77             }
78              
79             # make CAT (chromatic adaptation transform) object
80             # using linear Bradford transform (see Annex E of 'ICC1v43_2010-12.pdf')
81             # default PCS is ICC D50, normalized to adopted white point (SRC)
82             # parameters: (src_XYZ_vector, [pcs_XYZ_vector])
83             # returns: (ref_to_object)
84             sub bradford {
85              
86             # get object class
87 0     0 0 0 my $class = shift();
88              
89             # create empty matf object
90 0         0 my $self = [
91             {}, # header
92             [], # matrix
93             [] # offset
94             ];
95              
96             # local variables
97 0         0 my ($brad, $srct, $pcst, $ratio);
98              
99             # get parameters
100 0         0 my ($src, $pcs) = @_;
101              
102             # if pcs values are undefined
103 0 0       0 if (! defined($pcs)) {
104            
105             # set pcs xyz values to ICC D50 (normalized)
106 0         0 $pcs = [map {$_ * $src->[1]} 0.9642, 1, 0.8249];
  0         0  
107            
108             }
109              
110             # make Bradford matrix
111 0         0 $brad = Math::Matrix->new(
112             [0.8951, 0.2664, -0.1614],
113             [-0.7502, 1.7135, 0.0367],
114             [0.0389, -0.0685, 1.0296]
115             );
116              
117             # compute pcs cone values
118 0         0 $pcst = $brad * (Math::Matrix->new($pcs)->transpose);
119              
120             # compute src cone values
121 0         0 $srct = $brad * (Math::Matrix->new($src)->transpose);
122              
123             # make cone ratio matrix
124 0         0 $ratio = Math::Matrix->new(
125             [$srct->[0][0]/$pcst->[0][0], 0, 0],
126             [0, $srct->[1][0]/$pcst->[1][0], 0],
127             [0, 0, $srct->[2][0]/$pcst->[2][0]]
128             );
129              
130             # set header
131 0         0 $self->[0] = {'src' => $src, 'pcs' => $pcs, 'type' => 'bradford'};
132              
133             # set matrix
134 0         0 $self->[1] = ($ratio * $brad)->concat($brad)->solve;
135              
136             # bless object
137 0         0 bless($self, $class);
138              
139             # return object reference
140 0         0 return($self);
141              
142             }
143              
144             # make CAT (chromatic adaptation transform) object
145             # using CAT02 transform (see CIE CIECAM02)
146             # default PCS is ICC D50, normalized to adopted white point (SRC)
147             # parameters: (src_XYZ_vector, [pcs_XYZ_vector])
148             # returns: (ref_to_object)
149             sub cat02 {
150              
151             # get object class
152 0     0 0 0 my $class = shift();
153              
154             # create empty matf object
155 0         0 my $self = [
156             {}, # header
157             [], # matrix
158             [] # offset
159             ];
160              
161             # local variables
162 0         0 my ($cat02, $srct, $pcst, $ratio);
163              
164             # get parameters
165 0         0 my ($src, $pcs) = @_;
166              
167             # if pcs values are undefined
168 0 0       0 if (! defined($pcs)) {
169            
170             # set pcs xyz values to ICC D50 (normalized)
171 0         0 $pcs = [map {$_ * $src->[1]} 0.9642, 1, 0.8249];
  0         0  
172            
173             }
174              
175             # make CAT02 matrix
176 0         0 $cat02 = Math::Matrix->new(
177             [0.7328, 0.4296, -0.1624],
178             [-0.7036, 1.6975, 0.0061],
179             [0.0030, 0.0136, 0.9834]
180             );
181              
182             # compute pcs cone values
183 0         0 $pcst = $cat02 * (Math::Matrix->new($pcs)->transpose);
184              
185             # compute src cone values
186 0         0 $srct = $cat02 * (Math::Matrix->new($src)->transpose);
187              
188             # make cone ratio matrix
189 0         0 $ratio = Math::Matrix->new(
190             [$srct->[0][0]/$pcst->[0][0], 0, 0],
191             [0, $srct->[1][0]/$pcst->[1][0], 0],
192             [0, 0, $srct->[2][0]/$pcst->[2][0]]
193             );
194              
195             # set header
196 0         0 $self->[0] = {'src' => $src, 'pcs' => $pcs, 'type' => 'cat02'};
197              
198             # set matrix
199 0         0 $self->[1] = ($ratio * $cat02)->concat($cat02)->solve;
200              
201             # bless object
202 0         0 bless($self, $class);
203              
204             # return object reference
205 0         0 return($self);
206              
207             }
208              
209             # make CAT (chromatic adaptation transform) object
210             # just scales the src XYZ values to the pcs, not a true CAT
211             # default PCS is ICC D50, normalized to adopted white point (SRC)
212             # parameters: (src_XYZ_vector, [pcs_XYZ_vector])
213             # returns: (ref_to_object)
214             sub quasi {
215              
216             # get object class
217 0     0 0 0 my $class = shift();
218              
219             # create empty matf object
220 0         0 my $self = [
221             {}, # header
222             [], # matrix
223             [] # offset
224             ];
225              
226             # get parameters
227 0         0 my ($src, $pcs) = @_;
228              
229             # if pcs values are undefined
230 0 0       0 if (! defined($pcs)) {
231            
232             # set pcs xyz values to ICC D50 (normalized)
233 0         0 $pcs = [map {$_ * $src->[1]} (0.9642, 1, 0.8249)];
  0         0  
234            
235             }
236              
237             # set header
238 0         0 $self->[0] = {'src' => $src, 'pcs' => $pcs, 'type' => 'quasi'};
239              
240             # set matrix
241 0         0 $self->[1] = Math::Matrix->diagonal($pcs->[0]/$src->[0], $pcs->[1]/$src->[1], $pcs->[2]/$src->[2]);
242              
243             # bless object
244 0         0 bless($self, $class);
245              
246             # return object reference
247 0         0 return($self);
248              
249             }
250              
251             # create inverse 'matf' object
252             # requires the matrix element to be square
253             # returns: (ref_to_object)
254             sub inv {
255              
256             # get object
257 0     0 0 0 my $self = shift();
258              
259             # local variables
260 0         0 my ($inv, $sys);
261              
262             # make new empty object
263 0         0 $inv = ICC::Profile::matf->new();
264              
265             # if matrix is not empty
266 0 0       0 if (defined($self->[1][0][0])) {
267            
268             # verify matrix is square
269 0 0       0 (@{$self->[1]} == @{$self->[1][0]}) or croak('matrix must be square');
  0         0  
  0         0  
270            
271             # invert matrix
272 0         0 $inv->[1] = $self->[1]->invert();
273            
274             # if offset is not empty
275 0 0       0 if (defined($self->[2][0])) {
276            
277             # clone the parent matrix
278 0         0 $sys = Storable::dclone($self->[1]);
279            
280             # for each matrix row
281 0         0 for my $i (0 .. $#{$sys}) {
  0         0  
282            
283             # concatenate negative offsets
284 0         0 push(@{$sys->[$i]}, -$self->[2][$i]);
  0         0  
285            
286             }
287            
288             # solve for new offsets
289 0         0 $inv->[2] = $sys->solve->transpose->[0];
290            
291             }
292            
293             }
294              
295             # return object
296 0         0 return($inv);
297              
298             }
299              
300             # get/set reference to header hash
301             # parameters: ([ref_to_new_hash])
302             # returns: (ref_to_hash)
303             sub header {
304            
305             # get object reference
306 0     0 0 0 my $self = shift();
307            
308             # if there are parameters
309 0 0       0 if (@_) {
310            
311             # if one parameter, a hash reference
312 0 0 0     0 if (@_ == 1 && ref($_[0]) eq 'HASH') {
313            
314             # set header to new hash
315 0         0 $self->[0] = {%{shift()}};
  0         0  
316            
317             } else {
318            
319             # error
320 0         0 croak('\'matf\' header attribute must be a hash reference');
321            
322             }
323            
324             }
325            
326             # return reference
327 0         0 return($self->[0]);
328            
329             }
330              
331             # get/set reference to matrix array
332             # parameters: ([ref_to_new_array])
333             # returns: (ref_to_array)
334             sub matrix {
335            
336             # get object reference
337 2     2 0 8 my $self = shift();
338            
339             # if there are parameters
340 2 50       7 if (@_) {
341            
342             # if one parameter, a 2-D array reference
343 0 0 0     0 if (@_ == 1 && ref($_[0]) eq 'ARRAY' && @{$_[0]} == grep {ref() eq 'ARRAY'} @{$_[0]}) {
  0 0 0     0  
  0   0     0  
  0         0  
344            
345             # set matrix to clone of array
346 0         0 $self->[1] = bless(Storable::dclone($_[0]), 'Math::Matrix');
347            
348             # if one parameter, a Math::Matrix object
349             } elsif (@_ == 1 && UNIVERSAL::isa($_[0], 'Math::Matrix')) {
350            
351             # set matrix to object
352 0         0 $self->[1] = $_[0];
353            
354             } else {
355            
356             # error
357 0         0 croak('\'matf\' matrix must be a 2-D array reference or Math::Matrix object');
358            
359             }
360            
361             }
362            
363             # return object reference
364 2         6 return($self->[1]);
365            
366             }
367              
368             # get/set reference to offset array
369             # parameters: ([ref_to_new_array])
370             # returns: (ref_to_array)
371             sub offset {
372            
373             # get object reference
374 4     4 0 9 my $self = shift();
375            
376             # if there are parameters
377 4 50       10 if (@_) {
378            
379             # if one parameter, an array reference
380 0 0 0     0 if (@_ == 1 && ref($_[0]) eq 'ARRAY' && @{$_[0]} == grep {! ref()} @{$_[0]}) {
  0   0     0  
  0         0  
  0         0  
381            
382             # set offset to copy of array
383 0         0 $self->[2] = [@{shift()}];
  0         0  
384            
385             } else {
386            
387             # error
388 0         0 croak('\'matf\' offset must be an array reference');
389            
390             }
391            
392             }
393            
394             # return reference
395 4         22 return($self->[2]);
396            
397             }
398              
399             # get/set equivalent matrix-based profile primaries
400             # see appendix F.3 of 'ICC1v43_2010-12.pdf'
401             # each matrix row contains an XYZ primary
402             # parameters: ([Math::Matrix_object -or- ref_to_array])
403             # returns: (Math::Matrix_object)
404             sub primary {
405              
406             # get parameters
407 0     0 0 0 my ($self, $pri) = @_;
408              
409             # if primaries parameter is supplied
410 0 0       0 if (defined($pri)) {
411            
412             # if 3x3 Math::Matrix object or array
413 0 0 0     0 if ((UNIVERSAL::isa($pri, 'Math::Matrix') || ref($pri) eq 'ARRAY') && @{$pri} == 3 && @{$pri->[0]} == 3) {
  0   0     0  
  0   0     0  
414            
415             # compute matrix object from primary tags
416 0         0 $self->[1] = Math::Matrix->new(@{$pri})->transpose->multiply_scalar(32768/65535);
  0         0  
417            
418             } else {
419            
420             # error
421 0         0 croak('invalid primary matrix parameter');
422            
423             }
424            
425             } else {
426            
427             # if 'matf' matrix is defined
428 0 0       0 if (defined($self->[1])) {
429            
430             # compute primary tags from matrix object
431 0         0 $pri = $self->[1]->multiply_scalar(65535/32768)->transpose();
432            
433             } else {
434            
435             # error
436 0         0 croak('\'matf\' object has no matrix');
437            
438             }
439            
440             }
441              
442             # return
443 0         0 return($pri);
444              
445             }
446              
447             # fit matf object to data
448             # uses LAPACK dgels function to perform a least-squares fit
449             # fitting is done with or without offset, according to offset_flag
450             # input and output are 2D array references -or- Math::Matrix objects
451             # parameters: (ref_to_input_array, ref_to_output_array, [offset_flag])
452             # returns: (dgels_info_value)
453             sub fit {
454              
455             # get parameters
456 0     0 0 0 my ($self, $in, $out, $oflag) = @_;
457              
458             # local variables
459 0         0 my ($info, $ab);
460              
461             # check if ICC::Support::Lapack module is loaded
462 0         0 state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
463              
464             # verify ICC::Support::Lapack module is loaded
465 0 0       0 ($lapack) or croak('\'fit\' method requires ICC::Support::Lapack module');
466              
467             # resolve offset flag
468 0 0       0 $oflag = 0 if (! defined($oflag));
469              
470             # verify input array
471 0 0 0     0 (ref($in) eq 'ARRAY' && ref($in->[0]) eq 'ARRAY' && ! ref($in->[0][0])) || UNIVERSAL::isa($in, 'Math::Matrix') or croak('fit input not a 2-D array reference');
      0        
      0        
472              
473             # verify output array
474 0 0 0     0 (ref($out) eq 'ARRAY' && ref($out->[0]) eq 'ARRAY' && ! ref($out->[0][0])) || UNIVERSAL::isa($out, 'Math::Matrix') or croak('fit output not a 2-D array reference');
      0        
      0        
475              
476             # verify array dimensions
477 0 0       0 ($#{$in} == $#{$out}) or croak('\'fit\' input and output arrays have different number of rows');
  0         0  
  0         0  
478              
479             # fit the matrix
480 0         0 ($info, $ab) = ICC::Support::Lapack::matf_fit($in, $out, $oflag);
481              
482             # check result
483 0 0       0 carp('fit failed - bad parameter when calling dgels') if ($info < 0);
484 0 0       0 carp('fit failed - A matrix not full rank') if ($info > 0);
485              
486             # initialize matrix object
487 0         0 $self->[1] = Math::Matrix->new([]);
488              
489             # for each input
490 0         0 for my $i (0 .. $#{$in->[0]}) {
  0         0  
491            
492             # for each output
493 0         0 for my $j (0 .. $#{$out->[0]}) {
  0         0  
494            
495             # set matrix element (transposing)
496 0         0 $self->[1][$j][$i] = $ab->[$i][$j];
497            
498             }
499            
500             }
501            
502             # if offset flag
503 0 0       0 if ($oflag) {
504            
505             # set offset
506 0         0 $self->[2] = [@{$ab->[$#{$in->[0]} + 1]}];
  0         0  
  0         0  
507            
508             } else {
509            
510             # no offset
511 0         0 $self->[2] = [];
512            
513             }
514              
515             # return info value
516 0         0 return($info);
517              
518             }
519              
520             # transform data
521             # supported input types:
522             # parameters: (list, [hash])
523             # parameters: (vector, [hash])
524             # parameters: (matrix, [hash])
525             # parameters: (Math::Matrix_object, [hash])
526             # parameters: (structure, [hash])
527             # returns: (same_type_as_input)
528             sub transform {
529              
530             # set hash value (0 or 1)
531 0 0   0 0 0 my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
532              
533             # if input a 'Math::Matrix' object
534 0 0 0     0 if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
535            
536             # call matrix transform
537 0         0 &_trans2;
538            
539             # if input an array reference
540             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
541            
542             # if array contains numbers (vector)
543 0 0 0     0 if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0     0  
  0         0  
  0         0  
544            
545             # call vector transform
546 0         0 &_trans1;
547            
548             # if array contains vectors (2-D array)
549 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  
550            
551             # call matrix transform
552 0         0 &_trans2;
553            
554             } else {
555            
556             # call structure transform
557 0         0 &_trans3;
558            
559             }
560            
561             # if input a list (of numbers)
562 0         0 } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
563            
564             # call list transform
565 0         0 &_trans0;
566            
567             } else {
568            
569             # error
570 0         0 croak('invalid transform input');
571            
572             }
573              
574             }
575              
576             # inverse transform
577             # note: number of undefined output values must equal number of defined input values
578             # note: input array contains the final calculated input values upon return
579             # parameters: (ref_to_input_array, ref_to_output_array)
580             sub inverse {
581              
582             # get parameters
583 0     0 0 0 my ($self, $in, $out) = @_;
584              
585             # local variables
586 0         0 my ($i, $j, @si, @so);
587 0         0 my ($int, $info, $delta, $sys, $res, $mat);
588              
589             # check if ICC::Support::Lapack module is loaded
590 0         0 state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
591              
592             # initialize indices
593 0         0 $i = $j = -1;
594              
595             # build slice arrays while validating input and output arrays
596 0 0       0 ((grep {$i++; defined() && push(@si, $i)} @{$in}) == (grep {$j++; ! defined() && push(@so, $j)} @{$out})) or croak('wrong number of undefined values');
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
597              
598             # for each undefined output value
599 0         0 for my $i (@so) {
600            
601             # set to 0
602 0         0 $out->[$i] = 0;
603            
604             }
605              
606             # if ICC::Support::Lapack module is loaded
607 0 0       0 if ($lapack) {
608            
609             # compute initial transform values
610 0         0 $int = ICC::Support::Lapack::matf_vec_trans($out, $self->[1], $self->[2]);
611            
612             # for each input
613 0         0 for my $i (0 .. $#si) {
614            
615             # for each output
616 0         0 for my $j (0 .. $#so) {
617            
618             # copy Jacobian value to system matrix
619 0         0 $sys->[$i][$j] = $self->[1][$si[$i]][$so[$j]];
620            
621             }
622            
623             # compute residual value
624 0         0 $res->[$i][0] = $in->[$si[$i]] - $int->[$si[$i]];
625            
626             }
627            
628             # solve for delta values
629 0         0 ($info, $delta) = ICC::Support::Lapack::solve($sys, $res);
630            
631             # report linear system error
632 0 0       0 ($info) && print "matf inverse error $info: @{$in}\n";
  0         0  
633            
634             # for each output value
635 0         0 for my $i (0 .. $#so) {
636            
637             # add delta value
638 0         0 $out->[$so[$i]] += $delta->[$i][0];
639            
640             }
641            
642             # compute final transform values
643 0         0 @{$in} = @{ICC::Support::Lapack::matf_vec_trans($out, $self->[1], $self->[2])};
  0         0  
  0         0  
644            
645             } else {
646            
647             # compute initial transform values
648 0         0 $int = [_trans0($self, @{$out})];
  0         0  
649            
650             # for each input
651 0         0 for my $i (0 .. $#si) {
652            
653             # for each output
654 0         0 for my $j (0 .. $#so) {
655            
656             # copy Jacobian value to solution matrix
657 0         0 $mat->[$i][$j] = $self->[1][$si[$i]][$so[$j]];
658            
659             }
660            
661             # save residual value to solution matrix
662 0         0 $mat->[$i][$#si + 1] = $in->[$si[$i]] - $int->[$si[$i]];
663            
664             }
665            
666             # bless Matrix
667 0         0 bless($mat, 'Math::Matrix');
668            
669             # solve for delta values
670 0   0     0 $delta = $mat->solve || print "matf inverse error: @{$in}\n";
671            
672             # for each output value
673 0         0 for my $i (0 .. $#so) {
674            
675             # add delta value
676 0         0 $out->[$so[$i]] += $delta->[$i][0];
677            
678             }
679            
680             # compute final transform values
681 0         0 @{$in} = _trans0($self, @{$out});
  0         0  
  0         0  
682            
683             }
684            
685             }
686              
687             # compute Jacobian matrix
688             # note: input values only required for output values
689             # parameters: ([input_vector])
690             # returns: (ref_to_Jacobian_matrix, [output_vector])
691             sub jacobian {
692              
693             # get object reference
694 0     0 0 0 my $self = shift();
695              
696             # if output values wanted
697 0 0       0 if (wantarray) {
698            
699             # return Jacobian and output values
700 0         0 return(bless(Storable::dclone($self->[1]), 'Math::Matrix'), _trans1($self, $_[0]));
701            
702             } else {
703            
704             # return Jacobian only
705 0         0 return(bless(Storable::dclone($self->[1]), 'Math::Matrix'));
706            
707             }
708            
709             }
710              
711             # invert data
712             # requires the matrix element to be square
713             # supported input types:
714             # parameters: (list, [hash])
715             # parameters: (vector, [hash])
716             # parameters: (matrix, [hash])
717             # parameters: (Math::Matrix_object, [hash])
718             # parameters: (structure, [hash])
719             # returns: (same_type_as_input)
720             sub invsqr {
721              
722             # set hash value (0 or 1)
723 0 0   0 0 0 my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
724              
725             # if input a 'Math::Matrix' object
726 0 0 0     0 if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
727            
728             # call matrix transform
729 0         0 &_invsqr2;
730            
731             # if input an array reference
732             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
733            
734             # if array contains numbers (vector)
735 0 0 0     0 if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0     0  
  0         0  
  0         0  
736            
737             # call vector transform
738 0         0 &_invsqr1;
739            
740             # if array contains vectors (2-D array)
741 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  
742            
743             # call matrix transform
744 0         0 &_invsqr2;
745            
746             } else {
747            
748             # call structure transform
749 0         0 &_invsqr3;
750            
751             }
752            
753             # if input a list (of numbers)
754 0         0 } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
755            
756             # call list transform
757 0         0 &_invsqr0;
758            
759             } else {
760            
761             # error
762 0         0 croak('invalid transform input');
763            
764             }
765              
766             }
767              
768             # create matf object from ICC profile
769             # parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
770             # returns: (ref_to_object)
771             sub new_fh {
772              
773             # get object class
774 0     0 0 0 my $class = shift();
775              
776             # create empty matf object
777 0         0 my $self = [
778             {}, # header
779             [], # matrix
780             [] # offset
781             ];
782              
783             # verify 3 parameters
784 0 0       0 (@_ == 3) or croak('wrong number of parameters');
785              
786             # read matf data from profile
787 0         0 _readICCmatf($self, @_);
788              
789             # bless object
790 0         0 bless($self, $class);
791              
792             # return object reference
793 0         0 return($self);
794              
795             }
796              
797             # writes matf object to ICC profile
798             # parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
799             sub write_fh {
800              
801             # verify 4 parameters
802 0 0   0 0 0 (@_ == 4) or croak('wrong number of parameters');
803              
804             # write matf data to profile
805 0         0 goto &_writeICCmatf;
806              
807             }
808              
809             # get tag size (for writing to profile)
810             # returns: (clut_size)
811             sub size {
812              
813             # get parameter
814 0     0 0 0 my $self = shift();
815              
816             # set header size
817 0         0 my $size = 12;
818              
819             # add matrix and offset size
820 0         0 $size += 4 * @{$self->[1]} * (@{$self->[1][0]} + 1);
  0         0  
  0         0  
821              
822             # return size
823 0         0 return($size);
824              
825             }
826              
827             # get number of input channels
828             # returns: (number)
829             sub cin {
830              
831             # get object reference
832 6     6 0 14 my $self = shift();
833              
834             # return
835 6         11 return(scalar(@{$self->[1][0]}));
  6         267  
836              
837             }
838              
839             # get number of output channels
840             # returns: (number)
841             sub cout {
842              
843             # get object reference
844 6     6 0 22 my $self = shift();
845              
846             # return
847 6         67 return(scalar(@{$self->[1]}));
  6         35  
848              
849             }
850              
851             # print object contents to string
852             # format is an array structure
853             # parameter: ([format])
854             # returns: (string)
855             sub sdump {
856              
857             # get parameters
858 0     0 1 0 my ($self, $p) = @_;
859              
860             # local variables
861 0         0 my ($fmt, $s, $rows, $off, $fn);
862              
863             # resolve parameter to an array reference
864 0 0       0 $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
865              
866             # get format string
867 0 0 0     0 $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'm';
868              
869             # set string to object ID
870 0         0 $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
871              
872             # get matrix rows
873 0         0 $rows = $#{$self->[1]};
  0         0  
874              
875             # get offset size
876 0         0 $off = $#{$self->[2]};
  0         0  
877              
878             # if empty object
879 0 0 0     0 if ($rows < 0 && $off < 0) {
880            
881             # append string
882 0         0 $s .= "\n";
883            
884             } else {
885            
886             # if matrix
887 0 0       0 if ($rows >= 0) {
888            
889             # append string
890 0         0 $s .= "matrix values\n";
891            
892             # for each row
893 0         0 for my $i (0 .. $rows) {
894            
895             # make number format
896 0         0 $fn = ' %10.5f' x @{$self->[1][$i]};
  0         0  
897            
898             # append matrix row
899 0         0 $s .= sprintf("$fn\n", @{$self->[1][$i]});
  0         0  
900            
901             }
902            
903             }
904            
905             # if offset
906 0 0       0 if ($off >= 0) {
907            
908             # append string
909 0         0 $s .= "offset values\n";
910            
911             # make number format
912 0         0 $fmt = ' %10.5f' x @{$self->[2]};
  0         0  
913            
914             # append offset values
915 0         0 $s .= sprintf("$fmt\n", @{$self->[2]});
  0         0  
916            
917             }
918            
919             }
920              
921             # return string
922 0         0 return($s);
923              
924             }
925              
926             # recursive transform
927             # array structure is traversed until scalar arrays are found and transformed
928             # parameters: (ref_to_object, subroutine_reference, input_array_reference, output_array_reference)
929             sub _crawl {
930              
931             # get parameters
932 0     0   0 my ($self, $sub, $in, $out) = @_;
933              
934             # if input is a vector (reference to a numeric array)
935 0 0       0 if (@{$in} == grep {Scalar::Util::looks_like_number($_)} @{$in}) {
  0         0  
  0         0  
  0         0  
936            
937             # transform input vector and copy to output
938 0         0 @{$out} = @{$sub->($self, $in)};
  0         0  
  0         0  
939            
940             } else {
941            
942             # for each input element
943 0         0 for my $i (0 .. $#{$in}) {
  0         0  
944            
945             # if an array reference
946 0 0       0 if (ref($in->[$i]) eq 'ARRAY') {
947            
948             # transform next level
949 0         0 _crawl($self, $sub, $in->[$i], $out->[$i] = []);
950            
951             } else {
952            
953             # error
954 0         0 croak('invalid input structure');
955            
956             }
957            
958             }
959            
960             }
961            
962             }
963              
964             # transform list
965             # parameters: (object_reference, list, [hash])
966             # returns: (list)
967             sub _trans0 {
968              
969             # local variables
970 0     0   0 my ($self, $hash, @out);
971              
972             # get object reference
973 0         0 $self = shift();
974              
975             # get optional hash
976 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
977              
978             # validate number of input channels
979 0 0       0 (@_ == @{$self->[1][0]}) or croak('wrong number input channels');
  0         0  
980              
981             # set output to offset values or zeros
982 0 0       0 @out = defined($self->[2][0]) ? @{$self->[2]} : (0) x @{$self->[1]};
  0         0  
  0         0  
983              
984             # for each output
985 0         0 for my $i (0 .. $#{$self->[1]}) {
  0         0  
986            
987             # add matrix value
988 0         0 $out[$i] += ICC::Shared::dotProduct(\@_, $self->[1][$i]);
989            
990             }
991              
992             # return output data
993 0         0 return(@out);
994              
995             }
996              
997             # transform vector
998             # parameters: (object_reference, vector, [hash])
999             # returns: (vector)
1000             sub _trans1 {
1001              
1002             # get parameters
1003 0     0   0 my ($self, $in, $hash) = @_;
1004              
1005             # check if ICC::Support::Lapack module is loaded
1006 0         0 state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
1007              
1008             # validate number of input channels
1009 0 0       0 (@{$in} == @{$self->[1][0]}) or croak('wrong number input channels');
  0         0  
  0         0  
1010              
1011             # if ICC::Support::Lapack module is loaded
1012 0 0       0 if ($lapack) {
1013            
1014             # call the BLAS dgemv function
1015 0         0 return(ICC::Support::Lapack::matf_vec_trans($in, $self->[1], $self->[2]));
1016            
1017             } else {
1018            
1019             # return
1020 0         0 return([_trans0($self, @{$in})]);
  0         0  
1021            
1022             }
1023              
1024             }
1025              
1026             # transform matrix (2-D array -or- Math::Matrix object)
1027             # parameters: (object_reference, matrix, [hash])
1028             # returns: (matrix)
1029             sub _trans2 {
1030              
1031             # get parameters
1032 0     0   0 my ($self, $in, $hash) = @_;
1033              
1034             # local variables
1035 0         0 my ($info, $out, $offset);
1036              
1037             # check if ICC::Support::Lapack module is loaded
1038 0         0 state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
1039              
1040             # validate number of input channels
1041 0 0       0 (@{$in->[0]} == @{$self->[1][0]}) or croak('wrong number input channels');
  0         0  
  0         0  
1042              
1043             # if ICC::Support::Lapack module is loaded
1044 0 0       0 if ($lapack) {
1045            
1046             # compute output matrix using BLAS dgemm function
1047 0         0 $out = ICC::Support::Lapack::matf_mat_trans($in, $self->[1], $self->[2]);
1048            
1049             } else {
1050            
1051             # get offset vector (zeros if undefined)
1052 0 0       0 $offset = defined($self->[2][0]) ? $self->[2] : [(0) x @{$in->[0]}];
  0         0  
1053            
1054             # make output array (from offset vector)
1055 0         0 $out = [map{Storable::dclone($offset)} (0 .. $#{$in})];
  0         0  
  0         0  
1056            
1057             # for each row
1058 0         0 for my $i (0 .. $#{$in}) {
  0         0  
1059            
1060             # for each column
1061 0         0 for my $j (0 .. $#{$self->[1]}) {
  0         0  
1062            
1063             # add dot product
1064 0         0 $out->[$i][$j] += ICC::Shared::dotProduct($in->[$i], $self->[1][$j]);
1065            
1066             }
1067            
1068             }
1069            
1070             }
1071              
1072             # return output matrix (Math::Matrix object or 2-D array)
1073 0 0       0 return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
1074              
1075             }
1076              
1077             # transform structure
1078             # parameters: (object_reference, structure, [hash])
1079             # returns: (structure)
1080             sub _trans3 {
1081              
1082             # get parameters
1083 0     0   0 my ($self, $in, $hash) = @_;
1084              
1085             # transform the array structure
1086 0         0 _crawl($self, \&_trans1, $in, my $out = []);
1087              
1088             # return output structure
1089 0         0 return($out);
1090              
1091             }
1092              
1093             # invert list
1094             # parameters: (object_reference, list, [hash])
1095             # returns: (list)
1096             sub _invsqr0 {
1097              
1098             # local variables
1099 0     0   0 my ($self, $hash, @out);
1100              
1101             # get object reference
1102 0         0 $self = shift();
1103              
1104             # get optional hash
1105 0 0       0 $hash = pop() if (ref($_[-1]) eq 'HASH');
1106              
1107             # validate number of input channels
1108 0 0       0 (@_ == @{$self->[1][0]}) or croak('wrong number input channels');
  0         0  
1109              
1110             # return simple array
1111 0         0 return(@{_invsqr1($self, [@_])});
  0         0  
1112              
1113             }
1114              
1115             # invert vector
1116             # parameters: (object_reference, vector, [hash])
1117             # returns: (vector)
1118             sub _invsqr1 {
1119              
1120             # get parameters
1121 0     0   0 my ($self, $in, $hash) = @_;
1122              
1123             # validate number of input channels
1124 0 0       0 (@{$in} == @{$self->[1]}) or croak('wrong number input channels');
  0         0  
  0         0  
1125              
1126             # clone the 'matf' matrix
1127 0         0 my $sys = Storable::dclone($self->[1]);
1128              
1129             # for each input channel
1130 0         0 for my $i (0 .. $#{$in}) {
  0         0  
1131            
1132             # concatenate input value (minus offset, if any)
1133 0 0       0 push(@{$sys->[$i]}, defined($self->[2][$i]) ? $in->[$i] - $self->[2][$i] : $in->[$i]);
  0         0  
1134            
1135             }
1136              
1137             # return output vector
1138 0         0 return([map {$_->[0]} @{$sys->solve()}]);
  0         0  
  0         0  
1139              
1140             }
1141              
1142             # invert matrix (Math::Matrix object -or- 2-D array)
1143             # parameters: (object_reference, matrix, [hash])
1144             # returns: (output_matrix)
1145             sub _invsqr2 {
1146              
1147             # get parameters
1148 0     0   0 my ($self, $in, $hash) = @_;
1149              
1150             # local variables
1151 0         0 my ($info, $out, $sys);
1152              
1153             # check if ICC::Support::Lapack module is loaded
1154 0         0 state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
1155              
1156             # validate number of input channels
1157 0 0       0 (@{$in->[0]} == @{$self->[1]}) or croak('wrong number input channels');
  0         0  
  0         0  
1158              
1159             # if ICC::Support::Lapack module is loaded
1160 0 0       0 if ($lapack) {
1161            
1162             # compute output matrix using Lapack DGESV function
1163 0         0 ($info, $out) = ICC::Support::Lapack::matf_inv($in, $self->[1], $self->[2]);
1164            
1165             # check for DGESV error
1166 0 0       0 ($info) && croak("'ICC::Support::Lapack::matf_inv' error: $info");
1167            
1168             # return output matrix (Math::Matrix object or 2-D array)
1169 0 0       0 return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
1170            
1171             } else {
1172            
1173             # clone the 'matf' matrix
1174 0         0 $sys = Storable::dclone($self->[1]);
1175            
1176             # for each input channel
1177 0         0 for my $i (0 .. $#{$in->[0]}) {
  0         0  
1178            
1179             # for each data sample
1180 0         0 for my $j (0 .. $#{$in}) {
  0         0  
1181            
1182             # concatenate input value (minus offset, if any)
1183 0 0       0 push(@{$sys->[$i]}, defined($self->[2][$i]) ? $in->[$j][$i] - $self->[2][$i] : $in->[$j][$i]);
  0         0  
1184            
1185             }
1186            
1187             }
1188            
1189             # compute output matrix using 'Math::Matrix' methods
1190 0         0 $out = $sys->solve->transpose();
1191            
1192             # return output matrix (Math::Matrix object or 2-D array)
1193 0 0       0 return(UNIVERSAL::isa($in, 'Math::Matrix') ? $out : [@{$out}]);
  0         0  
1194            
1195             }
1196            
1197             }
1198              
1199             # invert structure
1200             # parameters: (object_reference, structure, [hash])
1201             # returns: (structure)
1202             sub _invsqr3 {
1203              
1204             # get parameters
1205 0     0   0 my ($self, $in, $hash) = @_;
1206              
1207             # transform the array structure
1208 0         0 _crawl($self, \&_invsqr1, $in, my $out = []);
1209              
1210             # return output structure
1211 0         0 return($out);
1212              
1213             }
1214              
1215             # make new matf object from attribute hash
1216             # hash keys are: ('header', 'matrix', 'offset')
1217             # object elements not specified in the hash are unchanged
1218             # parameters: (ref_to_object, ref_to_attribute_hash)
1219             sub _new_from_hash {
1220              
1221             # get parameters
1222 0     0   0 my ($self, $hash) = @_;
1223              
1224             # local variables
1225 0         0 my ($value);
1226            
1227             # if 'header' key defined
1228 0 0       0 if (defined($hash->{'header'})) {
1229            
1230             # if reference to hash
1231 0 0       0 if (ref($hash->{'header'}) eq 'HASH') {
1232            
1233             # set object element
1234 0         0 $self->[0] = {%{$hash->{'header'}}};
  0         0  
1235            
1236             } else {
1237            
1238             # wrong data type
1239 0         0 croak('wrong \'header\' data type');
1240            
1241             }
1242            
1243             }
1244            
1245             # if 'matrix' key defined
1246 0 0       0 if (defined($hash->{'matrix'})) {
1247            
1248             # get value
1249 0         0 $value = $hash->{'matrix'};
1250            
1251             # if a reference to a 2-D array
1252 0 0 0     0 if (ref($value) eq 'ARRAY' && @{$value} == grep {ref() eq 'ARRAY'} @{$value}) {
  0 0 0     0  
  0 0 0     0  
  0         0  
1253            
1254             # copy matrix to object
1255 0         0 $self->[1] = bless(Storable::dclone($value), 'Math::Matrix');
1256            
1257             # if a Math::Matrix object
1258             } elsif (UNIVERSAL::isa($value, 'Math::Matrix')) {
1259            
1260             # copy matrix to object
1261 0         0 $self->[1] = Storable::dclone($value);
1262            
1263             # if a positive integer
1264             } elsif (Scalar::Util::looks_like_number($value) && $value == int($value) && $value > 0) {
1265            
1266             # set matrix to identity matrix
1267 0         0 $self->[1] = Math::Matrix->new_identity($value);
1268            
1269             } else {
1270            
1271             # wrong data type
1272 0         0 croak('wrong \'matrix\' data type');
1273            
1274             }
1275            
1276             }
1277            
1278             # if 'offset' key defined
1279 0 0       0 if (defined($hash->{'offset'})) {
1280            
1281             # get value
1282 0         0 $value = $hash->{'offset'};
1283            
1284             # if a reference to an array of scalars
1285 0 0 0     0 if (ref($value) eq 'ARRAY' && @{$value} == grep {! ref()} @{$value}) {
  0 0       0  
  0         0  
  0         0  
1286            
1287             # copy offset to object
1288 0         0 $self->[2] = [@{$value}];
  0         0  
1289            
1290             # if a numeric value
1291             } elsif (Scalar::Util::looks_like_number($value)) {
1292            
1293             # if first 'matrix' row is defined
1294 0 0       0 if (defined($self->[1])) {
1295            
1296             # set offset to constant
1297 0         0 $self->[2] = [($value) x @{$self->[1]}];
  0         0  
1298            
1299             } else {
1300            
1301             # wrong data type
1302 0         0 croak('unknown \'matrix\' size');
1303            
1304             }
1305            
1306             } else {
1307            
1308             # wrong data type
1309 0         0 croak('wrong \'offset\' data type');
1310            
1311             }
1312            
1313             }
1314            
1315             }
1316              
1317             # read matf data
1318             # note: assumes file handle is positioned at start of matrix data
1319             # header information must be read separately by the calling function
1320             # setting offset flag enables reading of offset data following matrix data
1321             # number format is 2 (s15Fixed16Number) or 4 (floating point)
1322             # parameters: (ref_to_object, file_handle, input_channels, output_channels, offset_flag, format)
1323             sub _read_matf {
1324              
1325             # get parameters
1326 4     4   17 my ($self, $fh, $ci, $co, $oflag, $format) = @_;
1327              
1328             # local variables
1329 4         8 my ($buf);
1330              
1331             # if s15Fixed16Number format
1332 4 50       15 if ($format == 2) {
    0          
1333            
1334             # for each output channel
1335 4         17 for my $i (0 .. $co - 1) {
1336            
1337             # read into buffer
1338 12         45 read($fh, $buf, 4 * $ci);
1339            
1340             # unpack buffer and save
1341 12         64 $self->[1][$i] = [ICC::Shared::s15f162v(unpack('N*', $buf))];
1342            
1343             }
1344            
1345             # if offset data
1346 4 100       13 if ($oflag) {
1347            
1348             # read into buffer
1349 2         6 read($fh, $buf, 4 * $co);
1350            
1351             # unpack buffer and save
1352 2         7 $self->[2] = [ICC::Shared::s15f162v(unpack('N*', $buf))];
1353            
1354             }
1355            
1356             # if floating point format
1357             } elsif ($format == 4) {
1358            
1359             # for each output channel
1360 0         0 for my $i (0 .. $co - 1) {
1361            
1362             # read into buffer
1363 0         0 read($fh, $buf, 4 * $ci);
1364            
1365             # unpack buffer and save
1366 0         0 $self->[1][$i] = [unpack('f>*', $buf)];
1367            
1368             }
1369            
1370             # if offset data
1371 0 0       0 if ($oflag) {
1372            
1373             # read into buffer
1374 0         0 read($fh, $buf, 4 * $co);
1375            
1376             # unpack buffer and save
1377 0         0 $self->[2] = [unpack('f>*', $buf)];
1378            
1379             }
1380            
1381             } else {
1382            
1383             # error
1384 0         0 croak('unsupported format, must be 2 or 4');
1385            
1386             }
1387            
1388             # bless matrix array
1389 4         26 bless($self->[1], 'Math::Matrix');
1390            
1391             }
1392              
1393             # read matf tag from ICC profile
1394             # parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
1395             sub _readICCmatf {
1396              
1397             # get parameters
1398 0     0   0 my ($self, $parent, $fh, $tag) = @_;
1399              
1400             # local variables
1401 0         0 my ($buf, $ci, $co);
1402              
1403             # save tag signature
1404 0         0 $self->[0]{'signature'} = $tag->[0];
1405              
1406             # seek start of tag
1407 0         0 seek($fh, $tag->[1], 0);
1408              
1409             # read tag header
1410 0         0 read($fh, $buf, 12);
1411              
1412             # unpack header
1413 0         0 ($ci, $co) = unpack('x8 n2', $buf);
1414              
1415             # set number of input channels
1416 0         0 $self->[0]{'input_channels'} = $ci;
1417              
1418             # set number of output channels
1419 0         0 $self->[0]{'output_channels'} = $co;
1420              
1421             # read matrix w/offset
1422 0         0 _read_matf($self, $fh, $ci, $co, 1, 4);
1423              
1424             }
1425              
1426             # write matf data
1427             # note: assumes file handle is positioned at start of matf data
1428             # header information must be written separately by the calling function
1429             # setting offset flag enables writing of offset data following matrix data
1430             # if offset data array is undefined or empty, zeros are written
1431             # number format is 2 (s15Fixed16Number) or 4 (floating point)
1432             # parameters: (ref_to_object, file_handle, offset_flag, format)
1433             sub _write_matf {
1434              
1435             # get parameters
1436 2     2   10 my ($self, $fh, $oflag, $format) = @_;
1437              
1438             # local variables
1439 2         4 my ($buf);
1440              
1441             # if s15Fixed16Number format
1442 2 50       7 if ($format == 2) {
    0          
1443            
1444             # for each matrix row
1445 2         6 for my $i (0 .. $#{$self->[1]}) {
  2         10  
1446            
1447             # write matrix values as s15Fixed16Numbers
1448 6         13 print $fh pack('N*', ICC::Shared::v2s15f16(@{$self->[1][$i]}));
  6         51  
1449            
1450             }
1451            
1452             # if offset data
1453 2 50       14 if ($oflag) {
1454            
1455             # write offset values as s15Fixed16Numbers (if offset array is undefined or empty, write zeros)
1456 2 50 33     12 print $fh pack('N*', (defined($self->[2]) && @{$self->[2]} > 0) ? ICC::Shared::v2s15f16(@{$self->[2]}) : (0) x @{$self->[1][0]});
  2         14  
  0            
1457            
1458             }
1459            
1460             # if floating point format
1461             } elsif ($format == 4) {
1462            
1463             # for each matrix row
1464 0           for my $i (0 .. $#{$self->[1]}) {
  0            
1465            
1466             # write matrix values as big-endian 32-bit floating point
1467 0           print $fh pack('f>*', @{$self->[1][$i]});
  0            
1468            
1469             }
1470            
1471             # if offset data
1472 0 0         if ($oflag) {
1473            
1474             # write offset values as big-endian 32-bit floating point (if offset array is undefined or empty, write zeros)
1475 0 0 0       print $fh pack('f>*', (defined($self->[2]) && @{$self->[2]} > 0) ? @{$self->[2]} : (0) x @{$self->[1][0]});
  0            
  0            
1476            
1477             }
1478            
1479             } else {
1480            
1481             # error
1482 0           croak('unsupported format, must be 2 or 4');
1483            
1484             }
1485            
1486             }
1487              
1488             # write matf tag to ICC profile
1489             # parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
1490             sub _writeICCmatf {
1491              
1492             # get parameters
1493 0     0     my ($self, $parent, $fh, $tag) = @_;
1494              
1495             # local variables
1496 0           my ($ci, $co);
1497              
1498             # get number of input channels
1499 0           $ci = @{$self->[1][0]};
  0            
1500              
1501             # get number of output channels
1502 0           $co = @{$self->[1]};
  0            
1503            
1504             # validate number input channels (1 to 15)
1505 0 0 0       ($ci > 0 && $ci < 16) or croak('unsupported number of input channels');
1506              
1507             # validate number output channels (1 to 15)
1508 0 0 0       ($co > 0 && $co < 16) or croak('unsupported number of output channels');
1509              
1510             # seek start of tag
1511 0           seek($fh, $tag->[1], 0);
1512              
1513             # write 'matf' header
1514 0           print $fh pack('a4 x4 n2', 'matf', $ci, $co);
1515              
1516             # write matrix w/offset
1517 0           _write_matf($self, $fh, 1, 4);
1518              
1519             }
1520              
1521             1;