picasa_faces.config 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. #------------------------------------------------------------------------------
  2. # File: picasa_faces.config
  3. #
  4. # Description: User-defined Composite tag definitions to convert face regions
  5. # in .picasa.ini files to MWG region tags (Metadata Working Group
  6. # region, used by Picasa) and MP region tags (used by Microsoft
  7. # Photo Library).
  8. #
  9. # Tag definitions and examples:
  10. #
  11. # PicasaToMWGRegion
  12. # This will create the MWG region tag but will filter out the regions
  13. # that are still unnamed in Picasa. Picasa defaults to naming these
  14. # regions 'ffffffffffffffff' but normally will not save these to file.
  15. # Example:
  16. # exiftool -config picasa_faces.config "-RegionInfo<PicasaToMWGRegion" FILE
  17. #
  18. # PicasaToMPRegion
  19. # This will create the MP region tag but will filter out the regions that
  20. # are still unnamed in Picasa. Picasa defaults to naming these regions
  21. # 'ffffffffffffffff' but normally will not save these to file.
  22. # Example:
  23. # exiftool -config picasa_faces.config "-RegionInfoMP<PicasaToMPRegion" FILE
  24. #
  25. # PicasaRegionNames
  26. # Returns a list of the region names associated with the file. This
  27. # allows copying of the region names to XMP:Subject and/or IPTC:Keywords.
  28. # It also allows checking to see if regions need to be updated.
  29. # Example:
  30. # exiftool -config picasa_faces.config "-XMP:Subject<PicasaRegionNames" FILE
  31. #
  32. # PicasaToMWGRegionUnfiltered
  33. # This will create the MWG region tag. This version does not filter out
  34. # the unnamed regions. Picasa normally will filter out unnamed regions
  35. # when it saves regions in the file.
  36. # Example:
  37. # exiftool -config picasa_faces.config "-RegionInfo<PicasaToMWGRegionUnfiltered" FILE
  38. #
  39. # PicasaToMPRegionUnfiltered
  40. # This will create the MP region tag. This version does not filter out
  41. # the unnamed regions. Picasa normally will filter out unnamed regions
  42. # when it saves regions in the file.
  43. # Example:
  44. # exiftool -config picasa_faces.config "-RegionInfoMP<PicasaToMPRegionUnfiltered" FILE
  45. #
  46. # Notes: The face names are loaded from the Picasa contacts file, which
  47. # defaults to:
  48. #
  49. # C:/Users/MainUser/AppData/Local/PicasaData/Google/Picasa2/contacts/contacts.xml
  50. #
  51. # The default contacts file may be changed by editing the value
  52. # of $contactXML below, or on the command line with ExifTool 9.90
  53. # or later via the -userParam option, eg:
  54. # -userparam PicasaContactsFile=/path/to/contacts.xml
  55. #
  56. # Requires: ExifTool version 8.82 or later (9.90 or later for -userparam)
  57. #
  58. # Revisions: 2015/03/07 - Bryan K. Williams (aka StarGeek) Created
  59. # 2015/03/12 - PH Minor changes, optimizations and reformatting
  60. # 2015/05/11 - BKW Fix bug where Picasa writes region data for
  61. # rotated NEF and CR2 images as if the orientation
  62. # is not rotated.
  63. # 2015/05/12 - PH Minor code tweaks
  64. # 2015/10/26 - BKW Round off area sizes to 7 decimal places
  65. # 2016/01/18 - BKW Improved rounding algorithm
  66. #
  67. # References: http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,6354.0.html
  68. #------------------------------------------------------------------------------
  69. # Picasa contacts file name
  70. my $contactXML = 'C:/Users/MainUser/AppData/Local/PicasaData/Google/Picasa2/contacts/contacts.xml';
  71. # local variables
  72. my $lastIniFile = ''; # path of last .picasa.ini file loaded
  73. my $lastContactFile = ''; # path of last contacts.xml file loaded
  74. # raw file types that need additional processing to get regions correct
  75. my %isRawFile = map { $_ => 1 } qw(
  76. 3FR ARW CR2 CRW CS1 DCR DNG EIP ERF IIQ K25 KDC MEF MOS MRW NEF NRW
  77. ORF PEF RAF RAW RW2 RWL SR2 SRF SRW X3F), 'Canon 1D RAW';
  78. my %contactHash; # lookup for loaded contacts.xml entries
  79. my %fileHash; # lookup for loaded .picasa.ini entries
  80. #------------------------------------------------------------------------------
  81. # Load Picasa's contacts.xml and .picasa.ini files.
  82. # Inputs: 0) ExifTool object reference, 1) .picasa.ini directory
  83. # Returns: 1 if files were loaded and parsed, undef on error
  84. # Notes: If file has already been loaded, it isn't reloaded
  85. sub LoadPicasaFiles($$)
  86. {
  87. local (*CONTACTS, *INI);
  88. my ($et, $iniDir) = @_;
  89. # check ExifTool version to see if there might be
  90. # a command line setting for the contact file
  91. my $contactFile = ($Image::ExifTool::VERSION >= 9.89 and
  92. defined($et->Options(UserParam => 'PicasaContactsFile'))) ?
  93. $et->Options(UserParam => 'PicasaContactsFile') : $contactXML;
  94. # load Picasa contacts.xml file unless done already
  95. unless ($contactFile eq $lastContactFile) {
  96. $lastContactFile = $contactFile;
  97. undef %contactHash;
  98. # Picasa's default setting for unnamed faces.
  99. $contactHash{'ffffffffffffffff'} = 'unnamed';
  100. if (open(CONTACTS, $contactFile)) {
  101. require Image::ExifTool::HTML;
  102. while (<CONTACTS>) {
  103. /name="(.*?)"/ or next;
  104. my $name = $1;
  105. /id="([a-f0-9]+)"/ or next;
  106. my $id = $1;
  107. $contactHash{$id} = Image::ExifTool::HTML::UnescapeHTML($name);
  108. }
  109. close(CONTACTS);
  110. } else {
  111. local $SIG{'__WARN__'} = undef; # stop ExifTool from catching the warning
  112. warn "Error reading contacts file $contactFile\n";
  113. }
  114. }
  115. # load .picasa.ini file from the specified directory
  116. my $iniFile = "$iniDir/.picasa.ini";
  117. if ($iniFile eq $lastIniFile) {
  118. return %fileHash ? 1 : undef;
  119. }
  120. $lastIniFile = $iniFile;
  121. open(INI, $iniFile) or return undef;
  122. my $section = '';
  123. while (<INI>) {
  124. # Process New Section
  125. /^\s*\[(.+)\][\n\r]*$/ and $section = $1, next;
  126. # process entry (all we care about are the "faces" lines)
  127. /^faces=(.*)$/ or next;
  128. my @temp = split /;/, $1;
  129. foreach (@temp) {
  130. /rect64\(([\da-f]{1,16})\),([\da-f]{1,16})/ or next;
  131. # the string in parens after "rect64" is a 64 bit number in hex,
  132. # but Picasa doesn't add leading zeroes, so the length of the string
  133. # cannot be assumed to be 16 bytes. Handle this as two 32-bit numbers
  134. # for compatibility with 32-bit systems.
  135. my $hi = hex(substr($1, 0, -8));
  136. my $lo = hex(substr($1, -8));
  137. my $x0 = ($hi >> 16) /65535;
  138. my $y0 = ($hi & 0xffff)/65535;
  139. my $x1 = ($lo >> 16) /65535;
  140. my $y1 = ($lo & 0xffff)/65535;
  141. push @{ $fileHash{$section} }, {
  142. ContactID => $2,
  143. # round to 8 decimals (Picasa will ignore regions with greater than 10 decimals)
  144. X => int($x0*100000000+.5)/100000000,
  145. Y => int($y0*100000000+.5)/100000000,
  146. W => int(($x1 - $x0)*100000000+.5)/100000000,
  147. H => int(($y1 - $y0)*100000000+.5)/100000000,
  148. };
  149. }
  150. }
  151. close(INI);
  152. return %fileHash ? 1 : undef;
  153. }
  154. #------------------------------------------------------------------------------
  155. # Rotate region to specified orientation (for RAW file types only)
  156. # Input: 0) rectangle array ref (x,y,w,h), 1) EXIF orientation value, 2) file type
  157. sub RotateRegion($$$)
  158. {
  159. my ($rect, $orientation, $fileType) = @_;
  160. if ($orientation and $fileType and $isRawFile{$fileType}) {
  161. my ($x,$y,$w,$h) = @$rect;
  162. if ($orientation == 8) { # CW 90
  163. @$rect = (1-$h-$y, $x, $h, $w);
  164. } elsif ($orientation == 3) { # CW 180
  165. @$rect = (1-$x-$w, 1-$y-$h, $w, $h);
  166. } elsif ($orientation == 6) { # CW 270
  167. @$rect = ($y, 1-$x-$w, $h, $w);
  168. }
  169. }
  170. }
  171. #------------------------------------------------------------------------------
  172. # User-defined tag definitions
  173. #
  174. %Image::ExifTool::UserDefined = (
  175. 'Image::ExifTool::Composite' => {
  176. #
  177. # Versions that filter out unnamed regions (ContactID=ffffffffffffffff)
  178. #
  179. PicasaToMWGRegion => {
  180. Require => {
  181. 0 => 'Directory',
  182. 1 => 'FileName',
  183. 2 => 'ImageWidth',
  184. 3 => 'ImageHeight',
  185. },
  186. Desire => {
  187. 4 => 'Orientation',
  188. 5 => 'FileType',
  189. },
  190. ValueConv => sub {
  191. my ($val, $et) = @_;
  192. LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
  193. my $filename = $$val[1];
  194. my @regList;
  195. # convert to local variables for readability, and make
  196. # sure there is a region associated with the current file
  197. my $contactHashRef = \%contactHash;
  198. my $tempArrayRef = $fileHash{$filename} or return undef;
  199. foreach my $tempHash (@$tempArrayRef) {
  200. next if $$tempHash{ContactID} eq 'ffffffffffffffff';
  201. my $name = $$contactHashRef{$$tempHash{ContactID}};
  202. next unless defined $name;
  203. my @rect = @$tempHash{'X','Y','W','H'};
  204. RotateRegion(\@rect, $$val[4], $$val[5]);
  205. push @regList, {
  206. Area => {
  207. X => $rect[0] + $rect[2] / 2,
  208. Y => $rect[1] + $rect[3] / 2,
  209. W => $rect[2],
  210. H => $rect[3],
  211. Unit => 'normalized',
  212. },
  213. Name => $name,
  214. Type => 'Face',
  215. };
  216. }
  217. # make sure a region exists, otherwise return undef
  218. return @regList ? {
  219. AppliedToDimensions => { W => $$val[2], H => $$val[3], Unit => 'pixel' },
  220. RegionList => \@regList,
  221. } : undef;
  222. },
  223. },
  224. PicasaToMPRegion => {
  225. Require => {
  226. 0 => 'Directory',
  227. 1 => 'FileName',
  228. },
  229. Desire => {
  230. 2 => 'Orientation',
  231. 3 => 'FileType',
  232. },
  233. ValueConv => sub {
  234. my ($val, $et) = @_;
  235. LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
  236. my $filename = $$val[1];
  237. my @regList;
  238. # convert to local variables for readability, and make
  239. # sure there is a region associated with the current file
  240. my $contactHashRef = \%contactHash;
  241. my $tempArrayRef = $fileHash{$filename} or return undef;
  242. foreach my $tempHash (@$tempArrayRef) {
  243. next if $$tempHash{ContactID} eq 'ffffffffffffffff';
  244. my $name = $$contactHashRef{$$tempHash{ContactID}};
  245. next unless defined $name;
  246. my @rect = @$tempHash{'X','Y','W','H'};
  247. RotateRegion(\@rect, $$val[2], $$val[3]);
  248. push @regList, {
  249. PersonDisplayName => $name,
  250. Rectangle => join(', ', @rect),
  251. };
  252. }
  253. # make sure a region exists, otherwise return undef
  254. return @regList ? { Regions => \@regList } : undef;
  255. },
  256. },
  257. PicasaRegionNames => {
  258. Require => {
  259. 0 => 'Directory',
  260. 1 => 'FileName',
  261. },
  262. ValueConv => sub {
  263. my ($val, $et) = @_;
  264. LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
  265. my $filename = $$val[1];
  266. my @regList;
  267. # convert to local variables for readability, and make
  268. # sure there is a region associated with the current file
  269. my $contactHashRef = \%contactHash;
  270. my $tempArrayRef = $fileHash{$filename} or return undef;
  271. foreach my $tempHash (@$tempArrayRef) {
  272. next if $$tempHash{ContactID} eq 'ffffffffffffffff';
  273. my $name = $$contactHashRef{$$tempHash{ContactID}};
  274. push @regList, $name if defined $name;
  275. }
  276. # make sure a region exists, otherwise return undef
  277. return @regList ? \@regList : undef;
  278. },
  279. },
  280. #
  281. # Versions that do not filter out unnamed regions (ContactID=ffffffffffffffff)
  282. # Picasa normally does not add these regions when it saves names to the file.
  283. #
  284. PicasaToMWGRegionUnfiltered => {
  285. Require => {
  286. 0 => 'Directory',
  287. 1 => 'FileName',
  288. 2 => 'ImageWidth',
  289. 3 => 'ImageHeight',
  290. },
  291. Desire => {
  292. 4 => 'Orientation',
  293. 5 => 'FileType',
  294. },
  295. ValueConv => sub {
  296. my ($val, $et) = @_;
  297. LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
  298. my $filename = $$val[1];
  299. my @regList;
  300. # convert to local variables for readability, and make
  301. # sure there is a region associated with the current file
  302. my $contactHashRef = \%contactHash;
  303. my $tempArrayRef = $fileHash{$filename} or return undef;
  304. foreach my $tempHash (@$tempArrayRef) {
  305. my @rect = @$tempHash{'X','Y','W','H'};
  306. RotateRegion(\@rect, $$val[4], $$val[5]);
  307. push @regList, {
  308. Area => {
  309. X => $rect[0] + $rect[2] / 2,
  310. Y => $rect[1] + $rect[3] / 2,
  311. W => $rect[2],
  312. H => $rect[3],
  313. Unit => 'normalized',
  314. },
  315. Name => $$contactHashRef{$$tempHash{ContactID}} || 'unnamed',
  316. Type => 'Face',
  317. };
  318. }
  319. # make sure a region exists, otherwise return undef
  320. return @regList ? {
  321. AppliedToDimensions => { W => $$val[2], H => $$val[3], Unit => 'pixel' },
  322. RegionList => \@regList,
  323. } : undef;
  324. },
  325. },
  326. PicasaToMPRegionUnfiltered => {
  327. Require => {
  328. 0 => 'Directory',
  329. 1 => 'FileName',
  330. },
  331. Desire => {
  332. 2 => 'Orientation',
  333. 3 => 'FileType',
  334. },
  335. ValueConv => sub {
  336. my ($val, $et) = @_;
  337. LoadPicasaFiles($et, $$val[0]) or return undef; # load contacts.xml and Picasa.ini
  338. my $filename = $$val[1];
  339. my @regList;
  340. # convert to local variables for readability, and make
  341. # sure there is a region associated with the current file
  342. my $contactHashRef = \%contactHash;
  343. my $tempArrayRef = $fileHash{$filename} or return undef;
  344. foreach my $tempHash (@$tempArrayRef) {
  345. my @rect = @$tempHash{'X','Y','W','H'};
  346. RotateRegion(\@rect, $$val[2], $$val[3]);
  347. push @regList, {
  348. PersonDisplayName => $$contactHashRef{$$tempHash{ContactID}} || 'unnamed',
  349. Rectangle => join(', ', @rect),
  350. }
  351. }
  352. # make sure a region exists, otherwise return undef
  353. return @regList ? { Regions => \@regList } : undef;
  354. },
  355. },
  356. },
  357. );
  358. #------------------------------------------------------------------------------
  359. 1; #end