You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

382 lines
16 KiB

#------------------------------------------------------------------------------
# File: picasa_faces.config
#
# Description: User-defined Composite tag definitions to convert face regions
# in .picasa.ini files to MWG region tags (Metadata Working Group
# region, used by Picasa) and MP region tags (used by Microsoft
# Photo Library).
#
# Tag definitions and examples:
#
# PicasaToMWGRegion
# This will create the MWG region tag but will filter out the regions
# that are still unnamed in Picasa. Picasa defaults to naming these
# regions 'ffffffffffffffff' but normally will not save these to file.
# Example:
# exiftool -config picasa_faces.config "-RegionInfo<PicasaToMWGRegion" FILE
#
# PicasaToMPRegion
# This will create the MP region tag but will filter out the regions that
# are still unnamed in Picasa. Picasa defaults to naming these regions
# 'ffffffffffffffff' but normally will not save these to file.
# Example:
# exiftool -config picasa_faces.config "-RegionInfoMP<PicasaToMPRegion" FILE
#
# PicasaRegionNames
# Returns a list of the region names associated with the file. This
# allows copying of the region names to XMP:Subject and/or IPTC:Keywords.
# It also allows checking to see if regions need to be updated.
# Example:
# exiftool -config picasa_faces.config "-XMP:Subject<PicasaRegionNames" FILE
#
# PicasaToMWGRegionUnfiltered
# This will create the MWG region tag. This version does not filter out
# the unnamed regions. Picasa normally will filter out unnamed regions
# when it saves regions in the file.
# Example:
# exiftool -config picasa_faces.config "-RegionInfo<PicasaToMWGRegionUnfiltered" FILE
#
# PicasaToMPRegionUnfiltered
# This will create the MP region tag. This version does not filter out
# the unnamed regions. Picasa normally will filter out unnamed regions
# when it saves regions in the file.
# Example:
# exiftool -config picasa_faces.config "-RegionInfoMP<PicasaToMPRegionUnfiltered" FILE
#
# Notes: The face names are loaded from the Picasa contacts file, which
# defaults to:
#
# C:/Users/MainUser/AppData/Local/PicasaData/Google/Picasa2/contacts/contacts.xml
#
# The default contacts file may be changed by editing the value
# of $contactXML below, or on the command line with ExifTool 9.90
# or later via the -userParam option, eg:
# -userparam PicasaContactsFile=/path/to/contacts.xml
#
# Requires: ExifTool version 8.82 or later (9.90 or later for -userparam)
#
# Revisions: 2015/03/07 - Bryan K. Williams (aka StarGeek) Created
# 2015/03/12 - PH Minor changes, optimizations and reformatting
# 2015/05/11 - BKW Fix bug where Picasa writes region data for
# rotated NEF and CR2 images as if the orientation
# is not rotated.
# 2015/05/12 - PH Minor code tweaks
# 2015/10/26 - BKW Round off area sizes to 7 decimal places
# 2016/01/18 - BKW Improved rounding algorithm
# 2016/05/14 - BKW Increased rounding to 9 decimal places (Max Picasa accepts),
# moved rounding operation to subroutine
#
# References: https://exiftool.org/forum/index.php/topic,6354.0.html
#------------------------------------------------------------------------------
# Picasa contacts file name
my $contactXML = 'C:/Users/MainUser/AppData/Local/PicasaData/Google/Picasa2/contacts/contacts.xml';
# local variables
my $lastIniFile = ''; # path of last .picasa.ini file loaded
my $lastContactFile = ''; # path of last contacts.xml file loaded
# raw file types that need additional processing to get regions correct
my %isRawFile = map { $_ => 1 } qw(
3FR ARW CR2 CRW CS1 DCR DNG EIP ERF IIQ K25 KDC MEF MOS MRW NEF NRW
ORF PEF RAF RAW RW2 RWL SR2 SRF SRW X3F), 'Canon 1D RAW';
my %contactHash; # lookup for loaded contacts.xml entries
my %fileHash; # lookup for loaded .picasa.ini entries
#------------------------------------------------------------------------------
# Load Picasa's contacts.xml and .picasa.ini files.
# Inputs: 0) ExifTool object reference, 1) .picasa.ini directory
# Returns: 1 if files were loaded and parsed, undef on error
# Notes: If file has already been loaded, it isn't reloaded
sub LoadPicasaFiles($$)
{
local (*CONTACTS, *INI);
my ($et, $iniDir) = @_;
# check ExifTool version to see if there might be
# a command line setting for the contact file
my $contactFile = ($Image::ExifTool::VERSION >= 9.89 and
defined($et->Options(UserParam => 'PicasaContactsFile'))) ?
$et->Options(UserParam => 'PicasaContactsFile') : $contactXML;
# load Picasa contacts.xml file unless done already
unless ($contactFile eq $lastContactFile) {
$lastContactFile = $contactFile;
undef %contactHash;
# Picasa's default setting for unnamed faces.
$contactHash{'ffffffffffffffff'} = 'unnamed';
if (open(CONTACTS, $contactFile)) {
require Image::ExifTool::HTML;
while (<CONTACTS>) {
/name="(.*?)"/ or next;
my $name = $1;
/id="([a-f0-9]+)"/ or next;
my $id = $1;
$contactHash{$id} = Image::ExifTool::HTML::UnescapeHTML($name);
}
close(CONTACTS);
} else {
local $SIG{'__WARN__'} = undef; # stop ExifTool from catching the warning
warn "Error reading contacts file $contactFile\n";
}
}
# load .picasa.ini file from the specified directory
my $iniFile = "$iniDir/.picasa.ini";
if ($iniFile eq $lastIniFile) {
return %fileHash ? 1 : undef;
}
$lastIniFile = $iniFile;
open(INI, $iniFile) or return undef;
my $section = '';
while (<INI>) {
# Process New Section
/^\s*\[(.+)\][\n\r]*$/ and $section = $1, next;
# process entry (all we care about are the "faces" lines)
/^faces=(.*)$/ or next;
my @temp = split /;/, $1;
foreach (@temp) {
/rect64\(([\da-f]{1,16})\),([\da-f]{1,16})/ or next;
# the string in parens after "rect64" is a 64 bit number in hex,
# but Picasa doesn't add leading zeroes, so the length of the string
# cannot be assumed to be 16 bytes. Handle this as two 32-bit numbers
# for compatibility with 32-bit systems.
my $hi = hex(substr($1, 0, -8));
my $lo = hex(substr($1, -8));
my $x0 = ($hi >> 16) /65535;
my $y0 = ($hi & 0xffff)/65535;
my $x1 = ($lo >> 16) /65535;
my $y1 = ($lo & 0xffff)/65535;
push @{ $fileHash{$section} }, {
ContactID => $2,
X => $x0,
Y => $y0,
W => $x1 - $x0,
H => $y1 - $y0,
};
}
}
close(INI);
return %fileHash ? 1 : undef;
}
#------------------------------------------------------------------------------
# Rotate region to specified orientation (for RAW file types only)
# Input: 0) rectangle array ref (x,y,w,h), 1) EXIF orientation value, 2) file type
sub RotateRegion($$$)
{
my ($rect, $orientation, $fileType) = @_;
if ($orientation and $fileType and $isRawFile{$fileType}) {
my ($x,$y,$w,$h) = @$rect;
if ($orientation == 8) { # CW 90
@$rect = (1-$h-$y, $x, $h, $w);
} elsif ($orientation == 3) { # CW 180
@$rect = (1-$x-$w, 1-$y-$h, $w, $h);
} elsif ($orientation == 6) { # CW 270
@$rect = ($y, 1-$x-$w, $h, $w);
}
}
}
#------------------------------------------------------------------------------
# Rounds number to 9 decimal places, which is the limit to the number of decimal places that Picasa can read.
sub Rounded
{
my $DecAcc = 10**9;
return(int($_[0]*$DecAcc+.5)/$DecAcc);
}
#------------------------------------------------------------------------------
# User-defined tag definitions
#
%Image::ExifTool::UserDefined = (
'Image::ExifTool::Composite' => {
#
# Versions that filter out unnamed regions (ContactID=ffffffffffffffff)
#
PicasaToMWGRegion => {
Require => {
0 => 'Directory',
1 => 'FileName',
2 => 'ImageWidth',
3 => 'ImageHeight',
},
Desire => {
4 => 'Orientation',
5 => 'FileType',
},
ValueConv => sub {
my ($val, $et) = @_;
LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
my $filename = $$val[1];
my @regList;
# convert to local variables for readability, and make
# sure there is a region associated with the current file
my $contactHashRef = \%contactHash;
my $tempArrayRef = $fileHash{$filename} or return undef;
foreach my $tempHash (@$tempArrayRef) {
next if $$tempHash{ContactID} eq 'ffffffffffffffff';
my $name = $$contactHashRef{$$tempHash{ContactID}};
next unless defined $name;
my @rect = @$tempHash{'X','Y','W','H'};
RotateRegion(\@rect, $$val[4], $$val[5]);
push @regList, {
Area => {
X => Rounded($rect[0] + $rect[2] / 2),
Y => Rounded($rect[1] + $rect[3] / 2),
W => Rounded($rect[2]),
H => Rounded($rect[3]),
Unit => 'normalized',
},
Name => $name,
Type => 'Face',
};
}
# make sure a region exists, otherwise return undef
return @regList ? {
AppliedToDimensions => { W => $$val[2], H => $$val[3], Unit => 'pixel' },
RegionList => \@regList,
} : undef;
},
},
PicasaToMPRegion => {
Require => {
0 => 'Directory',
1 => 'FileName',
},
Desire => {
2 => 'Orientation',
3 => 'FileType',
},
ValueConv => sub {
my ($val, $et) = @_;
LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
my $filename = $$val[1];
my @regList;
# convert to local variables for readability, and make
# sure there is a region associated with the current file
my $contactHashRef = \%contactHash;
my $tempArrayRef = $fileHash{$filename} or return undef;
foreach my $tempHash (@$tempArrayRef) {
next if $$tempHash{ContactID} eq 'ffffffffffffffff';
my $name = $$contactHashRef{$$tempHash{ContactID}};
next unless defined $name;
my @rect = @$tempHash{'X','Y','W','H'};
RotateRegion(\@rect, $$val[2], $$val[3]);
@rect = map {Rounded($_)} @rect;
push @regList, {
PersonDisplayName => $name,
Rectangle => join(', ', @rect),
};
}
# make sure a region exists, otherwise return undef
return @regList ? { Regions => \@regList } : undef;
},
},
PicasaRegionNames => {
Require => {
0 => 'Directory',
1 => 'FileName',
},
ValueConv => sub {
my ($val, $et) = @_;
LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
my $filename = $$val[1];
my @regList;
# convert to local variables for readability, and make
# sure there is a region associated with the current file
my $contactHashRef = \%contactHash;
my $tempArrayRef = $fileHash{$filename} or return undef;
foreach my $tempHash (@$tempArrayRef) {
next if $$tempHash{ContactID} eq 'ffffffffffffffff';
my $name = $$contactHashRef{$$tempHash{ContactID}};
push @regList, $name if defined $name;
}
# make sure a region exists, otherwise return undef
return @regList ? \@regList : undef;
},
},
#
# Versions that do not filter out unnamed regions (ContactID=ffffffffffffffff)
# Picasa normally does not add these regions when it saves names to the file.
#
PicasaToMWGRegionUnfiltered => {
Require => {
0 => 'Directory',
1 => 'FileName',
2 => 'ImageWidth',
3 => 'ImageHeight',
},
Desire => {
4 => 'Orientation',
5 => 'FileType',
},
ValueConv => sub {
my ($val, $et) = @_;
LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
my $filename = $$val[1];
my @regList;
# convert to local variables for readability, and make
# sure there is a region associated with the current file
my $contactHashRef = \%contactHash;
my $tempArrayRef = $fileHash{$filename} or return undef;
foreach my $tempHash (@$tempArrayRef) {
my @rect = @$tempHash{'X','Y','W','H'};
RotateRegion(\@rect, $$val[4], $$val[5]);
push @regList, {
Area => {
X => Rounded($rect[0] + $rect[2] / 2),
Y => Rounded($rect[1] + $rect[3] / 2),
W => Rounded($rect[2]),
H => Rounded($rect[3]),
Unit => 'normalized',
},
Name => $$contactHashRef{$$tempHash{ContactID}} || 'unnamed',
Type => 'Face',
};
}
# make sure a region exists, otherwise return undef
return @regList ? {
AppliedToDimensions => { W => $$val[2], H => $$val[3], Unit => 'pixel' },
RegionList => \@regList,
} : undef;
},
},
PicasaToMPRegionUnfiltered => {
Require => {
0 => 'Directory',
1 => 'FileName',
},
Desire => {
2 => 'Orientation',
3 => 'FileType',
},
ValueConv => sub {
my ($val, $et) = @_;
LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
my $filename = $$val[1];
my @regList;
# convert to local variables for readability, and make
# sure there is a region associated with the current file
my $contactHashRef = \%contactHash;
my $tempArrayRef = $fileHash{$filename} or return undef;
foreach my $tempHash (@$tempArrayRef) {
my @rect = @$tempHash{'X','Y','W','H'};
RotateRegion(\@rect, $$val[2], $$val[3]);
@rect = map {Rounded($_)} @rect;
push @regList, {
PersonDisplayName => $$contactHashRef{$$tempHash{ContactID}} || 'unnamed',
Rectangle => join(', ', @rect),
}
}
# make sure a region exists, otherwise return undef
return @regList ? { Regions => \@regList } : undef;
},
},
},
);
#------------------------------------------------------------------------------
1; #end