bin/config_files/photoshop_paths.config
#------------------------------------------------------------------------------
# File: photoshop_paths.config
#
# Description: This config file generates user-defined tags for Photoshop
# paths, and may be used to extract path names and/or Bezier knot
# anchor points, or copy path information from one file to
# another.
#
# The anchor points may be extracted along with the path names by
# setting the "Anchor" user parameter (ie. "-userparam anchor"),
# or by themselves with "AnchorOnly".
#
# An "AllPaths" shortcut tag is also provided represent all
# Photoshop path tags. This shortcut must be used when copying
# because these tags are marked as "Protected" so they won't be
# copied by default (also see the notes below).
#
# Path anchor points are converted to pixel coordinates by the
# Composite PathPixXXX tags, and an "AllPathPix" shortcut is
# provided to represent these tags.
#
# Finally, a Composite PathCount tag is provided to return the
# number of paths in an image, and a TotalPathPoints tag counts
# the total number of path anchor points.
#
# Notes: 1) Print conversion must be disabled to be able to copy the paths
# (via either the -n option, or by adding a "#" to the tag name,
# eg. "-tagsfromfile SRC -allpaths#").
#
# 2) When copying the paths, OriginPathInfo must also be copied
# (otherwise Photoshop may give a "program error" and refuse to
# load the image).
#
# Usage:
#
# 1) Extract Photoshop path names:
#
# exiftool -config photoshop_paths.config -allpaths FILE
#
# 2) Extract Photoshop path names and anchor points:
#
# exiftool -config photoshop_paths.config -userparam anchor -allpaths FILE
#
# 3) Extract Photoshop path anchor points only:
#
# exiftool -config photoshop_paths.config -userparam anchoronly -allpaths FILE
#
# 4) Copy all Photoshop paths from one file (SRC) to another (DST):
# (note that OriginPathInfo must also be copied when copying all paths)
#
# exiftool -config photoshop_paths.config -tagsfromfile SRC -allpaths# -originpathinfo DST
#
# 5) Extract path names and anchor points in pixel coordinates:
#
# exiftool -config photoshop_paths.config -allpathpix FILE
#
# Requires: ExifTool version 9.95 or later
#
# Notes: A "-" before a set of Bezier path points indicates a closed subpath,
# and a "+ indicates the start of an open subpath.
#
# Revisions: 2015/05/07 - P. Harvey Created
# 2016/09/14 - PH Added feature to allow extracting anchor points
# 2017/01/24 - PH Added PathCount and PathPix Composite tags
# 2017/02/02 - PH Added support for copying OriginPathInfo
# 2017/02/22 - PH Fixed problem printing some paths
# 2017/03/19 - PH Added "-" or "+" at the start of closed or open
# subpath respectively
# 2017/06/03 - PH Added TotalPathPoints
# 2017/07/17 - PH Added UniquePathPoints
# 2022/02/03 - PH Added WorkingPath and WorkingPathPix
#
# References: https://exiftool.org/forum/index.php/topic,1621.0.html
# https://exiftool.org/forum/index.php/topic,3910.0.html
# https://exiftool.org/forum/index.php/topic,6647.0.html
#------------------------------------------------------------------------------
# Print Photoshop path name and/or anchor points
# Inputs: 0) reference to Photoshop path data, 1) ExifTool object reference
# 2-3) optional image width/height to convert anchor points to pixels
# 4) optional path name
# Returns: String with name and/or Bezier knot anchor points
sub PrintPath($$;$$$)
{
my ($val, $et, $w, $h, $nm) = @_;
my ($pos, $name, @rtn);
my $len = length($$val) - 26;
# recover exiftool-added path name if it exists
if ($$val =~ m{.*/#(.{0,255})#/$}s) {
$name = $1;
$len -= length($1) + 4;
$name = $nm if defined $nm and not length $name;
} else {
$name = defined $nm ? $nm : '<none>';
}
my $anchorOnly = $et->Options(UserParam => 'AnchorOnly');
push @rtn, $name unless $anchorOnly;
# loop through path points and extract anchor points if specified
if ($anchorOnly or $et->Options(UserParam => 'Anchor') or defined $w) {
SetByteOrder('MM');
for ($pos=0; $pos<=$len; $pos+=26) {
my $type = Get16u($val, $pos);
$type == 0 and push(@rtn, '-'), next;
$type == 3 and push(@rtn, '+'), next;
# Bezier knot records are types 1, 2, 4 and 5
next unless {1=>1,2=>1,4=>1,5=>1}->{$type};
# the anchor point is at offset 10 in the Bezier knot record
# (fixed-point values with 24-bits after the decimal point)
my $y = Get32s($val, $pos+10) / 0x1000000; # (vertical component first)
my $x = Get32s($val, $pos+14) / 0x1000000;
if (defined $w and defined $h) {
push @rtn, sprintf('(%g,%g)', $x * $w, $y * $h);
} else {
push @rtn, sprintf('(%g,%g)', $x, $y);
}
}
}
return join ' ', @rtn;
}
%Image::ExifTool::Shortcuts::UserDefined = (
# create "AllPaths" shortcut for all Photoshop path tags (except WorkingPath)
AllPaths => [
map { sprintf "Path%x", $_ } (0x7d0 .. 0xbb5),
],
AllPathPix => [
map { sprintf "PathPix%x", $_ } (0x7d0 .. 0xbb5),
],
);
%Image::ExifTool::UserDefined = (
'Image::ExifTool::Photoshop::Main' => {
0xbb8 => {
Name => 'OriginPathInfo',
Flags => [ qw(Writable Protected Binary SetResourceName) ],
},
0x401 => {
Name => 'WorkingPath',
Flags => [ qw(Writable Protected Binary ConvertBinary SetResourceName) ],
PrintConv => sub {
my ($val, $et) = @_;
PrintPath($val, $et, undef, undef, 'Work Path');
},
},
# generate tags for each of the 998 possible Photoshop paths
map { $_ => {
Name => sprintf('Path%x', $_),
Description => sprintf('Path %x', $_),
Flags => [ qw(Writable Protected Binary ConvertBinary SetResourceName) ],
PrintConv => \&PrintPath,
} } (0x7d0 .. 0xbb5),
},
'Image::ExifTool::Composite' => {
PathCount => {
# (PathCount statistics do not include WorkingPath)
Desire => {
map { $_-0x7d0 => sprintf('Path%x', $_) } (0x7d0 .. 0xbb5),
},
ValueConv => sub {
my ($val, $self) = @_;
my $count = 0;
my $pts = 0;
my $uniq = 0;
my %uniq;
foreach (@$val) {
next unless defined $_;
++$count;
# determine the total number of path anchor points
my $len = length($$_) - 26;
for ($pos=0; $pos<=$len; $pos+=26) {
SetByteOrder('MM');
my $type = Get16u($_, $pos);
last if $type == 0x2f23; # (start of path name added by ExifTool)
next unless {1=>1,2=>1,4=>1,5=>1}->{$type};
my $pt = substr($$_, $pos+10, 8);
$uniq{$pt} or ++$uniq, $uniq{$pt} = 1;
++$pts;
}
}
$$self{TotalPathPoints} = $pts;
$$self{UniquePathPoints} = $uniq;
return $count;
},
},
UniquePathPoints => {
Require => 'PathCount',
ValueConv => '$$self{UniquePathPoints}',
},
TotalPathPoints => {
Require => 'PathCount',
ValueConv => '$$self{TotalPathPoints}',
},
WorkingPathPix => {
Require => {
0 => 'ImageWidth',
1 => 'ImageHeight',
2 => 'WorkingPath',
},
ValueConv => sub {
my ($val, $et) = @_;
PrintPath($$val[2], $et, $$val[0], $$val[1], 'Work Path');
},
},
map { sprintf('PathPix%x', $_) => {
Require => {
0 => 'ImageWidth',
1 => 'ImageHeight',
2 => sprintf('Path%x', $_),
},
Description => sprintf('Path Pix %x', $_),
ValueConv => sub {
my ($val, $et) = @_;
PrintPath($$val[2], $et, $$val[0], $$val[1]);
},
} } (0x7d0 .. 0xbb5),
},
);
1; #end