package ICC::Support::spline; use strict; use Carp; our $VERSION = 0.12; # revised 2018-09-04 # # Copyright © 2004-2020 by William B. Birkett # inherit from Shared use parent qw(ICC::Shared); # add complex math functions use Math::Complex qw(root sqrt cbrt Im Re); # enable static variables use feature 'state'; # create new spline object # hash keys are: 'input', 'output', 'type', 'damp', 'fix_hl', 'fix_sh' # default values for 'input' and 'output' are [0, 1] # parameters: ([ref_to_attribute_hash]) # returns: (ref_to_object) sub new { # get object class my $class = shift(); # create empty spline object my $self = [ {}, # object header [], # input values [], # output values [], # derivative values [], # min/max output values [], # parametric derivative matrix ]; # if one parameter, a hash reference if (@_ == 1 && ref($_[0]) eq 'HASH') { # set object contents from hash _new_from_hash($self, $_[0]); } elsif (@_) { # error croak('invalid parameter(s)'); } # return blessed object return(bless($self, $class)); } # create inverse object # swaps the input and output arrays # returns: (ref_to_object) sub inv { # clone new object my $inv = Storable::dclone(shift()); # get min/max values my @s = _minmax($inv); # verify object is monotonic (! @s) or croak("cannot invert a non-monotonic 'spline' object"); # if input is a range if (@{$inv->[1]} == 2 && @{$inv->[2]} > 2) { # interpolate input array $inv->[1] = [map {(1 - $_/$#{$inv->[2]}) * $inv->[1][0] + $_/$#{$inv->[2]} * $inv->[1][-1]} (0 .. $#{$inv->[2]})]; } # swap input and output arrays ($inv->[1], $inv->[2]) = ($inv->[2], $inv->[1]); # clear parameteric derivative matrix $inv->[5] = []; # sort input/output values _sort_values($inv); # compute knot derivatives _objderv($inv); # add segment min/max y-values _minmax($inv); # return inverted object return($inv); } # write spline object to ICC profile # note: writes an equivalent 'curv' object # parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry) sub write_fh { # verify 4 parameters (@_ == 4) or croak('wrong number of parameters'); # write spline data to profile goto &_writeICCspline; } # get tag size (for writing to profile) # note: writes an equivalent curv object # returns: (tag_size) sub size { # get parameters my ($self) = @_; # get count value (defaults to 4096, the maximum allowed in an 'mft2' tag) my $n = $self->[0]{'curv_points'} // 4096; # return size return(12 + $n * 2); } # compute curve function # parameters: (input_value) # returns: (output_value) sub transform { # get parameters my ($self, $in) = @_; # local variable my ($low); # if an extrapolated solution (s < 0) if ($in < $self->[1][0]) { # return extrapolated value return($self->[2][0] + $self->[3][0] * ($in - $self->[1][0])); # if an extrapolated solution (s >= 1) } elsif ($in >= $self->[1][-1]) { # return extrapolated value return($self->[2][-1] + $self->[3][-1] * ($in - $self->[1][-1])); # if input array contains x-values } elsif ($#{$self->[1]} > 1) { # initilize segment $low = 0; # while input value > upper knot value while ($in > $self->[1][$low + 1]) { # increment segment $low++; } # return interpolated value return(_fwd($self, ($self->[1][$low] - $in)/($self->[1][$low] - $self->[1][$low + 1]), $low)); # input array contains range } else { # return interpolated value return(_fwd($self, POSIX::modf($#{$self->[2]} * ($in - $self->[1][0])/($self->[1][1] - $self->[1][0])))); } } # compute inverse curve function # note: there may be multiple solutions # parameters: (input_value) # returns: (output_value -or- array_of_output_values) sub inverse { # get parameters my ($self, $in) = @_; # local variables my ($ix, @sol, @t); # get output array upper index $ix = $#{$self->[2]}; # if an extrapolated solution (s < 0) if (($in < $self->[2][0] && $self->[3][0] > 0) || ($in > $self->[2][0] && $self->[3][0] < 0)) { # add solution push(@sol, $self->[1][0] + ($in - $self->[2][0])/$self->[3][0]); } # for each segment (0 <= s < 1) for my $i (0 .. $ix - 1) { # if input is lower knot if ($in == $self->[2][$i]) { # add solution (x-values : range) push(@sol, ($#{$self->[1]} > 1) ? $self->[1][$i] : ($self->[1][0] * ($ix - $i) + $self->[1][1] * $i)/$ix); } # if input lies within the y-value range if ($in >= $self->[4][$i][0] && $in <= $self->[4][$i][1]) { # compute inverse values (t-values) @t = _rev($self, $in, $i); # if input array contains x-values if ($#{$self->[1]} > 1) { # add solution(s) push(@sol, map {(1 - $_) * $self->[1][$i] + $_ * $self->[1][$i + 1]} @t); # input array contains range } else { # add solution(s) push(@sol, map {($self->[1][0] * ($ix - $i - $_) + $self->[1][1] * ($i + $_))/$ix} @t); } } } # if input is last knot (s == 1) if ($in == $self->[2][-1]) { # add solution push(@sol, $self->[1][-1]); } # if an extrapolated solution (s > 1) if (($in > $self->[2][-1] && $self->[3][-1] > 0) || ($in < $self->[2][-1] && $self->[3][-1] < 0)) { # add solution push(@sol, $self->[1][-1] + ($in - $self->[2][-1])/$self->[3][-1]); } # return result (array or first solution) return(wantarray ? @sol : $sol[0]); } # compute curve derivative # parameters: (input_value) # returns: (derivative_value) sub derivative { # get parameters my ($self, $in) = @_; # local variable my ($low); # if an extrapolated solution (s < 0) if ($in < $self->[1][0]) { # return endpoint derivative return($self->[3][0]); # if an extrapolated solution (s >= 1) } elsif ($in >= $self->[1][-1]) { # return endpoint derivative return($self->[3][-1]); # if input array contains x-values } elsif ($#{$self->[1]} > 1) { # initilize segment $low = 0; # while input value > upper knot value while ($in > $self->[1][$low + 1]) { # increment segment $low++; } # return interpolated derivative value return(_derv($self, ($self->[1][$low] - $in)/($self->[1][$low] - $self->[1][$low + 1]), $low)); # input array contains range } else { # return interpolated derivative value return(_derv($self, POSIX::modf($#{$self->[2]} * ($in - $self->[1][0])/($self->[1][1] - $self->[1][0])))); } } # compute parametric partial derivatives # parameters: (input_value) # returns: (partial_derivative_array) sub parametric { # get parameters my ($self, $in) = @_; # local variables my ($dx, $low, $t, $tc, $h00, $h01, $h10, $h11, @pj); # update parametric derivative matrix, if necessary _objpara($self) if (! defined($self->[5]) || $#{$self->[5]} != $#{$self->[2]}); # if an extrapolated solution (s < 0) if ($in < $self->[1][0]) { # for each output value (parameter) for my $i (0 .. $#{$self->[2]}) { # compute partial derivative $pj[$i] = ($i == 0) + $self->[5][0][$i] * ($in - $self->[1][0]); } # if an extrapolated solution (s >= 1) } elsif ($in >= $self->[1][-1]) { # for each output value (parameter) for my $i (0 .. $#{$self->[2]}) { # compute partial derivative $pj[$i] = ($i == $#{$self->[2]}) + $self->[5][-1][$i] * ($in - $self->[1][-1]); } } else { # if input array contains x-values if ($#{$self->[1]} > 1) { # initialize segment $low = 0; # while input value > upper knot value while ($in > $self->[1][$low + 1]) { # increment segment $low++; } # compute delta x-value $dx = $self->[1][$low + 1] - $self->[1][$low]; # compute t-value $t = ($in - $self->[1][$low])/$dx; # input array contains range } else { # compute delta x-value $dx = ($self->[1][1] - $self->[1][0])/$#{$self->[2]}; # compute t-value, index ($t, $low) = POSIX::modf(($in - $self->[1][0])/$dx); } # compute Hermite coefficients $tc = 1 - $t; $h00 = (1 + 2 * $t) * $tc * $tc; $h01 = 1 - $h00; $h10 = $t * $tc * $tc; $h11 = -$t * $t * $tc; # for each output value (parameter) for my $i (0 .. $#{$self->[2]}) { # compute partial derivative $pj[$i] = $h00 * ($low == $i) + $h01 * ($low + 1 == $i) + $h10 * $dx * $self->[5][$low][$i] + $h11 * $dx * $self->[5][$low + 1][$i]; } } # return return(@pj); } # get/set header hash reference # 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] = Storable::dclone(shift()); } else { # error croak('parameter must be a hash reference'); } } # return reference return($self->[0]); } # get/set input array reference # an array of two values is a range # otherwise, it contains input values # parameters: ([ref_to_array]) # returns: (ref_to_array) sub input { # get object reference my $self = shift(); # if one parameter supplied if (@_ == 1) { # verify 'input' vector (two or more numeric values) (ICC::Shared::is_num_vector($_[0]) && 2 <= @{$_[0]}) or croak('invalid input values'); # verify 'input' vector size (@{$_[0]} == 2 || @{$_[0]} == @{$self->[2]}) or carp('number of input values differs from object output values'); # store output values $self->[1] = [@{$_[0]}]; # update object internal elements update($self, 1); } elsif (@_) { # error carp('too many parameters'); } # return array reference return($self->[1]); } # get/set output array reference # parameters: ([ref_to_array]) # returns: (ref_to_array) sub output { # get object reference my $self = shift(); # if one parameter supplied if (@_ == 1) { # verify 'output' vector (two or more numeric values) (ICC::Shared::is_num_vector($_[0]) && 2 <= @{$_[0]}) or croak('invalid output values'); # verify 'output' vector size (@{$self->[1]} == 2 || @{$_[0]} == @{$self->[1]}) or carp('number of output values differs from object input values'); # store output values $self->[2] = [@{$_[0]}]; # update object internal elements update($self); } elsif (@_) { # error carp('too many parameters'); } # return array reference return($self->[2]); } # normalize transform # adjusts Bernstein coefficients linearly # adjusts 'input' and 'output' by default # hash keys: 'input', 'output' # parameter: ([hash_ref]) # returns: (ref_to_object) sub normalize { # get parameters my ($self, $hash) = @_; # local variables my ($m, $b, $val, $src, $x0, $x1, $y0, $y1, $f0, $f1, @minmax, $p); # set hash key array my @key = qw(input output); # for ('input', 'output') for my $i (0, 1) { # undefine slope $m = undef; # if no parameter (default) if (! defined($hash)) { # if array has 2 or more coefficients if (@{$self->[$i + 1]} > 1 && ($x0 = $self->[$i + 1][0]) != ($x1 = $self->[$i + 1][-1])) { # special case, if 'output' and 'fix_hl' was called $x0 = 0 if ($i && defined($self->[0]{'hl_retention'})); # compute adjustment values $m = abs(1/($x0 - $x1)); $b = -$m * ($x0 < $x1 ? $x0 : $x1); # set endpoints ($y0, $y1) = $x0 < $x1 ? (0, 1) : (1, 0); } } else { # if hash contains key if (defined($val = $hash->{$key[$i]})) { # if array has 2 or more coefficients if (@{$self->[$i + 1]} > 1 && $self->[$i + 1][0] != $self->[$i + 1][-1]) { # if hash value is an array reference if (ref($val) eq 'ARRAY') { # if two parameters if (@{$val} == 2) { # get parameters ($y0, $y1) = @{$val}; # get endpoints $x0 = $self->[$i + 1][0]; $x1 = $self->[$i + 1][-1]; # if three parameters } elsif (@{$val} == 3) { # get parameters ($src, $y0, $y1) = @{$val}; # if source is 'endpoints' if (! ref($src) && $src eq 'endpoints') { # get endpoints $x0 = $self->[$i + 1][0]; $x1 = $self->[$i + 1][-1]; # if source is 'minmax' } elsif (! ref($src) && $src eq 'minmax') { # if 'output' curve if ($i) { # get all min/max values @minmax = (map {@{$_}} @{$self->[4]}); # get min/max values $x0 = List::Util::min(@minmax); $x1 = List::Util::max(@minmax); # if 'input' curve } else { # get min/max values (endpoints) $x0 = $self->[1][0]; $x1 = $self->[1][-1]; } } # if four parameters } elsif (@{$val} == 4) { # get parameters ($x0, $x1, $y0, $y1) = @{$val}; } # if x/y values are numeric if ((4 == grep {Scalar::Util::looks_like_number($_)} ($x0, $x1, $y0, $y1)) && ($x0 != $x1)) { # compute slope $m = ($y0 - $y1)/($x0 - $x1); # compute offset $b = $y0 - $m * $x0; } else { # warning carp("invalid '$key[$i]' parameter(s)"); } } } else { # warning carp("can't normalize '$key[$i]' coefficients"); } } } # if slope is defined if (defined($m)) { # set endpoint flags $f0 = $self->[$i + 1][0] == $x0; $f1 = $self->[$i + 1][-1] == $x1; # adjust Bernstein coefficients (y = mx + b) @{$self->[$i + 1]} = map {$m * $_ + $b} @{$self->[$i + 1]}; # set endpoints (to ensure exact values) $self->[$i + 1][0] = $y0 if ($f0); $self->[$i + 1][-1] = $y1 if ($f1); # save input polarity $p = $m < 0 if ($i == 0); # if 'input' and 'hl_retention' is defined if (! $i && defined($val = $self->[0]{'hl_retention'})) { # adjust value $self->[0]{'hl_retention'} = $m * $val + $b; } # if 'output' and 'hl_original' is defined if ($i && defined($val = $self->[0]{'hl_original'})) { # adjust values $self->[0]{'hl_original'} = [map {$m * $_ + $b} @{$val}]; } # if 'output' and 'sh_original' is defined if ($i && defined($val = $self->[0]{'sh_original'})) { # adjust values $self->[0]{'sh_original'} = [map {$m * $_ + $b} @{$val}]; } } } # update object internals, sorting values if 'input' polarity changed update($self, $p); # return return($self); } # check if monotonic # returns min/max values # returns s-values by default # returns input-values if format is 'input' # returns output-values if format is 'output' # curve is monotonic if no min/max values # parameters: ([format]) # returns: (array_of_values) sub monotonic { # get object reference my ($self, $fmt) = @_; # local variables my ($t, $low); # if format undefined if (! defined($fmt)) { # return s-values return(_minmax($self)); # if format 'input' } elsif ($fmt eq 'input') { # if input array contains x-values if ($#{$self->[1]} > 1) { # return input values return(map {($t, $low) = POSIX::modf($_); (1 - $t) * $self->[1][$low] + $t * $self->[1][$low + 1]} _minmax($self)); # input array contains range } else { # return input values return(map {$t = $_/$#{$self->[2]}; (1 - $t) * $self->[1][0] + $t * $self->[1][1]} _minmax($self)); } # if format 'output' } elsif ($fmt eq 'output') { # return output values return(map {_fwd($self, POSIX::modf($_))} _minmax($self)); } else { # error croak('unsupported format for min/max values'); } } # update internal object elements # this method used primarily when optimizing # call after changing 'input' or 'output' values # if the 'input' values were changed, set the flag # parameter: ([flag]) sub update { # get parameters my ($self, $flag) = @_; # sort values if flag set _sort_values($self) if ($flag); # recompute knot derivatives _objderv($self); # recompute segment min/max y-values _minmax($self); # recompute parametric derivative matrix if flag set, or 'akima' type spline _objpara($self) if ($flag || (defined($self->[0]{'type'} && $self->[0]{'type'} eq 'akima'))); } # make table for 'curv' objects # assumes curve domain/range is (0 - 1) # parameters: (number_of_table_entries, [direction]) # returns: (ref_to_table_array) sub table { # get parameters my ($self, $n, $dir) = @_; # local variables my ($up, $table); # validate number of table entries ($n == int($n) && $n >= 2) or carp('invalid number of table entries'); # purify direction flag $dir = ($dir) ? 1 : 0; # array upper index $up = $n - 1; # for each table entry for my $i (0 .. $up) { # compute table value $table->[$i] = _transform($self, $dir, $i/$up); } # return table reference return($table); } # make equivalent 'curv' object # assumes curve domain/range is (0 - 1) # parameters: (number_of_table_entries, [direction]) # returns: (ref_to_curv_object) sub curv { # return 'curv' object reference return(ICC::Profile::curv->new(table(@_))); } # 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, $fmt2); # 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); # if input_parameter_array defined if (defined($self->[1])) { # set format $fmt2 = join(', ', ('%.3f') x @{$self->[1]}); # append parameter string $s .= sprintf(" input parameters [$fmt2]\n", @{$self->[1]}); } # if output_parameter_array defined if (defined($self->[2])) { # set format $fmt2 = join(', ', ('%.3f') x @{$self->[2]}); # append parameter string $s .= sprintf(" output parameters [$fmt2]\n", @{$self->[2]}); } # return return($s); } # combined transform # direction: 0 - normal, 1 - inverse # parameters: (ref_to_object, direction, input_value) # returns: (output_value) sub _transform { # get parameters my ($self, $dir, $in) = @_; # if inverse direction if ($dir) { # return inverse return(scalar(inverse($self, $in))); } else { # return transform return(transform($self, $in)); } } # combined derivative # direction: 0 - normal, 1 - inverse # parameters: (ref_to_object, direction, input_value) # returns: (derivative_value) sub _derivative { # get parameters my ($self, $dir, $in) = @_; # if inverse direction if ($dir) { # unimplemented function error croak('unimplemented function'); } else { # return derivative return(derivative($self, $in)); } } # compute knot derivatives # parameter: (ref_to_object) sub _objderv { # get parameter my $self = shift(); # verify input values are unique _unique($self->[1]) or croak('input values not unique'); # if spline type undefined or 'natural' if (! defined($self->[0]{'type'}) || $self->[0]{'type'} eq 'natural') { # compute knot derivatives for natural spline _natural_derv($self) # if spline type is 'akima' } elsif ($self->[0]{'type'} eq 'akima') { # compute knot derivatives for Akima spline _akima_derv($self); } else { # error croak('undefined spline type'); } } # compute knot derivatives for natural spline # parameter: (ref_to_object) sub _natural_derv { # get parameter my $self = shift(); # local variables my ($ix, $rhs, $dx, $info, $derv); # verify object has two or more knots (($ix = $#{$self->[2]}) > 0) or croak('object must have two or more knots'); # check if ICC::Support::Lapack module is loaded state $lapack = defined($INC{'ICC/Support/Lapack.pm'}); # make empty Math::Matrix object $rhs = bless([], 'Math::Matrix'); # if input array contains input values if ($#{$self->[1]} > 1) { # for each inner element for my $i (1 .. $ix - 1) { # compute rhs (6 * (y[i + 1] - y[i - 1])/(x[i + 1] - x[i - 1])) $rhs->[$i][0] = 6 * ($self->[2][$i + 1] - $self->[2][$i - 1])/($self->[1][$i + 1] - $self->[1][$i - 1]); } # set rhs endpoint values $rhs->[0][0] = 3 * ($self->[2][1] - $self->[2][0])/($self->[1][1] - $self->[1][0]); $rhs->[$ix][0] = 3 * ($self->[2][$ix] - $self->[2][$ix - 1])/($self->[1][$ix] - $self->[1][$ix - 1]); # input array contains range } else { # compute x-segment length $dx = ($self->[1][1] - $self->[1][0])/$ix; # for each inner element for my $i (1 .. $ix - 1) { # compute rhs (6 * (y[i + 1] - y[i - 1]))/(x[i + 1] - x[i - 1]) $rhs->[$i][0] = 3 * ($self->[2][$i + 1] - $self->[2][$i - 1])/$dx; } # set rhs endpoint values $rhs->[0][0] = 3 * ($self->[2][1] - $self->[2][0])/$dx; $rhs->[$ix][0] = 3 * ($self->[2][$ix] - $self->[2][$ix - 1])/$dx; } # if ICC::Support::Lapack module is loaded if ($lapack) { # solve for derivative matrix ($info, $derv) = ICC::Support::Lapack::trisolve([(1) x $ix], [2, (4) x ($ix - 1), 2], [(1) x $ix], $rhs); # otherwise, use Math::Matrix module } else { # solve for derivative matrix $derv = Math::Matrix->tridiagonal([2, (4) x ($ix - 1), 2])->concat($rhs)->solve(); } # for each knot for my $i (0 .. $ix) { # set derivative value $self->[3][$i] = $derv->[$i][0]; } } # compute knot derivatives for Akima spline # parameter: (ref_to_object) sub _akima_derv { # get parameter my $self = shift(); # local variables my ($ix, @m, $s, $b, $d1, $d2); # verify object has two or more knots (($ix = $#{$self->[2]}) > 0) or croak('object must have two or more knots'); # compute uniform segment length (for 'input' range) $s = ($self->[1][1] - $self->[1][0])/$ix if ($#{$self->[1]} == 1); # for each segment for my $i (0 .. ($ix - 1)) { # compute segment slope (range // x-values) $m[$i + 2] = ($self->[2][$i + 1] - $self->[2][$i])/($s // ($self->[1][$i + 1] - $self->[1][$i])); } # if 2 points if ($ix == 1) { # linear slopes $m[0] = $m[1] = $m[3] = $m[4] = $m[2]; # if 3 or more points } else { # quadratic slopes $m[1] = 2 * $m[2] - $m[3]; $m[0] = 2 * $m[1] - $m[2]; $m[$ix + 2] = 2 * $m[$ix + 1] - $m[$ix]; $m[$ix + 3] = 2 * $m[$ix + 2] - $m[$ix + 1]; } # if damping value is undefined if (! defined($self->[0]{'damp'})) { # set default damping value $self->[0]{'damp'} = List::Util::sum(map {$_ * $_} @m)/(@m * 50); } # Akima used abs() functions to calulate the slopes # this can lead to abrupt changes in curve shape and parametric derivatives # we replace abs(x) with sqrt(x^2 + b) to add stability # get damping value and verify >= 0 (($b = $self->[0]{'damp'}) >= 0) or carp('akima damping value is negative'); # compute Akima derivative for my $i (0 .. $ix) { # compute slope differences (with damping) $d1 = sqrt(($m[$i + 1] - $m[$i])**2 + $b); $d2 = sqrt(($m[$i + 3] - $m[$i + 2])**2 + $b); # if denominator not 0 if ($d1 + $d2) { # use Akima slope (weighted average with damping) $self->[3][$i] = ($d2 * $m[$i + 1] + $d1 * $m[$i + 2])/($d1 + $d2); } else { # otherwise, use average slope of adjoining segments $self->[3][$i] = ($m[$i + 1] + $m[$i + 2])/2; } } } # compute partial derivative matrix # parameter: (ref_to_object) sub _objpara { # get parameter my $self = shift(); # verify input values are unique _unique($self->[1]) or croak('input values not unique'); # if spline type undefined or 'natural' if (! defined($self->[0]{'type'}) || $self->[0]{'type'} eq 'natural') { # compute knot derivatives for natural spline _natural_para($self) # if spline type is 'akima' } elsif ($self->[0]{'type'} eq 'akima') { # compute knot derivatives for Akima spline _akima_para($self); } else { # error croak('undefined spline type'); } } # compute partial derivative matrix for natural spline # parameter: (ref_to_object) sub _natural_para { # get parameter my $self = shift(); # local variables my ($ix, $n, $rhs, $x, $info, $derv); # verify object has two or more knots (($ix = $#{$self->[2]}) > 0) or croak('object must have two or more knots'); # check if ICC::Support::Lapack module is loaded state $lapack = defined($INC{'ICC/Support/Lapack.pm'}); # make rhs matrix (filled with zeros) $rhs = $lapack ? ICC::Support::Lapack::zeros($ix + 1) : bless([map {[(0) x ($ix + 1)]} (0 .. $ix)], 'Math::Matrix'); # if input array contains input values if ($#{$self->[1]} > 1) { # for each inner element for my $i (1 .. $ix - 1) { # set rhs diagonal values $rhs->[$i][$i - 1] = $x = 6/($self->[1][$i - 1] - $self->[1][$i + 1]); $rhs->[$i][$i + 1] = -$x; } # set rhs endpoint values $rhs->[0][0] = $x = 3/($self->[1][0] - $self->[1][1]); $rhs->[0][1] = -$x; $rhs->[$ix][$ix] = $x = 3/($self->[1][$ix] - $self->[1][$ix - 1]); $rhs->[$ix][$ix -1] = -$x; # input array contains range } else { # compute rhs value $x = 3 * $ix/($self->[1][1] - $self->[1][0]); # for each inner element for my $i (1 .. $ix - 1) { # set rhs diagonal values $rhs->[$i - 1][$i] = $x; $rhs->[$i + 1][$i] = -$x; } # set rhs endpoint values $rhs->[0][0] = -$x; $rhs->[1][0] = -$x; $rhs->[$ix][$ix] = $x; $rhs->[$ix - 1][$ix] = $x; } # if ICC::Support::Lapack module is loaded if ($lapack) { # solve for derivative matrix ($info, $derv) = ICC::Support::Lapack::trisolve([(1) x $ix], [2, (4) x ($ix - 1), 2], [(1) x $ix], $rhs); # set object $self->[5] = bless($derv, 'Math::Matrix'); # otherwise, use Math::Matrix module } else { # solve for derivative matrix $self->[5] = Math::Matrix->tridiagonal([2, (4) x ($ix - 1), 2])->concat($rhs)->solve(); } } # compute partial derivative matrix for Akima spline # parameter: (ref_to_object) sub _akima_para { # get object my $self = shift(); # local variables my ($ix, $s, @m, @x, $b); my ($d1, $d2, $dif, @dm, $dd1, $dd2, $derv); # see 'akima_parametric.plx' for details on this function # verify object has two or more knots (($ix = $#{$self->[2]}) > 0) or croak('object must have two or more knots'); # compute uniform segment length (for 'input' range) $s = ($self->[1][1] - $self->[1][0])/$ix if ($#{$self->[1]} == 1); # for each segment for my $i (0 .. ($ix - 1)) { # compute segment x-length $x[$i + 2] = $s // ($self->[1][$i + 1] - $self->[1][$i]); # compute segment slope (range // x-values) $m[$i + 2] = ($self->[2][$i + 1] - $self->[2][$i])/$x[$i + 2]; } # if 2 points if ($ix == 1) { # linear slopes $m[0] = $m[1] = $m[3] = $m[4] = $m[2]; # if 3 or more points } else { # quadratic slopes $m[1] = 2 * $m[2] - $m[3]; $m[0] = 2 * $m[1] - $m[2]; $m[$ix + 2] = 2 * $m[$ix + 1] - $m[$ix]; $m[$ix + 3] = 2 * $m[$ix + 2] - $m[$ix + 1]; } # get the damping value $b = $self->[0]{'damp'}; # for each node for my $i (0 .. $ix) { # add row of zeros $derv->[$i] = [(0) x ($ix + 1)]; } # if 2 knots if ($ix == 1) { # set partial derivatives $derv = [[-1/$x[2], 1/$x[2]], [-1/$x[2], 1/$x[2]]]; } else { # for each row for my $i (0 .. $ix) { # compute slope differences (with damping) $d1 = sqrt(($m[$i + 1] - $m[$i])**2 + $b); $d2 = sqrt(($m[$i + 3] - $m[$i + 2])**2 + $b); # for each column for my $j (0 .. $ix) { # compute difference $dif = $j - $i; # skip outer corner regions next if (abs($dif) > 2); # the @dm array contains (∂m0/∂n, ∂m1/∂n, ∂m2/∂n, ∂m3/∂n) # where m0 - m3 are the chord slopes, and n is the node value # if difference -2 if ($dif == -2) { # if row 1 if ($i == $ix) { # set partial derivatives @dm = (-1/$x[$i], 0, 1/$x[$i], 2/$x[$i]); } else { # set partial derivatives @dm = (-1/$x[$i], 0, 0, 0); } # if difference -1 } elsif ($dif == -1) { # if row 1 if ($i == 1) { # if more than 3 knots if ($ix > 2) { # set partial derivatives @dm = (-2/$x[$i + 1], -1/$x[$i + 1], 0, 0); } else { # set partial derivatives @dm = (-2/$x[$i + 1], -1/$x[$i + 1], 0, 1/$x[$i + 1]); } # if next to last row } elsif ($i == $ix - 1) { # set partial derivatives @dm = (1/$x[$i], -1/$x[$i + 1], 0, 1/$x[$i + 1]); # if last row } elsif ($i == $ix) { # set partial derivatives @dm = (1/$x[$i], -1/$x[$i + 1], -2/$x[$i + 1] - 1/$x[$i], -3/$x[$i + 1] - 2/$x[$i]); } else { # set partial derivatives @dm = (1/$x[$i], -1/$x[$i + 1], 0, 0); } # if difference 0 } elsif ($dif == 0) { # if row 0 if ($i == 0) { # set partial derivatives @dm = (-3/$x[$i + 2], -2/$x[$i + 2], -1/$x[$i + 2], 0); # if row 1 } elsif ($i == 1) { # if more than three knots if ($ix > 2) { # set partial derivatives @dm = (2/$x[$i + 1] + 1/$x[$i + 2], 1/$x[$i + 1], -1/$x[$i + 2], 0); } else { # set partial derivatives @dm = (2/$x[$i + 1] + 1/$x[$i + 2], 1/$x[$i + 1], -1/$x[$i + 2], -2/$x[$i + 2] - 1/$x[$i + 1]); } # if next to last row } elsif ($i == $ix - 1) { # set partial derivatives @dm = (0, 1/$x[$i + 1], -1/$x[$i + 2], -2/$x[$i + 2] - 1/$x[$i + 1]); # if last row } elsif ($i == $ix) { # set partial derivatives @dm = (0, 1/$x[$i + 1], 2/$x[$i + 1], 3/$x[$i + 1]); } else { # set partial derivatives @dm = (0, 1/$x[$i + 1], -1/$x[$i + 2], 0); } # if difference 1 } elsif ($dif == 1) { # if row 0 if ($i == 0) { # set partial derivatives @dm = (3/$x[$i + 2] + 2/$x[$i + 3], 2/$x[$i + 2] + 1/$x[$i + 3], 1/$x[$i + 2], -1/$x[$i + 3]); # if row 1 } elsif ($i == 1) { # if more than 3 knots if ($ix > 2) { # set partial derivatives @dm = (-1/$x[$i + 2], 0, 1/$x[$i + 2], -1/$x[$i + 3]); } else { # set partial derivatives @dm = (-1/$x[$i + 2], 0, 1/$x[$i + 2], 2/$x[$i + 2]); } # if next to last row } elsif ($i == $ix - 1) { # set partial derivatives @dm = (0, 0, 1/$x[$i + 2], 2/$x[$i + 2]); } else { # set partial derivatives @dm = (0, 0, 1/$x[$i + 2], -1/$x[$i + 3]); } # if difference 2 } elsif ($dif == 2) { # if row 0 if ($i == 0) { # set partial derivatives @dm = (-2/$x[$i + 3], -1/$x[$i + 3], 0, 1/$x[$i + 3]); } else { # set partial derivatives @dm = (0, 0, 0, 1/$x[$i + 3]); } } # compute ∂d1/∂n $dd1 = akima_dpd($m[$i], $m[$i + 1], $dm[0], $dm[1], $d1); # compute ∂d2/∂n $dd2 = akima_dpd($m[$i + 2], $m[$i + 3], $dm[2], $dm[3], $d2); # compute ∂s/∂n $derv->[$i][$j] = akima_spd($d1, $d2, $m[$i + 1], $m[$i + 2], $dd1, $dd2, $dm[1], $dm[2]); } } } # set object $self->[5] = bless($derv, 'Math::Matrix'); } # compute partial derivative function for _akima_para # parameters: (d1, d2, m1, m2, ∂d1/∂n, ∂d2/∂n, ∂m1/∂n, ∂m2/∂n) # returns; (∂s/∂n) sub akima_spd { =pod https://www.wolframalpha.com/input/?i=d%2Fdx+(a(x)+*+c(x)+%2B+b(x)+*+d(x))%2F(a(x)+%2B+b(x)) d/dx((a(x) c(x) + b(x) d(x))/(a(x) + b(x))) = (c(x) a'(x) + a(x) c'(x) + d(x) b'(x) + b(x) d'(x))/(a(x) + b(x)) - ((a'(x) + b'(x)) (a(x) c(x) + b(x) d(x)))/(a(x) + b(x))^2 =cut # return partial derivative return(($_[2] * $_[5] + $_[1] * $_[6] + $_[3] * $_[4] + $_[0] * $_[7])/($_[1] + $_[0]) - (($_[5] + $_[4]) * ($_[1] * $_[2] + $_[0] * $_[3]))/($_[1] + $_[0])**2); } # compute partial derivative function for _akima_para # parameters: (m1, m2, ∂m1/∂n, ∂m2/∂n, d) # returns; (∂d/∂n) sub akima_dpd { =pod https://www.wolframalpha.com/input/?i=d%2Fdx+(sqrt((a(x)+-+b(x))%5E2+%2B+c)) d/dx(sqrt((a(x) - b(x))^2 + c)) = ((a(x) - b(x)) (a'(x) - b'(x)))/sqrt((a(x) - b(x))^2 + c) =cut # return partial derivative return(($_[0] - $_[1]) * ($_[2] - $_[3])/$_[4]); } # add local min/max values # parameter: (ref_to_object) # returns: (s-value_array) sub _minmax { # get object reference my $self = shift(); # local variables my (@t, @y, @s); # for each interval for my $i (0 .. $#{$self->[2]} - 1) { # get min/max location(s) @t = _local($self, $i); # add min/max s-values push(@s, map {$_ + $i} @t); # add inner knot s-value, if derivative is 0 push(@s, $i) if ($i && $self->[3][$i] == 0); # compute local min/max y-values @y = map {_fwd($self, $_, $i)} @t; # add the knot values push(@y, ($self->[2][$i], $self->[2][$i + 1])); # sort the values @y = sort {$a <=> $b} @y; # save y-value min/max span $self->[4][$i] = [$y[0], $y[-1]]; } # return min/max s-values return(sort {$a <=> $b} @s); } # compute local min/max value(s) # values are within the segment (t) # parameters: (ref_to_object, segment) # returns: (t-value_array) sub _local { # get parameters my ($self, $low) = @_; # local variables my ($dx, $y1, $y2, $m1, $m2); my ($a, $b, $c, $dscr, @t); # compute delta x-value (x-values : range) $dx = ($#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]}; # get endpoint values $y1 = $self->[2][$low]; $y2 = $self->[2][$low + 1]; $m1 = $self->[3][$low] * $dx; $m2 = $self->[3][$low + 1] * $dx; # compute coefficients of quadratic equation (at^2 + bt + c = 0) $a = 6 * ($y1 - $y2) + 3 * ($m1 + $m2); $b = -6 * ($y1 - $y2) - 2 * $m2 - 4 * $m1; $c = $m1; # return if constant return() if (abs($a) < 1E-15 && abs($b) < 1E-15); # if linear equation (a is zero) if (abs($a) < 1E-15) { # add solution push(@t, -$c/$b); # if quadratic equation } else { # compute discriminant $dscr = $b**2 - 4 * $a * $c; # if discriminant > zero if ($dscr > 0) { # add solutions (critical points) push(@t, -($b + sqrt($dscr))/(2 * $a)); push(@t, -($b - sqrt($dscr))/(2 * $a)); } } # return solution(s) within interval (0 < t < 0) return (grep {$_ > 0 && $_ < 1} @t); } # compute second derivative for unit spline segment # parameters: (ref_to_object, t-value, segment) # returns: (second_derivative) sub _derv2 { # get parameters my ($self, $t, $low) = @_; # local variables my ($dx, $h00, $h01, $h10, $h11); # compute delta x-value (x-values : range) $dx = ($#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]}; # compute Hermite derivative coefficients $h00 = 12 * $t - 6; $h01 = - $h00; $h10 = 6 * $t - 4; $h11 = 6 * $t - 2; # interpolate value return(($h00 * $self->[2][$low]/$dx + $h01 * $self->[2][$low + 1]/$dx + $h10 * $self->[3][$low] + $h11 * $self->[3][$low + 1])/$dx); } # compute derivative for unit spline segment # parameters: (ref_to_object, t-value, segment) # returns: (derivative) sub _derv { # get parameters my ($self, $t, $low) = @_; # local variables my ($dx, $tc, $ttc, $h00, $h01, $h10, $h11); # compute delta x-value (x-values : range) $dx = ($#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]}; # if position is non-zero if ($t) { # compute Hermite derivative coefficients $tc = (1 - $t); $ttc = -3 * $t * $tc; $h00 = 2 * $ttc; $h01 = -2 * $ttc; $h10 = $ttc + $tc; $h11 = $ttc + $t; # interpolate value return($h00 * $self->[2][$low]/$dx + $h01 * $self->[2][$low + 1]/$dx + $h10 * $self->[3][$low] + $h11 * $self->[3][$low + 1]); } else { # use lower knot derivative value return($self->[3][$low]); } } # compute transform value # parameters: (ref_to_object, t-value, segment) # returns: (y-value) sub _fwd { # get parameters my ($self, $t, $low) = @_; # local variables my ($dx, $tc, $h00, $h01, $h10, $h11); # check if ICC::Support::Lapack module is loaded state $lapack = defined($INC{'ICC/Support/Lapack.pm'}); # if position is non-zero if ($t) { # compute delta x-value (x-values : range) $dx = ($#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]}; # interpolate value (Lapack XS) # if ICC::Support::Lapack module is loaded if ($lapack) { return(ICC::Support::Lapack::hermite($t, $self->[2][$low], $self->[2][$low + 1], $dx * $self->[3][$low], $dx * $self->[3][$low + 1])); } else { # compute Hermite coefficients $tc = 1 - $t; $h00 = (1 + 2 * $t) * $tc * $tc; $h01 = 1 - $h00; $h10 = $t * $tc * $tc; $h11 = -$t * $t * $tc; # interpolate value (no Lapack XS) return($h00 * $self->[2][$low] + $h01 * $self->[2][$low + 1] + $h10 * $dx * $self->[3][$low] + $h11 * $dx * $self->[3][$low + 1]); } } else { # use lower knot output value return($self->[2][$low]); } } # compute inverse value # parameters: (ref_to_object, y-value, segment) # there are 0 to 3 solutions in the range 0 < t < 1 # returns: (t-value_array) sub _rev { # get parameters my ($self, $in, $low) = @_; # local variables my ($dx, $y1, $y2, $m1, $m2); my ($A, $B, $C, $D, $dscr, @t); my ($d0, $d1, $cs, $cc, @r, $ccr, $sol, $lim0, $lim1); # compute delta x-value (x-values : range) $dx = ($#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]}; # get endpoint values $y1 = $self->[2][$low]; $y2 = $self->[2][$low + 1]; $m1 = $self->[3][$low] * $dx; $m2 = $self->[3][$low + 1] * $dx; # compute coefficients of cubic equation (At^3 + Bt^2 + Ct + D = 0) $A = 2 * ($y1 - $y2) + $m1 + $m2; $B = -3 * ($y1 - $y2) -2 * $m1 - $m2; $C = $m1; $D = $y1 - $in; # constant equation (A, B, C are zero) if (abs($A) < 5E-15 && abs($B) < 5E-15 && abs($C) < 5E-15) { # add solution push(@t, 0.5) if ($D == 0); # linear equation (A, B are zero) } elsif (abs($A) < 5E-15 && abs($B) < 5E-15) { # add solution push(@t, -$D/$C); # quadratic equation (A is zero) } elsif (abs($A) < 5E-15) { # compute discriminant: > 0 two real, == 0 one real, < 0 two complex $dscr = $C**2 - 4 * $B * $D; # if discriminant is zero if (abs($dscr) < 5E-15) { # add solution (double root) push(@t, -$C/(2 * $B)); # if discriminant > zero } elsif ($dscr > 0) { # add solutions push(@t, -($C + sqrt($dscr))/(2 * $B)); push(@t, -($C - sqrt($dscr))/(2 * $B)); } # cubic equation } else { # compute discriminant: > 0 three real, == 0 one or two real, < 0 one real and two complex $dscr = 18 * $A * $B * $C * $D - 4 * $B**3 * $D + $B**2 * $C**2 - 4 * $A * $C**3 - 27 * $A**2 * $D**2; # compute ∆0 $d0 = $B**2 - 3 * $A * $C; # if discriminant is zero if (abs($dscr) < 5E-15) { # if ∆0 is zero if (abs($d0) < 5E-15) { # add solution (triple root) push(@t, -$B/(3 * $A)); } else { # add solutions (double root and single root) push(@t, (9 * $A * $D - $B * $C)/(2 * $d0)); push(@t, (4 * $A * $B * $C - 9 * $A**2 * $D - $B**3)/($A * $d0)); } } else { # compute ∆1 $d1 = 2 * $B**3 - 9 * $A * $B * $C + 27 * $A**2 * $D; # compute C (a complex number) $cs = sqrt($d1**2 - 4 * $d0**3); $cc = cbrt($d1 == $cs ? ($d1 + $cs)/2 : ($d1 - $cs)/2); # compute cube roots of 1 (three complex numbers) @r = root(1, 3); # for each root for my $i (0 .. 2) { # multiply by cube root of 1 $ccr = $r[$i] * $cc; # compute solution (a complex number) $sol = ($B + $ccr + $d0/$ccr)/(-3 * $A); # add solution, if real push(@t, Re($sol)) if ($dscr > 0 || abs(Im($sol)) < 1E-15); } } } # set test limits to avoid duplicates at knots $lim0 = ($in == $y1) ? 1E-14 : 0; $lim1 = ($in == $y2) ? (1 - 1E-14) : 1; # return valid solutions return(sort {$a <=> $b} grep {$_ > $lim0 && $_ < $lim1} @t); } # test vector values # returns true if values are unique # parameter: (vector) # returns: (flag) sub _unique { # get parameter my $vec = shift(); # local variable my (%x); # make count hash for (@{$vec}) {$x{$_}++} # return return(@{$vec} == keys(%x)); } # sort object input/output values # parameter: (ref_to_object) sub _sort_values { # get parameter my $self = shift(); # local variable my (@p); # if input contains x-values if ($#{$self->[1]} > 1) { # warn if vectors not same length ($#{$self->[1]} == $#{$self->[2]}) or carp('input and output vectors not same length'); # for each point for my $i (0 .. $#{$self->[1]}) { # copy x-y values $p[$i] = [$self->[1][$i], $self->[2][$i]]; } # sort points by input value @p = sort {$a->[0] <=> $b->[0]} @p; # for each point for my $i (0 .. $#{$self->[1]}) { # copy x-y values $self->[1][$i] = $p[$i]->[0]; $self->[2][$i] = $p[$i]->[1]; } } else { # if x-range is decreasing if ($self->[1][0] > $self->[1][-1]) { # reverse x and y vectors @{$self->[1]} = reverse(@{$self->[1]}); @{$self->[2]} = reverse(@{$self->[2]}); } } } # fix highlight region data # corrects the print-specific # problem of highlight dot retention # samples below the limit (0 - 1) are fixed # parameter: (ref_to_object, limit) sub _fix_hl { # get parameter my ($self, $limit) = @_; # local variable my ($span, $lo, $hi, $n, @ix, @xr, @x, @y, $bern, @new); # compute 'output' range $span = ($self->[2][-1] - $self->[2][0]); # set 'output' limits $lo = $self->[2][0] + 0.002 * $span; $hi = $self->[2][0] + 2 * $limit * $span; # get upper index of 'output' array $n = $#{$self->[2]}; # find samples within limits @ix = grep {$self->[2][$_] >= $lo && $self->[2][$_] <= $hi} (1 .. $n); # use at least 4 samples @ix = ($ix[0] .. $ix[0] + 3) if (@ix < 4); # if input is a range if (@{$self->[1]} == 2) { # interpolate x-values @xr = map {(1 - $_/$n) * $self->[1][0] + ($_/$n) * $self->[1][1]} (0 .. $n); # get 'fit' x-values @x = @xr[@ix]; } else { # get 'fit' x-values @x = @{$self->[1]}[@ix]; } # get 'fit' y-values @y = @{$self->[2]}[@ix]; # make 'bern' object of degree 3, fitted to x-y values (requires Lapack module) $bern = ICC::Support::bern->new({'input' => [$x[0], $x[-1]], 'output' => [(undef) x 4], 'fit' => [\@x, \@y]}); # set 'output' limit $hi = $self->[2][0] + $limit * $span; # for each sample for my $i (reverse(0 .. $n)) { # if 'output' value ≤ limit if (@new || $self->[2][$i] <= $hi) { # compute new 'output' value using 'bern' curve push(@new, $bern->transform(@{$self->[1]} > 2 ? $self->[1][$i] : $xr[$i])); } } # splice new values, saving originals in hash $self->[0]{'hl_original'} = [splice(@{$self->[2]}, 0, @new, reverse(@new))]; # save x-axis intercept (highlight dot retention value) $self->[0]{'hl_retention'} = $bern->inverse(0); # compute knot derivatives _objderv($self); # add segment min/max y-values _minmax($self); } # fix shadow region data # corrects the print-specific problem of # non-monotonic data is the shadow region # samples above the limit (0 - 1) are fixed # parameter: (ref_to_object, limit) sub _fix_sh { # get parameter my ($self, $limit) = @_; # local variable my ($span, $lo, $n, @ix, @xr, @x, @y, $bern, @new); # compute 'output' range $span = ($self->[2][-1] - $self->[2][0]); # set 'output' limit $lo = $self->[2][-1] - 2 * (1 - $limit) * $span; # get upper index of 'output' array $n = $#{$self->[2]}; # find samples with 'output' > limit @ix = grep {$self->[2][$_] >= $lo} (0 .. $n); # use at least 4 samples @ix = (($ix[-1] - 3) .. $ix[-1]) if (@ix < 4); # if input is a range if (@{$self->[1]} == 2) { # interpolate x-values @xr = map {(1 - $_/$n) * $self->[1][0] + ($_/$n) * $self->[1][1]} (0 .. $n); # get 'fit' x-values @x = @xr[@ix]; } else { # get 'fit' x-values @x = @{$self->[1]}[@ix]; } # get 'fit' y-values @y = @{$self->[2]}[@ix]; # make 'bern' object of degree 3, fitted to x-y values (requires Lapack module) $bern = ICC::Support::bern->new({'input' => [$x[0], $x[-1]], 'output' => [(undef) x 4], 'fit' => [\@x, \@y]}); # set 'output' limit $lo = $self->[2][0] + $limit * $span; # for each sample for my $i (0 .. $n) { # if 'output' value ≥ limit if (@new || $self->[2][$i] >= $lo) { # compute new 'output' value using 'bern' curve push(@new, $bern->transform(@{$self->[1]} > 2 ? $self->[1][$i] : $xr[$i])); } } # splice new values, saving originals in hash $self->[0]{'sh_original'} = [splice(@{$self->[2]}, -@new, @new, @new)]; # compute knot derivatives _objderv($self); # add segment min/max y-values _minmax($self); } # set object contents from parameter hash # parameters: (ref_to_object, ref_to_parameter_hash) sub _new_from_hash { # get parameters my ($self, $hash) = @_; # local variables my ($in, $out, $type, $damp, $fix, $limit, $pok, $fok); # if 'input' is defined if (defined($in = $hash->{'input'})) { # if 'input' is a vector (2 or more values) if (ICC::Shared::is_num_vector($in) && 2 <= @{$in}) { # set object 'input' array $self->[1] = [@{$in}]; # if 'input' is an integer > 0 (2 or more values) } elsif (Scalar::Util::looks_like_number($in) && $in == int($in) && $in > 0) { # make identity curve $self->[1] = [map {$_/$in} (0 .. $in)]; } else { # error croak("invalid 'input' values"); } } else { # use default $self->[1] = [0, 1]; } # if 'output' is defined if (defined($out = $hash->{'output'})) { # if 'output' is a vector (2 or more values) if (ICC::Shared::is_num_vector($out) && 2 <= @{$out}) { # set object 'output' array $self->[2] = [@{$out}]; # if 'output' is an integer > 0 (2 or more values) } elsif (Scalar::Util::looks_like_number($out) && $out == int($out) && $out > 0) { # make identity curve $self->[2] = [map {$_/$out} (0 .. $out)]; } else { # error croak("invalid 'output' values"); } } else { # use default $self->[2] = [0, 1]; } # if 'type' is defined if (defined($type = $hash->{'type'})) { # verify supported type ($type =~ m/^(natural|akima)$/) or croak('unsupported spline type'); # set object type in hash $self->[0]{'type'} = $type; } # if 'damp' is defined if (defined($damp = $hash->{'damp'})) { # verify supported type (Scalar::Util::looks_like_number($damp) && $damp >= 0) or croak('invalid akima damping value'); # set object type in hash $self->[0]{'damp'} = $damp; } # verify number of input values (@{$self->[1]} == 2 || @{$self->[1]} == @{$self->[2]}) or croak('wrong number of input values'); # sort input/output values _sort_values($self); # compute knot derivatives _objderv($self); # add segment min/max y-values _minmax($self); # if 'fix_hl' is defined if (defined($fix = $hash->{'fix_hl'})) { # verify polarity ($pok = $self->[2][-1] > $self->[2][0]) or carp("'fix_hl' requires increasing output values"); # verify fix limit (-1 to 1) ($fok = (Scalar::Util::looks_like_number($fix) && $fix >= -1 && $fix <= 1)) or carp("invalid 'fix_hl' limit"); # compute 'output' limit $limit = $self->[2][0] * (1 - $fix) + $self->[2][-1] * $fix; # fix the data if polarity and limit are OK, and spline is non-monotonic in highlights _fix_hl($self, abs($fix)) if ($pok && $fok && ($fix < 0 || grep {$_ <= $limit} monotonic($self, 'output'))); } # if 'fix_sh' is defined if (defined($fix = $hash->{'fix_sh'})) { # verify polarity ($pok = $self->[2][-1] > $self->[2][0]) or carp("'fix_sh' requires increasing output values"); # verify fix limit (-1 to 1) ($fok = (Scalar::Util::looks_like_number($fix) && $fix >= -1 && $fix <= 1)) or carp("invalid 'fix_sh' limit"); # compute 'output' limit $limit = $self->[2][0] * (1 - $fix) + $self->[2][-1] * $fix; # fix the data if polarity and limit are OK, and spline is non-monotonic in shadows _fix_sh($self, abs($fix)) if ($pok && $fok && ($fix < 0 || grep {$_ >= $limit} monotonic($self, 'output'))); } } # write spline object to ICC profile # note: writes an equivalent curv object # parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry) sub _writeICCspline { # get parameters my ($self, $parent, $fh, $tag) = @_; # local variables my ($n, $up, @table); # get count value (defaults to 4096, the maximum allowed in an 'mft2' tag) $n = $self->[0]{'curv_points'} // 4096; # validate number of table entries ($n == int($n) && $n >= 2) or carp('invalid number of table entries'); # array upper index $up = $n - 1; # for each table entry for my $i (0 .. $up) { # transform value $table[$i] = transform($self, $i/$up); } # seek start of tag seek($fh, $tag->[1], 0); # write tag type signature and count print $fh pack('a4 x4 N', 'curv', $n); # write array print $fh pack('n*', map {$_ < 0 ? 0 : ($_ > 1 ? 65535 : $_ * 65535 + 0.5)} @table); } 1;