package ICC::Support::Chart; use strict; use Carp; our $VERSION = 1.70; # revised 2018-03-28 # # Copyright © 2004-2018 by William B. Birkett # add development directory use lib 'lib'; # inherit from Shared use parent qw(ICC::Shared); # support modules use Config; use Data::Dumper; use Encode; use File::Glob; use POSIX (); use Time::Piece; use XML::LibXML; # enable static variables use feature 'state'; # create new chart object # parameters: ([hash]) # parameters: (ref_to_data_array, [hash]) # parameters: (path_to_file, [hash]) # parameters: (path_to_folder, [hash]) # returns: (ref_to_chart_object) -or- (ref_to_chart_object, error_string) sub new { # get object class my $class = shift(); # local variables my ($self, $hash, $array, $format, $offset, $path, $files, $result, $error); # create empty chart object $self = [ {}, # object header [[]], # chart data [[]], # colorimetry data [], # header lines {}, # SAMPLE_ID hash ]; # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # if there are additional parameters if (@_) { # if first parameter is an array or a Math::Matrix object if (ref($_[0]) eq 'ARRAY' || UNIVERSAL::isa($_[0], 'Math::Matrix')) { # get array reference $array = shift(); # get format header from hash $format = $hash->{'format'}; # copy format header to object, if defined $self->[1][0] = [@{$format}] if defined($format); # set array offset $offset = defined($format) ? 1 : 0; # for each row for my $i (0 .. $#{$array}) { # copy array $self->[1][$i + $offset] = [@{$array->[$i]}]; } # if first parameter is a scalar } elsif (! ref($_[0])) { # get path $path = shift(); # save path in object header $self->[0]{'file_path'} = $path; # get file list $files = _files($path); # no files if (@{$files} == 0) { # invalid path carp($error = "no files in path: $path\n"); # one file } elsif (@{$files} == 1) { # read chart ($result = _readChart($self, $files->[0], $hash)) && carp($error = "chart $files->[0] $result\n"); # add colorimetric metadata _addColorMeta($self); # multiple files } else { # if folder handling undefined or 'AVG' if (! defined($hash->{'folder'}) || $hash->{'folder'} eq 'AVG') { # read average chart _readChartAvg($self, $files, $hash) || carp($error = "no valid charts in path: $path\n"); # if folder handling 'APPEND' } elsif ($hash->{'folder'} eq 'APPEND') { # read appended chart _readChartAppend($self, $files, $hash) || carp($error = "no valid charts in path: $path\n"); } else { # invalid folder handling carp($error = "invalid folder handling: $hash->{'folder'}\n"); } } } else { # invalid parameter(s) carp($error = "invalid parameter(s)"); } # make SAMPLE_ID hash _makeSampleID($self); # if hash defined } elsif (defined($hash)) { # make patch set ($result = _makePatchSet($self, $hash)) && carp($error = "failed making patch set - $result\n"); } # bless object bless($self, $class); # return return(wantarray() ? ($self, $error) : $self); } # get/set reference to header hash # parameters: ([ref_to_new_hash]) # returns: (ref_to_hash) sub header { # get object reference my $self = shift(); # if there are parameters if (@_) { # if one parameter, a hash reference if (@_ == 1 && ref($_[0]) eq 'HASH') { # set header to copy of hash $self->[0] = {%{shift()}}; } else { # error croak('parameter must be a hash reference'); } } # return reference return($self->[0]); } # get/set reference to data array # note: row 0 contains the DATA_FORMAT field names # note: set updates the SAMPLE_ID hash and colorimetry array # parameters: ([ref_to_new_array]) # returns: (ref_to_array) sub array { # get object reference my $self = shift(); # if there are parameters if (@_) { # if one parameter, an array reference if (@_ == 1 && ref($_[0]) eq 'ARRAY') { # get array reference my $array = shift(); # initialize data array $self->[1] = []; # if array is not empty if (@{$array}) { # for each row for my $i (0 .. $#{$array}) { # copy to object $self->[1][$i] = [@{$array->[$i]}]; } # make SAMPLE_ID hash _makeSampleID($self); # add colorimetric metadata _addColorMeta($self); } } else { # error croak('parameter must be an array reference'); } } # return reference return($self->[1]); } # get data array size # returns: (number_rows) # returns: (number_rows, number_columns) sub size { # get object reference my $self = shift(); # return array or scalar return(wantarray ? ($#{$self->[1]}, $#{$self->[1][0]} + 1) : $#{$self->[1]}); } # get data matrix size # returns: (number_rows) # returns: (number_rows, number_columns) sub matrix_size { # get object reference my $self = shift(); # get row length from data my $rows = _getRowLength($self); # compute columns my $cols = $rows ? POSIX::ceil($#{$self->[1]}/$rows) : 0; # return array or scalar wantarray ? return($rows, $cols) : return($rows); } # get row slice from SAMPLE_ID values # id_keys is a list of scalars and/or array references # row_slice is reference to an array of row indices # note: returns undef if any key is missing # parameters: (id_keys) # returns: (row_slice) sub rows { # get object reference my $self = shift(); # local variable my (@keys, @rows); # flatten id key list @keys = @{ICC::Shared::flatten(@_)}; # get row list using SAMPLE_ID hash @rows = @{$self->[4]}{@keys}; # return row slice or undef if any rows are missing return((grep {! defined()} @rows) ? undef : \@rows); } # get column slice from DATA_FORMAT keys # format_keys is a list of scalars and/or array references # column_slice is reference to an array of column indices # note: tries to match ignoring context if exact match fails # note: returns 'undef' if any column is missing # parameters: (format_keys) # returns: (column_slice) sub cols { # get object reference my $self = shift(); # local variables my (@keys, %fmt, @cols); # flatten format key list @keys = @{ICC::Shared::flatten(@_)}; # make lookup hash %fmt = map {defined($self->[1][0][$_]) ? ($self->[1][0][$_], $_) : ()} (0 .. $#{$self->[1][0]}); # lookup format keys in hash @cols = @fmt{@keys}; # if any columns undefined if (grep {! defined()} @cols) { # make lookup hash without context prefixes %fmt = map {(defined($self->[1][0][$_]) && $self->[1][0][$_] =~ m/^(.*?)\|?([^\|\n]*)$/) ? ($2, $_) : ()} (0 .. $#{$self->[1][0]}); # lookup format keys in hash @cols = @fmt{@keys}; } # return column slice or undef if any columns undefined return((grep {! defined()} @cols) ? undef : \@cols); } # get DATA_FORMAT keys from column slice # column_slice is a list of scalars and/or array references # format_keys is an array reference # note: returns 'undef' if any key is missing # parameters: (column_slice) # returns: (format_keys) sub fmt_keys { # get object reference my $self = shift(); # local variable my (@keys); # if column slice an empty array reference ([]) if (@_ == 1 && ref($_[0]) eq 'ARRAY' && @{$_[0]} == 0) { # get all format keys @keys = @{$self->[1][0]}; } else { # get format keys per flattened slice @keys = map {$self->[1][0][$_]} @{ICC::Shared::flatten(@_)}; } # return format keys or undef if any keys are missing return((grep {! defined()} @keys) ? undef : \@keys); } # get/set context # 'undef' indicates no context (get or set) # returned context may be a scalar or an array # parameter: (column_slice) => returns: (context) # parameters: (column_slice, context) => returns: (modified_keys) sub context { # get object reference my $self = shift(); # local variables my ($cols, $context, @cx); # return if no parameters supplied return(undef) if (@_ == 0); # get column slice $cols = ICC::Shared::flatten(shift()); # use all columns if slice is empty $cols = [0 .. $#{$self->[1][0]}] if (@{$cols} == 0); # if no context parameter if (@_ == 0) { # match contexts @cx = map {$self->[1][0][$_] =~ m/^(.*)\|/ ? $1 : undef} @{$cols}; # return array if wanted return(@cx) if (wantarray); # warn if columns have different contexts (@cx == grep {(! defined($cx[0]) && ! defined($_)) || ($cx[0] eq $_)} @cx) || warn('columns have different contexts'); # return context of first column return($cx[0]); } else { # get context $context = shift(); # for each column for my $i (0 .. $#{$cols}) { # if context is defined if (defined($context)) { # replace current context $self->[1][0][$cols->[$i]] =~ s/^(?:.*\|)?(.*)$/$context\|$1/; # if context is 'undef' } else { # remove current context $self->[1][0][$cols->[$i]] =~ s/^.*\|//; } } # return modified keys return([@{$self->[1][0]}[@{$cols}]]); } } # test for a specified data class # returns list of matched indices or count # context parameter of '|' matches fields with no context # parameters: (class, [context]) # returns: (list -or- count) sub test { # get parameters my ($self, $class, $context) = @_; # local variables my (%regex, @fields); # hash of compiled regex %regex = ( 'RGB' => qr/^(?:(.*)\|)?RGB_[RGB]$/, 'CMYK' => qr/^(?:(.*)\|)?CMYK_[CMYK]$/, 'XYZ' => qr/^(?:(.*)\|)?XYZ_[XYZ]$/, 'XYY' => qr/^(?:(.*)\|)?XYY_(?:X|Y|CAPY)$/, 'LAB' => qr/^(?:(.*)\|)?LAB_[LAB]$/, 'LCH' => qr/^(?:(.*)\|)?LAB_[LCH]$/, 'NCLR' => qr/^(?:(.*)\|)?[2-9A-F]CLR_[1-9A-F]$/, 'SPECTRAL' => qr/^(?:(.*)\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3}$/, 'SPOT' => qr/^(?:(.*)\|)?SPOT_\d+$/, 'DENSITY' => qr/^(?:(.*)\|)?D_(?:RED|GREEN|BLUE|VIS)$/, 'STDEVXYZ' => qr/^(?:(.*)\|)?STDEV_[XYZ]$/, 'STDEVLAB' => qr/^(?:(.*)\|)?STDEV_[LAB]$/, 'MEAN_DE' => qr/^(?:(.*)\|)?MEAN_DE$/, 'ID' => qr/^(?:(.*)\|)?(?:SAMPLE_ID|SampleID)$/, 'NAME' => qr/^(?:(.*)\|)?SAMPLE_NAME$/, 'DEVICE' => qr/^(?:(.*)\|)?(?:RGB_[RGB]|CMYK_[CMYK]|[2-9A-F]CLR_[1-9A-F])$/, ); # verify class (! ref($class) && exists($regex{$class})) || croak('invalid data class'); # if context is undefined if (! defined($context)) { # match format fields (ignoring context) @fields = grep {$self->[1][0][$_] =~ /$regex{$class}/} (0 .. $#{$self->[1][0]}); # if context is '|' } elsif ($context eq '|') { # match format fields (no context) @fields = grep {$self->[1][0][$_] =~ /$regex{$class}/ && ! defined($1)} (0 .. $#{$self->[1][0]}); } else { # match format fields (matching context) @fields = grep {$self->[1][0][$_] =~ /$regex{$class}/ && defined($1) && ($1 eq $context)} (0 .. $#{$self->[1][0]}); } # return (list -or- count) return(wantarray() ? @fields : scalar(@fields)); } # get/set keyword value(s) # CGATS ASCII file header lines are stored as an array in the object header # most lines contain a keyword followed by a value, which this methods gets/sets # a keyword may be used more than once, so the value parameter is an array # if the keyword doesn't exist, a new line is added when setting its value # if the keyword is enclosed by angle brackets, existing lines are removed # parameters: () => returns: (file_header_array_reference) # parameters: (keyword) => returns: (value_array) # parameters: (keyword, value_array) => returns: (original_value_array) sub keyword { # get parameters my ($self, $key, @values) = @_; # local variables my ($del, @ix, @current); # if no keyword, return file header array reference (defined($key)) || return($self->[3]); # set delete flag, stripping angle brackets (if any) $del = ($key =~ s/^<(.*)>$/$1/); # get indices of existing keyword (if any) @ix = grep {$self->[3][$_][0] eq $key} (0 .. $#{$self->[3]}); # get current values array (if any) @current = map {$self->[3][$_][1]} @ix; # if delete flag set if ($del) { # while indices while (@ix) { # delete array element splice(@{$self->[3]}, pop(@ix), 1); } } # if there are supplied values if (@values) { # for each value for (@values) { # if not a number or already quoted if (! m/^([\d.-]+|".*")$/) { # remove any quotes s/"//g; # enclose in quotes $_ = "\"$_\""; } } # while indices and values while (@ix && @values) { # set keyword/value entry $self->[3][shift(@ix)] = [$key, shift(@values)]; } # for each remaining value (if any) for (@values) { # add keyword/value entry push(@{$self->[3]}, [$key, $_]); } } # return current values array, or scalar return(wantarray ? @current : $current[0]); } # get/set CREATED value # adds CREATED keyword when setting, if none # parameters: () # gets date/time from CREATED value # parameters: (string) # sets/adds CREATED keyword/value # parameters: (Time::Piece_object) # sets/adds CREATED keyword/value # returns: (Time::Piece_object) # default is localtime sub created { # get parameters my ($self, $t) = @_; # local variables my (@ix, $datetime); # get indices of existing CREATED lines (if any) @ix = grep {$self->[3][$_][0] eq 'CREATED'} (0 .. $#{$self->[3]}); # print warning if more than one CREATED line print "warning: more than one CREATED keyword\n" if (@ix > 1); # if date/time parameter given if (defined($t)) { # make Time::Piece object if reference is a scalar $t = _makeTimePiece($t) if (! ref($t)); # if not a Time::Piece object if (ref($t) ne 'Time::Piece') { # print warning print "warning: invalid date/time parameter, using localtime instead\n"; # use local time $t = localtime(); } # make ISO 8601 datetime string from Time::Piece object $datetime = $t->strftime('%Y-%m-%dT%T%z'); substr($datetime, -2, 0, ':'); # if CREATED lines if (@ix) { # replace value in first CREATED line $self->[3][$ix[0]][1] = "\"$datetime\""; } else { # if keyword lines exist if (@{$self->[3]}) { # insert CREATED line (as second line) splice(@{$self->[3]}, 1, 0, ['CREATED', "\"$datetime\""]); } else { # add CREATED line $self->[3][0] = ['CREATED', "\"$datetime\""]; } } # no parameter } else { # if CREATED lines if (@ix) { # make Time::Piece object from first CREATED value $t = _makeTimePiece($self->[3][$ix[0]][1]); } else { # print warning print "warning: no CREATED keyword, returning localtime instead\n"; # use local time $t = localtime(); } } # return Time::Piece object return($t); } # get/set data array slice # row_slice and column_slice may be either a scalar or array reference # replacement_data is reference to a 2-D array of replacement values # replacement data dimensions must match size of row_slice and column_slice # data_slice is reference to a 2-D array, selected by row_slice and column_slice # parameters: ([row_slice, [column_slice, [replacement_data]]]) # return: (data_slice) sub slice { # get parameters my ($self, $rows, $cols, $data) = @_; # select all rows if row slice undefined $rows = [] if (! defined($rows)); # select all fields if column slice undefined $cols = [] if (! defined($cols)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set colorimetry array slice # row_slice and column_slice may be either a scalar or array reference # replacement_data is reference to a 2-D array of replacement values # replacement data dimensions must match size of row_slice and column_slice # data_slice is reference to a 2-D array, selected by row_slice and column_slice # parameters: ([row_slice, [column_slice, [replacement_data]]]) # return: (data_slice) sub colorimetry { # get parameters my ($self, $rows, $cols, $data) = @_; # flatten row slice $rows = defined($rows) ? ICC::Shared::flatten($rows) : []; # select all rows if row slice empty $rows = [0 .. $#{$self->[2]}] if (@{$rows} == 0); # flatten column slice $cols = defined($cols) ? ICC::Shared::flatten($cols) : []; # select all fields if column slice empty $cols = [0 .. $#{$self->[1][0]}] if (@{$cols} == 0); # call get/set subroutine _getset($self, 2, $rows, $cols, $data); } # get/set SAMPLE_ID data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub id { # local variables my ($hash, %fmt, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # make lookup hash (context| -or- '||' => column) %fmt = map {($self->[1][0][$_] =~ m/^(.*\|)?(?:SAMPLE_ID|SampleID|ID)$/) ? (defined($1) ? $1 : '||', $_) : ()} (0 .. $#{$self->[1][0]}); # if context defined if (defined($hash->{'context'})) { # get id column with context $cols = $fmt{"$hash->{'context'}|"}; } else { # get id column without context $cols = $fmt{'||'}; # if id column undefined if (! defined($cols)) { # make lookup hash ignoring context ('||' => column) %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID|ID)$/) ? ('||', $_) : ()} (0 .. $#{$self->[1][0]}); # get id column $cols = $fmt{'||'}; } } # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set SAMPLE_NAME data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub name { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, defined($hash->{'context'}) ? "$hash->{'context'}|SAMPLE_NAME" : 'SAMPLE_NAME'); # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set RGB data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub rgb { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(RGB_R RGB_G RGB_B)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set CMYK data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub cmyk { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(CMYK_C CMYK_M CMYK_Y CMYK_K)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set 6CLR data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub hex { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(6CLR_1 6CLR_2 6CLR_3 6CLR_4 6CLR_5 6CLR_6)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set nCLR data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice and replacement_data are 2-D array references # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub nCLR { # local variables my ($hash, $context, %fmt, %fmt2, $chan, @cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get the context $context = $hash->{'context'}; # make lookup hash (key => column) %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?[2-9A-F]CLR_[1-9A-F]$/) ? ($self->[1][0][$_], $_) : ()} (0 .. $#{$self->[1][0]}); # make lookup hash (prefix -or- '||' => channels) %fmt2 = map {($self->[1][0][$_] =~ m/^(.*\|)?([2-9A-F])CLR_[1-9A-F]$/) ? (defined($1) ? ($1, $2) : ('||', $2)) : ()} (0 .. $#{$self->[1][0]}); # if context defined if (defined($context)) { # get the number of channels ($chan = $fmt2{"$context|"}) || return(); # append format $chan .= 'CLR_'; # get column slice (selected from %fmt columns) @cols = grep {$self->[1][0][$_] =~ m/^$context\|$chan[1-9A-F]$/} values(%fmt); } else { # if number of channels undefined if (! defined($chan = $fmt2{'||'})) { # make lookup hash ignoring prefix (key => column) %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?([2-9A-F]CLR_[1-9A-F])$/) ? ($1, $_) : ()} (0 .. $#{$self->[1][0]}); # make lookup hash ('||' => channels) %fmt2 = map {($self->[1][0][$_] =~ m/^(?:.*\|)?([2-9A-F])CLR_[1-9A-F]$/) ? ('||', $1) : ()} (0 .. $#{$self->[1][0]}); # get the number of channels ($chan = $fmt2{'||'}) || return(); # append format $chan .= 'CLR_'; # get column slice (selected from %fmt columns) @cols = grep {$self->[1][0][$_] =~ m/^(?:.*\|)?$chan[1-9A-F]$/} values(%fmt); } else { # append format $chan .= 'CLR_'; # get column slice (selected from %fmt columns) @cols = grep {$self->[1][0][$_] =~ m/^$chan[1-9A-F]$/} values(%fmt); } } # sort by color channel (1-9, A-F) @cols = sort {substr($self->[1][0][$a], -1) cmp substr($self->[1][0][$b], -1)} @cols; # match last format key $self->[1][0][$cols[-1]] =~ m/([2-9A-F])CLR_([1-9A-F])$/; # verify number of format keys (CORE::hex($1) == @cols && CORE::hex($2) == @cols) || croak('wrong number of nCLR keys'); # call get/set subroutine _getset($self, 1, $rows, \@cols, $data); } # get/set device data # device data is either RGB, CMYK or nCLR # device values have range (0 - 1) # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub device { # local variables my ($hash, $cols, $mult); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice or return empty $cols = rgb($self, $hash) || cmyk($self, $hash) || nCLR($self, $hash) || return(); # set multiplier (255 if RGB, else 100) $mult = ($self->[1][0][$cols->[0]] =~ m/RGB_R$/) ? 255 : 100; # call get/set subroutine _getset($self, 1, $rows, $cols, $data, sub {map {defined($_) ? $_/$mult : $_} @_}, sub {map {defined($_) ? $_ * $mult : $_} @_}); } # get/set CTV data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub ctv { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(CTV)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get/set L*a*b* data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub lab { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(LAB_L LAB_A LAB_B)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data, _lab_encoding($self, $hash)); } # get/set XYZ data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub xyz { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data, _xyz_encoding($self, $cols, $hash)); } # get/set density data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub density { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(D_RED D_GREEN D_BLUE D_VIS)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data, _density_encoding($self, $hash)); } # get/set reflectance/transmittance data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub rgbv { # local variables my ($hash, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get column slice $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(R_RED R_GREEN R_BLUE R_VIS)); # call get/set subroutine _getset($self, 1, $rows, $cols, $data, _rgbv_encoding($self, $hash)); } # get/set spectral data # optional hash contains supplementary parameters # row_slice and column_slice are 1-D array references # data_slice is a Math::Matrix object (2-D array) # replacement_data is a Math::Matrix object or 2-D array # parameters: ([hash]) => returns: (column_slice) # parameters: (row_slice, [hash]) => returns: (data_slice) # parameters: (row_slice, replacement_data, [hash]) => returns: (column_slice) sub spectral { # local variables my ($hash, $fields, $cols); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $rows, $data) = @_; # get spectral fields array $fields = _spectral($self, $hash->{'context'}); # get column slice from spectral fields array $cols = defined($fields) ? [map {$_->[0]} @{$fields}] : undef; # call get/set subroutine _getset($self, 1, $rows, $cols, $data); } # get spectral wavelength array # array is sorted (low to high) # parameters: ([hash]) # returns: (ref_to_wavelength_array) sub wavelength { # get parameters my ($self, $hash) = @_; # get spectral fields array or return empty my $fields = _spectral($self, $hash->{'context'}) || return(); # return return([map {$_->[1]} @{$fields}]); } # get spectral wavelength range # structure is [start_nm, end_nm, increment] # parameters: ([hash]) # returns: (range) sub nm { # get parameters my ($self, $hash) = @_; # local variables my ($fields, $inc); # get spectral fields array or return empty $fields = _spectral($self, $hash->{'context'}) || return(); # compute increment $inc = $fields->[1][1] - $fields->[0][1]; # verify wavelength increment ($inc > 0 && abs($#{$fields} * $inc - $fields->[-1][1] + $fields->[0][1]) < 1E-12) || warn('inconsistent wavelength values'); # return range return([$fields->[0][1], $fields->[-1][1], $inc]); } # get illuminant white point # parameters: ([hash]) # returns: (XYZ_vector) sub iwtpt { # get parameters my ($self, $hash) = @_; # local variables my ($encode, $cols, $iwtpt, $get); # extract encoding hash $encode = {'encoding' => delete($hash->{'encoding'})}; # get XYZ or L*a*b* column slice $cols = xyz($self, $hash) || lab($self, $hash) || croak('illuminant white point XYZ or L*a*b* column slice undefined'); # get illuminant white point $iwtpt = _illumWP($self, $cols, $hash); # get code reference ($get) = _xyz_encoding($self, $cols, $encode); # return encoded XYZ vector return([&$get(@{$iwtpt})]); } # get media white point # parameters: ([hash]) # returns: (XYZ_vector) sub wtpt { # get parameters my ($self, $hash) = @_; # local variables my ($encode, $cols, $get); # extract encoding hash $encode = {'encoding' => delete($hash->{'encoding'})}; # get XYZ or L*a*b* column slice $cols = xyz($self, $hash) || lab($self, $hash) || croak('white point XYZ or L*a*b* column slice undefined'); # if media white point undefined in colorimetry array if (! defined($self->[2][3][$cols->[0]])) { # compute media white point or return undefined (_mediaWP($self, $cols, $hash)) || return(); } # get code reference ($get) = _xyz_encoding($self, $cols, $encode); # return encoded XYZ vector return([&$get(@{$self->[2][3]}[@{$cols}])]); } # get media black point # parameters: ([hash]) # returns: (XYZ_vector) sub bkpt { # get parameters my ($self, $hash) = @_; # local variables my ($encode, $cols, $get); # extract encoding hash $encode = {'encoding' => delete($hash->{'encoding'})}; # get XYZ or L*a*b* column slice $cols = xyz($self, $hash) || lab($self, $hash) || croak('black point XYZ or L*a*b* column slice undefined'); # if media black point undefined in colorimetry array if (! defined($self->[2][4][$cols->[0]])) { # compute media black point or return undefined (_mediaBP($self, $cols, $hash)) || return(); } # get code reference ($get) = _xyz_encoding($self, $cols, $encode); # return encoded XYZ vector return([&$get(@{$self->[2][4]}[@{$cols}])]); } # compute media OBA index # requires M1 and M2 measurements # requires device values -or- sample number # optional hash keys are 'sample', 'device', and 'context' # parameters: ([hash]) # returns: (oba_index) # returns: (M1_XYZ_vector, M2_XYZ_vector) sub oba_index { # get parameters my ($self, $hash) = @_; # local variables my ($sample, $dev, $mwv, $wps, $wpdata, $context1, $context2, $m1, $m2, @xyz1, @xyz2, $nm, $color); # if 'sample' defined if (defined($hash->{'sample'})) { # get sample from hash $sample = $hash->{'sample'}; # if valid sample number if (Scalar::Util::looks_like_number($sample) && $sample == int($sample) && $sample > 0 && $sample <= $#{$self->[1]}) { # get sample data row $wpdata = $self->[1][$sample]; } else { # warn warn('invalid sample number'); # return empty return(); } } else { # if device data (using 'device' context) if ($dev = device($self, {'context' => $hash->{'device'}})) { # set media white device value (255 if RGB, 0 otherwise) $mwv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 255 : 0; # if paper white samples found if ($wps = find($self, sub {@_ == grep {$_ == $mwv} @_}, [], $dev)) { # add average paper white sample row add_avg($self, $wps); # get sample data row $wpdata = pop(@{$self->[1]}); } else { # warn warn('no paper white samples'); # return empty return(); } } else { # warn warn('no sample value or device data'); # return empty return(); } } # if 'context' defined if (defined($hash->{'context'})) { # if context an array reference containing two scalars if (ref($hash->{'context'}) eq 'ARRAY' && ! ref($hash->{'context'}->[0]) && ! ref($hash->{'context'}->[1])) { # get specified 'M1' context $context1 = $hash->{'context'}->[0]; # get specified 'M2' context $context2 = $hash->{'context'}->[1]; } else { # warn warn('OBA context is an array reference containing M1 and M2 contexts'); # return empty return(); } } else { # use standard 'M1' context $context1 = 'M1_Measurement'; # use standard 'M2' context $context2 = 'M2_Measurement'; } # if M1 and M2 spectral data if (($m1 = spectral($self, {'context' => $context1})) && ($m2 = spectral($self, {'context' => $context2}))) { # get spectral range $nm = nm($self, {'context' => $context1}); # if increment is 10 or 20 nm if ($nm->[2] == 10 || $nm->[2] == 20) { # make ASTM color object $color = ICC::Support::Color->new({'illuminant' => 'D50', 'increment' => $nm->[2]}); } else { # make CIE color object $color = ICC::Support::Color->new({'illuminant' => ['CIE', 'D50'], 'increment' => $nm->[2]}); } # compute M1 and M2 XYZ values @xyz1 = $color->transform(@{$wpdata}[@{$m1}]); @xyz2 = $color->transform(@{$wpdata}[@{$m2}]); # if M1 and M2 XYZ data } elsif (($m1 = xyz($self, {'context' => $context1})) && ($m2 = xyz($self, {'context' => $context2}))) { # get M1 and M2 XYZ values (assumes D50 illumination) @xyz1 = @{$wpdata}[@{$m1}]; @xyz2 = @{$wpdata}[@{$m2}]; # if M1 and M2 L*a*b* data } elsif (($m1 = lab($self, {'context' => $context1})) && ($m2 = lab($self, {'context' => $context2}))) { # compute M1 and M2 XYZ values (D50 illumination) @xyz1 = ICC::Shared::_Lab2XYZ(@{$wpdata}[@{$m1}], ICC::Shared::D50); @xyz2 = ICC::Shared::_Lab2XYZ(@{$wpdata}[@{$m2}], ICC::Shared::D50); } else { # warn warn('M1 and M2 data required for OBA index'); # return empty return(); } # return array (XYZ media white points) or scalar (OBA index) return(wantarray ? (\@xyz1, \@xyz2) : ($xyz1[2] - $xyz2[2])/82.49); } # get chromatic adaptation transform (CAT) object # a CAT is optionally created when adding XYZ data # optional hash contains supplementary parameters # parameters: ([hash]) # returns: (CAT_object) sub cat { # get parameters my ($self, $hash) = @_; # local variables my ($cols, $cat); # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)); # return if slice undefined return() if (! defined($cols)); # get CAT or illuminant $cat = $self->[2][2][$cols->[0]]; # return CAT if defined return((defined($cat) && UNIVERSAL::isa($cat, 'ICC::Profile::matf')) ? $cat : ()); } # get Color object # a Color object is created when adding XYZ data from spectral data # optional hash contains supplementary parameters # parameters: ([hash]) # returns: (Color_object) sub color { # get parameters my ($self, $hash) = @_; # local variables my ($cols, $color); # get column slice, adding optional context prefix $cols = cols($self, map {defined($hash->{'context'}) ? "$hash->{'context'}|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)); # return if slice undefined return() if (! defined($cols)); # get CAT or illuminant $color = $self->[2][1][$cols->[0]]; # return CAT if defined return((defined($color) && UNIVERSAL::isa($color, 'ICC::Support::Color')) ? $color : ()); } # append rows to data array # data matrix is the 2-D array of data values to be appended # column slice is a reference to an array of data matrix column indices # parameters: (data_matrix, [column_slice]) # returns: (row_slice) sub add_rows { # get parameters my ($self, $matrix, $cols) = @_; # set offset to upper index + 1, or 0 if a new object (row 0 is empty) my $offset = $#{$self->[1]} || @{$self->[1][0]} ? $#{$self->[1]} + 1 : 0; # call 'splice_rows' splice_rows($self, $offset, 0, $matrix, $cols); # return row slice return([$offset .. ($offset + $#{$matrix})]); } # append columns to data array # data matrix is the 2-D array of data values to be appended # header is a reference to an array of DATA_FORMAT keywords # parameters: (data_matrix, [header]) # returns: (column_slice) sub add_cols { # get parameters my ($self, $matrix, $header) = @_; # verify matrix a 2-D array or Math::Matrix object (ref($matrix) eq 'ARRAY' && ref($matrix->[0]) eq 'ARRAY') || (UNIVERSAL::isa($matrix, 'Math::Matrix')) || croak ('invalid matrix parameter'); # if header supplied if (defined($header)) { # verify header is 1-D array of scalars (ref($header) eq 'ARRAY' && @{$header} == grep {! ref()} @{$header}) || croak('invalid header parameter'); # verify header and matrix have same number of columns (@{$header} == @{$matrix->[0]}) || croak('header and matrix have different number of columns'); # add header to matrix $matrix = [$header, @{$matrix}]; } # warn if matrix and object have different number of rows (@{$matrix} == @{$self->[1]}) || carp('matrix and object have different number of rows'); # set offset to upper index + 1 my $offset = $#{$self->[1][0]} + 1; # call 'splice_cols' splice_cols($self, $offset, 0, $matrix); # return column slice return([$offset .. ($offset + $#{$matrix->[0]})]); } # add average sample # assumes device values (if any) are same for each sample # averages measurement values - spectral, XYZ, L*a*b* or density # L*a*b* values are converted to xyz for averaging, then back to L*a*b* # density values are converted to reflectance for averaging, then back to density # returns row slice of the appended average sample # parameters: (row_slice, [hash]) # returns: (row_slice) sub add_avg { # get parameters my ($self, $rows, $hash) = @_; # local variables my ($c1, $c2, $c3, @id, @name); # flatten row slice $rows = ICC::Shared::flatten($rows); # resolve empty row slice $rows = [1 .. $#{$self->[1]}] if (@{$rows} == 0); # get averaging groups ($c1, $c2, $c3) = _avg_groups($self, $hash); # for each format field for my $i (0 .. $#{$self->[1][0]}) { # add column if SAMPLE_ID field push(@id, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID)$/); # add column if SAMPLE_NAME field push(@name, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?SAMPLE_NAME$/); } # return average sample return([_add_avg($self, $rows, $c1, $c2, $c3, \@id, \@name, $hash)]); } # add format keys # keys are appended to row 0 of the data array # note: format_keys is a list of scalars and/or array references # note: format_keys are saved as given, with or without context # parameters: (format_keys) # returns: (column_slice) sub add_fmt { # get parameters my $self = shift(); # local variables my (@keys, $i, %fmt); # flatten format key list @keys = @{ICC::Shared::flatten(@_)}; # get upper column index $i = $#{$self->[1][0]}; # make format lookup hash of existing keys %fmt = map {$self->[1][0][$_], $_} (0 .. $#{$self->[1][0]}); # warn if duplicate keys warn('adding duplicate format key(s)') if (grep {exists($fmt{$_})} @keys); # push format keys onto format row push(@{$self->[1][0]}, @keys); # return slice array reference return([$i + 1 .. $#{$self->[1][0]}]); } # append CTV data to data array # computed from L*a*b* data, XYZ data, or spectral data # if CTV data already exists, return those slices # adds L*a*b* data, and XYZ data if missing # parameters: ([hash]) # returns: (column_slice) sub add_ctv { # get parameters my ($self, $hash) = @_; # local variables my ($context, $added, $cols, $Lab, $color); my ($iwtpt, $WPxyz, @wtpt, $dev, $mwv, $coef, @Ls); my ($den, $a, $b, $c, $d, $e, $f, $mat); # get base context $context = $hash->{'context'}; # get added context $added = defined($hash->{'added'}) ? $hash->{'added'} : $context; # return column slice if CTV data already exists return($cols) if ($cols = _cols($self, map {defined($added) ? "$added|$_" : $_} qw(CTV))); # if L*a*b* exists, or is added if ($Lab = (_cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)) || add_lab($self, $hash))) { # get L*a*b* colorimetry hash $color = $self->[2][6][$Lab->[0]]; # for each possible colorimetry key for my $key (qw(illuminant observer increment bandpass cat)) { # if key is specified if (defined($hash->{$key})) { # if YAML strings differ if (YAML::Tiny::Dump($hash->{$key}) ne YAML::Tiny::Dump($color->{$key})) { # print warning warn("$key parameter differs from source"); } } } # if 'context' and 'added' keys are undefined, and L*a*b* source has context if (! defined($added) && $self->[1][0][$Lab->[0]] =~ m/^(.*)\|/) { # set 'added' to L*a*b* context $added = $1; } # add CTV columns slice $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(CTV)); # get supplied illuminant white point $iwtpt = $hash->{'iwtpt'}; # if supplied illuminant white point is valid if (defined($iwtpt) && (3 == grep {defined() && ! ref() && $_ > 0} @{$iwtpt})) { # use it $WPxyz = $iwtpt; # if XYZ illuminant white point is valid } elsif (3 == grep {defined() && ! ref() && $_ > 0} @{$self->[2][2]}[@{$Lab}]) { # use it $WPxyz = [@{$self->[2][2]}[@{$Lab}]]; } else { # use D50 $WPxyz = ICC::Shared::D50; } # if media white point undefined in colorimetry array if (! defined($self->[2][3][$Lab->[0]])) { # compute media white point or return undefined (_mediaWP($self, $Lab, $hash)) || return(); } # get media white point (Lx, Ly, Lz) @wtpt = ICC::Shared::_xyz2Lxyz($self->[2][3][$Lab->[0]]/$WPxyz->[0], $self->[2][3][$Lab->[1]]/$WPxyz->[1], $self->[2][3][$Lab->[2]]/$WPxyz->[2]); # get device column slice $dev = device($self, {'context' => $hash->{'device'}}); # set media white device value (255 if RGB, 0 otherwise) $mwv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 255 : 0; # set origin $self->[2][0][$cols->[0]] = $Lab; # save media white CTV (0) $self->[2][3][$cols->[0]] = 0; # save colorimetry hash @{$self->[2][6]}[$cols->[0]] = $color; # get coefficient array $coef = defined($hash->{'coef'}) ? $hash->{'coef'} : [1, 1, 1, 0, 0, 0]; # compute denominator $den = $coef->[0]**2 + $coef->[1]**2 + $coef->[2]**2; # compute matrix elements $a = ($coef->[0]**2 + $coef->[4]**2 + $coef->[5]**2)/$den; $b = ($coef->[1]**2 + $coef->[3]**2 + $coef->[5]**2)/$den; $c = ($coef->[2]**2 + $coef->[3]**2 + $coef->[4]**2)/$den; $d = -$coef->[5]**2/$den; $e = -$coef->[4]**2/$den; $f = -$coef->[3]**2/$den; # make Mahalanobis matrix $mat = [ [$a, $d, $e], [$d, $b, $f], [$e, $f, $c] ]; # bless the object bless($mat, 'Math::Matrix'); # for each sample for my $i (1 .. $#{$self->[1]}) { # if all device channels are white if (@{$dev} == grep {$_ == $mwv} @{$self->[1][$i]}[@{$dev}]) { # save CTV (0) $self->[1][$i][$cols->[0]] = 0; } else { # compute sample Lx, Ly, Lz values @Ls = ICC::Shared::_Lab2Lxyz(@{$self->[1][$i]}[@{$Lab}]); # save CTV (computed as Mahalanobis distance) $self->[1][$i][$cols->[0]] = _mahal(\@wtpt, \@Ls, $mat); } } } else { # warn warn('spectral, XYZ or L*a*b* data is required'); # return empty return(); } # return column slice return($cols); } # append L*a*b* data to data array # computed from XYZ data or spectral data # if L*a*b* data already exists, returns that slice # adds XYZ data, if only spectral data exists # parameter: ([hash]) # returns: (column_slice) sub add_lab { # get parameters my ($self, $hash) = @_; # local variables my ($context, $added, $cols, $xyz, $color, $iwtpt, $WPxyz); # get base context $context = $hash->{'context'}; # get added context $added = defined($hash->{'added'}) ? $hash->{'added'} : $context; # return column slice if L*a*b* data already exists return($cols) if ($cols = _cols($self, map {defined($added) ? "$added|$_" : $_} qw(LAB_L LAB_A LAB_B))); # if XYZ data exists, or is added if ($xyz = (_cols($self, map {defined($context) ? "$context|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)) || add_xyz($self, $hash))) { # get XYZ colorimetry hash $color = $self->[2][6][$xyz->[0]]; # for each possible colorimetry key for my $key (qw(illuminant observer increment bandpass cat)) { # if key is specified if (defined($hash->{$key})) { # if YAML strings differ if (YAML::Tiny::Dump($hash->{$key}) ne YAML::Tiny::Dump($color->{$key})) { # print warning warn("$key parameter differs from source"); } } } # if 'context' and 'added' keys are undefined, and XYZ source has context if (! defined($added) && $self->[1][0][$xyz->[0]] =~ m/^(.*)\|/) { # set 'added' to XYZ context $added = $1; } # add L*a*b* columns slice $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(LAB_L LAB_A LAB_B)); # get supplied illuminant white point $iwtpt = $hash->{'iwtpt'}; # if supplied illuminant white point is valid if (defined($iwtpt) && (3 == grep {defined() && ! ref() && $_ > 0} @{$iwtpt})) { # use it $WPxyz = $iwtpt; # if XYZ illuminant white point is valid } elsif (3 == grep {defined() && ! ref() && $_ > 0} @{$self->[2][2]}[@{$xyz}]) { # use it $WPxyz = [@{$self->[2][2]}[@{$xyz}]]; } else { # use D50 $WPxyz = ICC::Shared::D50; } # set origin @{$self->[2][0]}[@{$cols}] = ($xyz) x 3; # save illuminant white point @{$self->[2][2]}[@{$cols}] = @{$WPxyz}; # save colorimetry hash @{$self->[2][6]}[@{$cols}] = ($color) x 3; # for each sample for my $s (1 .. $#{$self->[1]}) { # compute L*a*b* values from XYZ values @{$self->[1][$s]}[@{$cols}] = ICC::Shared::_XYZ2Lab(@{$self->[1][$s]}[@{$xyz}], $WPxyz); } } else { # warn warn('spectral or XYZ data is required'); # return empty return(); } # return column slice return($cols); } # append XYZ data to data array # computed from spectral data or L*a*b* data # if XYZ data already exists, returns that slice # default colorimetry is D50, 2 degree observer # parameters: ([hash]) # returns: (column_slice) sub add_xyz { # get parameters my ($self, $hash) = @_; # local variables my ($oba, $spec1, $spec2, $context, $added); my ($spec, $color, $illum, $specv, $nm, $cols); my ($cat, $spectral, $xyz, $Lab, @WPlab, $WPxyz); # if 'oba' defined if (defined($hash->{'oba'})) { # get oba factor $oba = $hash->{'oba'}; # if 'context' defined if (defined($hash->{'context'})) { # if context an array reference containing two scalars if (ref($hash->{'context'}) eq 'ARRAY' && ! ref($hash->{'context'}->[0]) && ! ref($hash->{'context'}->[1])) { # get specified 'M1' spectral slice $spec1 = spectral($self, {'context' => $hash->{'context'}->[0]}); # get specified 'M2' spectral slice $spec2 = spectral($self, {'context' => $hash->{'context'}->[1]}); # use specified 'M2' context $context = $hash->{'context'}->[1]; } else { # warn warn('OBA context is an array reference containing M1 and M2 contexts'); # return empty return(); } } else { # get spectral slice using standard 'M1' context $spec1 = spectral($self, {'context' => 'M1_Measurement'}); # get spectral slice using standard 'M2' context $spec2 = spectral($self, {'context' => 'M2_Measurement'}); # use standard 'M2' context $context = 'M2_Measurement'; } # verify spectral slices if (! $spec1 || ! $spec2 || $#{$spec1} != $#{$spec2}) { # warn warn('M1 and M2 spectral data required for OBA effect'); # return empty return(); } # get added context $added = defined($hash->{'added'}) ? $hash->{'added'} : 'OBA'; } else { # get base context $context = $hash->{'context'}; # get added context $added = defined($hash->{'added'}) ? $hash->{'added'} : $context; } # return column slice if XYZ data already exists return($cols) if ($cols = _cols($self, map {defined($added) ? "$added|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z))); # if spectral data exists if (test($self, 'SPECTRAL', $context)) { # get spectral slice $spec = spectral($self, {'context' => $context}); # add chart wavelength range to hash $hash->{'range'} = nm($self, {'context' => $context}); # make empty 'Color.pm' object $color = ICC::Support::Color->new(); # if illuminant is defined, an array reference if (defined($hash->{'illuminant'}) && ref($hash->{'illuminant'}) eq 'ARRAY') { # if illuminant is ['DATA'] (ProfileMaker convention) if (defined($hash->{'illuminant'}->[0]) && $hash->{'illuminant'}->[0] eq 'DATA') { # verify chart object contains illuminant data (defined($self->[0]{'illuminant'}) && ref($self->[0]{'illuminant'}) eq 'ARRAY') || croak('no illuminant data'); # make new chart object from illuminant data $illum = ICC::Support::Chart->new($self->[0]{'illuminant'}); # get spectral values ($specv = $illum->spectral([1])->[0]) || croak('illuminant chart has no spectral data'); # get wavelength range $nm = $illum->nm(); # update 'illuminant' value in hash $hash->{'illuminant'} = [$nm, $specv]; } # initialize object for CIE method ICC::Support::Color::_cie($color, $hash); } else { # initialize object for ASTM method ICC::Support::Color::_astm($color, $hash); } # if 'context' and 'added' keys are undefined, and spectral source has context if (! defined($added) && $self->[1][0][$spec->[0]] =~ m/^(.*)\|/) { # set 'added' to spectral context $added = $1; } # add XYZ columns slice $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)); # set origin @{$self->[2][0]}[@{$cols}] = ($spec) x 3; # save reference to Color.pm object @{$self->[2][1]}[@{$cols}] = ($color) x 3; # if chromatic adaptation transform (cat) is specified if (defined($hash->{'cat'})) { # if cat is 'matf' object if (UNIVERSAL::isa($hash->{'cat'}, 'ICC::Profile::matf')) { # use it $cat = $hash->{'cat'}; # if cat is 'bradford' } elsif ($hash->{'cat'} eq 'bradford') { # make 'bradford' object $cat = ICC::Profile::matf->bradford($color->iwtpt()); # if cat is 'cat02' } elsif ($hash->{'cat'} eq 'cat02') { # make 'cat02' object $cat = ICC::Profile::matf->cat02($color->iwtpt()); # if cat is 'quasi' } elsif ($hash->{'cat'} eq 'quasi') { # make 'quasi' object $cat = ICC::Profile::matf->quasi($color->iwtpt()); } else { # warn warn('invalid cat type'); } } # if cat defined if (defined($cat)) { # save cat reference @{$self->[2][2]}[@{$cols}] = ($cat) x 3; } else { # save white point @{$self->[2][2]}[@{$cols}] = @{$color->iwtpt()}; } # save colorimetry hash @{$self->[2][6]}[@{$cols}] = ({map {defined($hash->{$_}) ? ($_, $hash->{$_}) : ()} qw(illuminant observer bandpass method ibandpass imethod oba cat increment range encoding)}) x 3; # for each sample for my $i (1 .. $#{$self->[1]}) { # get spectral slice $spectral->[$i - 1] = [@{$self->[1][$i]}[@{$spec}]]; } # transform to XYZ data (hash may contain 'encoding' key) $xyz = ICC::Support::Color::_trans2($color, $spectral, $hash); # add OBA effect, if enabled _add_oba($self, $spec1, $spec2, $xyz, $oba, $hash) if $oba; # for each sample for my $i (1 .. $#{$self->[1]}) { # if cat defined if (defined($cat)) { # set XYZ slice with cat @{$self->[1][$i]}[@{$cols}] = ICC::Profile::matf::_trans0($cat, @{$xyz->[$i - 1]}); } else { # set XYZ slice @{$self->[1][$i]}[@{$cols}] = @{$xyz->[$i - 1]}; } } # if L*a*b* data exists } elsif (test($self, 'LAB', $context)) { # warn if illuminant is specified (! defined($hash->{'illuminant'})) || warn('illuminant specified but no spectral data!'); # get L*a*b* slice $Lab = cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)); # if 'context' and 'added' keys are undefined, and L*a*b* source has context if (! defined($added) && $self->[1][0][$Lab->[0]] =~ m/^(.*)\|/) { # set 'added' to L*a*b* context $added = $1; } # get L*a*b* white point values @WPlab = @{$self->[2][2]}[@{$Lab}]; # use scalar values or D50 $WPxyz = (3 == grep {defined() && ! ref() && $_ > 0} @WPlab) ? [@WPlab] : ICC::Shared::D50; # add XYZ columns slice $cols = add_fmt($self, map {defined($added) ? "$added|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)); # set origin @{$self->[2][0]}[@{$cols}] = ($Lab) x 3; # save illuminant white point @{$self->[2][2]}[@{$cols}] = @{$WPxyz}; # for each sample for my $s (1 .. $#{$self->[1]}) { # compute XYZ values from L*a*b* values @{$self->[1][$s]}[@{$cols}] = ICC::Shared::_Lab2XYZ(@{$self->[1][$s]}[@{$Lab}], $WPxyz); } } else { # warn warn('spectral or L*a*b* data is required'); # return empty return(); } # return column slice return($cols); } # append ISO 5-3 density data to data array # computed from spectral data only # if density data already exists, return that slice # default status is 'T', encoding is 'density' # parameters: ([hash]) # returns: (column_slice) sub add_density { # get parameters my ($self, $hash) = @_; # local variables my ($context, $added, $encode, $fp, $cols, $spec, $temp, $color, $spectral, $rgbv); # get base context $context = $hash->{'context'}; # get added context $added = defined($hash->{'added'}) ? $hash->{'added'} : $context; # get encoding $encode = $hash->{'encoding'} // 'density'; # if invalid encoding if ($encode ne 'density' && $encode ne 'linear') { # warn warn('invalid density encoding, using \'density\''); # set encoding $encode = 'density'; } # set format prefix $fp = $encode eq 'density' ? 'D' : 'R'; # return column slice if density/reflectance data already exists return($cols) if ($cols = cols($self, map {defined($added) ? "$added|$fp$_" : "$fp$_"} qw(_RED _GREEN _BLUE _VIS))); # if spectral data if (test($self, 'SPECTRAL', $context)) { # get spectral slice $spec = spectral($self, $hash); # make copy of hash $temp = Storable::dclone($hash); # add chart wavelength range to hash $temp->{'range'} = nm($self, $hash); # make empty 'Color.pm' object $color = ICC::Support::Color->new(); # initialize object for ISO 5-3 method ICC::Support::Color::_iso($color, $temp); # if 'context' and 'added' keys are undefined, and spectral source has context if (! defined($added) && $self->[1][0][$spec->[0]] =~ m/^(.*)\|/) { # set 'added' to spectral context $added = $1; } # add density/reflectance columns slice $cols = add_fmt($self, map {defined($added) ? "$added|$fp$_" : "$fp$_"} qw(_RED _GREEN _BLUE _VIS)); # set origin @{$self->[2][0]}[@{$cols}] = ($spec) x 4; # save reference to Color.pm object @{$self->[2][1]}[@{$cols}] = ($color) x 4; # save colorimetry hash @{$self->[2][6]}[@{$cols}] = ({map {defined($temp->{$_}) ? ($_, $temp->{$_}) : ()} qw(status increment range encoding)}) x 4; # for each sample for my $i (1 .. $#{$self->[1]}) { # get spectral slice $spectral->[$i - 1] = [@{$self->[1][$i]}[@{$spec}]]; } # set encoding $temp->{'encoding'} = $encode; # transform to density/reflectance data (per encoding) $rgbv = ICC::Support::Color::_trans2($color, $spectral, $temp); # for each sample for my $i (1 .. $#{$self->[1]}) { # set data values @{$self->[1][$i]}[@{$cols}] = @{$rgbv->[$i - 1]}; } } else { # warn warn('spectral data is required'); # return empty return(); } # return column slice return($cols); } # add computed values to data array # processing is done by a user-defined function (udf) # data groups are defined by one or more column slice(s) # supported hash keys: 'element', 'sample', 'device', 'rows', 'start', 'added' # either an 'element' udf or a 'sample' udf are required, but not both # an 'element' udf computes a single value from single slice value(s) # a 'sample' udf computes all values at once from slice value array(s) # setting the 'device' flag converts RGB/CMYK/nCLR values to device values # the 'rows' parameter is the row slice computed, default is all rows # the 'start' parameter is the first column computed, default is to append # the 'added' parameter may be a scalar or an array reference # an 'added' scalar will be used as a context prefix # an 'added' array must be the same size as the columns added # parameters: (column_slice_0, column_slice_1, ... hash) # returns: (added_column_slice) sub add_udf { # local variables my ($self, $hash, @cs, $rows, $m, $n, @div, $udfe, $udfs); my (@p, @u, @s, $cx, $added); # get object reference $self = shift(); # get parameter hash $hash = pop(); # verify a hash reference (ref($hash) eq 'HASH') || croak('last parameter must be a hash reference'); # verify number of slices (@cs = @_) || croak('one or more column slices are required'); # get row slice, all rows by default $rows = defined($hash->{'rows'}) ? $hash->{'rows'} : []; # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # flatten row slice $rows = ICC::Shared::flatten($rows); # verify row slice contents (@{$rows} == grep {Scalar::Util::looks_like_number($_) && int($_) == $_ && $_ > 0 && $_ <= $#{$self->[1]}} @{$rows}) || croak('invalid row slice'); } # get starting column, append by default $n = defined($hash->{'start'}) ? $hash->{'start'} : $#{$self->[1][0]} + 1; # if an array reference (slice), use the first value $n = $n->[0] if (ref($n) eq 'ARRAY'); # verify starting column (Scalar::Util::looks_like_number($n) && int($n) eq $n && $n >= 0) || croak('invalid \'start\' parameter'); # if 'device' flag if ($hash->{'device'}) { # for each data format for my $i (0 .. $#{$self->[1][0]}) { # set divisor to 255 for RGB data $div[$i] = 255 if ($self->[1][0][$i] =~ m/RGB_[RGB]$/); # set divisor to 100 for CMYK data $div[$i] = 100 if ($self->[1][0][$i] =~ m/CMYK_[CMYK]$/); # set divisor to 100 for nCLR data $div[$i] = 100 if ($self->[1][0][$i] =~ m/[2-9A-F]CLR_[1-9A-F]$/); } } # get udf CODE refs $udfe = $hash->{'element'}; $udfs = $hash->{'sample'}; # if both udfs defined if (defined($udfe) && defined($udfs)) { # error croak('both \'element\' and \'sample\' udfs are defined'); # if 'element' udf defined } elsif (defined($udfe)) { # verify udf is a code reference (ref($udfe) eq 'CODE') || croak('\'element\' udf is not a CODE reference'); # for each parameter for my $i (0 .. $#cs) { # if an array reference if (ref($cs[$i]) eq 'ARRAY') { # if first slice if (! defined($m)) { # get upper index $m = $#{$cs[0]}; # compute added slice @s = ($n .. $n + $m); } else { # verify slice size ($#{$cs[$i]} == $m) || croak('column slices are different sizes'); } # verify a valid column slice (ref($cs[$i]) eq 'ARRAY' || @{$cs[$i]} == grep {Scalar::Util::looks_like_number($_) && int($_) == $_ && $_ >= 0 && $_ <= $#{$self->[1][0]}} @{$cs[$i]}) || croak('invalid column slice'); # if a scalar } elsif (! ref($cs[$i])) { # verify a valid column index (Scalar::Util::looks_like_number($cs[$i]) && int($cs[$i]) == $cs[$i] && $cs[$i] >= 0 && $cs[$i] <= $#{$self->[1][0]}) || croak('invalid column index'); } else { # error croak('parameter must be a scalar or an array reference'); } } # verify at least one column slice parameter (defined($m)) || croak('at least one column slice is required'); # for each sample for my $i (@{$rows}) { # for each column index for my $j (0 .. $m) { # for each parameter for my $k (0 .. $#cs) { # get column index (slice -or- scalar) $cx = (ref($cs[$k]) eq 'ARRAY') ? $cs[$k][$j] : $cs[$k]; # get parameter value $p[$k] = $self->[1][$i][$cx]; # adjust device values, if divisor defined $p[$k] /= $div[$cx] if defined($div[$cx]); } # call 'element' udf $self->[1][$i][$n + $j] = &$udfe(@p, $j); } } # if 'sample' udf defined } elsif (defined($udfs)) { # verify udf is a code reference (ref($udfs) eq 'CODE') || croak('\'sample\' udf is not a CODE reference'); # for each parameter for my $i (0 .. $#cs) { # verify a valid column slice (ref($cs[$i]) eq 'ARRAY' || @{$cs[$i]} == grep {Scalar::Util::looks_like_number($_) && int($_) == $_ && $_ >= 0 && $_ <= $#{$self->[1][0]}} @{$cs[$i]}) || croak('invalid column slice'); } # verify at least one parameter (@cs) || croak('at least one column slice is required'); # for each sample for my $i (@{$rows}) { # for each column slice for my $j (0 .. $#cs) { # for each slice element for my $k (0 .. $#{$cs[$j]}) { # get column index $cx = $cs[$j][$k]; # get parameter value $p[$j][$k] = $self->[1][$i][$cx]; # adjust device values, if divisor defined $p[$j][$k] /= $div[$cx] if defined($div[$cx]); } } # if first sample if (! defined($m)) { # call 'sample' udf @u = &$udfs(@p); # get upper index $m = $#u; # compute added slice @s = ($n .. $n + $m); # copy values to object @{$self->[1][$i]}[@s] = @u; } else { # call 'sample' udf @{$self->[1][$i]}[@s] = &$udfs(@p); } } } else { # error croak('no udf is defined'); } # get 'added' parameter, default is 'udf', could be undefined $added = exists($hash->{'added'}) ? $hash->{'added'} : 'udf'; # if 'added' is undefined if (! defined($added)) { # if 'element' udf -or- size of first column slice equals number of added columns if (defined($udfe) || @{$cs[0]} == @s) { # add data format stripping context from first column slice @{$self->[1][0]}[@s] = map {m/^(?:.*\|)?(.*)$/; $1} @{$self->[1][0]}[@{$cs[0]}]; } else { # add data format as 'colxxx' @{$self->[1][0]}[@s] = map {"col$_"} @s; } # if 'added' a scalar } elsif (! ref($added)) { # if 'element' udf -or- size of first column slice equals number of added columns if (defined($udfe) || @{$cs[0]} == @s) { # add data format using 'added' as context with first column slice format keys @{$self->[1][0]}[@s] = map {m/^(?:.*\|)?(.*)$/; "$added|$1"} @{$self->[1][0]}[@{$cs[0]}]; } else { # add data format using 'added' as context to 'colxxx' @{$self->[1][0]}[@s] = map {"$added|col$_"} @s; } # if 'added' is an array ref and size equals number of added columns } elsif (ref($added) eq 'ARRAY' && @{$added} == @s) { # add data format using 'added' as array @{$self->[1][0]}[@s] = @{$added}; } else { # error croak('invalid \'added\' parameter'); } # return added column slice return([@s]); } # append date column to data array # adds same date/time to each sample # supported hash keys: 'date', 'format', 'added' # parameter: ([hash]) # returns: (column_slice) sub add_date { # get parameters my ($self, $hash) = @_; # local variables my ($cols, $added, $date, $fmt, $str); # get added context $added = $hash->{'added'}; # return column slice if date column already exists return($cols) if ($cols = _cols($self, defined($added) ? "$added|CREATED" : 'CREATED')); # add date column slice $cols = add_fmt($self, defined($added) ? "$added|CREATED" : 'CREATED'); # if date supplied if (defined($date = $hash->{'date'})) { # if date is a number if (Scalar::Util::looks_like_number($date)) { # make Time::Piece object $date = localtime($date); # if date not a Time::Piece object } elsif (ref($date) ne 'Time::Piece') { # error croak('invalid date parameter'); } } else { # use 'CREATED' value from chart $date = created($self); } # compute the date/time string (same for each sample) $str = defined($fmt = $hash->{'format'}) ? $date->strftime($fmt) : $date->epoch(); # for each row for my $i (1 .. $#{$self->[1]}) { # set the date time string $self->[1][$i][$cols->[0]] = $str; } # return column slice return($cols); } # splice rows into data array # offset and length are as used by Perl's 'splice' function # data matrix is the 2-D array of data values to be spliced # column slice is a reference to an array of data matrix column indices # parameters: ([offset, [length, [data_matrix, [column_slice]]]]) # returns: (removed_data_matrix) sub splice_rows { # get parameters my ($self, $offset, $length, $matrix, $cols) = @_; # local variables my (@ix, @list, @s, $removed); # if offset supplied if (defined($offset)) { # verify offset a scalar (! ref($offset) && (int($offset) == $offset)) || croak('invalid offset parameter'); } # if length supplied if (defined($length)) { # verify length an integer scalar (! ref($length) && int($length) == $length) || croak('invalid length parameter'); } # if matrix supplied if (defined($matrix)) { # verify matrix a 2-D array or Math::Matrix object (ref($matrix) eq 'ARRAY' && ref($matrix->[0]) eq 'ARRAY') || (UNIVERSAL::isa($matrix, 'Math::Matrix')) || croak ('invalid matrix parameter'); } # if column slice supplied if (defined($cols)) { # verify column slice an array reference (ref($cols) eq 'ARRAY') || croak('invalid cols parameter'); # verify length, offset and matrix supplied (defined($length) && defined($offset) && defined($matrix)) || croak('cols requires length, offset and matrix'); # flatten column slice @ix = @{ICC::Shared::flatten($cols)}; # make splice list using column slice @list = map {@s[@ix] = @{$_}; [@s]} @{$matrix}; # splice the data $removed = [splice(@{$self->[1]}, $offset, $length, @list)]; } else { # if matrix supplied if (defined($matrix)) { # verify length, offset (defined($length) && defined($offset)) || croak('matrix requires length and offset'); # make splice list from full matrix @list = map {[@{$_}]} @{$matrix}; # splice the data $removed = [splice(@{$self->[1]}, $offset, $length, @list)]; } else { # if length supplied if (defined($length)) { # verify offset supplied (defined($offset)) || croak('length requires offset'); # splice the data $removed = [splice(@{$self->[1]}, $offset, $length)]; } else { # if offset supplied if (defined($offset)) { # splice the data $removed = [splice(@{$self->[1]}, $offset)]; } else { # get data array reference $removed = $self->[1]; # init data array $self->[1] = [[]]; # init colorimetry array $self->[2] = [[]]; } } } } # update the SAMPLE_ID hash _makeSampleID($self); # return removed data return(bless($removed, 'Math::Matrix')); } # splice columns into data array # offset and length are as used by Perl's 'splice' function # data matrix is the 2-D array of data values to be spliced # row slice is a reference to an array of data matrix row indices # parameters: ([offset, [length, [data_matrix, [row_slice]]]]) # returns: (removed_data_matrix) sub splice_cols { # get parameters my ($self, $offset, $length, $matrix, $rows) = @_; # local variables my (@ix, @s, @filler, $removed); # if offset supplied if (defined($offset)) { # verify offset a scalar (! ref($offset) && (int($offset) == $offset)) || croak('invalid offset parameter'); } # if length supplied if (defined($length)) { # verify length an integer scalar (! ref($length) && int($length) == $length) || croak('invalid length parameter'); } # if matrix supplied if (defined($matrix)) { # verify matrix a 2-D array or Math::Matrix object (ref($matrix) eq 'ARRAY' && ref($matrix->[0]) eq 'ARRAY') || (UNIVERSAL::isa($matrix, 'Math::Matrix')) || croak ('invalid matrix parameter'); } # if row slice supplied if (defined($rows)) { # verify row slice an array reference (ref($rows) eq 'ARRAY') || croak('invalid cols parameter'); # verify length, offset and matrix supplied (defined($length) && defined($offset) && defined($matrix)) || croak('rows requires length, offset and matrix'); # flatten row slice @ix = @{ICC::Shared::flatten($rows)}; # make list of matrix row refs @s[@ix] = @{$matrix}; # make filler data @filler = (undef) x @{$matrix->[0]}; # for each data row for my $i (0 .. $#{$self->[1]}) { # if matrix data defined if (defined($s[$i])) { # splice matrix data $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @{$s[$i]})]; } else { # splice filler data $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @filler)]; } } # for each colorimetry row for my $i (0 .. $#{$self->[2]}) { # splice filler data splice(@{$self->[2][$i]}, $offset, $length, @filler) if (defined($self->[2][$i][$offset])); } } else { # if matrix supplied if (defined($matrix)) { # verify length, offset (defined($length) && defined($offset)) || croak('matrix requires length and offset'); # make filler data @filler = (undef) x @{$matrix->[0]}; # for each data row for my $i (0 .. $#{$self->[1]}) { # if matrix data defined if (defined($matrix->[$i])) { # splice matrix data $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @{$matrix->[$i]})]; } else { # splice filler data $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length, @filler)]; } } # for each colorimetry row for my $i (0 .. $#{$self->[2]}) { # splice filler data splice(@{$self->[2][$i]}, $offset, $length, @filler) if (defined($self->[2][$i][$offset])); } } else { # if length supplied if (defined($length)) { # verify offset supplied (defined($offset)) || croak('length requires offset'); # for each data row for my $i (0 .. $#{$self->[1]}) { # splice the data $removed->[$i] = [splice(@{$self->[1][$i]}, $offset, $length)]; } # for each colorimetry row for my $i (0 .. $#{$self->[2]}) { # splice filler data splice(@{$self->[2][$i]}, $offset, $length) if (defined($self->[2][$i][$offset])); } } else { # if offset supplied if (defined($offset)) { # for each data row for my $i (0 .. $#{$self->[1]}) { # splice the data $removed->[$i] = [splice(@{$self->[1][$i]}, $offset)]; } # for each colorimetry row for my $i (0 .. $#{$self->[2]}) { # splice filler data splice(@{$self->[2][$i]}, $offset) if (defined($self->[2][$i][$offset])); } } else { # get data array reference $removed = $self->[1]; # init data array $self->[1] = [[]]; # init colorimetry array $self->[2] = [[]]; } } } } # initialize SAMPLE_ID hash if no SAMPLE_ID field $self->[4] = {} if (0 == test($self, 'ID')); # return removed data return(bless($removed, 'Math::Matrix')); } # remove rows from data array # parameters: (row_slice) # returns: (removed_data_matrix) sub remove_rows { # get parameters my ($self, $rows) = @_; # local variables my ($f, $up, @r, @s, $removed); # return empty matrix if row slice undefined return(bless([[]], 'Math::Matrix')) if (! defined($rows)); # flatten row slice $f = ICC::Shared::flatten($rows); # if row slice is empty if (! defined($f->[0])) { # remove all rows, except row 0 (DATA_FORMAT) $removed = [splice(@{$self->[1]}, 1)]; # clear SAMPLE_ID hash $self->[4] = {}; # return removed data return(bless($removed, 'Math::Matrix')); } # get upper row index $up = $#{$self->[1]}; # verify row slice (grep {$_ != int($_) || $_ < 1 || $_ > $up} @{$f}) && carp('row slice contains invalid index value(s)'); # initialize slice (always keep row 0) @s = (0); # for each row for my $i (1 .. $up) { # if index contained in row slice if (grep {$i == $_} @{$f}) { # add to slice (remove) push(@r, $i); } else { # add to slice (keep) push(@s, $i) } } # if rows to remove if (@r) { # set removed data (@r) $removed = [@{$self->[1]}[@r]]; # set kept data (@s) $self->[1] = [@{$self->[1]}[@s]]; # update the SAMPLE_ID hash _makeSampleID($self); } else { # set removed data (none) $removed = [[]]; } # return removed data return(bless($removed, 'Math::Matrix')); } # remove columns from data array # parameters: (column_slice) # returns: (removed_data_matrix) sub remove_cols { # get parameters my ($self, $cols) = @_; # local variables my ($f, $up, @r, @s, $removed, $kept, $color); # return empty matrix if column slice undefined return(bless([[]], 'Math::Matrix')) if (! defined($cols)); # flatten column slice $f = ICC::Shared::flatten($cols); # if columns slice is empty if (! defined($f->[0])) { # copy all rows $removed = [@{$self->[1]}]; # clear data array $self->[1] =[[]]; # clear colorimetry array $self->[2] = [[]]; # clear SAMPLE_ID hash $self->[4] = {}; # return removed data return(bless($removed, 'Math::Matrix')); } # get upper column index $up = $#{$self->[1][0]}; # verify column slice (grep {$_ != int($_) || $_ < 0 || $_ > $up} @{$f}) && carp('column slice contains invalid index value(s)'); # for each column for my $i (0 .. $up) { # if index contained in column slice if (grep {$i == $_} @{$f}) { # add to slice (remove) push(@r, $i); } else { # add to slice (keep) push(@s, $i) } } # if columns to remove if (@r) { # for each data row for my $i (0 .. $#{$self->[1]}) { # set removed data (@r) $removed->[$i] = [@{$self->[1][$i]}[@r]]; # set kept data (@s) $kept->[$i] = [@{$self->[1][$i]}[@s]]; } # update object data $self->[1] = $kept; # for each colorimetry row for my $i (0 .. $#{$self->[2]}) { # set kept colorimetry (@s) $color->[$i] = [@{$self->[2][$i]}[@s]]; } # update colorimetry data $self->[2] = $color; # initialize SAMPLE_ID hash if no SAMPLE_ID field $self->[4] = {} if (0 == test($self, 'ID')); } else { # set removed data (none) $removed = [[]]; } # return removed data return(bless($removed, 'Math::Matrix')); } # get sample selection based on 2-D location # indices are one-based, with origin at the upper left # row matrix slice may contain indices of undefined rows # entire chart is used when the row and column indices are omitted # chart row length is provided as a parameter, or obtained from the data # parameters: ([upper_row_index, lower_row_index, left_column_index, right_column_index], [chart_row_length]) # returns: (row_matrix_slice) sub select_matrix { # get object reference my $self = shift(); # local variables my ($sn, $cmax, @rows, @cols, $matrix); my ($row_length, $upper, $lower, $left, $right); # get number of samples $sn = $#{$self->[1]}; # if 0 or 4 parameters if (@_ == 0 || @_ == 4) { # get row length from data $row_length = _getRowLength($self); # if 1 or 5 parameters } elsif (@_ == 1 || @_ == 5) { # get row length $row_length = pop(); # verify row length (Scalar::Util::looks_like_number($row_length) && $row_length == int($row_length) && $row_length > 0) || croak('invalid chart row length'); } else { # error croak('wrong number of parameters'); } # if row and column parameters provided if (@_) { # get row and column parameters ($upper, $lower, $left, $right) = @_; # verify upper and lower indices (! ref($upper) && $upper == int($upper) && $upper > 0 && $upper <= $row_length) || warn('invalid upper row index'); (! ref($lower) && $lower == int($lower) && $lower > 0 && $lower <= $row_length) || warn('invalid lower row index'); # get maximum column index $cmax = $sn % $row_length ? int($sn/$row_length) + 1 : int($sn/$row_length); # verify left and right indices (! ref($left) && $left == int($left) && $left > 0 && $left <= $cmax) || warn('invalid left column index'); (! ref($right) && $right == int($right) && $right > 0 && $right <= $cmax) || warn('invalid right column index'); # if upper index < lower index if ($upper < $lower) { # make rows array @rows = ($upper .. $lower); } else { # make rows array @rows = reverse($lower .. $upper); } # if left index < right index if ($left < $right) { # make columns array @cols = ($left .. $right); } else { # make columns array @cols = reverse($right .. $left); } # use entire chart } else { # make rows array @rows = (1 .. $row_length); # if chart is rectangular if ($sn % $row_length == 0) { # make columns array @cols = (1 .. $sn/$row_length); } else { # warning warn('chart is not rectangular'); # make columns array @cols = (1 .. int($sn/$row_length) + 1); } } # for each row for my $i (0 .. $#rows) { # for each column for my $j (0 .. $#cols) { # set matrix element $matrix->[$j][$i] = ($cols[$j] - 1) * $row_length + $rows[$i]; } } # return row matrix slice return(bless($matrix, 'Math::Matrix')); } # get sample selection using template # samples are matched by their device values # supported hash keys: 'dups', 'rows', 'context', 'template_context', 'sid_context', 'method', 'copy' # duplicate handling: 0 - sample average (default), 1 - FIFO, 2 - LIFO, 3 - first sample, 4 - last sample # parameters: (template_chart_object, [hash]) # returns: (row_matrix_slice, [sid_matrix_slice]) sub select_template { # get parameters my ($self, $template, $hash) = @_; # local variables my ($row_length, $dups, $copys, $copyt); my ($devcs, $devct, $devs, $devt); my ($sx, $c1, $c2, $c3, $n, @src, $cmp); my ($target, $low, $high, $interval, @m, $nomatch); my ($rows, $avg, $matrix, $devp, $sidt, $sid); # verify template is a chart object (UNIVERSAL::isa($template, 'ICC::Support::Chart')) || croak('template not an ICC::Support::Chart object'); # get template row length $row_length = _getRowLength($template, $hash); # set duplicate handling $dups = defined($hash->{'dups'}) ? $hash->{'dups'} : 0; # if copy slice is defined if (defined($hash->{'copy'})) { # flatten the copy slice $copys = ICC::Shared::flatten($hash->{'copy'}); # add copied fields to template $copyt = add_fmt($template, @{$self->[1][0]}[@{$copys}]); } # verify parameters (! ref($row_length) && $row_length == int($row_length) && $row_length > 0) || croak('invalid chart_row_length parameter'); ($dups == int($dups) && $dups >= 0 && $dups <= 4) || croak('invalid duplicate_handling parameter'); # get object device column slice $devcs = device($self, $hash); # get template device column slice $devct = device($template, {'context' => $hash->{'template_context'}}); # verify object and template column slices (defined($devcs)) || croak ('object device data missing'); (defined($devct)) || croak ('template device data missing'); ($#{$devcs} == $#{$devct}) || croak('object and template have different number of channels'); # get object device values $devs = device($self, [], $hash); # get template device values $devt = device($template, [], {'context' => $hash->{'template_context'}}); # get index of next object sample $sx = $#{$self->[1]} + 1; # get averaging groups if duplicates are averaged ($c1, $c2, $c3) = _avg_groups($self, $hash) if ($dups == 0); # get number of channels $n = @{$devcs}; # initialize sample list @src = (); # for each sample for my $i (0 .. $#{$devs}) { # if all device values defined if ($n == grep {defined()} @{$devs->[$i]}) { # add sample to source list push(@src, [@{$devs->[$i]}, $i + 1]); } } # sort object device values @src = sort { # for each channel for my $i (0 .. $#{$a}) { # quit loop if device values are unequal last if ($cmp = $a->[$i] <=> $b->[$i]) # use last comparison for sort test } $cmp } @src; # for each template sample for my $i (0 .. $#{$devt}) { # initialize search indices $low = 0; $high = $#src; # initialize no match flag $nomatch = 0; # for each channel for my $j (0 .. $#{$devt->[0]}) { # get the target value $target = $devt->[$i][$j]; # locate interval containing or bounding the target value $interval = _bin_search(\@src, $target, $j, $low, $high); # find indices matching the target value @m = grep {$src[$_][$j] == $target} @{$interval}; # if no object values exactly match the target value if (@m == 0) { # sort interval indices by distance to target value @m = sort {$a->[1] <=> $b->[1]} map {[$_, abs($src[$_][$j] - $target)]} @{$interval}; # if distance to closest object value > 0.00201 if (abs($target - $src[$m[0][0]][$j]) > 0.00201) { # print warning print "no match to template sample $i\n"; print "device values: @{$devt->[$i]}\n"; # set no match flag $nomatch = 1; # quit channel loop last; } # set target to closest object value $target = $src[$m[0][0]][$j]; # locate interval containing the target value $interval = _bin_search(\@src, $target, $j, $low, $high); # find indices matching the target value @m = grep {$src[$_][$j] == $target} @{$interval}; } # update interval $low = $m[0]; $high = $m[-1]; } # if no match found if ($nomatch) { # locate nearest object sample(s) using linear search ($low, $high) = _lin_search(\@src, $devt->[$i]); # print message print "closest match is object sample $src[$low][-1]\n"; print "device values @{$src[$low]}[0 .. $#{$devt->[0]}]\n"; } # single sample if ($low == $high) { # set matrix element to first row matching object sample $matrix->[$i/$row_length][$i % $row_length] = $src[$low][-1]; # duplicate samples } else { # duplicates are averaged if ($dups == 0) { # for each appended avg sample for my $j ($sx .. $#{$self->[1]}) { # set avg $avg = $j; # get device values $devp = $self->device([$j]); # for each channel for my $k (0 .. $#{$devp->[0]}) { # clear avg if device values differ $avg = 0 if ($devp->[0][$k] != $devt->[$i][$k]); } # quit loop if device values match last if ($avg); } # if existing avg sample found if ($avg) { # set matrix element to existing avg sample $matrix->[$i/$row_length][$i % $row_length] = $avg; } else { # make row slice of duplicate samples $rows = [map {$src[$_][-1]} ($low .. $high)]; # set matrix element to new avg sample $matrix->[$i/$row_length][$i % $row_length] = _add_avg($self, $rows, $c1, $c2, $c3); } # use FIFO sample } elsif ($dups == 1) { # from low to high for my $j ($low .. $high) { # if index > 0 if ($src[$j][-1] > 0) { # set matrix element to object sample index $matrix->[$i/$row_length][$i % $row_length] = $src[$j][-1]; # invert sample index to indicate it was used $src[$j][-1] = - $src[$j][-1]; # quit loop last; } } # if matrix element undefined if (! defined($matrix->[$i/$row_length][$i % $row_length])) { # print message print "FIFO stack empty for @{$devt->[$i]}\n"; print "using last stack sample\n"; # set matrix element to last row matching object sample $matrix->[$i/$row_length][$i % $row_length] = - $src[$high][-1]; } # use LIFO sample } elsif ($dups == 2) { # from high to low for my $j (reverse($low .. $high)) { # if index > 0 if ($src[$j][-1] > 0) { # set matrix element to object sample index $matrix->[$i/$row_length][$i % $row_length] = $src[$j][-1]; # invert sample index to indicate it was used $src[$j][-1] = - $src[$j][-1]; # quit loop last; } } # if matrix element undefined if (! defined($matrix->[$i/$row_length][$i % $row_length])) { # print message print "LIFO stack empty for @{$devt->[$i]}\n"; print "using last stack sample\n"; # set matrix element to first row matching object sample $matrix->[$i/$row_length][$i % $row_length] = - $src[$low][-1]; } # use first duplicate sample } elsif ($dups == 3) { # set matrix element to first row matching object sample $matrix->[$i/$row_length][$i % $row_length] = $src[$low][-1]; # use last duplicate sample } elsif ($dups == 4) { # set matrix element to last row matching object sample $matrix->[$i/$row_length][$i % $row_length] = $src[$high][-1]; } else { # error croak('invalid duplicate handling'); } } # if 'copy' slice defined if (defined($copys)) { # get the object row $n = $matrix->[$i/$row_length][$i % $row_length]; # copy selected values from object to template @{$template->[1][$i + 1]}[@{$copyt}] = @{$self->[1][$n]}[@{$copys}]; # if device values differ if ($nomatch) { # copy device values from object to template @{$template->[1][$i + 1]}[@{$devct}] = @{$self->[1][$n]}[@{$devcs}]; } } } # if sid-matrix is wanted and template has sid values if (wantarray() && ($sidt = id($template, [], {'context' => $hash->{'sid_context'}}))) { # for each template sample for my $i (0 .. $#{$sidt}) { # set sid matrix element to sid slice value $sid->[$i/$row_length][$i % $row_length] = $sidt->[$i][0]; } # return row matrix slice and sid matrix slice return(bless($matrix, 'Math::Matrix'), bless($sid, 'Math::Matrix')); } else { # return row matrix slice return(bless($matrix, 'Math::Matrix')); } } # get sample selection # array of data values is supplied to code block # sample is included if code block returns 'true' value # default row_slice is all samples # default column_slice is all columns # parameters: (code_reference, row_slice, column_slice) # returns: (row_slice) sub find { # get parameters my ($self, $code, $rows, $cols) = @_; # local variables my (@s); # verify code reference (ref($code) eq 'CODE') || croak('selection parameter must be a code reference'); # if row slice undefined or empty if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # flatten slice $rows = ICC::Shared::flatten($rows); } # if column slice undefined or empty if (! defined($cols) || (ref($cols) eq 'ARRAY' && @{$cols} == 0)) { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } else { # flatten slice $cols = ICC::Shared::flatten($cols); } # select samples @s = grep {&$code(@{$self->[1][$_]}[@{$cols}])} @{$rows}; # return selection, or undef if none selected return(scalar(@s) ? \@s : undef); } # get sample selection based on device values # array of device values is supplied to code block # sample is included if code block returns 'true' value # default row_slice is all samples # context may be specified with parameter hash # parameters: (code_reference, [row_slice], [hash]) # returns: (row_slice) sub ramp { # local variables my ($hash, $cols, $mult, @s); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $code, $rows) = @_; # verify code reference (ref($code) eq 'CODE') || croak('selection parameter must be a code reference'); # if row slice undefined or empty if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # flatten slice $rows = ICC::Shared::flatten($rows); } # get device column slice (defined($cols = device($self, $hash))) || croak('device values required'); # set multiplier (255 if RGB, else 100) $mult = ($self->[1][0][$cols->[0]] =~ m/RGB_R$/) ? 255 : 100; # select samples @s = grep {&$code(map {$_/$mult} @{$self->[1][$_]}[@{$cols}])} @{$rows}; # return selection, or undef if none selected return(scalar(@s) ? \@s : undef); } # get sample selection based on L*a*b* values # array of L*a*b* values is supplied to code block # sample is included if code block returns 'true' value # default row_slice is all samples # context may be specified with parameter hash # parameters: (code_reference, [row_slice], [hash]) # returns: (row_slice) sub range { # local variables my ($hash, $cols, @s); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $code, $rows) = @_; # verify code reference (ref($code) eq 'CODE') || croak('selection parameter must be a code reference'); # if row slice undefined or empty if (! defined($rows) || (ref($rows) eq 'ARRAY' && @{$rows} == 0)) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # flatten slice $rows = ICC::Shared::flatten($rows); } # get L*a*b* column slice (defined($cols = lab($self, $hash))) || croak('L*a*b* values required'); # select samples @s = grep {&$code(@{$self->[1][$_]}[@{$cols}])} @{$rows}; # return selection, or undef if none selected return(scalar(@s) ? \@s : undef); } # generate randomized sample slice # parameter: ([row_slice]) # returns: (row_slice) sub randomize { # get parameters my ($self, $rows) = @_; # if row slice defined if (defined($rows)) { # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # flatten row slice $rows = ICC::Shared::flatten($rows); # verify row slice contents (@{$rows} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$rows}) || croak('invalid row slice'); } # return row slice, randomized return([List::Util::shuffle(@{$rows})]); } else { # return all samples, randomized return([List::Util::shuffle(1 .. $#{$self->[1]})]); } } # analyze chart device values # creates an array structure with an element for each device channel. # each element contains a hash, a keys array, and a ramp array. # hash keys are device values, and hash values are arrays of samples. # if row-slice is omitted, all samples are used. # if the dup_flag is false (default), a new sample is added # containing average measurement values, and the new sample # is substituted for the anonymous array of duplicates. # if the dup_flag is true, duplicate samples are included in # array of samples grouped within anonymous arrays. # dup_flag and/or device context are specified with parameter hash # parameters: ([row_slice], [hash]) # returns: (ref_to_structure) sub analyze { # get object reference my $self = shift(); # local variables my ($hash, $rows, $dup, $ramp, $dev, $c1, $c2, $c3, @id, @name, $mult); my (@d, %dev_hash, $key, $avg, $value, $struct); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get device column slice ($dev = device($self, $hash)) || croak('chart has no device values'); # get row slice $rows = shift() if (ref($_[0]) eq 'ARRAY'); # flatten row slice $rows = $rows ? ICC::Shared::flatten($rows) : []; # use all samples if slice is empty $rows = [1 .. $#{$self->[1]}] if (@{$rows} == 0); # get dup flag $dup = defined($hash->{'dups'}) ? $hash->{'dups'} : 0; # get ramp value $ramp = defined($hash->{'ramp'}) ? $hash->{'ramp'} : 0; # get averaging groups ($c1, $c2, $c3) = _avg_groups($self, $hash); # for each column for my $i (0 .. $#{$self->[1][0]}) { # add column if SAMPLE_ID field push(@id, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID)$/); # add column if SAMPLE_NAME field push(@name, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?SAMPLE_NAME$/); } # set device multiplier (255 for RGB values, otherwise 100) $mult = ($self->[1][0][$dev->[0]] =~ m/^(?:.*\|)?RGB_[RGB]$/) ? 255 : 100; # for each sample for my $i (0 .. $#{$rows}) { # get device values @d = @{$self->[1][$rows->[$i]]}[@{$dev}]; # divide by multiplier (setting -0 to 0) @d = map {$_ == 0 ? 0 : $_/$mult} @d; # make device value key $key = join(':', @d); # if key exists if (exists($dev_hash{$key})) { # add sample to existing hash entry push(@{$dev_hash{$key}}, $rows->[$i]); } else { # add device hash entry $dev_hash{$key} = [$rows->[$i]]; } } # if dup flag is not set if (! $dup) { # for each key for my $key (keys(%dev_hash)) { # if duplicate samples if (@{$dev_hash{$key}} > 1) { # if measurement data if (@{$c1} || @{$c2} || @{$c3}) { # add average sample $avg = _add_avg($self, $dev_hash{$key}, $c1, $c2, $c3, \@id, \@name); # update hash to average sample $dev_hash{$key} = [$avg]; } else { # update hash to first sample $dev_hash{$key} = [$dev_hash{$key}[0]]; } } } # update the SAMPLE_ID hash _makeSampleID($self); } # make empty structure $struct = [map {[{}, [], []]} (0 .. $#{$dev})]; # for each key for my $key (keys(%dev_hash)) { # split key to device values @d = split(/:/, $key); # get value $value = $dev_hash{$key}; # resolve single value to scalar $value = $value->[0] if (@{$value} == 1); # for each device channel for my $i (0 .. $#d) { # if key exists if (exists($struct->[$i][0]{$d[$i]})) { # add sample to hash entry push(@{$struct->[$i][0]{$d[$i]}}, $value); } else { # add hash entry $struct->[$i][0]{$d[$i]} = [$value]; # add device value to keys array push(@{$struct->[$i][1]}, $d[$i]); } # if all other device values equal ramp value if (@d == grep {$_ == $i || $d[$_] == $ramp} (0 .. $#d)) { # add sample to ramp array push(@{$struct->[$i][2]}, $value); } } } # for each device channel for my $i (0 .. $#{$dev}) { # sort keys array (decreasing frequency) $struct->[$i][1] = [sort {@{$struct->[$i][0]{$b}} <=> @{$struct->[$i][0]{$a}}} @{$struct->[$i][1]}]; # sort ramp array (increasing values) $struct->[$i][2] = [sort {$self->[1][(! ref($a) ? $a : $a->[0])][$dev->[$i]] <=> $self->[1][(! ref($b) ? $b : $b->[0])][$dev->[$i]]} @{$struct->[$i][2]}]; } # return return($struct); } # write chart to ISO 28178 (CGATS.17) ASCII file # optional slice parameters are either scalars, array references or 'Math::Matrix' objects # optional hash parameter keys: 'sid', 'append' # parameters: (path_to_file, [row_slice, [column_slice]], [hash]) sub write { # local variables my ($hash, $row_length, $m, $n, @files, $sid, $fh, $rs, @fields); my (%cspec, $keyword, $value, $source, $std_key, @s, $sidx, $append); my ($null, $undef); # get optional hash parameter $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $path, $rows, $cols) = @_; # if row slice defined if (defined($rows)) { # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # get row length if row slice is Math::Matrix object $row_length = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix')); # flatten row slice $rows = ICC::Shared::flatten($rows); } } else { # use all rows $rows = [1 .. $#{$self->[1]}]; } # get number of rows $m = @{$rows}; # warn if invalid samples (@{$rows} == grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows})|| warn('row slice contains invalid samples'); # if column slice defined if (defined($cols)) { # if column slice an empty array reference if (ref($cols) eq 'ARRAY' && @{$cols} == 0) { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } else { # flatten column slice $cols = ICC::Shared::flatten($cols); } } else { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } # get number of columns $n = @{$cols}; # filter column slice @{$cols} = grep {$_ == int($_) && defined($self->[1][0][$_])} @{$cols}; # warn if invalid fields ($n == @{$cols}) || warn('column slice contains invalid fields'); # if 'sid' hash value defined if (defined($sid = $hash->{'sid'})) { # if array reference or Math::Matrix object if (ref($sid) eq 'ARRAY' || UNIVERSAL::isa($sid, 'Math::Matrix')) { # flatten 'sid' slice $sid = ICC::Shared::flatten($sid); # warn row slice and sid slice are different sizes ($m == @{$sid}) || warn('row slice and sid slice are different sizes'); } elsif ($sid eq 'row') { # use sequential row list $sid = [1 .. $m]; } else { # error croak('invalid \'sid\' hash value'); } } # resolve file list from path (defined($path) && (! ref($path)) && (@files = File::Glob::bsd_glob($path))) || croak("invalid path: $path, stopped"); # verify file path is unique (@files == 1) || warn('file path not unique'); # open the file open($fh, '>', $files[0]) || croak("$! when opening $files[0], stopped"); # get the record separator $rs = $self->[0]{'write_rs'} || $self->[0]{'read_rs'} || "\n"; # initialize color specification hash # so lines with 'FileInformation' source are printed %cspec = ('FileInformation' => 1); # add referenced sources to color specification hash for (@{$self->[2][5]}[@{$cols}]) {$cspec{$_}++ if defined()}; # for each header line for (@{$self->[3]}) { # get keyword, value and source ($keyword, $value, $source) = @{$_}; # if keyword defined and length > 0 if (defined($keyword) && length($keyword)) { # make uppercase $keyword = uc($keyword); # skip certain keywords next if ($keyword =~ m/KEYWORD|NUMBER_OF_FIELDS|NUMBER_OF_SETS/); next if (defined($row_length) && $keyword =~ m/LGOROWLENGTH/); # if no source or referenced source if (! defined($source) || $cspec{$source}) { # if value defined and length > 0 if (defined($value) && length($value)) { # print keyword/value print $fh "$keyword\t$value$rs"; } else { # print keyword only print $fh "$keyword$rs"; } } } else { # print empty line print $fh "$rs"; } } # get format fields @fields = @{$self->[1][0]}[@{$cols}]; # remove any context, trim leading and trailing white space, and replace spaces with underscores for (@fields) {s/^.*\|//; s/^\s*(.*?)\s*$/$1/; s/ /_/g} # make standard format keyword regex (per ISO 28178 and common usage) $std_key = '^(?:' . join('|', qw(SAMPLE_ID SAMPLE_NO STRING RGB_[RGB] CMYK_[CMYK] [2-9A-F]CLR_[1-9A-F] PC\d+_\d+ SPOT_\d+ (?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3} D_(?:RED|GREEN|BLUE|VIS|MAJOR_FILTER) XYZ_[XYZ] XYY_(?:X|Y|CAPY) LAB_[LABCH] LAB_DE LAB_DE_94 LAB_DE_CMC LAB_DE_2000 MEAN_DE STDDEV_[XYZ] STDDEV_[LAB] CHI_SQD_PAR)) . ')$'; # for each format field for (@fields) { # if not a standard keyword if (! /$std_key/) { # print KEYWORD printf $fh "KEYWORD\t%s$rs", $_; } } # if 'sid' slice defined if (defined($sid)) { # if 'SAMPLE_ID' keyword(s) if (@s = grep {uc($fields[$_]) eq 'SAMPLE_ID'} (0 .. $#fields)) { # save index of first match $sidx = $s[0]; } else { # insert 'SAMPLE_ID' keyword unshift(@fields, 'SAMPLE_ID'); } } # print LGOROWLENGTH (if $row_length defined) printf $fh "LGOROWLENGTH\t%d$rs", $row_length if (defined($row_length)); # print NUMBER_OF_FIELDS printf $fh "NUMBER_OF_FIELDS\t%d$rs", scalar(@fields); # print BEGIN_DATA_FORMAT print $fh 'BEGIN_DATA_FORMAT', $rs; # print format string (if any) print $fh join("\t", @fields), $rs if (@fields); # print END_DATA_FORMAT print $fh 'END_DATA_FORMAT', $rs; # print NUMBER_OF_SETS printf $fh "NUMBER_OF_SETS\t%d$rs", scalar(@{$rows}); # print BEGIN_DATA print $fh 'BEGIN_DATA', $rs; # get null replacement value $null = $hash->{'null'} // 'null'; # get undef replacement value $undef = $hash->{'undef'} // 'undef'; # for each row for my $i (0 .. $#{$rows}) { # get data fields, replacing null and undefined values @fields = map {defined() ? length() ? $_ : $null : $undef} @{$self->[1][$rows->[$i]]}[@{$cols}]; # trim leading and trailing white space, and replace spaces with underscores for (@fields) {s/^\s*(.*?)\s*$/$1/; s/ /_/g}; # if 'sid' slice defined if (defined($sid)) { # if 'sid' index defined if (defined($sidx)) { # replace 'sid' value $fields[$sidx] = $sid->[$i]; } else { # insert 'sid' value unshift(@fields, $sid->[$i]); } } # print the data record print $fh join("\t", @fields), $rs; } # print END_DATA print $fh 'END_DATA', $rs; # if 'append' hash value defined if (defined($append = $hash->{'append'})) { # replace line endings, if any $append =~ s/\n/$rs/g; # print appended data print $fh $append; } # close the file close($fh); } # write chart to CxF3 file # optional slice parameters are either scalars, array references or 'Math::Matrix' objects # optional hash parameter keys: 'cc:FileInformation' # parameters: (path_to_file, [row_slice, [column_slice]], [hash]) sub writeCxF3 { # local variables my ($hash, $row_length, $n); my ($dom, $root, $ns, $nsURI); my ($datetime, $id, $ops, $objcol); my ($prefix, $nid, $obj, $xpath, $node); my (%lookup, @data, @files, $sub, $spot); # get optional hash parameter $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $path, $rows, $cols) = @_; # if row slice defined if (defined($rows)) { # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # get row length if row slice is Math::Matrix object $row_length = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix')); # flatten row slice $rows = ICC::Shared::flatten($rows); } } else { # set array reference to all rows $rows = [1 .. $#{$self->[1]}]; } # get number of rows $n = @{$rows}; # filter row slice @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows}; # warn if invalid samples ($n == @{$rows}) || warn('row slice contains invalid samples'); # open CxF3 template eval {$dom = XML::LibXML->load_xml('location' => ICC::Shared::getICCPath('Templates/CxF3_template.xml'))} || croak('can\'t load CxF3 template'); # get the root element $root = $dom->documentElement(); # get the namespace prefix and URI $ns = $root->prefix(); $nsURI = $root->namespaceURI(); # make 'FileInformation' nodes $datetime = _makeCxF3fileinfo($self, $root, $ns, $nsURI, $hash); # make write operations array from column slice # array structure: [[[class, prefix, XPath, [sub_paths], [columns], {attributes}, sort_order], ...], ...] $ops = _makeCxF3writeops($self, $root, $ns, $cols); # make 'ColorSpecification' nodes _makeCxF3colorspec($self, $root, $ns, $nsURI, $ops); # get the 'ObjectCollection' node ($objcol) = $root->findnodes("$ns:Resources/$ns:ObjectCollection"); # init object Id index $id = 1; # for each group of operations for my $i (0 .. $#{$ops}) { # get prefix (ObjectType) $prefix = $ops->[$i][0][1]; # initialize name Id $nid = 0; # for each row in slice for my $j (@{$rows}) { # increment name Id $nid++; # add 'Object' node $obj = $objcol->appendChild(XML::LibXML::Element->new("$ns:Object")); $obj->setAttribute('ObjectType', $prefix); $obj->setAttribute('Name', "$prefix$nid"); $obj->setAttribute('Id', "c$id"); $obj->setNamespace($nsURI, $ns); # add 'CreationDate' node $node = $obj->appendChild(XML::LibXML::Element->new("$ns:CreationDate")); $node->appendText($datetime); $node->setNamespace($nsURI, $ns); # init XPath node hash %lookup = (); # for each operation in the group for my $k (0 .. $#{$ops->[$i]}) { # set current node to Object $node = $obj; # initialize XPath $xpath = undef; # for each XPath segment for (split(/\//, $ops->[$i][$k][2])) { # add segment to XPath $xpath = defined($xpath) ? "$xpath/$_" : $_; # if segment exists if (exists($lookup{$xpath})) { # use node $node = $lookup{$xpath}; } else { # add node $node = $node->appendChild(XML::LibXML::Element->new($_)); $node->setNamespace($nsURI, $ns); # add hash entry (except Tag elements) $lookup{$xpath} = $node if ($_ ne "$ns:Tag"); } } # for each attribute key (if any) for (keys(%{$ops->[$i][$k][5]})) { # set node attribute using either data element or hash value $node->setAttribute($_, (ref($ops->[$i][$k][5]{$_}) eq 'ARRAY') ? $self->[1][$j][$ops->[$i][$k][5]{$_}[0]] : $ops->[$i][$k][5]{$_}); } # get data @data = @{$self->[1][$j]}[@{$ops->[$i][$k][4]}]; # warn on undefined data (@data == grep {defined()} @data) || warn("undefined data in sample $j when writing CxF3 file"); # if subpaths if (@{$ops->[$i][$k][3]}) { # for each subpath for my $s (0 .. $#{$ops->[$i][$k][3]}) { # add node # CxF3 schema requires integer values for RGB data $sub = $node->appendChild(XML::LibXML::Element->new($ops->[$i][$k][3][$s])); $sub->appendText($ops->[$i][$k][0] eq 'RGB' ? int($data[$s] + 0.5) : $data[$s]); $sub->setNamespace($nsURI, $ns); } # if NCLR class if ($ops->[$i][$k][0] eq 'NCLR') { # for each spot color for my $s (4 .. $#data) { # add SpotColor elements $spot = $node->appendChild(XML::LibXML::Element->new("$ns:SpotColor")); $spot->setNamespace($nsURI, $ns); $sub = $spot->appendChild(XML::LibXML::Element->new("$ns:Name")); $sub->appendText('Spot' . ($s + 1)); $sub->setNamespace($nsURI, $ns); $sub = $spot->appendChild(XML::LibXML::Element->new("$ns:Percentage")); $sub->appendText($data[$s]); $sub->setNamespace($nsURI, $ns); } } # no subpaths and one data value } elsif (@data == 1) { # add data as text content $node->appendText($data[0]); # no subpaths and multiple data values } elsif (@data > 1) { # if DENSITY class if ($ops->[$i][$k][0] eq 'DENSITY') { ##### to be done } else { # join data and add as text content $node->appendText(join(' ', @data)); } } } # add Name attribute to TagCollection element $lookup{"$ns:TagCollection"}->setAttribute('Name', 'Location') if exists($lookup{"$ns:TagCollection"}); # if nothing was added to Object if ($node->isSameNode($obj)) { # unbind the node $node->unbindNode(); } else { # increment Object Id $id++; } } } # validate the CxF3 document _validateCxF3($dom) if (defined($hash->{'validate'}) && $hash->{'validate'}); # resolve file list from path (@files = File::Glob::bsd_glob($path)) || croak('invalid file path'); # verify file path is unique (@files == 1) || warn('file path not unique'); # write CxF3 file $dom->toFile($files[0], 1); } # write chart data array as delimited ASCII file (for Excel, R, MATLAB, etc.) # optional slice parameters are either scalars, array references or 'Math::Matrix' objects # optional hash parameter keys: 'header', 'sep', 'eol', and 'undef' # parameters: (path_to_file, [row_slice, [column_slice]], [hash]) sub writeASCII { # local variables my ($hash, $row_length, $n, @files, $fh); my ($fs, $rs, $undef, $hdr, @fields); # get optional hash parameter $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $path, $rows, $cols) = @_; # if row slice defined if (defined($rows)) { # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # get row length if row slice is Math::Matrix object $row_length = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix')); # flatten row slice $rows = ICC::Shared::flatten($rows); } } else { # use all rows $rows = [1 .. $#{$self->[1]}]; } # get number of rows $n = @{$rows}; # filter row slice @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows}; # warn if invalid samples ($n == @{$rows}) || warn('row slice contains invalid samples'); # if column slice defined if (defined($cols)) { # if column slice an empty array reference if (ref($cols) eq 'ARRAY' && @{$cols} == 0) { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } else { # flatten column slice $cols = ICC::Shared::flatten($cols); } } else { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } # get number of columns $n = @{$cols}; # filter column slice @{$cols} = grep {$_ == int($_) && defined($self->[1][0][$_])} @{$cols}; # warn if invalid fields ($n == @{$cols}) || warn('column slice contains invalid fields'); # resolve file list from path (defined($path) && (! ref($path)) && (@files = File::Glob::bsd_glob($path))) || croak("invalid path: $path, stopped"); # verify file path is unique (@files == 1) || warn('file path not unique'); # open the file open($fh, '>', $files[0]) || croak("$! when opening $files[0], stopped"); # get header mode $hdr = $hash->{'header'} || 1; # get the field separator $fs = $hash->{'sep'} || "\t"; # get the record separator $rs = $hash->{'eol'} || "\n"; # get the undefined string $undef = $hash->{'undef'} || ''; # if header enabled if ($hdr) { # if format fields, replacing undefined values if (@fields = map {defined() ? $_ : $undef} @{$self->[1][0]}[@{$cols}]) { # if header mode 2, remove contexts if ($hdr == 2) {for (@fields) {s/^.*\|//}}; # trim leading and trailing white space, and replace spaces with underscores for (@fields) {s/^\s*(.*?)\s*$/$1/; s/ /_/g}; # print format record print $fh join($fs, @fields), $rs; } } # for each row for my $i (@{$rows}) { # get data fields, replacing undefined values @fields = map {defined() ? $_ : $undef} @{$self->[1][$i]}[@{$cols}]; # trim leading and trailing white space, and replace spaces with underscores for (@fields) {s/^\s*(.*?)\s*$/$1/; s/ /_/g}; # print the data record print $fh join($fs, @fields), $rs; } # close the file close($fh); } # write TIFF file # RGB, CMYK, and CIE L*a*b* color spaces supported # 8-bit, 16-bit or 32-bit, Intel or Motorola byte order supported # alpha and spot channels in RGB and CMYK files supported # supported hash keys: 'width', 'height', 'gap', 'left', 'right', 'rows', 'bits', 'dither', 'endian', 'xres', 'yres', 'unit' # parameters: (path_to_file, [row_slice, [column_slice]], [hash]) sub writeTIFF { # local variables my ($hash, $trows, $tcols, $n, @files, $fh); my ($base, $cs, %fields, @alpha, $pi, $rcols, $fmt, $mult, $mab, $samples); my ($width, $height, $gap, $left, $right, $bits, $xres, $yres, $unit); my ($le, $short, $long, $fp, $max, $minab, $maxab); my ($tags, $imagewidth, $bytecount, $stripsize); my ($ifd, $data, @cmyk, @spot); # get optional hash $hash = pop() if (ref($_[-1]) eq 'HASH'); # get remaining parameters my ($self, $path, $rows, $cols) = @_; # if row slice defined if (defined($rows)) { # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # get row length if row slice is Math::Matrix object $trows = @{$rows->[0]} if (UNIVERSAL::isa($rows, 'Math::Matrix')); # flatten row slice $rows = ICC::Shared::flatten($rows); } } else { # use all rows $rows = [1 .. $#{$self->[1]}]; } # get number of rows $n = @{$rows}; # filter row slice @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows}; # warn if invalid samples ($n == @{$rows}) || warn('row slice contains invalid samples'); # get target row length, if not defined by row matrix $trows = _getRowLength($self, $hash) if (! defined($trows)); # limit to number of samples $trows = $trows > $n ? $n : $trows; # verify row length ($trows == int($trows) && $trows > 0) || croak('invalid row length, stopped'); # compute target columns $tcols = int($n/$trows) + ($n % $trows ? 1 : 0); # if column slice defined if (defined($cols)) { # if column slice an empty array reference if (ref($cols) eq 'ARRAY' && @{$cols} == 0) { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } else { # flatten column slice $cols = ICC::Shared::flatten($cols); } } else { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } # get number of columns $n = @{$cols}; # filter column slice @{$cols} = grep {$_ == int($_) && defined($self->[1][0][$_])} @{$cols}; # warn if invalid fields ($n == @{$cols}) || warn('column slice contains invalid fields'); # for each column in slice for (@{$self->[1][0]}[@{$cols}]) { # if a supported color space if (m/^((?:.*\|)?(RGB|CMYK|[4-9A-F]CLR|LAB)_)/) { # set base and color space $base = $1; $cs = $2; # quit loop last(); } } # verify color space (defined($cs)) || croak('column slice does not contain a supported color space, stopped'); # get bits per sample and verify $bits = defined($hash->{'bits'}) ? $hash->{'bits'} : 16; ($bits == 8 || $bits == 16 || $bits == 32) || croak('invalid \'bits\' parameter, stopped'); # set little-endian flag from system config $le = ($Config{'byteorder'} =~ m/1234/); # if endian parameter provided if (defined($hash->{'endian'})) { # if little-endian if ($hash->{'endian'} eq 'little') { # set flag $le = 1; # if big-endian } elsif ($hash->{'endian'} eq 'big') { # clear flag $le = 0; } else { # warn warn('invalid \'endian\' parameter'); } } # if little-endian if ($le) { # set 'pack' formats $short = 'v'; $long = 'V'; $fp = 'f<'; } else { # set 'pack' formats $short = 'n'; $long = 'N'; $fp = 'f>'; } # make lookup hash of column slice fields %fields = map {defined($self->[1][0][$_]) ? ($self->[1][0][$_], $_) : ()} @{$cols}; # if color space is RGB if ($cs eq 'RGB') { # set photometric interpretation $pi = 2; # get alpha channels (if any) @alpha = map {defined($fields{"$base$_"}) ? $fields{"$base$_"} : ()} ('A', 'A0' .. 'A9'); # get refined column slice (including alpha channels) $rcols = [(map {$fields{"$base$_"}} qw(R G B)), @alpha]; # set pack format (8, 16 or 32 bits) $fmt = ($bits == 8) ? 'C*' : ($bits == 16) ? "$short*" : "$fp*"; # set multiplier (8, 16 or 32 bits) $mult = ($bits == 8) ? 1 : ($bits == 16) ? 257 : 1/255; # if color space is CMYK (8 or 16 bits) } elsif ($cs eq 'CMYK' && $bits != 32) { # set photometric interpretation $pi = 5; # get refined column slice $rcols = [map {$fields{"$base$_"}} qw(C M Y K)]; # set pack format (8 or 16 bits) $fmt = ($bits == 8) ? 'C*' : "$short*"; # set multiplier (8 or 16 bits) $mult = ($bits == 8) ? 2.55 : 655.35; # if color space is nCLR (8 or 16 bits) } elsif ($cs =~ m/^([4-9A-F])CLR$/ && $bits != 32) { # set photometric interpretation $pi = 5; # get refined column slice $rcols = [map {$fields{sprintf('%s%x', $base, $_)}} (1 .. CORE::hex($1))]; # set pack format (8 or 16 bits) $fmt = ($bits == 8) ? 'C*' : "$short*"; # set multiplier (8 or 16 bits) $mult = ($bits == 8) ? 2.55 : 655.35; # if color space if L*a*b* (8 or 16 bits) } elsif ($cs eq 'LAB' && $bits != 32) { # set photometric interpretation $pi = 8; # get refined column slice $rcols = [map {$fields{"$base$_"}} qw(L A B)]; # set pack format (8 or 16 bits) $fmt = ($bits == 8) ? '(Ccc)*' : "$short*"; # set multipliers (8 or 16 bits) $mult = ($bits == 8) ? 2.55 : 655.35; # L* $mab = ($bits == 8) ? 1 : 256; # a* and b* } else { # error croak('invalid TIFF format'); } # verify all fields defined (@{$rcols} == grep {defined()} @{$rcols}) || croak('column slice has missing fields, stopped'); # set number of samples $samples = @{$rcols}; # get the sample patch width and verify $width = defined($hash->{'width'}) ? $hash->{'width'} : 1; ($width == int($width) && $width > 0) || croak('invalid \'width\' parameter, stopped'); # get the sample patch height and verify $height = defined($hash->{'height'}) ? $hash->{'height'} : 1; ($height == int($height) && $height > 0) || croak('invalid \'height\' parameter, stopped'); # get the sample patch gap and verify $gap = defined($hash->{'gap'}) ? $hash->{'gap'} : 0; ($gap == int($gap) && $gap >= 0) || croak('invalid \'gap\' parameter, stopped'); # get the left edge width and verify $left = defined($hash->{'left'}) ? $hash->{'left'} : 0; ($left =~ m/^([0-9]+)(?:\.([0-9]+))?$/ && (! defined($2) || $1 >= $2)) || croak('invalid \'left\' parameter, stopped'); $left = [$1, defined($2) ? $2 : 0]; # get the right edge width and verify $right = defined($hash->{'right'}) ? $hash->{'right'} : 0; ($right =~ m/^([0-9]+)(?:\.([0-9]+))?$/ && (! defined($2) || $1 >= $2)) || croak('invalid \'right\' parameter, stopped'); $right = [$1, defined($2) ? $2 : 0]; # get the x-resolution and verify $xres = defined($hash->{'xres'}) ? $hash->{'xres'} : 72; ($xres > 0 && $xres <= 4E4) || croak('invalid \'xres\' parameter, stopped'); # get the y-resolution and verify $yres = defined($hash->{'yres'}) ? $hash->{'yres'} : 72; ($yres > 0 && $yres <= 4E4) || croak('invalid \'yres\' parameter, stopped'); # get the resolution unit and verify $unit = defined($hash->{'unit'}) ? $hash->{'unit'} : 2; ($unit == 1 || $unit == 2 || $unit == 3) || croak('invalid \'unit\' parameter, stopped'); # compute image width $imagewidth = $tcols * $width + ($tcols - 1) * $gap + $left->[0] - $left->[1] + $right->[0] - $right->[1]; # compute strip byte count $bytecount = $imagewidth * $height * $samples * $bits/8; # compute strip size (strips must begin on word boundary) $stripsize = $bytecount + $bytecount % 2; # set image tags [type, data] $tags->{'256'} = [3, $imagewidth]; # ImageWidth $tags->{'257'} = [3, $trows * $height]; # ImageLength $tags->{'258'} = [3, ($bits) x $samples]; # BitsPerSample $tags->{'259'} = [3, 1]; # Compression $tags->{'262'} = [3, $pi]; # PhotometricInterpretation $tags->{'273'} = [4, map {$_ * $stripsize + 8} (0 .. $trows - 1)]; # StripOffsets $tags->{'277'} = [3, $samples]; # SamplesPerPixel $tags->{'278'} = [3, $height]; # RowsPerStrip $tags->{'279'} = [4, ($bytecount) x $trows]; # StripByteCounts $tags->{'282'} = [5, $xres * 1E4, 1E4]; # XResolution $tags->{'283'} = [5, $yres * 1E4, 1E4]; # YResolution $tags->{'296'} = [3, $unit]; # ResolutionUnit $tags->{'339'} = [3, (3) x $samples] if ($bits == 32); # SampleFormat # resolve file list from path (defined($path) && (! ref($path)) && (@files = File::Glob::bsd_glob($path))) || croak("invalid path: $path, stopped"); # verify file path is unique (@files == 1) || warn('file path not unique'); # open the file open($fh, '>', $files[0]) || croak("$! when opening $files[0], stopped"); # set binary mode binmode($fh); # write TIFF header print $fh pack("A2$short$long", $le ? 'II' : 'MM', 42, $ifd = $trows * $stripsize + 8); # set min/max values $max = ($bits == 8) ? 255 : ($bits == 16) ? 65535 : 1; $minab = ($bits == 8) ? -128 : -32768; $maxab = ($bits == 8) ? 127 : 32767; # for each strip for my $i (0 .. $trows - 1) { # for each patch in strip for my $j (0 .. $tcols - 1) { # if patch in row slice if (defined($rows->[$trows * $j + $i])) { # if L*a*b* data if ($pi == 8) { # get the data $data->[$j][0] = $mult * $self->[1][$rows->[$trows * $j + $i]][$rcols->[0]]; $data->[$j][1] = $mab * $self->[1][$rows->[$trows * $j + $i]][$rcols->[1]]; $data->[$j][2] = $mab * $self->[1][$rows->[$trows * $j + $i]][$rcols->[2]]; # limit the data $data->[$j][0] = $data->[$j][0] < 0 ? 0 : ($data->[$j][0] > $max ? $max : $data->[$j][0]); $data->[$j][1] = $data->[$j][1] < $minab ? $minab : ($data->[$j][1] > $maxab ? $maxab : $data->[$j][1]); $data->[$j][2] = $data->[$j][2] < $minab ? $minab : ($data->[$j][2] > $maxab ? $maxab : $data->[$j][2]); # if CMYK + spot data } elsif ($pi == 5 && @{$rcols} > 4) { # get CMYK values @cmyk = @{$self->[1][$rows->[$trows * $j + $i]]}[@{$rcols}[0 .. 3]]; # get spot values @spot = @{$self->[1][$rows->[$trows * $j + $i]]}[@{$rcols}[4 .. $#{$rcols}]]; # get the data (spot channels are inverted) $data->[$j] = [(map {$_ * $mult} @cmyk), (map {(100 - $_) * $mult} @spot)]; # limit the data @{$data->[$j]} = map {$_ < 0 ? 0 : ($_ > $max ? $max : $_)} @{$data->[$j]}; # RGB data } else { # get the data $data->[$j] = [map {$_ * $mult} @{$self->[1][$rows->[$trows * $j + $i]]}[@{$rcols}]]; # limit the data (8 or 16 bits) @{$data->[$j]} = map {$_ < 0 ? 0 : ($_ > $max ? $max : $_)} @{$data->[$j]} if ($bits != 32); } # patch undefined } else { # if L*a*b* data if ($pi == 8) { # if last patch if ($i == ($trows - 1) && $j == ($tcols - 1)) { # set gray value $data->[$j] = [$max * 0.7, 0, 0]; } else { # set white value $data->[$j] = [$max, 0, 0]; } # if CMYK + spot data } elsif ($pi == 5) { # if last patch if ($i == ($trows - 1) && $j == ($tcols - 1)) { # set gray value $data->[$j] = [0, 0, 0, $max * 0.4, ($max) x ($samples - 4)]; } else { # set white value $data->[$j] = [0, 0, 0, 0, ($max) x ($samples - 4)]; } # RGB data } else { # if last patch if ($i == ($trows - 1) && $j == ($tcols - 1)) { # set gray value $data->[$j] = [($max * 0.7) x $samples]; } else { # set white value $data->[$j] = [($max) x $samples]; } } } } # write TIFF strip _writeTIFFstrip($fh, $tags, $width, $gap, $left, $right, $i, $data, $fmt, $hash->{'dither'}); } # write TIFF IFD _writeTIFFdir($fh, $ifd, $short, $long, $tags); # close file close($fh); } # write chart to Adobe Swatch Exchange (.ase) file # column slice must be CMYK, RGB or L*a*b* # color type: 0 - global, 1 - spot, 2 - normal (default) # parameters: (path_to_file, row_slice, column_slice, [color_type]) sub writeASE { # get parameters my ($self, $path, $rows, $cols, $type) = @_; # local variables my ($n, @fmt, $cs, $le, $sn, @files, $fh); my ($name, $slen, $blen); my ($cmyk, $rgb, $Lab, $val); # verify row_slice and column_slice are supplied (defined($rows) && defined($cols)) || croak('missing parameters'); # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[1]}]; } else { # flatten row slice $rows = ICC::Shared::flatten($rows); } # get number of rows $n = @{$rows}; # filter row slice @{$rows} = grep {$_ == int($_) && $_ != 0 && defined($self->[1][$_])} @{$rows}; # warn if invalid samples ($n == @{$rows}) || warn('row slice contains invalid samples'); # get format array @fmt = @{$self->[1][0]}[@{$cols}]; # if column slice is CMYK if (4 == @fmt && 4 == grep {m/^(?:.*\|)?CMYK_[CMYK]$/} @fmt) { # set color space $cs = 'CMYK'; # if column slice is RGB } elsif (3 == @fmt && 3 == grep {m/^(?:.*\|)?RGB_[RGB]$/} @fmt) { # set color space $cs = 'RGB '; # if column slice is L*a*b* } elsif (3 == @fmt && 3 == grep {m/^(?:.*\|)?LAB_[LAB]$/} @fmt) { # set color space $cs = 'LAB '; } else { # error croak('invalid column slice'); } # if color type is undefined, set default (2 - normal) $type = 2 if (! defined($type)); # verify color type ($type == int($type) && $type >= 0 && $type <= 2) || croak('invalid ASE color type'); # get little-endian flag $le = ($Config{'byteorder'} =~ m/1234/); # get sample name slice (could be undefined) $sn = $self->name; # resolve file list from path (@files = File::Glob::bsd_glob($path)) || croak('invalid file path'); # verify file path is unique (@files == 1) || warn('file path not unique'); # open file open($fh, '>', $files[0]); # set binary mode binmode($fh); # print header (file signature, version, number of blocks) print $fh pack('A4nnN', 'ASEF', 1, 0, scalar(@{$rows})); # for each sample for my $s (@{$rows}) { # if color space is CMYK if ($cs eq 'CMYK') { # get the CMYK values $cmyk = $self->slice([$s], $cols); # if SAMPLE_NAME is defined if (defined($sn)) { # get color name $name = $self->[1][$s][$sn->[0]]; # replace underscores with spaces $name =~ s/_/ /g; } else { # build color name from CMYK values $name = sprintf('C=%d M=%d Y=%d K=%d', @{$cmyk->[0]}); } # compute string length $slen = length($name) + 1; # compute block length $blen = 2 * $slen + 24; # print block print $fh pack('nNn', 1, $blen, $slen); print $fh encode('UTF-16BE', $name . "\x00"); print $fh pack('A4', 'CMYK'); # for each CMYK value for my $i (0 .. 3) { # convert to floating point $val = pack('f', $cmyk->[0][$i]/100); # reverse if little-endian system $val = reverse($val) if ($le); # print value print $fh $val; } # print color type print $fh pack('n', $type); # if color space is RGB } elsif ($cs eq 'RGB ') { # get the RGB values $rgb = $self->slice([$s], $cols); # if SAMPLE_NAME is defined if (defined($sn)) { # get color name $name = $self->[1][$s][$sn->[0]]; # replace underscores with spaces $name =~ s/_/ /g; } else { # build color name from RGB values $name = sprintf('R=%d G=%d B=%d', @{$rgb->[0]}); } # compute string length $slen = length($name) + 1; # compute block length $blen = 2 * $slen + 20; # print block print $fh pack('nNn', 1, $blen, $slen); print $fh encode('UTF-16BE', $name . "\x00"); print $fh pack('A4', 'RGB '); # for each RGB value for my $i (0 .. 2) { # convert to floating point $val = pack('f', $rgb->[0][$i]/255); # reverse if little-endian system $val = reverse($val) if ($le); # print value print $fh $val; } # print color type print $fh pack('n', $type); # if color space is L*a*b* } elsif ($cs eq 'LAB ') { # get the L*a*b* values $Lab = $self->slice([$s], $cols); # if SAMPLE_NAME is defined if (defined($sn)) { # get color name $name = $self->[1][$s][$sn->[0]]; # replace underscores with spaces $name =~ s/_/ /g; } else { # build color name from L*a*b* values $name = sprintf('L=%d a=%d b=%d', @{$Lab->[0]}); } # compute string length $slen = length($name) + 1; # compute block length $blen = 2 * $slen + 20; # print block print $fh pack('nNn', 1, $blen, $slen); print $fh encode('UTF-16BE', $name . "\x00"); print $fh pack('A4', 'LAB '); # modify L* value $Lab->[0][0] /= 100; # for each L*a*b* value for my $i (0 .. 2) { # convert to floating point $val = pack('f', $Lab->[0][$i]); # reverse if little-endian system $val = reverse($val) if ($le); # print value print $fh $val; } # print color type print $fh pack('n', $type); } } # close file close($fh); } # print object contents to string # format is an array structure # parameter: ([format]) # returns: (string) sub sdump { # get parameters my ($self, $p) = @_; # local variables my ($s, $fmt); # resolve parameter to an array reference $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : []; # get format string $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef'; # set string to object ID $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self); # return return($s); } # get column slice from DATA_FORMAT keys # format_keys is a list of keys with optional context # column_slice is reference to an array of column indices # note: returns 'undef' if any column is missing # parameters: (format_keys) # returns: (column_slice) sub _cols { # get object reference my $self = shift(); # local variables my (%fmt, @cols); # make lookup hash of DATA_FORMAT keys %fmt = map {defined($self->[1][0][$_]) ? ($self->[1][0][$_], $_) : ()} (0 .. $#{$self->[1][0]}); # lookup format keys in hash @cols = @fmt{@_}; # return column slice or undef if any columns undefined return((grep {! defined()} @cols) ? undef : \@cols); } # get spectral fields array # array contains column indices and wavelength # and is sorted by wavelength (low to high) # parameters: (object_reference, [context]) # returns: (array_reference) sub _spectral { # get parameters my ($self, $context) = @_; # local variables my (%fmt, @fields); # make lookup hash (context|wavelength -or- wavelength => column) %fmt = map {($self->[1][0][$_] =~ m/^(.*\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)(\d{3})$/) ? (defined($1) ? "$1$2" : $2, $_) : ()} (0 .. $#{$self->[1][0]}); # if context defined if (defined($context)) { # make list of matching fields @fields = map {m/^$context\|(\d{3})$/ ? [$fmt{$_}, $1] : ()} keys(%fmt); } else { # make list of matching fields @fields = map {m/^(\d{3})$/ ? [$fmt{$_}, $1] : ()} keys(%fmt); # if no matching fields if (@fields == 0) { # make lookup hash (wavelength => column) %fmt = map {($self->[1][0][$_] =~ m/^(?:.*\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)(\d{3})$/) ? ($1, $_) : ()} (0 .. $#{$self->[1][0]}); # make list of fields @fields = map {[$fmt{$_}, $_]} keys(%fmt); } } # return undef if no match return() if (@fields == 0); # sort by wavelength @fields = sort {$a->[1] <=> $b->[1]} @fields; # return array reference return(\@fields); } # binary search # locates the interval containing or bounding the target value # returns an array of four index values, which indicate upper and lower transitions # parameters: (source_array, target_value, channel_index, low_index, high_index) # returns: (interval_index_array) sub _bin_search { # get parameters my ($source, $target, $channel, $low, $high) = @_; # local variables my ($k, $interval); # copy low and high indices $interval->[0] = $low; $interval->[1] = $high; # while interval is open while ($interval->[1] - $interval->[0] > 1) { # compute the midpoint $k = int(($interval->[1] + $interval->[0])/2); # if midpoint value >= target value if ($source->[$k][$channel] >= $target) { # set higher index to midpoint $interval->[1] = $k; } else { # set lower index to midpoint $interval->[0] = $k; } } # copy low and high indices $interval->[2] = $low; $interval->[3] = $high; # while interval is open while ($interval->[3] - $interval->[2] > 1) { # compute the midpoint $k = int(($interval->[3] + $interval->[2])/2); # if midpoint value > target value if ($source->[$k][$channel] > $target) { # set higher index to midpoint $interval->[3] = $k; } else { # set lower index to midpoint $interval->[2] = $k; } } # return interval array return($interval); } # linear search # locates the closest source sample based on Manhattan distance # parameters: (source_array, target_vector) # returns: (low_index, high_index) sub _lin_search { # get parameters my ($source, $target) = @_; # local variables my ($d0, $d1, $d2, $low, $high); # set initial difference $d0 = @{$target}; # for each source sample for my $i (0 .. $#{$source}) { # clear differences $d1 = $d2 = 0; # for each channel for my $j (0 .. $#{$target}) { # add difference to target sample $d1 += abs($source->[$i][$j] - $target->[$j]); # add difference to previous sample $d2 += abs($source->[$i][$j] - $source->[$i - 1][$j]) if ($i > 0); } # if new difference less if ($d1 < $d0) { # save index $low = $high = $i; # update difference $d0 = $d1; } # if duplicate sample if ($d0 == $d1 && $d2 == 0) { # save index $high = $i; } } # return return($low, $high); } # add average sample # assumes device values (if any) are same for each sample # averages measurements values - spectral, XYZ, L*a*b*, or density # L*a*b* values are converted to xyz for averaging, then back to L*a*b* # density values are converted to reflectance for averaging, then back to density # parameters: (object_reference, row_slice, linear_slice, L*a*b*_slice, density_slice, id_slice, name_slice, hash) # returns: (average_sample_index) sub _add_avg { # get parameters my ($self, $rows, $c1, $c2, $c3, $id, $name, $hash) = @_; # local variables my ($n, $next, @xyz, $sid, $sn); # get number of samples $n = @{$rows}; # get index of next data row $next = $#{$self->[1]} + 1; # copy first sample $self->[1][$next] = [@{$self->[1][shift(@{$rows})]}]; # for each group of L*a*b* columns for (my $j = 0; $j < @{$c2}; $j += 3) { # convert to L*a*b* values to xyz @{$self->[1][$next]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_Lab2xyz(@{$self->[1][$next]}[@{$c2}[$j .. $j + 2]]); } # for each density column for my $j (@{$c3}) { # convert to density to reflectance $self->[1][$next][$j] = POSIX::pow(10, -$self->[1][$next][$j]); } # for remaining samples for my $i (@{$rows}) { # for each linear column for my $j (@{$c1}) { # add value $self->[1][$next][$j] += $self->[1][$i][$j]; } # for each group of L*a*b* columns for (my $j = 0; $j < @{$c2}; $j += 3) { # get xyz values @xyz = ICC::Shared::_Lab2xyz(@{$self->[1][$i]}[@{$c2}[$j .. $j + 2]]); # add to self $self->[1][$next][$c2->[$j]] += $xyz[0]; $self->[1][$next][$c2->[$j + 1]] += $xyz[1]; $self->[1][$next][$c2->[$j + 2]] += $xyz[2]; } # for each density column for my $j (@{$c3}) { # add temp reflectance $self->[1][$next][$j] += POSIX::pow(10, -$self->[1][$i][$j]); } } # for each measurement column for my $j (@{$c1}, @{$c2}, @{$c3}) { # divide by number of samples $self->[1][$next][$j] /= $n; } # for each group of L*a*b* columns for (my $j = 0; $j < @{$c2}; $j += 3) { # convert to xyz values to L*a*b* @{$self->[1][$next]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_xyz2Lab(@{$self->[1][$next]}[@{$c2}[$j .. $j + 2]]); } # for each density column for my $j (@{$c3}) { # convert to reflectance to density $self->[1][$next][$j] = -POSIX::log10($self->[1][$next][$j]); } # get SAMPLE_ID value from hash $sid = $hash->{'id'}; # for each SAMPLE_ID column for my $i (@{$id}) { # if SAMPLE_ID defined if (defined($sid)) { # set to hash value $self->[1][$next][$i] = $sid; } else { # set to row index $self->[1][$next][$i] = $next; } } # get SAMPLE_NAME value from hash $sn = $hash->{'name'}; # for each SAMPLE_NAME column for my $i (@{$name}) { # if SAMPLE_NAME defined if (defined($sn)) { # set to hash value $self->[1][$next][$i] = $sn; } else { # append '_AVG' to existing value $self->[1][$next][$i] .= '_AVG'; } } # return row return($next); } # get averaging groups # returns column slices for each averaging method # parameters: (object_reference, hash) # returns: (linear_slice, L*a*b*_slice, density_slice) sub _avg_groups { # get parameters my ($self, $hash) = @_; # local variables my (@c1, @c2, @c3, @cs); # for each format field for my $i (0 .. $#{$self->[1][0]}) { # add column if XYZ or spectral field push(@c1, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?(?:XYZ_[XYZ]|(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3})$/); # add column if L*a*b* field push(@c2, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?LAB_[LAB]$/); # add column if density field push(@c3, $i) if ($self->[1][0][$i] =~ m/^(?:.*\|)?D_(?:RED|GREEN|BLUE|VIS)$/); } # linear averaging method (L*a*b* values are converted to xyz, density values are converted to reflectance) if (! defined($hash->{'method'}) || $hash->{'method'} eq 'LINEAR') { # verify number of L*a*b* fields (@c2 % 3 == 0) || croak('wrong number of L*a*b* fields'); # for each group of L*a*b* columns for (my $j = 0; $j < @c2; $j += 3) { # sort by field name @cs = sort {$self->[1][0][$a] cmp $self->[1][0][$b]} @c2[$j .. $j + 2]; # verify field consistency (join('', map {substr($_, -1, 1)} @{$self->[1][0]}[@cs]) eq 'ABL') || croak('L*a*b* field inconsistency'); # save columns in LAB order @c2[$j .. $j + 2] = @cs[2, 0, 1]; } # if simple averaging method } elsif (defined($hash->{'method'}) && $hash->{'method'} eq 'SIMPLE') { # copy L*a*b* and density columns to XYZ or spectral array push(@c1, @c2, @c3); # clear L*a*b* and density arrays @c2 = (); @c3 = (); } else { # error croak('unsupported averaging method'); } # return slices return(\@c1, \@c2, \@c3); } # add OBA effect to XYZ array # parameters: (chart_object, M1_slice, M2_slice, XYZ_array, oba_factor, hash) sub _add_oba { # get parameters my ($self, $spec1, $spec2, $xyz, $oba, $hash) = @_; # local variables my ($color, $illum, @m1, @m2, $spectral, $xyzoba); # save illuminant $illum = $hash->{'illuminant'}; # if illuminant an array reference if (defined($hash->{'illuminant'}) && ref($hash->{'illuminant'}) eq 'ARRAY') { # set illuminant to CIE D50 $hash->{'illuminant'} = ['CIE', 'D50']; } else { # set illuminant to ASTM D50 $hash->{'illuminant'} = 'D50'; } # make 'Color.pm' object (D50 illuminant) $color = ICC::Support::Color->new($hash); # restore illuminant $hash->{'illuminant'} = $illum; # for each sample for my $i (1 .. $#{$self->[1]}) { # get M1 spectral values @m1 = @{$self->[1][$i]}[@{$spec1}]; # get M2 spectral values @m2 = @{$self->[1][$i]}[@{$spec2}]; # compute (M1 - M2) spectral values $spectral->[$i - 1] = [map {$m1[$_] - $m2[$_]} (0 .. $#m1)]; } # transform (M1 - M2) spectral to D50 XYZ (hash may contain 'encoding' key) $xyzoba = ICC::Support::Color::_trans2($color, $spectral, $hash); # for each sample for my $i (0 .. $#{$xyz}) { # for each XYZ for my $j (0 .. 2) { # add scaled OBA effect $xyz->[$i][$j] += $xyzoba->[$i][$j] * $oba; } } } # get/set data # common routine called by get/set methods # row_slice and column_slice may be either a scalar or array reference # an empty array reference indicates all samples or fields # replacement_data is reference to a 2-D array of replacement values # array dimensions must match size of row_slice and column_slice # data_slice is Math::Matrix object, defined by row_slice and column_slice # get_code_ref and set_code_ref transform the data when getting and setting # parameters: (object_ref, object_index, row_slice, column_slice, replacement_data, get_code_ref, set_code_ref) # if column_slice undefined, returns: () # if row_slice undefined, returns: (column_slice) # if replacement_data undefined, returns: (data_slice) # otherwise, sets replacement data and returns: (column_slice) sub _getset { # get parameters my ($self, $ix, $rows, $cols, $data, $get, $set) = @_; # return empty if no column slice defined($cols) || return(); # if column slice an empty array reference if (ref($cols) eq 'ARRAY' && @{$cols} == 0) { # use all columns $cols = [0 .. $#{$self->[$ix][0]}]; } else { # flatten column slice $cols = ICC::Shared::flatten($cols); # verify column slice contents (@{$cols} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$cols}) || croak('invalid column slice'); } # return columns slice if no row slice defined($rows) || return($cols); # if row slice an empty array reference if (ref($rows) eq 'ARRAY' && @{$rows} == 0) { # use all rows $rows = [1 .. $#{$self->[$ix]}]; } else { # flatten row slice $rows = ICC::Shared::flatten($rows); # verify row slice contents (@{$rows} == grep {! ref() && $_ == int($_) && $_ >= 0} @{$rows}) || croak('invalid row slice'); } # no replacement data (get) if (! defined($data)) { # verify 'get' code ref, or use identity function $get = (defined($get) && ref($get) eq 'CODE') ? $get : sub {@_}; # for each row for my $i (0 .. $#{$rows}) { # get transformed data row @{$data->[$i]} = &$get(@{$self->[$ix][$rows->[$i]]}[@{$cols}]); } # return data slice as a Math::Matrix object return(bless($data, 'Math::Matrix')); # with replacement data (set) } else { # verify replacement data is 2-D array or Math::Matrix object ((ref($data) eq 'ARRAY' || UNIVERSAL::isa($data, 'Math::Matrix')) && ref($data->[0]) eq 'ARRAY') || croak('replacement data not a 2-D array reference'); # verify replacement data size ($#{$data} == $#{$rows} && $#{$data->[0]} == $#{$cols}) || croak('replacement data is wrong sized'); # verify 'set' code ref, or use identity function $set = (defined($set) && ref($set) eq 'CODE') ? $set : sub {@_}; # for each row for my $i (0 .. $#{$rows}) { # set transformed data row @{$self->[$ix][$rows->[$i]]}[@{$cols}] = &$set(@{$data->[$i]}); } # return column slice return($cols); } } # get accumulated sample values # sample dimensions are in pixels # used by _readChartTIFF to extract samples from a data stripe # parameters: (reference_to_data, sample_offset, sample_width, number_channels) # returns: (accumulated_sample_values) sub _getSample { # get parameters my ($data, $so, $sx, $c) = @_; # initialize sample values my @sv = (0) x $c; # for each row for my $i (0 .. $#{$data}) { # for each pixel for my $j (0 .. $sx - 1) { # for each channel for my $k (0 .. $c - 1) { # accumulate sample value $sv[$k] += $data->[$i][($so + $j) * $c + $k]; } } } # return sample values return(@sv); } # get row length # parameters: (object_reference, hash) # returns: (row_length) sub _getRowLength { # get parameters my ($self, $hash) = @_; # local variables my ($rows, $n, $square); # if 'rows' hash key is defined if (defined($hash->{'rows'})) { # get row length value $rows = $hash->{'rows'}; # if valid row length if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) { # return return($rows); } else { # warn warn('invalid \'rows\' parameter'); } } # if LGOROWLENGTH keyword if ($rows = keyword($self, 'LGOROWLENGTH')) { # if valid row length if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) { # return return($rows); } else { # warn warn('invalid \'LGOROWLENGTH\' value'); } } # if NUMBER_OF_STRIPS keyword if ($rows = keyword($self, 'NUMBER_OF_STRIPS')) { # if valid row length if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) { # return return($rows); } else { # warn warn('invalid \'NUMBER_OF_STRIPS\' value'); } } # if 'NumberPatchRows' key is defined if (defined($self->[0]{'xrp:CustomAttributes'}{'NumberPatchRows'})) { # get row length value $rows = $self->[0]{'xrp:CustomAttributes'}{'NumberPatchRows'}; # if valid row length if (Scalar::Util::looks_like_number($rows) && $rows > 0 && $rows == int($rows)) { # return return($rows); } else { # warn warn('invalid \'NumberPatchRows\' attribute'); } } # get number of samples $n = $#{$self->[1]}; # return if 0 return(0) if ($n == 0); # return if 1 or 2 return(1) if ($n < 3); # compute size of square chart $square = POSIX::ceil(sqrt($n)); # return if chart is square return($square) if ($n == $square**2); # set row length one less than square chart $rows = $square - 1; # while modulus is non-zero, decrement row length while ($n % $rows) {$rows--} # return row length, choosing full rectangle if possible return($rows > $square/2 ? $rows : $square); } # get illuminant white point # returns XYZ vector from colorimetry array # returns D50 if CAT or undefined # parameter: (object_reference, column_slice, [hash]) # returns: (XYZ_vector) sub _illumWP { # get parameters my ($self, $cols, $hash) = @_; # if XYZ values are valid if (3 == grep {defined() && ! ref() && $_ > 0} @{$self->[2][2]}[@{$cols}]) { # return XYZ vector return([@{$self->[2][2]}[@{$cols}]]); } else { # return D50 vector return(ICC::Shared::D50); } } # compute media white point # multiple samples are averaged # result also stored in colorimetry array # parameter: (object_reference, column_slice, [hash]) # returns: (XYZ_vector) sub _mediaWP { # get parameters my ($self, $cols, $hash) = @_; # local variables my ($WPxyz, $dev, $mwv, $n, @XYZ, @XYZs); # if column slice is L*a*b* if ((3 == grep {$self->[1][0][$_] =~ m/LAB_[LAB]$/} @{$cols})) { # get illuminant white point $WPxyz = defined($self->[2][2][$cols->[0]]) ? [@{$self->[2][2]}[@{$cols}]] : ICC::Shared::D50; # if column slice is not XYZ } elsif ((3 != grep {$self->[1][0][$_] =~ m/XYZ_[XYZ]$/} @{$cols})) { # warning warn('column slice not XYZ or L*a*b* data'); # return empty return(); } # if no device data (using 'device' context) if (! ($dev = device($self, {'context' => $hash->{'device'}}))) { # warning warn('no device data'); # return empty return(); } # set media white device value (255 if RGB, 0 otherwise) $mwv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 255 : 0; # for each sample for my $i (1 .. $#{$self->[1]}) { # if all device channels are white if (@{$dev} == grep {$_ == $mwv} @{$self->[1][$i]}[@{$dev}]) { # if L*a*b* data if ($WPxyz) { # convert L*a*b* values to XYZ @XYZs = ICC::Shared::_Lab2XYZ(@{$self->[1][$i]}[@{$cols}], $WPxyz); # accumulate XYZ values $XYZ[0] += $XYZs[0]; $XYZ[1] += $XYZs[1]; $XYZ[2] += $XYZs[2]; # if XYZ data } else { # accumulate XYZ values $XYZ[0] += $self->[1][$i][$cols->[0]]; $XYZ[1] += $self->[1][$i][$cols->[1]]; $XYZ[2] += $self->[1][$i][$cols->[2]]; } # increment count $n++; } } # if media white sample(s) if ($n) { # store average XYZ values in colorimetry array, and return XYZ vector return([@{$self->[2][3]}[@{$cols}] = map {$_/$n} @XYZ]); } else { # warning warn('no media white sample found'); # return empty return(); } } # compute media black point # multiple samples are averaged # result also stored in colorimetry array # parameter: (object_reference, column_slice, [hash]) # returns: (XYZ_vector) sub _mediaBP { # get parameters my ($self, $cols, $hash) = @_; # local variables my ($WPxyz, $dev, $mbv, $n, @XYZ, @XYZs); # if column slice is L*a*b* if ((3 == grep {$self->[1][0][$_] =~ m/LAB_[LAB]$/} @{$cols})) { # get illuminant white point $WPxyz = defined($self->[2][2][$cols->[0]]) ? [@{$self->[2][2]}[@{$cols}]] : ICC::Shared::D50; # if column slice is not XYZ } elsif ((3 != grep {$self->[1][0][$_] =~ m/XYZ_[XYZ]$/} @{$cols})) { # warning warn('column slice not XYZ or L*a*b* data'); # return empty return(); } # if no device data (using 'device' context) if (! ($dev = device($self, {'context' => $hash->{'device'}}))) { # warning warn('no device data'); # return empty return(); } # set media black device value (0 if RGB, 100 otherwise) $mbv = ($self->[1][0][$dev->[0]] =~ m/RGB_R$/) ? 1 : 100; # for each sample for my $i (1 .. $#{$self->[1]}) { # if all device channels are black if (@{$dev} == grep {$_ == $mbv} @{$self->[1][$i]}[@{$dev}]) { # increment count $n++; # if L*a*b* data if ($WPxyz) { # convert L*a*b* values to XYZ @XYZs = ICC::Shared::_Lab2XYZ(@{$self->[1][$i]}[@{$cols}], $WPxyz); # accumulate XYZ values $XYZ[0] += $XYZs[0]; $XYZ[1] += $XYZs[1]; $XYZ[2] += $XYZs[2]; # if XYZ data } else { # accumulate XYZ values $XYZ[0] += $self->[1][$i][$cols->[0]]; $XYZ[1] += $self->[1][$i][$cols->[1]]; $XYZ[2] += $self->[1][$i][$cols->[2]]; } } } # if media black sample(s) if ($n) { # store average XYZ values in colorimetry array, and return XYZ vector return([@{$self->[2][4]}[@{$cols}] = map {$_/$n} @XYZ]); } else { # warning warn('no media black sample found'); # return empty return(); } } # make SAMPLE_ID hash # if no SAMPLE_ID field, hash is initialized # parameter: (object_reference) sub _makeSampleID { # get object reference my $self = shift(); # if SAMPLE_ID column(s) exist if (my @id = grep {$self->[1][0][$_] =~ m/^(?:.*\|)?(?:SAMPLE_ID|SampleID)$/} (0 .. $#{$self->[1][0]})) { # make the SAMPLE_ID hash, omitting undefined ID values $self->[4] = {map {defined($self->[1][$_][$id[0]]) ? ($self->[1][$_][$id[0]], $_) : ()} (1 .. $#{$self->[1]})}; } else { # initialize the hash $self->[4] = {}; } } # add colorimetry metadata # called when creating a new object # parameter: (object_reference) sub _addColorMeta { # get object reference my $self = shift(); # local variables my (@cols, $hash, $illum, $spec, $nm, $str, $color, $WPxyz, @values); # if object contains colorimetric data if (@cols = grep {$self->[1][0][$_] =~ m/^(?:(.*)\|)?(?:LAB_[LAB]|XYZ_[XYZ]|STDEV_[LABXYZ]|MEAN_DE|STDEV_DE|CHI_SQD_PAR)$/} (0 .. $#{$self->[1][0]})) { # set default hash values $hash = {'illuminant' => 'D50', 'observer' => '2'}; # if CxF3 'TristimulusSpec' node if (defined($self->[0]{'CxF3_dom'}) && 0) { ##### to be implemented ##### # if 'WEIGHTING_FUNCTION' keyword(s) } elsif (@values = keyword($self, 'WEIGHTING_FUNCTION')) { # join values into string $str = join(';', @values); # match illuminant and save in hash $hash->{'illuminant'} = $1 if ($str =~ m/ILLUMINANT\s*,\s*(\w+)"/); # match observer and save in hash $hash->{'observer'} = $1 if ($str =~ m/OBSERVER\s*,\s*(\d+).*"/); } # if non-standard illuminant if ($hash->{'illuminant'} ne 'D50' || $hash->{'observer'} ne '2') { # make an empty 'Color.pm' object $color = ICC::Support::Color->new(); # if illuminant is an ARRAY reference if (ref($hash->{'illuminant'}) eq 'ARRAY') { # initialize object for CIE method ICC::Support::Color::_cie($color, $hash); } else { # initialize object for ASTM method ICC::Support::Color::_astm($color, $hash); } # use computed white point $WPxyz = $color->iwtpt(); } else { # use D50 $WPxyz = ICC::Shared::D50; } # for each colorimetric field for my $i (@cols) { # if field name ends in L or X if ($self->[1][0][$i] =~ m/[LX]$/) { # save WP X-value $self->[2][2][$i] = $WPxyz->[0]; # if field name ends in A or Y } elsif ($self->[1][0][$i] =~ m/[AY]$/) { # save WP Y-value $self->[2][2][$i] = $WPxyz->[1]; # if field name ends in B or Z } elsif ($self->[1][0][$i] =~ m/[BZ]$/) { # save WP Z-value $self->[2][2][$i] = $WPxyz->[2]; } } } } # read chart from list of data files # averages color measurement data (spectral, XYZ, L*a*b* or density) # files must have identical structure (rows and cols) # parameters: (object_reference, ref_to_file_list, hash) # returns: (number_of_files_averaged) sub _readChartAvg { # get parameters my ($self, $list, $hash) = @_; # local variables my ($n, $result, $c1, $c2, $c3, $keys, $temp, @xyz); my ($charts, $fstat, @ctx1, @ctx2, $add_hash); # initialize file count $n = 0; # if hash is defined if (defined($hash)) { # for each hash key for (keys(%{$hash})) { # if XYZ based stat requested if (m/^STDEV_XYZ$/) { # if value is a scalar if (! ref($hash->{$_})) { # save XYZ context push(@ctx1, $hash->{$_}); } elsif (ref($hash->{$_}) eq 'ARRAY') { # save XYZ contexts push(@ctx1, @{$hash->{$_}}); } # increment flag $fstat++; # if L*a*b* based stat requested } elsif (m/^(MEAN_DE|STDEV_LAB|CHI_SQD_PAR)$/) { # if value is a scalar if (! ref($hash->{$_})) { # save L*a*b* context push(@ctx2, $hash->{$_}); } elsif (ref($hash->{$_}) eq 'ARRAY') { # save L*a*b* contexts push(@ctx2, @{$hash->{$_}}); } # increment flag $fstat++; } } } # for each file for my $file (@{$list}) { # if first file if ($n == 0) { # if file read successfully if (! ($result = _readChart($self, $file, $hash))) { # add colorimetric metadata _addColorMeta($self); # make format key string $keys = join(':', map {defined() ? $_ : '-'} @{$self->[1][0]}); # for each XYZ context for my $ctx (@ctx1) { # copy the hash $add_hash = Storable::dclone($hash); # set the context (undef for no context) $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef; # delete the 'added' context delete($add_hash->{'added'}); # add the XYZ values add_xyz($self, $add_hash); } # for each L*a*b* context for my $ctx (@ctx2) { # copy the hash $add_hash = Storable::dclone($hash); # set the context (undef for no context) $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef; # delete the 'added' context delete($add_hash->{'added'}); # add the L*a*b* values add_lab($self, $add_hash); } # save copy of chart data, if needed for stats $charts->[0] = Storable::dclone($self->[1]) if ($fstat); # get averaging groups ($c1, $c2, $c3) = _avg_groups($self, $hash); # if there are L*a*b* or density groups if (@{$c2} || @{$c3}) { # for each sample for my $i (1 .. $#{$self->[1]}) { # for each group of L*a*b* columns for (my $j = 0; $j < @{$c2}; $j += 3) { # convert to L*a*b* values to xyz @{$self->[1][$i]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_Lab2xyz(@{$self->[1][$i]}[@{$c2}[$j .. $j + 2]]); } # for each density column for my $j (@{$c3}) { # convert to density to reflectance $self->[1][$i][$j] = POSIX::pow(10, -$self->[1][$i][$j]); } } } # increment file count $n++; } else { # print warning warn("chart $file $result, ignored\n"); } } else { # make temporary Chart object $temp = ICC::Support::Chart->new(); # if file read successfully if (! ($result = _readChart($temp, $file, $hash))) { # if charts have same structure (rows and cols) if ($#{$self->[1]} == $#{$temp->[1]} && $keys eq join(':', map {defined() ? $_ : '-'} @{$temp->[1][0]})) { # for each XYZ context for my $ctx (@ctx1) { # copy the hash $add_hash = Storable::dclone($hash); # set the context (undef for no context) $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef; # delete the 'added' context delete($add_hash->{'added'}); # add the XYZ values add_xyz($temp, $add_hash); } # for each L*a*b* context for my $ctx (@ctx2) { # copy the hash $add_hash = Storable::dclone($hash); # set the context (undef for no context) $add_hash->{'context'} = defined($ctx) && length($ctx) ? $ctx : undef; # delete the 'added' context delete($add_hash->{'added'}); # add the L*a*b* values add_lab($temp, $add_hash); } # save copy of chart data, if needed for stats $charts->[$n] = $temp->[1] if ($fstat); # for each sample for my $i (1 .. $#{$self->[1]}) { # for each linear column for my $j (@{$c1}) { # add temp value $self->[1][$i][$j] += $temp->[1][$i][$j]; } # for each group of L*a*b* columns for (my $j = 0; $j < @{$c2}; $j += 3) { # get temp xyz values @xyz = ICC::Shared::_Lab2xyz(@{$temp->[1][$i]}[@{$c2}[$j .. $j + 2]]); # add to self $self->[1][$i][$c2->[$j]] += $xyz[0]; $self->[1][$i][$c2->[$j + 1]] += $xyz[1]; $self->[1][$i][$c2->[$j + 2]] += $xyz[2]; } # for each density column for my $j (@{$c3}) { # add temp reflectance $self->[1][$i][$j] += POSIX::pow(10, -$temp->[1][$i][$j]); } } # increment file count $n++; } else { # print warning warn("chart $file has different structure, ignored\n"); } } else { # print warning warn("chart $file $result, ignored\n"); } } } # if any files were read if ($n) { # if there are measurement values if (@{$c1} || @{$c2} || @{$c3}) { # for each sample for my $i (1 .. $#{$self->[1]}) { # for each measurement column for my $j (@{$c1}, @{$c2}, @{$c3}) { # divide by n $self->[1][$i][$j] /= $n; } # for each group of L*a*b* columns for (my $j = 0; $j < @{$c2}; $j += 3) { # convert to xyz values to L*a*b* @{$self->[1][$i]}[@{$c2}[$j .. $j + 2]] = ICC::Shared::_xyz2Lab(@{$self->[1][$i]}[@{$c2}[$j .. $j + 2]]); } # for each density column for my $j (@{$c3}) { # convert reflectance to density $self->[1][$i][$j] = -POSIX::log10($self->[1][$i][$j]); } } } # add ISO statistics, if requested _addStats($self, $charts, $hash) if ($fstat); # print message print "$n files read in directory $self->[0]{'file_path'}\n\n"; # save number of files read $self->[0]{'files_read'} = $n; } # return return($n); } # add ISO statistics # the object_reference contains the mean values # the individual charts are in the array_of_chart_objects # parameters: (object_reference, array_of_chart_objects, hash) sub _addStats { # get parameters my ($self, $charts, $hash) = @_; # local variables my (@ctx, $cols, $scols); # for each hash key for (keys(%{$hash})) { # if value is a scalar if (! ref($hash->{$_})) { # save context value @ctx = ($hash->{$_}); } elsif (ref($hash->{$_}) eq 'ARRAY') { # save context values @ctx = @{$hash->{$_}}; } # if 'STDEV_XYZ' if (m/^STDEV_XYZ$/) { # for each context for my $context (@ctx) { # resolve context value $context = defined($context) && length($context) ? $context : undef; # if no STDEV_XYZ columns with context if (! test($self, 'STDEVXYZ', $context)) { # get XYZ columns $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(XYZ_X XYZ_Y XYZ_Z)); # add STDEV_XYZ columns $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(STDEV_X STDEV_Y STDEV_Z)); # for each XYZ for my $i (0 .. 2) { # add STDEV_XYZ values _addStdDevCol($self, $charts, $cols->[$i], $scols->[$i]); } } # set origin @{$self->[2][0]}[@{$scols}] = ($cols) x 3; # save illuminant white point @{$self->[2][2]}[@{$scols}] = @{$self->[2][2]}[@{$cols}]; } # if 'STDEV_LAB' or 'CHI_SQD_PAR' } elsif (m/^(STDEV_LAB|CHI_SQD_PAR)$/) { # for each context for my $context (@ctx) { # resolve context value $context = defined($context) && length($context) ? $context : undef; # if no STDEV_LAB columns with context if (! test($self, 'STDEVLAB', $context)) { # get L*a*b* columns $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)); # add STDEV_LAB columns $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(STDEV_L STDEV_A STDEV_B)); # for each L*a*b* for my $i (0 .. 2) { # add STDEV_LAB values _addStdDevCol($self, $charts, $cols->[$i], $scols->[$i]); } # set origin @{$self->[2][0]}[@{$scols}] = ($cols) x 3; # save illuminant white point @{$self->[2][2]}[@{$scols}] = @{$self->[2][2]}[@{$cols}]; } # if 'CHI_SQD_PAR' if ($1 eq 'CHI_SQD_PAR') { # get STDEV_LAB columns $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(STDEV_L STDEV_A STDEV_B)); # add CHI_SQD_PAR column $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(CHI_SQD_PAR)); # for each sample for my $i (1 .. $#{$self->[1]}) { # set CHI_SQD_PAR value (average of L*a*b* standard deviations) $self->[1][$i][$scols->[0]] = List::Util::sum(@{$self->[1][$i]}[@{$cols}])/3; } # set origin $self->[2][0][$scols->[0]] = $cols; } } # if 'MEAN_DE' } elsif (m/^MEAN_DE$/) { # for each context for my $context (@ctx) { # resolve context value $context = defined($context) && length($context) ? $context : undef; # if no MEAN_DE columns with context if (! test($self, 'MEAN_DE', $context)) { # get L*a*b* columns $cols = cols($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)); # add MEAN_DE column $scols = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(MEAN_DE)); # add MEAN_DE values _addMeanDECol($self, $charts, $cols, $scols->[0]); # set origin $self->[2][0][$scols->[0]] = $cols; } } } } } # add standard deviation column # the object_reference contains the mean values # the individual charts are in the array_of_chart_objects # parameters: (object_reference, array_of_chart_objects, mean_column, std_dev_column) sub _addStdDevCol { # get parameters my ($self, $charts, $m, $s) = @_; # local variables my ($n); # get number of charts $n = @{$charts}; # for each sample for my $i (1 .. $#{$self->[1]}) { # initialize value $self->[1][$i][$s] = 0; # if number of charts > 0 if ($n) { # for each chart for my $j (0 .. $#{$charts}) { # add squared difference $self->[1][$i][$s] += ($charts->[$j][$i][$m] - $self->[1][$i][$m])**2; } # complete calculation $self->[1][$i][$s] = sqrt($self->[1][$i][$s]/$n); } else { # error croak('can\'t compute standard deviation with zero samples'); } } } # add mean dEab column # the object_reference contains the mean values # the individual charts are in the array_of_chart_objects # parameters: (object_reference, array_of_chart_objects, mean_L*a*b*_columns, mean_dE_column) sub _addMeanDECol { # get parameters my ($self, $charts, $m, $s) = @_; # local variables my ($n, $dE); # get number of charts $n = @{$charts}; # for each sample for my $i (1 .. $#{$self->[1]}) { # initialize value $self->[1][$i][$s] = 0; # if number of charts > 0 if ($n) { # for each chart for my $j (0 .. $#{$charts}) { # initialize dE $dE = 0; # for each L*a*b* for my $k (0 .. 2) { # add squared difference $dE += ($self->[1][$i][$m->[$k]] - $charts->[$j][$i][$m->[$k]])**2; } # add dE for this chart $self->[1][$i][$s] += sqrt($dE); } # complete calculation $self->[1][$i][$s] /= $n; } else { # error croak('can\'t compute mean dE with zero samples'); } } } # read chart from list of data files # files must have identical structure (cols) # reads first chart, then appends other charts # parameters: (object_reference, ref_to_file_list, hash) # returns: (number_of_files_appended) sub _readChartAppend { # get parameters my ($self, $list, $hash) = @_; # local variables my ($n, $result, $keys, $temp); # initialize file counter $n = 0; # for each file for my $file (@{$list}) { # if first file if ($n == 0) { # if file read successfully if (! ($result = _readChart($self, $file, $hash))) { # add colorimetric metadata _addColorMeta($self); # make format key string $keys = join(':', map {defined() ? $_ : '-'} @{$self->[1][0]}); # increment counter $n++; } else { # print warning warn("chart $file $result, ignored\n"); } } else { # make temporary Chart object $temp = ICC::Support::Chart->new(); # if file read successfully if (! ($result = _readChart($temp, $file, $hash))) { # if charts have same structure (cols) if ($keys eq join(':', map {defined() ? $_ : '-'} @{$temp->[1][0]})) { # append temp samples push(@{$self->[1]}, @{$temp->[1]}[1 .. $#{$temp->[1]}]); # increment counter $n++; } else { # print warning warn("chart $file has different structure, ignored\n"); } } else { # print warning warn("chart $file $result, ignored\n"); } } } # print message if any files were read print "$n files read in directory $self->[0]{'file_path'}\n\n" if ($n); # return return($n); } # read chart # parameters: (object_reference, path_to_file, hash) # returns: (result) sub _readChart { # get parameters my ($self, $path, $hash) = @_; # local variables my ($fh, $buf, $result); # open the file (read-only) open($fh, '<', $path) || return("$! when opening $path"); # set binary mode binmode($fh); # read start of file read($fh, $buf, 1024); # close file close($fh); # re-open the file in text mode (read-only) open($fh, '<', $path) || return("$! when opening $path"); # if an ASE file if ($buf =~ m/^ASEF/) { # save file type $self->[0]{'file_type'} = 'ASEF'; # read ASE file $result = _readChartASE($self, $fh, $hash); # if a TIFF file } elsif ($buf =~ m/^(II\*\x00|MM\x00\*)/) { # save file type $self->[0]{'file_type'} = 'TIFF'; # read TIFF file $result = _readChartTIFF($self, $fh, $hash); # if an ICC profile } elsif (substr($buf, 36, 4) eq 'acsp') { # save file type $self->[0]{'file_type'} = 'prof'; # read ICC file $result = _readChartICC($self, $fh, $hash); # if an XML file } elsif ($buf =~ m/<\?xml/) { # save file type $self->[0]{'file_type'} = 'CXFX'; # read CxF3 file $result = _readChartCxF3($self, $fh, $hash); # if an SS3 file } elsif (substr($buf, 0, 4) eq "\x00\x20\x00\x00" || substr($buf, 0, 4) eq "\x00\x32\x00\x00") { # save file type $self->[0]{'file_type'} = 'SS3'; # read SS3 file $result = _readChartSS3($self, $fh, $hash); } else { # check for CR-LF (DOS/Windows) if ($buf =~ m/\015\012/) { # set record separator $self->[0]{'read_rs'} = "\015\012"; # check for LF (Unix/OSX) } elsif ($buf =~ m/\012/) { # set record separator $self->[0]{'read_rs'} = "\012"; # check for CR (Mac) } elsif ($buf =~ m/\015/) { # set record separator $self->[0]{'read_rs'} = "\015"; # not a text file } else { # close the file close($fh); # return return('unknown file type'); } # save file type $self->[0]{'file_type'} = 'TEXT'; # read ASCII file $result = _readChartASCII($self, $fh, $hash); } # close the file close($fh); # return return($result); } # read chart from ISO 28178 ASCII data file # parameters: (object_reference, file_handle, hash) # returns: (result) sub _readChartASCII { # get parameters my ($self, $fh, $hash) = @_; # local variables my ($state, $iflag, $eflag, $index); my (@fields, $illum, $append); # localize input record separator local $/ = $self->[0]{'read_rs'}; # localize loop variable local $_; # initialize variables $self->[1] = [[]]; $illum = [[]]; $index = 1; $state = 0; $iflag = 0; # reset file pointer seek($fh, 0, 0); # read the file, line by line while (<$fh>) { # add appended text, as is $append .= $_ if ($state == 4); # remove leading spaces/tabs and trailing whitespace s/^[ \t]*(.*?)[\s,]*$/$1/; # if normal comment line (all comments are removed) if (s/#[\s]*(.*)// && $state == 0) { # if remaining line blank if (length() == 0) { # add comment to header array push(@{$self->[3]}, ['#', $1]); } else { # restore comment to header line # preserves time in ProfileMaker 'CREATED' lines $_ .= "# $1"; } } # skip blank lines next if (length() == 0); # begin data format if (m/^BEGIN_DATA_FORMAT$/) { # set state $state = 1; # end data format } elsif (m/^END_DATA_FORMAT$/) { # set state $state = 2; # begin data } elsif (m/^BEGIN_DATA$/) { # set state $state = 3; # end data } elsif (m/^END_DATA$/) { # set state $state = 4; # begin ProfileMaker illuminant section } elsif (m/^BEGIN_DATA_EMISSION$/) { # set illuminant flag $iflag = 1; # reset index $index = 1; # end ProfileMaker illuminant section } elsif (m/^END_DATA_EMISSION$/) { # clear illuminant flag $iflag = 0; # reset appended data $append = ''; # anything else } else { # format if ($iflag == 0 && $state == 1) { # change 'SampleID' to 'SAMPLE_ID' # non-standard notation used by ProfileMaker s/SampleID/SAMPLE_ID/; # parse and save format keys push(@{$self->[1][0]}, split(/[\s,]+/)); # data } elsif ($iflag == 0 && $state == 3) { # if Euro flag not defined if (! defined($eflag)) { # split data @fields = split(/[\s,]+/); # set flag for Euro decimal notation (e.g. 6,3 becomes 6.3) $eflag = m/,/ && @fields > (@{$self->[1][0]} || 0); } # fix Euro decimal notation (e.g. 6,3 becomes 6.3) s/(\d),(\d)/$1.$2/g if ($eflag); # parse and save data $self->[1][$index++] = [split(/[\s,]+/)]; # illuminant format # may be different from data format } elsif ($iflag == 1 && $state == 1) { # change 'SampleID' to 'SAMPLE_ID' # non-standard notation used by ProfileMaker s/SampleID/SAMPLE_ID/; # parse and save illuminant format keys push(@{$illum->[0]}, split(/[\s,]+/)); # illuminant data } elsif ($iflag == 1 && $state == 3) { # fix Euro decimal notation (e.g. 6,3 becomes 6.3) s/(\d),(\d)/$1.$2/g if ($eflag); # parse and save illuminant data $illum->[$index++] = [split(/[\s,]+/)]; # header lines } elsif ($iflag == 0 && ($state == 0 || $state == 2)) { # match keyword/value m/^([^\s,]*)[\s,]*(.*?)$/; # add to header array push(@{$self->[3]}, [$1, $2]) if (length($1)); } } } # save illuminant data, if any $self->[0]{'illuminant'} = $illum if defined($illum->[1]); # save appended data, if any $self->[0]{'append'} = $append if (defined($append)); # apply rotation/flip (special keywords) _rotateChartASCII($self); # return success flag return($state == 4 ? () : "ASCII read failed with state $state"); } # apply rotation/flip to ASCII chart data # if LGOROWLENGTH and (DPLGROTATE or DPLGFLIP) keywords are present # parameter: (object_reference) sub _rotateChartASCII { # get object reference my $self = shift(); # local variables my ($rot, $flip, $mat, $rows); # get the rotation and flip values $rot = keyword($self, 'DPLGROTATE'); $flip = keyword($self, 'DPLGFLIP'); # if LGOROWLENGTH and (DPLGROTATE or DPLGFLIP) keywords if (keyword($self, 'LGOROWLENGTH') && ($rot || $flip)) { # get selection matrix $mat = select_matrix($self)->rotate($rot)->flip($flip); # flatten matrix $rows = ICC::Shared::flatten($mat); # prepend DATA_FORMAT row index (0) unshift(@{$rows}, 0); # rearrange chart data $self->[1] = [@{$self->[1]}[@{$rows}]]; # update LGOROWLENGTH keyword($self, 'LGOROWLENGTH', scalar(@{$mat->[0]})); } } # read chart from Adobe Swatch Exchange (.ase) file # optional hash key: 'colorspace' # 'colorspace' values: 'CMYK', 'LAB ', 'RGB ' or 'Gray' # 'Gray' swatches are mapped to CMYK values # parameters: (object_reference, file_handle, hash) # returns: (result) sub _readChartASE { # get parameters my ($self, $fh, $hash) = @_; # local variables my ($cs, $le, $buf, @header, $sn); my ($mark, $type, $blen, $slen); my ($name, $space, $cmyk, $rgb, $Lab, $dev); # set colorspace selector $cs = $hash->{'colorspace'} if defined($hash->{'colorspace'}); # get little-endian flag $le = ($Config{'byteorder'} =~ m/1234/); # set binary mode binmode($fh); # read header (file signature, version, number of blocks) read($fh, $buf, 12); # unpack buffer @header = unpack('A4nnN', $buf); # verify file signature ($header[0] eq 'ASEF') || return('not a valid ASE file'); # add SAMPLE_NAME field $sn = add_fmt($self, 'SAMPLE_NAME'); # set file pointer $mark = 12; # for each block for my $s (1 .. $header[3]) { # read block type, block length, and string length read($fh, $buf, 8); # unpack buffer ($type, $blen, $slen) = unpack('nNn', $buf); # if color entry type if ($type == 1) { # read color name read($fh, $buf, 2 * $slen); # decode color name $name = decode('UTF-16BE', $buf); # trim trailing '0' $name =~ s/\x00$//; # change spaces to underscores $name =~ s/\s/_/g; # read color space read($fh, $space, 4); # if colorspace is CMYK if (($space eq 'CMYK' && (! defined($cs)) || $cs eq 'CMYK')) { # store color as SAMPLE_NAME $self->[1][$s][$sn->[0]] = $name; # init device array $dev = []; # for each CMYK value for my $i (0 .. 3) { # read 32-bit floating point value read($fh, $buf, 4); # reverse bytes if long-endian system $buf = reverse($buf) if ($le); # unpack buffer $dev->[$i] = unpack('f', $buf); } # if CMYK slice undefined if (! defined($cmyk)) { # add CMYK slice $cmyk = add_fmt($self, qw(CMYK_C CMYK_M CMYK_Y CMYK_K)); } # store CMYK values @{$self->[1][$s]}[@{$cmyk}] = map {100 * $_} @{$dev}; # if colorspace is RGB } elsif (($space eq 'RGB ' && (! defined($cs)) || $cs eq 'RGB ')) { # store color as SAMPLE_NAME $self->[1][$s][$sn->[0]] = $name; # init device array $dev = []; # for each RGB value for my $i (0 .. 2) { # read 32-bit floating point value read($fh, $buf, 4); # reverse bytes if long-endian system $buf = reverse($buf) if ($le); # unpack buffer $dev->[$i] = unpack('f', $buf); } # if RGB slice undefined if (! defined($rgb)) { # add RGB slice $rgb = add_fmt($self, qw(RGB_R RGB_G RGB_B)); } # store RGB values @{$self->[1][$s]}[@{$rgb}] = map {255 * $_} @{$dev}; # if colorspace is L*a*b* } elsif (($space eq 'LAB ' && (! defined($cs)) || $cs eq 'LAB ')) { # store color as SAMPLE_NAME $self->[1][$s][$sn->[0]] = $name; # init device array $dev = []; # for each L*a*b* value for my $i (0 .. 2) { # read 32-bit floating point value read($fh, $buf, 4); # reverse bytes if long-endian system $buf = reverse($buf) if ($le); # unpack buffer $dev->[$i] = unpack('f', $buf); } # if L*a*b* slice undefined if (! defined($Lab)) { # add L*a*b* fields $Lab = add_fmt($self, qw(LAB_L LAB_A LAB_B)); } # store L*a*b* values @{$self->[1][$s]}[@{$Lab}] = (100 * $dev->[0], $dev->[1], $dev->[2]); # if colorspace is Grayscale } elsif (($space eq 'Gray' && (! defined($cs)) || $cs eq 'Gray')) { # store color as SAMPLE_NAME $self->[1][$s][$sn->[0]] = $name; # read 32-bit floating point value read($fh, $buf, 4); # reverse bytes if long-endian system $buf = reverse($buf) if ($le); # unpack buffer $dev = [unpack('f', $buf)]; # if CMYK slice is undefined if (! defined($cmyk)) { # add CMYK slice $cmyk = add_fmt($self, qw(CMYK_C CMYK_M CMYK_Y CMYK_K)); } # store CMYK values @{$self->[1][$s]}[@{$cmyk}] = (0, 0, 0, 100 * (1 - $dev->[0])); } } # set file pointer to next block $mark += $blen + 6; # seek next block seek($fh, $mark, 0); } # return return(); } # read chart from ICC profile # some profiles have tags containing chart data # parameters: (object_reference, file_handle, hash) # returns: (result) sub _readChartICC { # get parameters my ($self, $fh, $hash) = @_; # local variables my (@header, @tagtab, %offset, %tags, $type, $class); my ($temp, $data, $text, $result); # load ICC::Profile modules, if not already included require ICC::Profile; # set binary mode binmode($fh); # read the profile header ICC::Profile::_readICCheader($fh, \@header) || return('failed reading ICC profile header'); # read the profile tag table ICC::Profile::_readICCtagtable($fh, \@tagtab) || return('failed reading ICC profile tag table'); # for each tag for my $tag (@tagtab) { # if tag contains measurement data if ($tag->[0] =~ m/^(?:CxF |DevD|CIED|DEVD|targ)$/) { # if a duplicate tag if (exists($offset{$tag->[1]})) { # use original tag $tags{$tag->[0]} = $offset{$tag->[1]}; } else { # seek to start of tag seek($fh, $tag->[1], 0); # read tag type signature read($fh, $type, 4); # convert non-word characters to underscores $type =~ s|\W|_|g; # form class specifier $class = 'ICC::Profile::' . $type; # if 'class->new_fh' method exists if ($class->can('new_fh')) { # create specific tag object $tags{$tag->[0]} = $class->new_fh($self, $fh, $tag); } else { # create generic tag object $tags{$tag->[0]} = ICC::Profile::Generic->new_fh($self, $fh, $tag); } # save tag in hash $offset{$tag->[1]} = $tags{$tag->[0]}; } } } # if creator is i1Profiler and 'CxF ' tag exists if ($header[23] eq 'XRCM' && exists($tags{'CxF '})) { # close file handle close($fh); # open file handle to CxF3 string open($fh, '<', \$tags{'CxF '}->text()); # make chart from CxF3 string return(_readChartCxF3($self, $fh, $hash)); # if creator is ProfileMaker and 'DevD' / 'CIED' tags exist } elsif ($header[23] eq 'LOGO' && exists($tags{'DevD'}) && exists($tags{'CIED'})) { # close file handle close($fh); # open file handle to 'DevD' text string open($fh, '<', \$tags{'DevD'}->text()); # read chart from text ($result = _readChartASCII($self, $fh, $hash)) && return("failed reading ICC profile DEVD tag, $result"); # close file handle close($fh); # make temporary chart object $temp = ICC::Support::Chart->new(); # open file handle to 'CIED' text string open($fh, '<', \$tags{'CIED'}->text()); # read chart from text ($result = _readChartASCII($temp, $fh, $hash)) && return("failed reading ICC profile CIED tag, $result"); # get data slice (all rows, spectral, XYZ and L*a*b* columns) $data = slice($temp, [0 .. $#{$temp->[1]}], [grep {$temp->[1][0][$_] =~ m/^(nm\d{3}|XYZ_(X|Y|Z)|LAB_(L|A|B))$/} (0 .. $#{$temp->[1][0]})]); # append to chart add_cols($self, $data); # for each keyword for my $key (@{$temp->[3]}) { # if keyword not in main chart if (0 == grep {$key->[0] eq $_->[0]} @{$self->[3]}) { # append keyword/value push(@{$self->[3]}, $key); } } # return return(); # if creator is Monaco and 'DEVD' tag exists (some old profiles are identified by preferred CMM) } elsif (($header[23] eq 'MONS' || $header[1] eq 'mnco') && exists($tags{'DEVD'})) { # read chart from Monaco 'DEVD' tag return(_readMonacoDEVD($self, $tags{'DEVD'}->data(), \@header)); # if 'targ' tag exists } elsif (exists($tags{'targ'})) { # get 'targ' tag text string $text = $tags{'targ'}->text(); # if reference to ICC Characterization Data Registry if ($text =~ m/^ICCHDAT (.*)$/) { # return return("profile derived from $1 characterization data, available at www.color.org"); } else { # close file handle close($fh); # open file handle to text string open($fh, '<', \$text); # read chart from text return(_readChartASCII($self, $fh, $hash)); } } # return return('failed reading ICC profile characterization data'); } # read chart from Monaco 'DEVD' tag # parameters: (object_reference, tag_data, profile_header) # returns: (result) sub _readMonacoDEVD { # get parameters my ($self, $data, $header) = @_; # local variables my ($big, %cshash, $cs, $nc, $fix, $ix, $tag, $tac, $limit, $mult, $dev, $lab); my ($ns, $sec, @devfix, @nd, $nt, @devstep, @dev, @cmy, @sum, @temp, $m, @dat); # get big-endian flag (true if our system is big-endian) $big = ($Config{'byteorder'} =~ m/4321/); # colorspace hash (colorspace => number_channels) %cshash = ('RGB ' => 3, 'CMYK' => 4, '5CLR' => 5, '6CLR' => 6, '7CLR' => 7, '8CLR' => 8); # initialize fixed value array @devfix = (); # get colorspace from profile header $cs = $header->[4]; # lookup number of channels $nc = $cshash{$cs}; # set number of fixed channels $fix = $nc - 3; # set index to start of first tag $ix = 28; # set tag value $tag = pack('N', 0x002D); # find TAC tag do {$ix = index($data, $tag, $ix)} while ($ix >= 0 && $ix % 4 && $ix++); # verify tag found ($ix >= 0) || return('failed reading TAC from Monaco DEVD tag'); # get TAC value $tac = 100 * unpack('d', $big ? substr($data, $ix + 4, 8) : reverse(substr($data, $ix + 4, 8))); # if RGB colorspace if ($cs eq 'RGB ') { # add device fields $dev = add_fmt($self, qw(RGB_R RGB_G RGB_B)); # set device multiplier $mult = 255; # if CMYK colorspace } elsif ($cs eq 'CMYK') { # add device fields $dev = add_fmt($self, qw(CMYK_C CMYK_M CMYK_Y CMYK_K)); # set device multiplier $mult = 100; } else { # add device fields $dev = add_fmt($self, map {"$cs\_$_"} (1 .. $nc)); # set device multiplier $mult = 100; } # add L*a*b* fields $lab = add_fmt($self, qw(LAB_L LAB_A LAB_B)); # advance index $ix += 12; # set tag value $tag = pack('N', 0x0027); # find data group tag do {$ix = index($data, $tag, $ix)} while ($ix >= 0 && $ix % 4 && $ix++); # verify tag found ($ix >= 0) || return('failed reading data group from Monaco DEVD tag'); # get number data sections in group $ns = unpack('N', substr($data, $ix + 4, 4)); # advance index $ix += 8; # for data each section for my $s (0 .. $ns - 1) { # verify tag 0x0028 (substr($data, $ix, 4) eq pack('N', 0x0028)) || return(0); # get section index $sec = unpack('N', substr($data, $ix + 4, 4)); # verify section index is correct ($sec == $s) || return('failed reading section index from Monaco DEVD tag'); # advance index $ix += 8; # verify tag 0x002A (fixed device values) (substr($data, $ix, 4) eq pack('N', 0x002A)) || return('failed reading fixed device values from Monaco DEVD tag'); # if fixed device values (none for RGB) if ($fix) { # get fixed device values (black plus any extra colors, e.g. orange or green) @devfix = unpack("d$fix", $big ? substr($data, $ix + 4, 8 * $fix) : reverse(substr($data, $ix + 4, 8 * $fix))); # reverse array if little-endian @devfix = reverse(@devfix) if (! $big); # apply multiplier @devfix = map {$_ * $mult} @devfix; } # advance index $ix += 8 * $fix + 4; # verify tag 0x002B (step counts by color) (substr($data, $ix, 4) eq pack('N', 0x002B)) || return('failed reading step counts from Monaco DEVD tag'); # get device step counts @nd = unpack('N3', substr($data, $ix + 4, 12)); # get total number of steps $nt = $nd[0] + $nd[1] + $nd[2]; # advance index $ix += 16; # verify tag 0x002C (step values by color) (substr($data, $ix, 4) eq pack('N', 0x002C)) || return('failed reading step values from Monaco DEVD tag'); # get step values @devstep = unpack("d$nt", $big ? substr($data, $ix + 4, 8 * $nt) : reverse(substr($data, $ix + 4, 8 * $nt))); # reverse array if little-endian @devstep = reverse(@devstep) if (! $big); # apply multiplier @devstep = map {$_ * $mult} @devstep; # advance index $ix += 8 * $nt + 4; # initialize arrays @dev = (); @sum = (); @temp = (); # if RGB colorspace if ($cs eq 'RGB ') { # for each blue step for my $i (0 .. $nd[2] - 1) { # for each green step for my $j (0 .. $nd[1] - 1) { # for each red step for my $k (0 .. $nd[0] - 1) { # save RGB values push(@dev, $devstep[$k], $devstep[$j + $nd[0]], $devstep[$i + $nd[0] + $nd[1]]); } } } # if CMYK or NCLR colorspace } else { # for each yellow step for my $i (0 .. $nd[2] - 1) { # for each cyan step for my $j (0 .. $nd[0] - 1) { # for each magenta step for my $k (0 .. $nd[1] - 1) { # get CMY values @cmy = ($devstep[$j], $devstep[$k + $nd[0]], $devstep[$i + $nd[0] + $nd[1]]); # save CMY values push(@temp, [@cmy]); # save total ink value push(@sum, List::Util::sum(@cmy, @devfix)); } } } # initialize actual ink limit $limit = $nc * 100; # find actual ink limit (smallest value greater than TAC) for (@sum) {$limit = $_ if ($_ > $tac && $_ < $limit)}; # for each sample for my $i (0 .. $#sum) { # get cmy values @cmy = @{$temp[$i]}; # if sample within ink limit, or a corner point if ($sum[$i] <= $limit || ((0 < grep {$_ == 0} @cmy) && (0 < grep {$_ == 100} @cmy))) { # copy cmy values push(@dev, @cmy); } } } # verify tag 0x0032 (L*a*b* sample data) (substr($data, $ix, 4) eq pack('N', 0x0032)) || return('failed reading L*a*b* data from Monaco DEVD tag'); # get number of values $m = unpack('N', substr($data, $ix + 4, 4)) * 3; # get L*a*b* color data @dat = unpack("d$m", $big ? substr($data, $ix + 8, 8 * $m) : reverse(substr($data, $ix + 8, 8 * $m))); # reverse array if little-endian @dat = reverse(@dat) if (! $big); # advance index $ix += 8 * $m + 8; # verify @dev and @dat are same size (scalar(@dev) == scalar(@dat)) || return('failed comparing data counts of Monaco DEVD tag'); # for each sample (3 values per sample) for my $i (0 .. ($m/3 - 1)) { # add sample data to object push (@{$self->[1]}, [@dev[($i * 3) .. ($i * 3 + 2)], @devfix, @dat[($i * 3) .. ($i * 3 + 2)]]); } # verify tag 0x0029 (end of section) (substr($data, $ix, 4) eq pack('N', 0x0029)) || return('failed reading section end from Monaco DEVD tag'); # advance index $ix += 4; } # verify tag 0x0030 (end of data) (substr($data, $ix, 4) eq pack('N', 0x0030)) || return('failed reading data end from Monaco DEVD tag'); # add 'CREATED' keyword/value from header date/time push(@{$self->[3]}, ['CREATED', sprintf('%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ', @{$header}[6 .. 11])]); # return return(); } # read chart from SpectraShop (.ss3) file # parameters: (object_reference, file_handle, hash) sub _readChartSS3 { # get parameters my ($self, $fh, $hash) = @_; # local variables my (%fmt, $buf, @data, $notes); my ($meta, $measure, %tally, @keys, $value, $nm); # metadata format array (v32) $fmt{'32'} = [ [qw(Identifier_1 SAMPLE_NAME P)], [qw(Identifier_2 SAMPLE_ID2 P)], [qw(Identifier_3 SAMPLE_ID3 P)], [qw(Material MATERIAL P)], [qw(Manufacturer MANUFACTURER P)], [qw(Model MODEL P)], [qw(Serial_Number SERIAL_NUMBER P)], [qw(Production_Date PROD_DATE P)], [qw(Surface SURFACE P)], [qw(Originator ORIGINATOR P)], [qw(Creation_Date CREATED P)], [qw(Comments NOTE P)], [qw(Instrument INSTRUMENTATION P)], [qw(Spectrum_Type SPECTRUM_TYPE n), [qw(Emissive-light Emissive-monitor Observer Reflective Transmissive)]], [qw(Filter MEASUREMENT_FILTER P)], [qw(Geometry MEASUREMENT_GEOMETRY P)], [qw(Aperture MEASUREMENT_APERTURE P)], [qw(Data_Reference DATA_REFERENCE P)], [qw(Illuminant MEASUREMENT_SOURCE P)], [qw(Backing SAMPLE_BACKING P)], [qw(Measurements NSAMPLES n)], [qw(Notes ACQUIRE_NOTE P)], ]; # metadata format array (v50) $fmt{'50'} = [ [qw(Identifier_1 SAMPLE_NAME P)], [qw(Identifier_2 SAMPLE_ID2 P)], [qw(Identifier_3 SAMPLE_ID3 P)], [qw(Material MATERIAL P)], [qw(Manufacturer MANUFACTURER P)], [qw(Model MODEL P)], [qw(Serial_Number SERIAL_NUMBER P)], [qw(Production_Date PROD_DATE P)], [qw(Surface SURFACE P)], [qw(Originator ORIGINATOR P)], [qw(Creation_Date CREATED P)], [qw(Comments NOTE P)], [qw(Instrument INSTRUMENTATION P)], [qw(Serial_Number INSTRUMENT_SERIAL_NUMBER P)], [qw(Spectrum_Type SPECTRUM_TYPE n), [qw(Emissive-light Emissive-monitor Observer Reflective Transmissive)]], [qw(Filter MEASUREMENT_FILTER P)], [qw(Geometry MEASUREMENT_GEOMETRY P)], [qw(Aperture MEASUREMENT_APERTURE P)], [qw(Data_Reference DATA_REFERENCE P)], [qw(Illuminant MEASUREMENT_SOURCE P)], [qw(Backing SAMPLE_BACKING P)], [qw(Measurements NSAMPLES n)], [qw(Notes ACQUIRE_NOTE P)], ]; # set binary mode binmode($fh); # read version, samples, Collection Notes length read($fh, $buf, 7); # unpack @data = unpack('nx2nC', $buf); # read Collection Notes string read($fh, $notes, $data[2]); # for each sample for my $i (0 .. ($data[1] - 1)) { # for each metadata field for my $j (0 .. $#{$fmt{$data[0]}}) { # if a Pascal string if ($fmt{$data[0]}[$j][2] eq 'P') { # read string length read($fh, $buf, 1); # read string read($fh, $meta->[$i][$j], unpack('C', $buf)); # if an unsigned short integer } elsif ($fmt{$data[0]}[$j][2] eq 'n') { # read short integer read($fh, $buf, 2); # unpack $meta->[$i][$j] = unpack('n', $buf); } } # read wavelength parameters (start, end, increment, count) read($fh, $buf, 8); # unpack (unsigned short integer) $measure->[$i][0] = [unpack('n4', $buf)]; # for each wavelength for my $j (1 .. $measure->[$i][0][3]) { # read measurements (avg, low, high, std_dev) read($fh, $buf, 16); # unpack (32-bit float, big endian) $measure->[$i][$j] = [unpack('(f4)>', $buf)]; } } # add Collection Notes to header line array, if not null string push(@{$self->[3]}, ['FILE_DESCRIPTOR', "\"$notes\""]) if (length($notes)); # for each metadata field for my $j (0 .. $#{$meta->[0]}) { # init hash %tally = (); # for each sample for my $i (0 .. $#{$meta}) { # increment hash value $tally{$meta->[$i][$j]}++; } # get hash keys @keys = keys(%tally); # if one hash key if (@keys == 1) { # if not the null string if (length($keys[0])) { # if value is string if ($fmt{$data[0]}[$j][2] eq 'P') { # wrap in quotes $value = "\"$keys[0]\""; } else { # if value is an enumeration if (defined($fmt{$data[0]}[$j][3])) { # look up enumerated value and wrap in quotes $value = "\"$fmt{$data[0]}[$j][3][$keys[0]]\""; } else { # use value as-is $value = $keys[0]; } } # add KEYWORD/VALUE to header line array push(@{$self->[3]}, [$fmt{$data[0]}[$j][1], $value]); } } else { # add keyword to DATA_FORMAT array push(@{$self->[1][0]}, $fmt{$data[0]}[$j][1]); # for each sample for my $i (0 .. $#{$meta}) { # if value is an enumeration if (defined($fmt{$data[0]}[$j][3])) { # look up enumerated value $value = $fmt{$data[0]}[$j][3][$meta->[$i][$j]]; } else { # use value as-is $value = $meta->[$i][$j]; } # add value to DATA array push(@{$self->[1][$i + 1]}, $meta->[$i][$j]); } } } # for each wavelength parameter (start, end, increment, count) for my $j (0 .. 3) { # init hash %tally = (); # for each sample for my $i (0 .. $#{$measure}) { # increment hash value $tally{$measure->[$i][0][$j]}++; } # get hash keys @keys = keys(%tally); # verify all samples have same wavelength parameter value (@keys == 1) || return('samples have varied spectral range'); } # for each wavelength for my $j (0 .. ($#{$measure->[0]} - 1)) { # compute wavelength from start and increment values $nm = $measure->[0][0][0] + $j * $measure->[0][0][2]; # add keyword to DATA_FORMAT array push(@{$self->[1][0]}, "nm$nm"); # for each sample for my $i (0 .. $#{$measure}) { # add average measurement to DATA array push(@{$self->[1][$i + 1]}, $measure->[$i][$j + 1][0]); } } # return return(); } # read data from TIFF file # RGB, CMYK, and CIE L*a*b* color spaces supported # 8-bit, 16-bit or 32-bit, Intel or Motorola byte order supported # alpha and spot channels in RGB and CMYK files supported # optional hash keys: 'rows', 'columns', 'crop', 'ratio', 'aperture', 'udf', 'format' # default 'rows' and 'columns' are taken from image size, default 'ratio' is 0.5 # 'crop' is an array containing the left, right, upper and lower crop values in pixels # 'ratio' is a value between 0 and 1, sample is a single pixel when 'ratio' is 0 # 'aperture' is in millimeters, and take precedence over 'ratio' # 'udf' is a code reference to a pixel processing function # 'format' is an array reference containing the format fields # parameters: (object_reference, file_handle, hash) # returns: (result) sub _readChartTIFF { # get parameters my ($self, $fh, $hash) = @_; # local variables my ($buf, $short, $long, $fp, @header, $tags); my ($cols, $rows, $bits, $pi, $samples); my ($context, $fmt, $upf, $udf, $dev, $div, $dab); my ($trows, $tcols, $crop, $roff, $coff); my ($res, $size, $frac, $ratio, $rxo, $cxo, $pixels, $width); my ($lower, $upper, $left, $right, $band, $pval, @data, @pix); # set binary mode binmode($fh); # read the header read($fh, $buf, 8); # if big-endian (Motorola) if (substr($buf, 0, 2) eq 'MM') { # set 'unpack' formats $short = 'n'; $long = 'N'; $fp = 'f>'; # might not be IEEE FP on some platforms # if little-endian (Intel) } elsif (substr($buf, 0, 2) eq 'II'){ # set 'unpack' formats $short = 'v'; $long = 'V'; $fp = 'f<'; # might not be IEEE FP on some platforms } else { # error return('TIFF byte order incorrect'); } # unpack the header @header = unpack("A2 $short $long", $buf); # verify file signature ($header[1] == 42) || return('TIFF file signature incorrect'); # read TIFF image file directory (IFD) $tags = _readTIFFdir($fh, $header[2], $short, $long); # verify compression (1 = uncompressed) ($tags->{'259'}[0] == 1) || return('TIFF compression unsupported'); # verify orientation (1 = normal) (! exists($tags->{'274'}) || $tags->{'274'}[0] == 1) || warn('TIFF orientation rotated and/or flipped'); # verify planar configuration (1 = chunky) (! exists($tags->{'284'}) || $tags->{'284'}[0] == 1) || return('TIFF planar configuration unsupported'); # verify not tiled (! exists($tags->{'322'})) || return('TIFF tiled layout unsupported'); # get TIFF columns (width) $cols = $tags->{'256'}[0]; # get TIFF rows (length) $rows = $tags->{'257'}[0]; # get TIFF bits per sample $bits = $tags->{'258'}[0]; # verify bits per sample ($bits == 8 || $bits == 16 || $bits == 32) || return('TIFF bits per sample unsupported'); # get the photometric interpretation $pi = $tags->{'262'}[0]; # if 32-bits per sample if ($bits == 32) { # verify 32-bit IEEE FP format, RGB image ($tags->{'339'}[0] == 3 && $pi == 2) || return('TIFF format unsupported'); } # get TIFF samples per pixel $samples = $tags->{'277'}[0]; # verify bits per sample array ($samples == grep {$_ == $bits} @{$tags->{'258'}}) || return('TIFF image structure unsupported'); # get context (if any) $context = $hash->{'context'}; # get user defined function (if any) $udf = $hash->{'udf'}; # verify UDF is a code reference (ref($udf) eq 'CODE') || return('UDF not a code reference') if (defined($udf)); # set device value divisor $dev = ($bits == 8) ? 255 : 65535; # add fields for udf (if any) $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} @{$hash->{'format'}}) if defined($hash->{'format'}); # if RGB file if ($pi == 2 && $samples < 13) { # add RGB and ALPHA fields, if not already defined $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} (qw(RGB_R RGB_G RGB_B), map {"RGB_A$_"} (1 .. $samples - 3))) if (! defined($fmt)); # set unpack format (8, 16 or 32 bits) $upf = ($bits == 8) ? 'C*' : ($bits == 16) ? "$short*" : "$fp*"; # set divisor (8, 16 or 32 bits) $div = ($bits == 8) ? 1 : ($bits == 16) ? 257 : 1/255; # if CMYK file } elsif ($pi == 5 && $samples == 4) { # add CMYK fields, if not already defined $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(CMYK_C CMYK_M CMYK_Y CMYK_K)) if (! defined($fmt)); # set unpack format (8 or 16 bits) $upf = ($bits == 8) ? 'C*' : "$short*"; # set divisor (8 or 16 bits) $div = ($bits == 8) ? 2.55 : 655.35; # if nCLR file } elsif ($pi == 5 && $samples > 4 && $samples < 16) { # add nCLR fields, if not already defined $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} map {sprintf('%xCLR_%x', $samples, $_)} (1 .. $samples)) if (! defined($fmt)); # set unpack format (8 or 16 bits) $upf = ($bits == 8) ? 'C*' : "$short*"; # set divisor (8 or 16 bits) $div = ($bits == 8) ? 2.55 : 655.35; # if CIE L*a*b* file } elsif ($pi == 8 && $samples == 3) { # add L*a*b* fields, if not already defined $fmt = add_fmt($self, map {defined($context) ? "$context|$_" : $_} qw(LAB_L LAB_A LAB_B)) if (! defined($fmt)); # set unpack format (8 or 16 bits) $upf = ($bits == 8) ? '(Ccc)*' : "$short*"; # set divisors (8 or 16 bits) $div = ($bits == 8) ? 2.55 : 655.35; # L* $dab = ($bits == 8) ? 1 : 256; # a* and b* } else { # return error return('TIFF color space unsupported'); } # get target rows (could be undefined) $trows = $hash->{'rows'}; # get target columns (could be undefined) $tcols = $hash->{'columns'}; # if 'crop' parameter is defined if (defined($hash->{'crop'})) { # get crop parameter $crop = $hash->{'crop'}; # verify array reference (ref($crop) eq 'ARRAY') || return('TIFF crop parameter not an array reference'); # verify array contains four non-negative integers (4 == @{$crop} && 4 == grep {$_ == int($_) && $_ >= 0} @{$crop}) || return('TIFF crop parameter(s) invalid'); # adjust rows and columns $rows -= $crop->[2] + $crop->[3]; $cols -= $crop->[0] + $crop->[1]; # verify cropped size ($rows > 0 && $cols > 0) || return('TIFF crop size too small'); # set offset values $roff = $crop->[2]; $coff = $crop->[0]; } else { # set offset values $roff = 0; $coff = 0; } # if aperture is defined in hash if (defined($hash->{'aperture'})) { # compute image resolution $res = $tags->{'283'}[0]/$tags->{'283'}[1]; # convert to lines/mm if resolution unit is inch $res /= 25.4 if ($tags->{'296'}[0] == 2); # convert to lines/mm if resolution unit is cm $res /= 10 if ($tags->{'296'}[0] == 3); # if target rows or target columns are defined if (defined($trows) || defined($tcols)) { # use image rows if target rows undefined $trows = $rows if (! defined($trows)); # use image columns if target columns undefined $tcols = $cols if (! defined($tcols)); # compute aperture size (diameter in pixels) ($frac, $size) = POSIX::modf(sqrt(ICC::Shared::PI/4) * $res * $hash->{'aperture'}); # if fractional part < 0.25 if ($frac < 0.25) { # set row and column index offsets $rxo = $cxo = $size - 1; # if fractional part < 0.75 } elsif ($frac < 0.75) { # set row index offset $rxo = $size - 1; # set column index offset $cxo = $size; } else { # set row and column index offsets $rxo = $cxo = $size; } # verify aperture is within sample area ($rxo <= $rows/$trows && $cxo <= $cols/$tcols) || croak('TIFF aperture exceeds sample area') } else { # compute aperture area (in pixels) $size = ICC::Shared::PI * ($res * $hash->{'aperture'}/2)**2; # compute the target rows $trows = int(sqrt($size * $rows/$cols) + 0.5); # compute the target columns $tcols = int($size/$trows + 0.5); # set row and column indices (single pixel sample) $rxo = $cxo = 0; } } else { # use image rows if target rows undefined $trows = $rows if (! defined($trows)); # use image columns if target columns undefined $tcols = $cols if (! defined($tcols)); # get mask ratio (default 0.5) $ratio = defined($hash->{'ratio'}) ? $hash->{'ratio'} : 0.5; # verify mask ratio ($ratio >= 0 && $ratio <= 1) || croak('TIFF mask ratio < 0 or > 1'); # compute row index offset $rxo = int($ratio * $rows/$trows - 0.5); # compute column index offset $cxo = int($ratio * $cols/$tcols - 0.5); } # warn if large target size ($trows * $tcols <= 10000) || warn('TIFF target size > 10000 samples'); # compute number of pixels $pixels = ($rxo + 1) * ($cxo + 1); # compute row width (bytes) $width = $tags->{'256'}[0] * List::Util::sum(@{$tags->{'258'}})/8; # for each target row for my $i (0 .. $trows - 1) { # compute sample lower row $lower = int(($i + 0.5) * $rows/$trows - $rxo/2) + $roff; # compute sample upper row $upper = $lower + $rxo; # get sample band data $band = _readTIFFband($fh, $tags, $lower, $upper, $width, $upf); # for each target column for my $j (0 .. $tcols - 1) { # compute sample left column $left = int(($j + 0.5) * $cols/$tcols - $cxo/2) + $coff; # compute sample right column $right = $left + $cxo; # initialize data @data = (); # for each row (band) for my $m (0 .. $#{$band}) { # for each column for my $n ($left .. $right) { # get pixel value (all samples) @pix = @{$band->[$m]}[$n * $samples .. ($n + 1) * $samples - 1]; # if 16-bit L*a*b* if ($pi == 8 && $bits == 16) { # adjust a* and b* if pixel value negative (signed 16-bit) $pix[1] += -65536 if ($pix[1] > 32767); $pix[2] += -65536 if ($pix[2] > 32767); } # if user defined function provided if (defined($udf)) { # if L*a*b* file if ($pi == 8) { # convert values $pix[0] /= $div; $pix[1] /= $dab; $pix[2] /= $dab; } else { # convert to device values @pix = map {$_/$dev} @pix; # if a CMYK file if ($pi == 5) { # for alpha/spot colors (if any) for my $s (4 .. $samples - 1) { # invert device value $pix[$s] = 1 - $pix[$s]; } } } # call user defined function @pix = &$udf(@pix); } # for each channel (may be different from TIFF samples) for my $s (0 .. $#pix) { # accumulate pixel values $data[$s] += $pix[$s] } } } # if user defined function provided if (defined($udf)) { # save data in object @{$self->[1][$j * $trows + $i + 1]}[@{$fmt}] = map {$_/$pixels} @data; # if L*a*b* file } elsif ($pi == 8) { # save data in object $self->[1][$j * $trows + $i + 1][$fmt->[0]] = $data[0]/($pixels * $div); $self->[1][$j * $trows + $i + 1][$fmt->[1]] = $data[1]/($pixels * $dab); $self->[1][$j * $trows + $i + 1][$fmt->[2]] = $data[2]/($pixels * $dab); # all others } else { # normalize data values @data = map {$_/($pixels * $div)} @data; # if a CMYK file if ($pi == 5) { # for alpha/spot colors (if any) for my $s (4 .. $samples - 1) { # invert %-dot value $data[$s] = 100 - $data[$s]; } } # save data in object @{$self->[1][$j * $trows + $i + 1]}[@{$fmt}] = @data; } } } # save the tag hash in object header $self->[0]{'TIFF_tag'} = $tags; # add LGOROWLENGTH keyword keyword($self, 'LGOROWLENGTH', $trows); # return return(); } # read TIFF image file directory (IFD) # parameters: (file_handle, offset, short_format, long_format) # returns: (IFD_hash) sub _readTIFFdir { # get parameters my ($fh, $start, $short, $long) = @_; # local variables my (@ts, $buf, $id, $type, $count, $size, $mark, $offset, $num, $denom, $tags); # field type size (in bytes) @ts = (0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4); # seek start of IFD seek($fh, $start, 0); # read number entries read($fh, $buf, 2); # read the directory for (1 .. unpack($short, $buf)) { # read first part of IFD entry read($fh, $buf, 8); # unpack first three fields (ID, type, count) ($id, $type, $count) = unpack("$short$short$long", $buf); # read last part of IFD entry read($fh, $buf, 4); # determine value/offset size (size * count) + (1 if ASCII string) $size = $ts[$type] * $count + (($type == 2) ? 1 : 0); # if an offset if ($size > 4) { # mark file location $mark = tell($fh); # unpack offset $offset = unpack($long, $buf); # seek values seek($fh, $offset, 0); # if binary string if ($type == 1 || $type == 7) { # read binary string read($fh, $buf, $count); # unpack value $tags->{$id} = [unpack("a$count", $buf)]; # if ASCII string } elsif ($type == 2) { # read ASCII string read($fh, $buf, $count); # unpack null-terminated ASCII string $tags->{$id} = [unpack("Z$count", $buf)]; # if short values } elsif ($type == 3) { # read values read($fh, $buf, 2 * $count); # unpack values $tags->{$id} = [unpack("$short$count", $buf)]; # if long values } elsif ($type == 4) { # read values read($fh, $buf, 4 * $count); # unpack values $tags->{$id} = [unpack("$long$count", $buf)]; # if rational values } elsif ($type == 5) { # double count (one rational value is two long values) $count *= 2; # read values read($fh, $buf, 4 * $count); # unpack values $tags->{$id} = [unpack("$long$count", $buf)]; } # reset file pointer seek($fh, $mark, 0); # if binary string } elsif ($type == 1 || $type == 7) { # unpack binary string $tags->{$id} = [unpack("a$count", $buf)]; # if ASCII string } elsif ($type == 2) { # unpack null-terminated ASCII string $tags->{$id} = [unpack("Z$count", $buf)]; # if short value(s) } elsif ($type == 3) { # unpack value(s) $tags->{$id} = [unpack("$short$count", $buf)]; # if long value } elsif ($type == 4) { # unpack value $tags->{$id} = [unpack($long, $buf)]; } else { # save packed value $tags->{$id} = [$buf]; } } # return return($tags); } # read TIFF image band # row zero is top of image # parameters: (file_handle, IFD_hash, lower_row, upper_row, row_width, unpack_format) # returns: (2D_array) sub _readTIFFband { # get parameters my ($fh, $tags, $lower, $upper, $width, $upf) = @_; # local variables my ($offset, $rows, $buf, $band); # get strip offset array $offset = $tags->{'273'}; # get rows per strip $rows = $tags->{'278'}[0]; # for each row for my $i ($lower .. $upper) { # set file pointer seek($fh, $offset->[int($i/$rows)] + ($i % $rows) * $width, 0); # read row data read($fh, $buf, $width); # unpack data $band->[$i - $lower] = [unpack($upf, $buf)]; } # return return($band); } # write TIFF image file directory (IFD) # parameters: (file_handle, offset, short_format, long_format, IFD_hash) sub _writeTIFFdir { # get parameters my ($fh, $ifd, $short, $long, $tags) = @_; # local variables my (@ts, @sid, $mark, $type, $count, $size, $fmt); # field type size (in bytes) @ts = (0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4); # make list of tag ids, sorted numerically @sid = sort {$a <=> $b} keys(%{$tags}); # seek start of IFD seek($fh, $ifd, 0); # write number of tags print $fh pack($short, scalar(@sid)); # set data pointer $mark = $ifd + 12 * @sid + 6; # for each tag for my $id (@sid) { # get data type $type = $tags->{$id}[0]; # if a binary string if ($type == 1 || $type == 7) { # set count to string length $count = length($tags->{$id}[1]); # if an ASCII string } elsif ($type == 2) { # set count to string length + 1 $count = length($tags->{$id}[1]) + 1; # if a rational value } elsif ($type == 5) { # set count to number of values/2 $count = $#{$tags->{$id}}/2; } else { # set count to number of values $count = $#{$tags->{$id}}; } # if size of value/offset > 4 if (($size = $count * $ts[$type]) > 4) { # write directory entry with offset print $fh pack("$short$short$long$long", $id, $type, $count, $mark); # increment data pointer $mark += $size; # make a word boundary $mark += $mark % 2; } else { # if a binary string if ($type == 1 || $type == 7) { # set pack format $fmt = 'a4'; # if an ASCII string } elsif ($type == 2) { # set pack format $fmt = 'Z4'; # if a short value } elsif ($type == 3) { # set pack format (one or two values) $fmt = $count == 1 ? $short . 'x2' : $short . '2'; # if a long value } elsif ($type == 4) { # set pack format $fmt = $long; } else { # error croak('unsupported TIFF data type, stopped'); } # write directory entry (12 bytes) with value(s) print $fh pack("$short$short$long$fmt", $id, $type, $count, @{$tags->{$id}}[1 .. $#{$tags->{$id}}]); } } # set data pointer $mark = $ifd + 12 * @sid + 6; # for each tag for my $id (@sid) { # get data type $type = $tags->{$id}[0]; # if a binary string if ($type == 1 || $type == 7) { # set count to string length $count = length($tags->{$id}[1]); # if an ASCII string } elsif ($type == 2) { # set count to string length + 1 $count = length($tags->{$id}[1]) + 1; # if a rational value } elsif ($type == 5) { # set count to number of values/2 $count = $#{$tags->{$id}}/2; } else { # set count to number of values $count = $#{$tags->{$id}}; } # if size of value/offset > 4 if (($size = $count * $ts[$type]) > 4) { # if a binary string if ($type == 1 || $type == 7) { # set pack format $fmt = "a$count"; # if an ASCII string } elsif ($type == 2) { # set pack format $fmt = "Z$count"; # if a short value } elsif ($type == 3) { # set pack format $fmt = "$short$count"; # if a long value } elsif ($type == 4) { # set pack format $fmt = "$long$count"; # if a rational value } elsif ($type == 5) { # set pack format $fmt = "$long$#{$tags->{$id}}"; } else { # error croak('unsupported TIFF data type, stopped'); } # set file pointer seek($fh, $mark, 0); # write the data value(s) print $fh pack($fmt, @{$tags->{$id}}[1 .. $#{$tags->{$id}}]); # increment data pointer $mark += $size; # make a word boundary $mark += $mark % 2; } } } # write TIFF data strip # parameters: (file_handle, IFD_hash, patch_width, gap_width, left_edge_width, right_edge_width, strip_index, strip_data_array, pack_format, dither_value) sub _writeTIFFstrip { # get parameters my ($fh, $tags, $width, $gap, $left, $right, $sx, $data, $fmt, $dither) = @_; # local variables my ($pi, $samples, $bits, $max, $diff, $edge, $w, @spot, $rms, $gdata, @row, $strip); # get photometric interpretation $pi = $tags->{'262'}[1]; # get number of samples (channels) $samples = $tags->{'277'}[1]; # get bits per sample $bits = $tags->{'258'}[1]; # max binary value (8, 16 or 32 bits) $max = ($bits == 8) ? 255 : ($bits == 16) ? 65535 : 1; # make list of spot channel indices @spot = (4 .. $tags->{'277'}[1] - 1); # for each patch for my $i (0 .. $#{$data}) { # if RGB data if ($pi == 2) { # compute white and black differences $diff->[$i][0] = sqrt(($max - $data->[$i][0])**2 + ($max - $data->[$i][1])**2 + ($max - $data->[$i][2])**2); $diff->[$i][1] = sqrt($data->[$i][0]**2 + $data->[$i][1]**2 + $data->[$i][2]**2); # if CMYK data } elsif ($pi == 5) { # compute rms value of CMY + spot channels (CMY weighted and spot channels inverted) $rms = sqrt(List::Util::sum(1.5 * $data->[$i][0]**2, $data->[$i][1]**2, 0.5 * $data->[$i][2]**2, (map {($max - $data->[$i][$_])**2} @spot))/(3 + @spot)); # compute white and black differences (black * color) $diff->[$i][0] = $max - ($max - $data->[$i][3]) * ($max - $rms)/$max; $diff->[$i][1] = ($max - $data->[$i][3]) * ($max - $rms)/$max; # L*a*b* data } else { # compute white and black differences (approx dEab) $diff->[$i][0] = sqrt(($max - $data->[$i][0])**2 + 6.55 * $data->[$i][1]**2 + 6.55 * $data->[$i][2]**2); $diff->[$i][1] = sqrt($data->[$i][0]**2 + 6.55 * $data->[$i][1]**2 + 6.55 * $data->[$i][2]**2); } # skip first patch if ($i > 0) { # if RGB data if ($pi == 2) { # if max white difference > max black difference if (($diff->[$i - 1][0] > $diff->[$i][0] ? $diff->[$i - 1][0] : $diff->[$i][0]) > ($diff->[$i - 1][1] > $diff->[$i][1] ? $diff->[$i - 1][1] : $diff->[$i][1])) { # gap is white $gdata->[$i - 1] = [($max) x 3]; } else { # gap is black $gdata->[$i - 1] = [0, 0, 0]; } # if CMYK data } elsif ($pi == 5) { # if max white difference > max black difference if (($diff->[$i - 1][0] > $diff->[$i][0] ? $diff->[$i - 1][0] : $diff->[$i][0]) > ($diff->[$i - 1][1] > $diff->[$i][1] ? $diff->[$i - 1][1] : $diff->[$i][1])) { # gap is white $gdata->[$i - 1] = [0, 0, 0, 0, ($max) x ($samples - 4)]; } else { # gap is black $gdata->[$i - 1] = [0, 0, 0, ($max) x ($samples - 3)]; } # L*a*b* data } else { # if max white difference > max black difference if (($diff->[$i - 1][0] > $diff->[$i][0] ? $diff->[$i - 1][0] : $diff->[$i][0]) > ($diff->[$i - 1][1] > $diff->[$i][1] ? $diff->[$i - 1][1] : $diff->[$i][1])) { # gap is white $gdata->[$i - 1] = [$max, 0, 0]; } else { # gap is black $gdata->[$i - 1] = [0, 0, 0]; } } } } # compute edge pixel values (black) $edge = ($pi == 5) ? [0, 0, 0, ($max) x ($samples - 3)] : [0, 0, 0]; # for each patch for my $i (0 .. $#{$data}) { # if first patch if ($i == 0) { # add left edge data push(@row, (@{$edge}) x $left->[0]); # set patch width $w = $width - $left->[1]; # if last patch } elsif ($i == $#{$data}) { # set patch width $w = $width - $right->[1]; # others } else { # set patch width $w = $width; } # if dither enabled or 32-bits if (defined($dither) || $bits == 32) { # add patch data push(@row, (@{$data->[$i]}) x $w); } else { # add patch data, adding/subtracting 0.5 to round to the nearest integer (by 'pack', below) push(@row, (map {$_ < 0 ? $_ - 0.5 : $_ + 0.5} @{$data->[$i]}) x $w); } # if last patch if ($i == $#{$data}) { # add right edge data push(@row, (@{$edge}) x $right->[0]); } else { # add gap data push(@row, (@{$gdata->[$i]}) x $gap); } } # set file pointer to strip offset seek($fh, $tags->{'273'}[$sx + 1], 0); # if dither enabled and 8-bit if (defined($dither) && $bits == 8) { # for each strip row for my $i (0 .. $tags->{'278'}[1] - 1) { # write packed data with dithering print $fh pack($fmt, map {$_ < 0 ? $_ - rand() : $_ + rand()} @row); } } else { # for each strip row for my $i (0 .. $tags->{'278'}[1] - 1) { # write packed data print $fh pack($fmt, @row); } } } # read chart from CxF3 data file # parameters: (object_reference, file_handle, hash) # returns: (result) sub _readChartCxF3 { # get parameters my ($self, $fh, $hash) = @_; # local variables my ($dom, $root, $ns, $uri, %keys, @info, @obj, $ops_hash, $ops); my ($ix, $type, $name, $value, $node, @data, $xrp, @attr); # parse CxF3 document eval{$dom = XML::LibXML->load_xml('IO' => $fh)} || return('failed parsing CxF3 document'); # validate the CxF3 document _validateCxF3($dom) if (defined($hash->{'validate'}) && $hash->{'validate'}); # get root element $root = $dom->documentElement(); # get the namespace prefix and URI $ns = $root->prefix(); $uri = $root->namespaceURI(); # verify CxF3 document or return ($uri eq 'http://colorexchangeformat.com/CxF3-core') || return('CxF3 document has wrong URI'); # get cc:Object elements @obj = $root->findnodes("$ns:Resources/$ns:ObjectCollection/$ns:Object"); # get cc:FileInformation elements (optional) @info = $root->findnodes("$ns:FileInformation/*"); # save root element # this method only reads data from cc:Object nodes # all other CxF3 info is kept in the dom object # and accessed as needed by other methods $self->[0]{'CxF3_dom'} = $root; # save record separator in header # note: XML files might not have record separators # so we use Perl's input record separator instead $self->[0]{'read_rs'} = $/; # make CxF3 => ASCII mapping table (from ISO 17972-1, Annex A) %keys = ('Creator' => 'ORIGINATOR', 'Description' => 'FILE_DESCRIPTOR', 'CreationDate' => 'CREATED', 'Comment' => 'CXF3_COMMENT'); # for each cc:FileInformation element for my $s (@info) { # if cc:Tag node if ($s->nodeName() eq "$ns:Tag") { # get name attribute $name = $s->getAttribute('Name'); # get value attribute $value = $s->getAttribute('Value'); } else { # get node name $name = $s->nodeName(); # remove namespace prefix $name =~ s/^\w+://; # lookup name in hash $name = defined($keys{$name}) ? $keys{$name} : $name; # get node value $value = $s->textContent(); } # add name/value to header array push(@{$self->[3]}, [$name, "\"$value\"", 'FileInformation']); } # make the operations hash and add format fields $ops_hash = _makeCxF3readops($self, $root, $ns, \@obj, $hash); # initialize sample index $ix = 0; # for each cc:Object element for my $s (@obj) { # get the ObjectType attribute $type = $s->getAttribute('ObjectType'); # get the Name attribute $name = $s->getAttribute('Name'); # if ObjectType is 'Target' or '...Measurement' if ($type =~ m/^Target$|Measurement$/) { # match numeric part of Name attribute $name =~ m/(\d+)$/; # set row index $ix = $1; } else { # increment row index $ix++; } # get operation list for this ObjectType $ops = $ops_hash->{$type}; # for each operation for my $i (0 .. $#{$ops}) { # get main Xpath node ($node) = $s->findnodes($ops->[$i][1]); # if subpaths if (@{$ops->[$i][2]}) { # if data class is NCLR if ($ops->[$i][0] eq 'NCLR') { # get the CMYK values @data = map {$node->findvalue($_)} @{$ops->[$i][2]}; # for each SpotColor for my $spotcolor ($node->findnodes("$ns:SpotColor")) { # push SpotColor value push(@data, $spotcolor->findvalue("$ns:Percentage")); } # set chart data (CMYK + SPOT values) @{$self->[1][$ix]}[@{$ops->[$i][3]}] = @data; } else { # set chart data using subpaths @{$self->[1][$ix]}[@{$ops->[$i][3]}] = map {$node->findvalue($_)} @{$ops->[$i][2]}; } # if no subpaths and one field } elsif (@{$ops->[$i][3]} == 1) { # set chart data to text content $self->[1][$ix][$ops->[$i][3][0]] = $node->textContent(); # if no subpaths and multiple fields (e.g. spectral data) } elsif (@{$ops->[$i][3]} > 1) { # set chart data splitting text content @{$self->[1][$ix]}[@{$ops->[$i][3]}] = split(/ /, $node->textContent()); } } } # read CxF3 ColorSpecification nodes _readCxF3colorspec($self, $root, $ns); # make XPathContext object for X-Rite Prism namespace $xrp = XML::LibXML::XPathContext->new($root); $xrp->registerNs('xrp', 'http://www.xrite.com/products/prism'); # get the xrp:CustomAttributes node if (($node) = $xrp->findnodes("$ns:CustomResources/xrp:Prism/xrp:CustomAttributes")) { # get the attribute list @attr = $node->attributes(); # add xrp:CustomAttributes hash $self->[0]{'xrp:CustomAttributes'} = {map {$_->nodeName, $_->getValue()} @attr}; } # return return(); } # make CxF3 read operations hash # adds the format fields to object # parameters: (object_reference, CxF3_root, CxF3_prefix, CxF3_object_array_reference, hash) # returns: (hash_ref) sub _makeCxF3readops { # get parameters my ($self, $root, $ns, $obj, $hash) = @_; # local variables my (@attr, @tags, %keys, $table, $k, $m, $n, $t, $type, $entry, $ops_hash); my (@format, @nodes, $node, @data, $name, $colorspec, $start, $inc); # if cc:Object filter parameter provided if (defined($hash->{'cc:Object'}) && ref($hash->{'cc:Object'}) eq 'ARRAY') { # for each entry for (@{$hash->{'cc:Object'}}) { # match type/attribute m/^([^\s\/]*?)\/?([^\s\/]*)$/; # save matched values $entry = [$1, $2]; # if a valid attribute (see CxF3_Core.xsd) if ($2 =~ m/^(?:|ObjectType|Name|Id|GUID|\*)$/) { # push on array push(@attr, $entry); } else { # print warning warn('invalid cc:Object attribute'); } } } # if cc:Tag filter parameter provided if (defined($hash->{'cc:Tag'}) && ref($hash->{'cc:Tag'}) eq 'ARRAY') { # for each entry for (@{$hash->{'cc:Tag'}}) { # match type/key m/^([^\s\/]*?)\/?([^\s\/]*)$/; # push on array push(@tags, [$1, $2]); } } # make hash for sort order of certain keys %keys = ('SampleID' => -2, 'SampleName' => -1, 'Id' => -2, 'Name' => -1); # table [data_class, CxF3_main_path, [CxF3_sub_paths], [CGATS/ASCII field names]] # some mappings have no sub-paths, which is indicated by an empty sub_path array # the 'NCLR', 'SPECTRAL' and 'DENSITY' data classes are special cases $table = [ ['RGB', "$ns:DeviceColorValues/$ns:ColorRGB", ["$ns:R", "$ns:G", "$ns:B"], [qw(RGB_R RGB_G RGB_B)]], ['CMYK', "$ns:DeviceColorValues/$ns:ColorCMYK", ["$ns:Cyan", "$ns:Magenta", "$ns:Yellow", "$ns:Black"], [qw(CMYK_C CMYK_M CMYK_Y CMYK_K)]], ['NCLR', "$ns:DeviceColorValues/$ns:ColorCMYKPlusN", ["$ns:Cyan", "$ns:Magenta", "$ns:Yellow", "$ns:Black"], [qw(nCLR)]], ['SPECTRAL', "$ns:ColorValues/$ns:ReflectanceSpectrum", [], [qw(nm)]], ['DENSITY', "$ns:ColorValues/$ns:ColorDensity/$ns:Density", [], [qw(D_RED D_GREEN D_BLUE D_VIS)]], ['XYZ', "$ns:ColorValues/$ns:ColorCIEXYZ", ["$ns:X", "$ns:Y", "$ns:Z"], [qw(XYZ_X XYZ_Y XYZ_Z)]], ['XYY', "$ns:ColorValues/$ns:ColorCIExyY", ["$ns:x", "$ns:y", "$ns:Y"], [qw(XYY_X XYY_Y XYY_YCAP)]], ['LAB', "$ns:ColorValues/$ns:ColorCIELab", ["$ns:L", "$ns:A", "$ns:B"], [qw(LAB_L LAB_A LAB_B)]], ['LCH', "$ns:ColorValues/$ns:ColorCIELCh", ["$ns:L", "$ns:C", "$ns:H"], [qw(LAB_L LAB_C LAB_H)]], ['DE', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE", [], [qw(LAB_DE)]], ['DE94', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE94", [], [qw(LAB_DE94)]], ['DECMC', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dEcmc", [], [qw(LAB_CMC)]], ['DE2000', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE2000", [], [qw(LAB_2000)]], ]; # set next table index $m = $#{$table} + 1; # for each CxF3 'Object' for my $s (@{$obj}) { # get the 'ObjectType' attribute $type = $s->getAttribute('ObjectType'); # if 'ObjectType' not in hash if (! defined($ops_hash->{$type})) { # add 'ObjectType' to hash $ops_hash->{$type} = []; # if 'Object' attributes are mapped if (@attr) { # for each 'Object' attribute (GUID is optional) for my $t (qw(ObjectType Name Id GUID)) { # if attribute exists and is mapped if ($s->exists("\@$t") && grep {($_->[0] eq $type || $_->[0] =~ m/^\*?$/) && ($_->[1] eq $t || $_->[1] =~ m/^\*?$/)} @attr) { # get sort order $k = defined($keys{$t}) ? $keys{$t} : $m++; # push table entry on hash array (note: attribute XPaths begin with @) push(@{$ops_hash->{$type}}, $entry = ["ATTR:$t", "\@$t", [], [$t], $type, $k]); # push table entry on format array push(@format, $entry); } } } else { # if ObjectType not 'Target' or '...Measurement' if ($type !~ m/^Target$|Measurement$/) { # push table entry on hash array (note: attribute XPaths begin with @) push(@{$ops_hash->{$type}}, $entry = ['NAME', '@Name', [], ['SAMPLE_NAME'], $type, -1]); # push table entry on format array push(@format, $entry); } } # for each table entry for my $i (0 .. $#{$table}) { # get table entry $t = $table->[$i]; # if main XPath exists if ($s->exists($t->[1])) { # get ColorSpecification attribute (if any) $colorspec = $s->findvalue("$t->[1]/\@ColorSpecification"); # push table entry on hash array push(@{$ops_hash->{$type}}, $entry = [@{$t}, $type, $i, $colorspec]); # push table entry on format array push(@format, $entry); # if an 'NCLR' entry if ($entry->[0] eq 'NCLR') { # get cc:SpotColor nodes @nodes = $s->findnodes(".//$ns:SpotColor"); # get number of colors $n = @nodes + 4; # add format fields $entry->[3] = [map {sprintf('%xCLR_%x', $n, $_)} (1 .. $n)]; # if a 'SPECTRAL' entry } elsif ($entry->[0] eq 'SPECTRAL') { # get the ReflectanceSpectrum data @data = split(/ /, $s->findvalue($t->[1])); # get the ColorSpecification node (linked by the ColorSpecification attribute) ($node) = $root->findnodes("$ns:Resources/$ns:ColorSpecificationCollection/$ns:ColorSpecification[\@Id='$colorspec']"); # get the StartWL attribute $start = $node->findvalue("$ns:MeasurementSpec/$ns:WavelengthRange/\@StartWL"); # get the Increment attribute $inc = $node->findvalue("$ns:MeasurementSpec/$ns:WavelengthRange/\@Increment"); # add format fields $entry->[3] = [map {'nm' . ($start + $_ * $inc)} (0 .. $#data)]; } } } # if Tags are mapped if (@tags) { # for each Tag for my $t ($s->findnodes("$ns:TagCollection/$ns:Tag")) { # get Tag Name attribute $name = $t->getAttribute('Name'); # if this Tag is mapped if (grep {($_->[0] eq $type || $_->[0] =~ m/^\*?$/) && ($_->[1] eq $name || $_->[1] =~ m/^\*?$/)} @tags) { # get sort order $k = defined($keys{$name}) ? $keys{$name} : $m++; # push table entry on hash array (note: attribute XPaths begin with @) push(@{$ops_hash->{$type}}, $entry = ["TAG:$name", "$ns:TagCollection/$ns:Tag[\@Name = '$name']/\@Value", [], [$name], $type, $k]); # push table entry on format array push(@format, $entry); } } } } } # sort format array by table index @format = sort {$a->[5] <=> $b->[5]} @format; # for each format entry for my $fmt (@format) { # add format fields to data array and replace keys with column indices $fmt->[3] = add_fmt($self, map {"$fmt->[4]|$_"} @{$fmt->[3]}); # if entry has ColorSpecification if (defined($fmt->[6])) { # add ColorSpecification attribute to colorimetry array for (@{$fmt->[3]}) {$self->[2][5][$_] = $fmt->[6]} } } # return return($ops_hash); } # read CxF3 ColorSpecification nodes # parameters: (object_reference, CxF3_root, CxF3_prefix) sub _readCxF3colorspec { # get parameters my ($self, $root, $ns) = @_; # local variables my (@keys, @cspec, $id, $node, $child, $value); # make CxF3 => ASCII mapping table (from ISO 17972-1, Annex A) @keys = ( ["$ns:MeasurementSpec/$ns:GeometryChoice" => 'MEASUREMENT_GEOMETRY'], ["$ns:MeasurementSpec/$ns:Device/$ns:DeviceFilter" => 'FILTER'], ["$ns:MeasurementSpec/$ns:Device/$ns:DeviceIllumination" => 'MEASUREMENT_SOURCE'], ["$ns:MeasurementSpec/$ns:CalibrationStandard" => 'DEVCALSTD'], ); # find the ColorSpecification nodes @cspec = $root->findnodes("$ns:Resources/$ns:ColorSpecificationCollection/$ns:ColorSpecification"); # for each ColorSpecification node for my $s (@cspec) { # get the Id attribute and skip if 'Unknown' next if (($id = $s->getAttribute('Id')) eq 'Unknown'); # for each entry in mapping table for my $i (0 .. $#keys) { # if XPath is found if (($node) = $s->findnodes($keys[$i][0])) { # get the first non-blank child node if (($child) = $node->nonBlankChildNodes()) { # if child is an element node if ($child->nodeType() == 1) { # serialize node $value = $node->toString(1); # remove tabs and endlines $value =~ s/[\t\n]+//g; # remove namespace prefix $value =~ s/([<\/])$ns:/$1/g; # if child is a text node } elsif ($child->nodeType() == 3) { # get the value $value = $node->textContent(); } # save in header line array push(@{$self->[3]}, [$keys[$i][1], "\"$value\"", $id]); } } } } } # make CxF3 FileInformation nodes # optional hash parameter contains 'cc:FileInformation' filter array # parameters: (object_reference, CxF3_root, CxF3_prefix, CxF3_namespace_URI, hash) # returns: (datetime) sub _makeCxF3fileinfo { # get parameters my ($self, $root, $ns, $nsURI, $hash) = @_; # local variables my (@filter, $t, $datetime, $info, %keys); my ($keyword, $value, $source, $node, $child); # get filter array (if any) @filter = @{ICC::Shared::flatten($hash->{'cc:FileInformation'})} if (defined($hash->{'cc:FileInformation'})); # make Time::Piece object $t = localtime(); # get the 'FileInformation' node ($info) = $root->findnodes("$ns:FileInformation"); # make ASCII => CxF3 mapping table (from ISO 17972-1, Annex A) %keys = ('ORIGINATOR' => "$ns:Creator", 'FILE_DESCRIPTOR' => "$ns:Description", 'CXF3_COMMENT' => "$ns:Comment"); # for each file header line for (@{$self->[3]}) { # get keyword, value and source ($keyword, $value, $source) = @{$_}; # strip quotes from value $value =~ s/^\"(.*)\"$/$1/; # if keyword is 'CREATED' if ($keyword eq 'CREATED') { # make Time::Piece object from 'CREATED' value $t = _makeTimePiece($value); # if source is 'FileInformation' or keyword is in filter array } elsif ((defined($source) && $source eq 'FileInformation') || grep {$_ eq $keyword} @filter) { # if keyword in mapping table if (exists($keys{$keyword})) { # if XPath exists in FileInformation element if (($node) = $info->findnodes($keys{$keyword})) { # if text content exists if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) { # update text content $child->setData($value); } } # must be a 'Tag' element } else { # if XPath exists in FileInformation element if (($node) = $info->findnodes("$ns:Tag[\@Name='$keyword']")) { # update the Value attribute $node->setAttribute('Value', $value); } else { # add new Tag element $node = $info->appendChild(XML::LibXML::Element->new('Tag')); $node->setAttribute('Name', $keyword); $node->setAttribute('Value', $value); $node->setNamespace($nsURI, $ns); } } } } # make ISO 8601 datetime string from Time::Piece object $datetime = $t->strftime('%Y-%m-%dT%T%z'); substr($datetime, -2, 0, ':'); # get the 'CreationDate' node ($node) = $info->findnodes("$ns:CreationDate"); # if text content exists if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) { # update text content $child->setData($datetime); } # return datetime return($datetime); } # make CxF3 write operations array # parameters: (object_reference, CxF3_root, CxF3_prefix, column_slice) # returns: (array_ref) sub _makeCxF3writeops { # get parameters my ($self, $root, $ns, $cols) = @_; # local variables my ($n, %keys, $table, $class, $prefix, $key, $ops, $groups, $sort); # if column slice defined if (defined($cols)) { # if column slice an empty array reference if (ref($cols) eq 'ARRAY' && @{$cols} == 0) { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } else { # flatten column slice $cols = ICC::Shared::flatten($cols); } } else { # use all columns $cols = [0 .. $#{$self->[1][0]}]; } # get number of fields $n = @{$cols}; # remove undefined keys @{$cols} = grep {defined($self->[1][0][$_])} @{$cols}; # warn if undefined keys ($n == @{$cols}) || warn('undefined keys in column slice'); # get number of fields $n = @{$cols}; # remove duplicate keys @{$cols} = grep {! $keys{$self->[1][0][$_]}++} @{$cols}; # warn if duplicate keys ($n == @{$cols}) || warn('duplicate keys in column slice'); # table structure: [data_class, CxF3_main_path, [CxF3_sub_paths], regex, sort_order] # some mappings have no sub-paths, which is indicated by an empty sub_path array # sort_order array contains the last character(s) of the format keys, and is optional # the 'NCLR', 'SPECTRAL' and 'DENSITY' data classes are special cases $table = [ ['RGB', "$ns:DeviceColorValues/$ns:ColorRGB", ["$ns:R", "$ns:G", "$ns:B"], qr/^(?:(.*)\|)?RGB_[RGB]$/, [qw(R G B)]], ['CMYK', "$ns:DeviceColorValues/$ns:ColorCMYK", ["$ns:Cyan", "$ns:Magenta", "$ns:Yellow", "$ns:Black"], qr/^(?:(.*)\|)?CMYK_[CMYK]$/, [qw(C M Y K)]], ['NCLR', "$ns:DeviceColorValues/$ns:ColorCMYKPlusN", ["$ns:Cyan", "$ns:Magenta", "$ns:Yellow", "$ns:Black"], qr/^(?:(.*)\|)?[2-9A-F]CLR_[1-9A-F]$/], ['SPECTRAL', "$ns:ColorValues/$ns:ReflectanceSpectrum", [], qr/^(?:(.*)\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3}$/], ['DENSITY', "$ns:ColorValues/$ns:ColorDensity/$ns:Density", [], qr/^(?:(.*)\|)?D_(?:RED|GREEN|BLUE|VIS)$/, [qw(RED GREEN BLUE VIS)]], ['XYZ', "$ns:ColorValues/$ns:ColorCIEXYZ", ["$ns:X", "$ns:Y", "$ns:Z"], qr/^(?:(.*)\|)?XYZ_[XYZ]$/, [qw(X Y Z)]], ['XYY', "$ns:ColorValues/$ns:ColorCIExyY", ["$ns:x", "$ns:y", "$ns:Y"], qr/^(?:(.*)\|)?XYY_(?:X|Y|CAPY)$/, [qw(_X _Y _CAPY)]], ['LAB', "$ns:ColorValues/$ns:ColorCIELab", ["$ns:L", "$ns:A", "$ns:B"], qr/^(?:(.*)\|)?LAB_[LAB]$/, [qw(L A B)]], ['LCH', "$ns:ColorValues/$ns:ColorCIELCh", ["$ns:L", "$ns:C", "$ns:H"], qr/^(?:(.*)\|)?LAB_[LCH]$/, [qw(L C H)]], ['DE', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE", [], qr/^(?:(.*)\|)?LAB_DE$/], ['DE94', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE94", [], qr/^(?:(.*)\|)?LAB_DE94$/], ['DECMC', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dEcmc", [], qr/^(?:(.*)\|)?LAB_CMC$/], ['DE2000', "$ns:ColorDifferenceValues/$ns:DeltaCIELab/$ns:dE2000", [], qr/^(?:(.*)\|)?LAB_2000$/], ]; # following section builds operations array from column slice # # sort keys alphabetically @{$cols} = sort {$self->[1][0][$a] cmp $self->[1][0][$b]} @{$cols}; # for each field for my $i (@{$cols}) { # if key matches current class and prefix (prefix could be undefined) if (defined($class) && $self->[1][0][$i] =~ /$table->[$class][3]/ && (defined($prefix) ? $prefix : "\n") eq (defined($1) ? $1 : "\n")) { # add index to current operation push(@{$ops->[-1][4]}, $i); } else { # for each data class for my $j (0 .. $#{$table}) { # if key matches class if ($self->[1][0][$i] =~ /$table->[$j][3]/) { # save current prefix $prefix = $1; # save current class $class = $j; # add new operation push(@{$ops}, [$table->[$j][0], $prefix, $table->[$j][1], $table->[$j][2], [$i], {}, $j]); # quit loop last; # if no match found in table } elsif ($j == $#{$table}) { # match prefix/key $self->[1][0][$i] =~ m/^(?:(.*)\|)?(.*)/; # save matched values $prefix = $1; $key = $2; # set current class $class = undef; # if prefix defined, and not Target or ...Measurement, and key is SAMPLE_NAME if (defined($prefix) && $prefix !~ m/^Target$|Measurement$/ && $key =~ m/^SAMPLE_NAME$|^SampleName$/) { # add special operation to set 'Object' 'Name' attribute to SAMPLE_NAME push(@{$ops}, ['TAG', $prefix, '', [], [], {'Name' => [$i]}, -1]); } else { # add Tag operation push(@{$ops}, ['TAG', $prefix, "$ns:TagCollection/$ns:Tag", [], [], {'Name' => $key, 'Value' => [$i]}, 100]); } } } } } # following section sorts and verifies column slices, sets default prefixes and checks for multiple elements # # init loop variable %keys = (); # for each array entry for my $t (@{$ops}) { # if sort order is defined if (defined($table->[$t->[6]][4])) { # arrange column indices in sort order @{$t->[4]} = map {my $end = $_; grep {$self->[1][0][$_] =~ m/$end$/} @{$t->[4]}} @{$table->[$t->[6]][4]}; } # if class is SPECTRAL if ($t->[0] eq 'SPECTRAL') { # verify spectral slice (@{$t->[4]} == @{_spectral($self, $t->[1])}) || warn("invalid column slice - SPECTRAL class"); # if class is DENSITY } elsif ($t->[0] eq 'DENSITY') { # to be done # if class is NCLR } elsif ($t->[0] eq 'NCLR') { # match first key to get number of channels $self->[1][0][$t->[4][0]] =~ m/([2-9A-F])CLR_[1-9A-F]$/; # verify nCLR slice (@{$t->[4]} == CORE::hex($1)) || warn("invalid column slice - NCLR class"); # all others } else { # verify subpaths match column slice (@{$t->[4]} == @{$t->[3]} || (@{$t->[4]} == 1 && @{$t->[3]} == 0)) || warn("invalid column slice - $t->[0] class"); } # if prefix undefined if (! defined($t->[1])) { # if XPath contains 'ColorValues' or 'ColorDifferenceValues' if ($t->[2] =~ m/^$ns:(?:ColorValues|ColorDifferenceValues)\//) { # set prefix to M0_Measurement $t->[1] = 'M0_Measurement'; # if XPath contains 'DeviceColorValues' } elsif ($t->[2] =~ m/^$ns:DeviceColorValues\//) { # set prefix to Target $t->[1] = 'Target'; # all others } else { # set prefix to '~~' $t->[1] = '~~'; } } # for 'ColorValues' or 'DeviceColorValues' if ($t->[2] =~ m/^$ns:(ColorValues|DeviceColorValues)\//) { # warn on multiple elements (not allowed by i1Profiler) print "warning: multiple $1 elements in CxF3 $t->[1] object\n" if ($keys{"$1/$t->[1]"}++ == 1); } } # following section groups operations by prefix # # sort by prefix, then by table index @{$ops} = sort {($a->[1] cmp $b->[1]) or ($a->[6] <=> $b->[6])} @{$ops}; # init loop variable $prefix = undef; # for each operation for my $t (@{$ops}) { # if same prefix as last operation if (defined($prefix) && $prefix eq $t->[1]) { # add operation to last group push(@{$groups->[-1]}, $t); # if class is TAG and prefix is '~~' } elsif ($t->[0] eq 'TAG' && $t->[1] eq '~~') { # for each group for my $g (@{$groups}) { # add operation push(@{$g}, $t); } # set prefix $prefix = undef; # others } else { # add new group push(@{$groups}, [$t]); # set prefix $prefix = $t->[1]; } } # return return($groups); } # make CxF3 ColorSpecification nodes # parameters: (object_reference, CxF3_root, CxF3_prefix, CxF3_namespace_URI, operations_array) sub _makeCxF3colorspec { # get parameters my ($self, $root, $ns, $nsURI, $ops) = @_; # local variables my (@illum, @filter, $cscol, $template, $unknown); my (%table, %cspec, %hash, $keyword, $value, $source); my ($Id, $cs, @nodes, $node, $child, @wav); my ($parser, $frag, $std, $xpath); # illumination types @illum = qw(M0_Incandescent M1_Daylight M2_UVExcluded M3_Polarized); # filter types @filter = qw(Filter_None Filter_None Filter_UVExcluded Filter_None); # get the 'ColorSpecificationCollection' node ($cscol) = $root->findnodes("$ns:Resources/$ns:ColorSpecificationCollection"); # get the 'ColorSpecification' node with Id = 'template' ($template) = $cscol->findnodes("$ns:ColorSpecification[\@Id='template']"); # get the 'ColorSpecification' node with Id = 'Unknown' ($unknown) = $cscol->findnodes("$ns:ColorSpecification[\@Id='Unknown']"); # make ASCII => CxF3 mapping table (from ISO 17972-1, Annex A) %table = ( 'MANUFACTURER' => "$ns:MeasurementSpec/$ns:Device/$ns:Manufacturer", 'MODEL' => "$ns:MeasurementSpec/$ns:Device/$ns:Model", 'SERIAL_NUMBER' => "$ns:MeasurementSpec/$ns:Device/$ns:SerialNumber", 'MEASUREMENT_GEOMETRY' => "$ns:MeasurementSpec/$ns:GeometryChoice", 'MEASUREMENT_SOURCE' => "$ns:MeasurementSpec/$ns:Device/$ns:DeviceIllumination", 'FILTER' => "$ns:MeasurementSpec/$ns:Device/$ns:DeviceFilter", 'POLARIZATION' => "$ns:MeasurementSpec/$ns:Device/$ns:DevicePolarization", 'SAMPLE_BACKING' => "$ns:MeasurementSpec/$ns:Backing", 'DEVCALSTD' => "$ns:MeasurementSpec/$ns:CalibrationStandard", ); # for each group for my $group (@{$ops}) { # for each operation for my $t (@{$group}) { # if ColorValues (only ColorValues reference a ColorSpecification) if ($t->[2] =~ m/^$ns:ColorValues\//) { # set Id to saved value, if defined, or add '_spec' to prefix # the ColorSpecification Id is saved in the Colorimetry array when reading a CxF3 file $Id = defined($self->[2][5][$t->[4][0]]) ? $self->[2][5][$t->[4][0]] : "$t->[1]\_spec"; # set attribute hash $t->[5]{'ColorSpecification'} = $Id; # if 'ColorSpecification' undefined if (! $cspec{$Id}++) { # initialize keyword hash %hash = (); # add cloned 'ColorSpecification' element to 'ColorSpecificationCollection' $cs = $cscol->appendChild($template->cloneNode(1)); # set the Id $cs->setAttribute('Id', $Id); # if spectral data # there are three types of spectral data, reflective, transmissive and emissive # spectral data has a WavelengthRange node which contains the starting wavelength and increment if ($t->[2] =~ m/(Reflectance|Transmittance|Emissive)Spectrum$/) { # get the 'MeasurementType' node ($node) = $cs->findnodes("$ns:MeasurementSpec/$ns:MeasurementType"); # if text content exists if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) { # update text content $child->setData("Spectrum_$1"); } # for first two data columns for ($t->[4][0], $t->[4][1]) { # match wavelength in format key $self->[1][0][$_] =~ m/(\d{3})$/; # push to array push(@wav, $1); } # find the 'WavelengthRange' node ($node) = $cs->findnodes("$ns:MeasurementSpec/$ns:WavelengthRange"); # set the 'StartWL' attribute $node->setAttribute('StartWL', $wav[0]); # set the 'Increment' attribute $node->setAttribute('Increment', $wav[1] - $wav[0]); # set operation 'StartWL' attribute $t->[5]{'StartWL'} = $wav[0]; } else { # find the 'WavelengthRange' node ($node) = $cs->findnodes("$ns:MeasurementSpec/$ns:WavelengthRange"); # unbind the node (used only with spectral data) $node->unbindNode(); } # for each file header entry for (@{$self->[3]}) { # get keyword, value and source ($keyword, $value, $source) = @{$_}; # strip quotes from value $value =~ s/^\"(.*)\"$/$1/; # if source is ColorSpecification Id if (defined($source) && $source eq $Id) { # add keyword to hash $hash{$keyword}++; # if keyword in table if (exists($table{$keyword})) { # if XPath does not exist in ColorSpecification element if (! (($node) = $cs->findnodes($table{$keyword}))) { # set node $node = $cs; # initialize XPath $xpath = undef; # for each segment for (split(/\//, $table{$keyword})) { # add segment to XPath $xpath = defined($xpath) ? "$xpath/$_" : $_; # if XPath does not exist in ColorSpecification element if (! (($node) = $cs->findnodes($xpath))) { # add element for XPath segment $node = $node->appendChild(XML::LibXML::Element->new($_)); $node->setNamespace($nsURI, $ns); } } } # get the first non-blank child node ($child) = $node->nonBlankChildNodes(); # make a parser object $parser = XML::LibXML->new(); # if value is an XML balanced chunk if ($value =~ m/</ && eval{$frag = $parser->parse_balanced_chunk($value)}) { # get all element nodes @nodes = $frag->findnodes('//*'); # replace existing node $node->replaceNode($frag); # set namespace of each element for (@nodes) {$_->setNamespace($nsURI, $ns)}; # if no child node } elsif (! defined($child)) { # set text content to value $node->appendText($value); # if child node is text type } elsif ($child->nodeType == 3) { # modify existing text content $child->setData($value); } } } } # match illumination standard in prefix (M0, M1, M2, M3) $std = ($t->[1] =~ m/^M([0-3])/) ? $1 : 0; # if 'MEASUREMENT_SOURCE' not a keyword if (! exists($hash{'MEASUREMENT_SOURCE'})) { # find the 'DeviceIllumination' node ($node) = $cs->findnodes("$ns:MeasurementSpec/$ns:Device/$ns:DeviceIllumination"); # if text content exists if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) { # update text content $child->setData($illum[$std]); } } # if 'FILTER' not a keyword if (! exists($hash{'FILTER'})) { # find the 'DeviceFilter' node ($node) = $cs->findnodes("$ns:MeasurementSpec/$ns:Device/$ns:DeviceFilter"); # if text content exists if ((($child) = $node->nonBlankChildNodes) && $child->nodeType == 3) { # update text content $child->setData($filter[$std]); } } # if 'POLARIZATION' not a keyword and M3 standard if (! exists($hash{'POLARIZATION'}) && $std == 3) { # get the 'Device' node ($node) = $cs->findnodes("$ns:MeasurementSpec/$ns:Device"); # add 'Polarization' node $node = $node->appendChild(XML::LibXML::Element->new("$ns:Polarization")); $node->appendText(XML::LibXML::Boolean->True); $node->setNamespace($nsURI, $ns); } } # if 'DeviceColorValues' } elsif ($t->[2] =~ m/^$ns:DeviceColorValues\//) { # set attributes hash $t->[5]{'ColorSpecification'} = 'Unknown'; # increment 'ColorSpecification' hash $cspec{'Unknown'}++; } } } # unbind 'template' node $template->unbindNode(); # unbind 'Unknown' node, if not referenced $unknown->unbindNode() if (! $cspec{'Unknown'}); } # validate CxF3 document # prints warning and error info # parameters: (document_reference) sub _validateCxF3 { # get document reference my $doc = shift(); # load CxF3 schema state $xmlschema = XML::LibXML::Schema->new('location' => ICC::Shared::getICCPath('Templates/CxF3_Core.xsd')); # validate the document if (! defined(eval {$xmlschema->validate($doc)})) { # print warning on failure print "warning: invalid CxF3 document\n$@\n"; } } # make patch set # supported hash keys: 'colorspace', 'template', 'sort', 'limit' # parameters: (object_reference, hash) # returns: (result) sub _makePatchSet { # get parameters my ($self, $hash) = @_; # local variables my ($cs, $template, $sort, $tac, $n, $data, $eps); my (@fields, $loop, $limit, @inc, $init, $s, $code); # get the colorspace parameter (defined($cs = $hash->{'colorspace'})) || return('colorspace parameter missing'); # get the template parameter (defined($template = $hash->{'template'})) || return('template parameter missing'); # get the sort parameter (optional) $sort = $hash->{'sort'}; # get the ink limit parameter (optional) $tac = $hash->{'limit'}; # get number of elements in first group $n = @{$template->[0]}; # for each group for my $i (0 .. $#{$template}) { # verify number of elements ($n == @{$template->[$i]}) || return("wrong number of elements in template group $i"); # verify number of array references ($n == grep {ref() eq 'ARRAY'} @{$template->[$i]}) || return("non-array element(s) in template group $i"); # for each element for my $j (0 .. $#{$template->[$i]}) { # verify element contains only numeric scalars (@{$template->[$i][$j]} > 0 && @{$template->[$i][$j]} == grep {! ref() && Scalar::Util::looks_like_number($_)} @{$template->[$i][$j]}) || return("non-numeric element in template group $i, $j"); } } # if RGB colorspace if ($cs eq 'RGB') { # verify number of channels ($n == 3) || return('wrong number of template elements for RGB colorspace'); # set fields @fields = qw(RGB_R RGB_G RGB_B); # if CMYK colorspace } elsif ($cs eq 'CMYK') { # verify number of channels ($n == 4) || return('wrong number of template elements for CMYK colorspace'); # set fields @fields = qw(CMYK_C CMYK_M CMYK_Y CMYK_K); # if nCLR colorspace } elsif ($cs eq 'nCLR') { # verify number of channels ($n > 0 && $n < 16) || return('wrong number of template elements for nCLR colorspace'); # set fields @fields = map {$n . "CLR_$_"} (1 .. $n); # if L*a*b* colorspace } elsif ($cs eq 'Lab') { # verify number of channels ($n == 3) || return('wrong number of template elements for L*a*b* colorspace'); # set fields @fields = qw(LAB_L LAB_A LAB_B); } else { # error return('invalid colorspace parameter'); } # make loop variable list $loop = join(', ', map {"\$i$_"} (0 .. $n - 1)); # make initial code fragment $init = "\$data->[\$s++] = [$loop]"; # initialize index $s = 0; # for each group for my $i (0 .. $#{$template}) { # copy initial code fragment $code = $init; # for each device channel (in reverse order) for my $j (reverse(0 .. $#{$template->[$i]})) { # add loop code to fragment $code = "for my \$i$j (" . join(', ', @{$template->[$i][$j]}) . ") {$code}"; } # evaluate code fragment eval($code); } # if ink limit defined and color space is CMYK or nCLR if (defined($tac) && ($cs eq 'CMYK' || $cs eq 'nCLR')) { # compute max comparison error $eps = 1E-12; # verify ink limit is a number if (! ref($tac) && Scalar::Util::looks_like_number($tac)) { # for each patch for (@{$data}) { # add the total ink value push(@{$_}, List::Util::sum(@{$_})); } # make sort code fragment (sorts in ascending order by columns K ... total_ink_value) $code = '@{$data} = sort {' . join(' || ', map {"\$a->[$_] <=> \$b->[$_]"} (3 .. $n)) . '} @{$data}'; # sort data eval($code); # for each patch for my $i (0 .. $#{$data}) { # undefine limit if new group (different black or spot values) undef($limit) if (grep {$data->[$i][$_] != $data->[$i ? $i - 1 : 0][$_]} (3 .. $n - 1)); # select patch if limit undefined or total ink <= limit or a CMY corner point push(@inc, [@{$data->[$i]}[0 .. $n - 1]]) if (! defined($limit) || ($data->[$i][-1] - $limit <= $eps) || ((grep {$data->[$i][$_] == 0} (0 .. 2)) && (grep {$data->[$i][$_] == 100} (0 .. 2)))); # set limit if undefined and total ink > TAC $limit = $data->[$i][-1] if (! defined($limit) && $data->[$i][-1] - $tac > $eps); } # set data to selected patches $data = \@inc; } else { # display warning carp("invalid ink limit parameter, ink limiting failed\n"); } } # if sort parameter defined if (defined($sort)) { # verify sort parameter is an array of integer scalars if (ref($sort) eq 'ARRAY' && @{$sort} == grep {! ref() && Scalar::Util::looks_like_number($_) && $_ == int($_) && abs($_) > 0 && abs($_) <= $n} @{$sort}) { # make sort code fragment $code = '@{$data} = sort {' . join(' || ', map {my $dir = m/-/; my $col = abs($_) - 1; $dir ? "\$b->[$col] <=> \$a->[$col]" : "\$a->[$col] <=> \$b->[$col]"} @{$sort}) . '} @{$data}'; # evaluate code fragment eval($code); } else { # display warning carp("invalid sort parameter, sorting failed\n"); } } # add format fields unshift(@{$data}, [@fields]); # set object reference $self->[1] = $data; # return return(); } # make Time::Piece object from text string # parses most common date/time notations # no object returned if parsing fails # parameter: (string -or- value) # returns: (object) sub _makeTimePiece { # get parameter my $str = shift(); # local variables my ($parse, $fmt, $hr, $sec, $month); # if a numeric value (Unix time) if (Scalar::Util::looks_like_number($str)) { # return Time::Piece object from Unix time return(scalar(localtime($str))); } else { # if UTC offset matched (time string ends in '+/-hh:mm', '+/-hhmm', or '+/-hh') if ($str =~ s/(T[\d:]+)([+-]\d{2}):?(\d{2})?/$1/) { # set UTC offset to matched value $parse = $2 . (defined($3) ? $3 : '00'); # set UTC format $fmt = '%z'; # if Zulu time (time string ends in 'Z') } elsif ($str =~ s/(T[\d:]+)Z/$1/) { # set UTC offset to 0 $parse = '+0000'; # set UTC format $fmt = '%z'; } else { # initialize strings $parse = $fmt = ''; } # if time matched (time string 'hh:mm' or 'hh:mm:ss', 'AM' or 'PM' optional) if ($str =~ s/(\d{1,2})(:\d{1,2})(:\d{1,2})?\s*(AM|PM)?//) { # if 12 AM if (defined($4) && $4 eq 'AM' && $1 == 12) { # set hour $hr = 0; # if 1 PM - 11 PM } elsif (defined($4) && $4 eq 'PM' && $1 > 0 && $1 < 12) { # set hour $hr = $1 + 12; } else { # set hour $hr = $1; } # set seconds $sec = defined($3) ? $3 : ':00'; # add time string $parse = "T$hr$2$sec$parse"; # add time format $fmt = "T%T$fmt"; } # if three number date matched if ($str =~ m/(\d{1,4})[\/-](\d{1,2})[\/-](\d{1,4})/) { # if first value > 99 if ($1 > 99) { # add date string $parse = "$1-$2-$3$parse"; # if last value > 99 } elsif ($3 > 99) { # add date string $parse = "$3-$1-$2$parse"; # last value is two digit year } else { # add date string $parse = ($3 > 68 ? 1900 + $3 : 2000 + $3) . "-$1-$2$parse"; } # add date format $fmt = "%Y-%m-%d$fmt"; # if text month matched } elsif (uc($str) =~ m/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/) { # save month $month = $1; # if two numbers matched if ($str =~ m/(\d{1,4})[^\d]+(\d{1,4})/) { # if first value > 99 if ($1 > 99) { # add date string $parse = "$1-$month-$2$parse"; # if last value > 99 } elsif ($2 > 99) { # add date string $parse = "$2-$month-$1$parse"; # last value is two digit year } else { # add date string $parse = ($2 > 68 ? 1900 + $2 : 2000 + $2) . "/$month/$1$parse"; } # if one number matched } elsif ($str =~ m/(\d{1,4})/) { # if value > 99 if ($1 > 99) { # add date string $parse = "$1-$month-1$parse"; } else { # add date string $parse = ($1 > 68 ? 1900 + $1 : 2000 + $1) . "/$month/1$parse"; } } # add date format $fmt = "%Y-%b-%d$fmt"; } # return Time::Piece object, if string parsed successfully return(Time::Piece->strptime($parse, $fmt)) if (length($parse)); } } # get file list # uses Perl 'bsd_glob' function # parameter: (path) # returns: (ref_to_file_list) sub _files { # get path my $path = shift(); # get list of files and/or directories my @files = grep {-e} File::Glob::bsd_glob($path); # if list is just one directory if (@files == 1 && -d $files[0]) { # get files in that directory @files = grep {-f} File::Glob::bsd_glob("$path/*"); } else { # filter the files @files = grep {-f} @files; } # return file list return(\@files); } # compute Mahalanobis distance # assumes parameters are valid # parameters: (vector1, vector2, inverse_covariance_matrix) # returns: (distance) sub _mahal { # get parameters my ($x, $y, $sinv) = @_; # local variables my ($d, $dT); # for each dimension for my $i (0 .. $#{$x}) { # save difference $d->[0][$i] = $dT->[$i][0] = $x->[$i] - $y->[$i]; } # bless matrices bless($d, 'Math::Matrix'); bless($dT, 'Math::Matrix'); # return Mahalanobis distance return(sqrt(($d * $sinv * $dT)->[0][0])); } # get L*a*b* encoding code refs # parameter: (object_reference, hash) # returns: (get_code_ref, set_code_ref) sub _lab_encoding { # get object reference my ($self, $hash) = @_; # local variable my ($encode); # get encoding parameter from hash $encode = $hash->{'encoding'}; # if encoding parameter undefined if (! defined($encode)) { # return code refs (identity) return(sub {@_}, sub {@_}); # if encoding is 8/16-bit ICC CIELAB } elsif ($encode == 0) { # return code refs return(sub {defined($_[0]) ? $_[0] / 100 : $_[0], defined($_[1]) ? ($_[1] + 128)/255 : $_[1], defined($_[2]) ? ($_[2] + 128)/255 : $_[2]}, sub {defined($_[0]) ? $_[0] * 100 : $_[0], defined($_[1]) ? $_[1] * 255 - 128 : $_[1], defined($_[2]) ? $_[2] * 255 - 128 : $_[2]}); # if encoding is 16-bit ICC legacy L*a*b* } elsif ($encode == 1) { # return code refs return(sub {defined($_[0]) ? $_[0] * 256/25700 : $_[0], defined($_[1]) ? ($_[1] + 128) * 256/65535 : $_[1], defined($_[2]) ? ($_[2] + 128) * 256/65535 : $_[2]}, sub {defined($_[0]) ? $_[0] * 25700/256 : $_[0], defined($_[1]) ? $_[1] * 65535/256 - 128 : $_[1], defined($_[2]) ? $_[2] * 65535/256 - 128 : $_[2]}); # if encoding is 16-bit EFI/Monaco L*a*b* } elsif ($encode == 2) { # return code refs return(sub {defined($_[0]) ? $_[0]/100 : $_[0], defined($_[1]) ? ($_[1] + 128) * 256/65535 : $_[1], defined($_[2]) ? ($_[2] + 128) * 256/65535 : $_[2]}, sub {defined($_[0]) ? $_[0] * 100 : $_[0], defined($_[1]) ? $_[1] * 65535/256 - 128 : $_[1], defined($_[2]) ? $_[2] * 65535/256 - 128 : $_[2]}); # if encoding is L*a*b* } elsif ($encode == 3) { # return code refs (identity) return(sub {@_}, sub {@_}); # if encoding is LxLyLz } elsif ($encode == 4) { # return code refs return(sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {$_[0] + 116 * $_[1]/500, $_[0], $_[0] - 116 * $_[2]/200} else {@_}}, sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {$_[1], 500 * ($_[0] - $_[1])/116, 200 * ($_[1] - $_[2])/116} else {@_}}); # if encoding is unit LxLyLz } elsif ($encode == 5) { # return code refs return(sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {map {$_/100} ($_[0] + 116 * $_[1]/500, $_[0], $_[0] - 116 * $_[2]/200)} else {@_}}, sub {if (defined($_[0]) && defined($_[1]) && defined($_[2])) {map {$_ * 100} ($_[1], 500 * ($_[0] - $_[1])/116, 200 * ($_[1] - $_[2])/116)} else {@_}}); } else { # error croak('invalid L*a*b* encoding'); } } # get XYZ encoding code refs # assumes there are XYZ columns # parameter: (object_reference, column_slice, [hash]) # returns: (get_code_ref, set_code_ref) sub _xyz_encoding { # get object reference my ($self, $cols, $hash) = @_; # local variable my ($encode, $wtpt); # get encoding parameter from hash $encode = $hash->{'encoding'}; # if encoding parameter undefined if (! defined($encode)) { # return code refs (identity) return(sub {@_}, sub {@_}); # if encoding is L* } elsif ($encode eq 'L*' || $encode == 4) { # get illuminant white point ($wtpt = _illumWP($self, $cols, $hash)) || croak('illuminant white point required for LxLyLz encoding'); # return code refs return(sub {defined($_[0]) ? ICC::Shared::x2L($_[0] / $wtpt->[0]) : $_[0], defined($_[1]) ? ICC::Shared::x2L($_[1] / $wtpt->[1]) : $_[1], defined($_[2]) ? ICC::Shared::x2L($_[2] / $wtpt->[2]) : $_[2]}, sub {defined($_[0]) ? ICC::Shared::L2x($_[0]) * $wtpt->[0] : $_[0], defined($_[1]) ? ICC::Shared::L2x($_[1]) * $wtpt->[1] : $_[1], defined($_[2]) ? ICC::Shared::L2x($_[2]) * $wtpt->[2] : $_[2]}); # if encoding is 16-bit ICC XYZ } elsif ($encode == 7) { # return code refs return(sub {map {defined() ? $_ / 199.9969482421875 : $_} @_}, sub {map {defined() ? $_ * 199.9969482421875 : $_} @_}); # if encoding is 32-bit ICC XYZNumber } elsif ($encode == 8) { # return code refs return(sub {map {defined() ? $_ / 100 : $_} @_}, sub {map {defined() ? $_ * 100 : $_} @_}); # if encoding is xyz } elsif ($encode == 9) { # get illuminant white point ($wtpt = _illumWP($self, $cols, $hash)) || croak('illuminant white point required for xyz encoding'); # return code refs return(sub {defined($_[0]) ? $_[0] / $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] / $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] / $wtpt->[2] : $_[2]}, sub {defined($_[0]) ? $_[0] * $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] * $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] * $wtpt->[2] : $_[2]}); # if encoding is XYZ } elsif ($encode == 10) { # return code refs (identity) return(sub {@_}, sub {@_}); # if encoding is media relative xyz } elsif ($encode == 11) { # get media white point ($wtpt = _mediaWP($self, $cols, $hash)) || croak('media white point required for media relative xyz encoding'); # return code refs return(sub {defined($_[0]) ? $_[0] / $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] / $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] / $wtpt->[2] : $_[2]}, sub {defined($_[0]) ? $_[0] * $wtpt->[0] : $_[0], defined($_[1]) ? $_[1] * $wtpt->[1] : $_[1], defined($_[2]) ? $_[2] * $wtpt->[2] : $_[2]}); } else { # error croak('invalid XYZ encoding'); } } # get density encoding code refs # parameter: (object_reference, hash) # returns: (get_code_ref, set_code_ref) sub _density_encoding { # get object reference my ($self, $hash) = @_; # get encoding parameter from hash my $encode = $hash->{'encoding'}; # if encoding parameter undefined or density if (! defined($encode) || $encode eq 'density') { # return code refs (identity) return(sub {@_}, sub {@_}); # if encoding is linear (RGBV) } elsif ($encode eq 'linear') { # return code refs return(sub {map {defined() ? 100 * POSIX::pow(10, -$_) : $_} @_}, sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10($_/100)} else {warn("log of $_"); 99}} else {$_}} @_}); # if encoding is unit } elsif ($encode eq 'unit') { # return code refs return(sub {map {defined() ? POSIX::pow(10, -$_) : $_} @_}, sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10($_)} else {warn("log of $_"); 99}} else {$_}} @_}); # if encoding is L* } elsif ($encode eq 'L*') { # return code refs return(sub {map {defined() ? ICC::Shared::x2L(POSIX::pow(10, -$_)) : $_} @_}, sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10(ICC::Shared::L2x($_))} else {warn("log of $_"); 99}} else {$_}} @_}); } else { # error croak('invalid density encoding'); } } # get rgbv encoding code refs # parameter: (object_reference, hash) # returns: (get_code_ref, set_code_ref) sub _rgbv_encoding { # get object reference my ($self, $hash) = @_; # get encoding parameter from hash my $encode = $hash->{'encoding'}; # if encoding parameter undefined or linear if (! defined($encode) || $encode eq 'linear'|| $encode eq 'RGBV') { # return code refs (identity) return(sub {@_}, sub {@_}); # if encoding is unit } elsif ($encode eq 'unit') { # return code refs return(sub {map {$_/100} @_}, sub {map {$_ * 100} @_}); # if encoding is density } elsif ($encode eq 'density') { # return code refs return(sub {map {if (defined()) {if ($_ > 0) {-POSIX::log10($_/100)} else {warn("log of $_"); 99}} else {$_}} @_}, sub {map {defined() ? 100 * POSIX::pow(10, -$_) : $_} @_}); # if encoding is L* } elsif ($encode eq 'L*') { # return code refs return(sub {map {ICC::Shared::x2L($_/100)} @_}, sub {map {ICC::Shared::L2x($_) * 100} @_}); } else { # error croak('invalid rgbv encoding'); } } #--------- additional Math::Matrix methods --------- package Math::Matrix; # rotate matrix # rotation: 0 = None, 1 = 90° CW, 2 = 180°, 3 = 90° CCW # note: rotation describes appearance in MeasureTool # parameter: (rotation) # returns: (rotated_matrix) sub rotate { # get parameters my ($self, $rot) = @_; # local variables my ($rows, $cols, $replace); # return if rotation undefined return($self) if (! defined($rot)); # resolve rotation parameter $rot = int($rot) % 4; # get upper row index $rows = $#{$self}; # get upper column index $cols = $#{$self->[0]}; # if rotation = 0 (none) if ($rot == 0) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$i][$j] = $self->[$i][$j]; } } # if rotation = 1 (90° CW) } elsif ($rot == 1) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$j][$i] = $self->[$i][$cols - $j]; } } # if rotation = 2 (180°) } elsif ($rot == 2) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$i][$j] = $self->[$rows - $i][$cols - $j]; } } # if rotation = 3 (90° CCW) } elsif ($rot == 3) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$j][$i] = $self->[$rows - $i][$j]; } } } # return new object return(bless($replace, 'Math::Matrix')); } # flip matrix # flip: 0 = transpose, 1 = horizontal, 2 = cross transpose, 3 = vertical # note: flip describes appearance in MeasureTool # parameter: (flip) # returns: (flipped_matrix) sub flip { # get parameters my ($self, $flip) = @_; # local variables my ($rows, $cols, $replace); # return if flip undefined return($self) if (! defined($flip)); # resolve flip parameter $flip = int($flip) % 4; # get upper row index $rows = $#{$self}; # get upper column index $cols = $#{$self->[0]}; # if flip = 0 (transpose) if ($flip == 0) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$j][$i] = $self->[$i][$j]; } } # if flip = 1 (horizontal) } elsif ($flip == 1) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$i][$j] = $self->[$rows - $i][$j]; } } # if flip = 2 (cross transpose) } elsif ($flip == 2) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$j][$i] = $self->[$rows - $i][$cols - $j]; } } # if flip = 3 (vertical) } elsif ($flip == 3) { # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # copy matrix element $replace->[$i][$j] = $self->[$i][$cols - $j]; } } } # return new object return(bless($replace, 'Math::Matrix')); } # randomize matrix # returns: (randomized_matrix) sub randomize { # get object reference my $self = shift(); # local variables my (@ix, $rows, $cols, $replace); # flatten and randomize matrix @ix = List::Util::shuffle(@{ICC::Shared::flatten($self)}); # get upper row index $rows = $#{$self}; # get upper column index $cols = $#{$self->[0]}; # for each row for my $i (0 .. $rows) { # for each column for my $j (0 .. $cols) { # set element $replace->[$i][$j] = $ix[$i * ($cols + 1) + $j]; } } # return new object return(bless($replace, 'Math::Matrix')); } 1;