File Coverage

blib/lib/Image/ExifTool/WriteXMP.pl
Criterion Covered Total %
statement 712 880 80.9
branch 454 648 70.0
condition 237 370 64.0
subroutine 20 23 86.9
pod 0 20 0.0
total 1423 1941 73.3


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: WriteXMP.pl
3             #
4             # Description: Write XMP meta information
5             #
6             # Revisions: 12/19/2004 - P. Harvey Created
7             #------------------------------------------------------------------------------
8             package Image::ExifTool::XMP;
9              
10 37     37   318 use strict;
  37         115  
  37         1530  
11 37     37   254 use vars qw(%specialStruct %dateTimeInfo %stdXlatNS);
  37         103  
  37         2420  
12              
13 37     37   300 use Image::ExifTool qw(:DataAccess :Utils);
  37         101  
  37         472200  
14              
15             sub CheckXMP($$$;$);
16             sub CaptureXMP($$$;$);
17             sub SetPropertyPath($$;$$$$);
18              
19             my $debug = 0;
20             my $numPadLines = 24; # number of blank padding lines
21              
22             # when writing extended XMP, resources bigger than this get placed in their own
23             # rdf:Description so they can be moved to the extended segments if necessary
24             my $newDescThresh = 10240; # 10 kB
25              
26             # individual resources and namespaces to place last in separate rdf:Description's
27             # so they can be moved to extended XMP segments if required (see Oct. 2008 XMP spec)
28             my %extendedRes = (
29             'photoshop:History' => 1,
30             'xap:Thumbnails' => 1,
31             'xmp:Thumbnails' => 1,
32             'crs' => 1,
33             'crss' => 1,
34             );
35              
36             my $rdfDesc = 'rdf:Description';
37             #
38             # packet/xmp/rdf headers and trailers
39             #
40             my $pktOpen = "\n";
41             my $xmlOpen = "\n";
42             my $xmpOpenPrefix = "
43             my $rdfOpen = "\n";
44             my $rdfClose = "\n";
45             my $xmpClose = "\n";
46             my $pktCloseW = ""; # writable by default
47             my $pktCloseR = "";
48             my ($sp, $nl);
49              
50             #------------------------------------------------------------------------------
51             # Get XMP opening tag (and set x:xmptk appropriately)
52             # Inputs: 0) ExifTool object ref
53             # Returns: x:xmpmeta opening tag
54             sub XMPOpen($)
55             {
56 123     123 0 293 my $et = shift;
57 123         623 my $nv = $$et{NEW_VALUE}{$Image::ExifTool::XMP::x{xmptk}};
58 123         254 my $tk;
59 123 100       414 if (defined $nv) {
60 1         5 $tk = $et->GetNewValue($nv);
61 1 50       11 $et->VerboseValue(($tk ? '+' : '-') . ' XMP-x:XMPToolkit', $tk);
62 1         3 ++$$et{CHANGED};
63             } else {
64 122         444 $tk = "Image::ExifTool $Image::ExifTool::VERSION";
65             }
66 123 50       858 my $str = $tk ? (" x:xmptk='" . EscapeXML($tk) . "'") : '';
67 123         600 return "$xmpOpenPrefix$str>\n";
68             }
69              
70             #------------------------------------------------------------------------------
71             # Validate XMP packet and set read or read/write mode
72             # Inputs: 0) XMP data reference, 1) 'r' = read only, 'w' or undef = read/write
73             # Returns: true if XMP is good (and adds packet header/trailer if necessary)
74             sub ValidateXMP($;$)
75             {
76 4     4 0 16 my ($xmpPt, $mode) = @_;
77 4         17 $$xmpPt =~ s/^\s*\s*//s; # remove leading comment if it exists
78 4 50       36 unless ($$xmpPt =~ /^\0*<\0*\?\0*x\0*p\0*a\0*c\0*k\0*e\0*t/) {
79 0 0       0 return '' unless $$xmpPt =~ /^
80             # add required xpacket header/trailer
81 0         0 $$xmpPt = $pktOpen . $$xmpPt . $pktCloseW;
82             }
83 4 100       18 $mode = 'w' unless $mode;
84 4         16 my $end = substr($$xmpPt, -32, 32);
85             # check for proper xpacket trailer and set r/w mode if necessary
86 4 50       55 return '' unless $end =~ s/(e\0*n\0*d\0*=\0*['"]\0*)([rw])(\0*['"]\0*\?\0*>)/$1$mode$3/;
87 4 50       19 substr($$xmpPt, -32, 32) = $end if $2 ne $mode;
88 4         16 return 1;
89             }
90              
91             #------------------------------------------------------------------------------
92             # Validate XMP property
93             # Inputs: 0) ExifTool ref, 1) validate hash ref, 2) attribute hash ref
94             # - issues warnings if problems detected
95             sub ValidateProperty($$;$)
96             {
97 0     0 0 0 my ($et, $propList, $attr) = @_;
98              
99 0 0 0     0 if ($$et{XmpValidate} and @$propList > 2) {
100 0 0 0     0 if ($$propList[0] =~ /^x:x[ma]pmeta$/ and
    0 0        
      0        
101             $$propList[1] eq 'rdf:RDF' and
102             $$propList[2] =~ /rdf:Description( |$)/)
103             {
104 0 0       0 if (@$propList > 3) {
105 0 0       0 if ($$propList[-1] =~ /^rdf:(Bag|Seq|Alt)$/) {
106 0         0 $et->Warn("Ignored empty $$propList[-1] list for $$propList[-2]", 1);
107             } else {
108 0 0 0     0 if ($$propList[-2] eq 'rdf:Alt' and $attr) {
109 0         0 my $lang = $$attr{'xml:lang'};
110 0 0 0     0 if ($lang and @$propList >= 5) {
111 0         0 my $langPath = join('/', @$propList[3..($#$propList-2)]);
112 0   0     0 my $valLang = $$et{XmpValidateLangAlt} || ($$et{XmpValidateLangAlt} = { });
113 0 0       0 $$valLang{$langPath} or $$valLang{$langPath} = { };
114 0 0       0 if ($$valLang{$langPath}{$lang}) {
115 0         0 $et->WarnOnce("Duplicate language ($lang) in lang-alt list: $langPath");
116             } else {
117 0         0 $$valLang{$langPath}{$lang} = 1;
118             }
119             }
120             }
121 0         0 my $xmpValidate = $$et{XmpValidate};
122 0         0 my $path = join('/', @$propList[3..$#$propList]);
123 0 0       0 if (defined $$xmpValidate{$path}) {
124 0         0 $et->Warn("Duplicate XMP property: $path");
125             } else {
126 0         0 $$xmpValidate{$path} = 1;
127             }
128             }
129             }
130             } elsif ($$propList[0] ne 'rdf:RDF' or
131             $$propList[1] !~ /rdf:Description( |$)/)
132             {
133 0         0 $et->Warn('Improperly enclosed XMP property: ' . join('/',@$propList));
134             }
135             }
136             }
137              
138             #------------------------------------------------------------------------------
139             # Check XMP date values for validity and format accordingly
140             # Inputs: 1) EXIF-format date string
141             # Returns: XMP date/time string (or undef on error)
142             sub FormatXMPDate($)
143             {
144 127     127 0 336 my $val = shift;
145 127         300 my ($y, $m, $d, $t, $tz);
146 127 100       1142 if ($val =~ /(\d{4}):(\d{2}):(\d{2}) (\d{2}:\d{2}(?::\d{2}(?:\.\d*)?)?)(.*)/) {
    50          
    0          
147 99         680 ($y, $m, $d, $t, $tz) = ($1, $2, $3, $4, $5);
148 99         460 $val = "$y-$m-${d}T$t";
149             } elsif ($val =~ /^\s*\d{4}(:\d{2}){0,2}\s*$/) {
150             # this is just a date (YYYY, YYYY-mm or YYYY-mm-dd)
151 28         98 $val =~ tr/:/-/;
152             } elsif ($val =~ /^\s*(\d{2}:\d{2}(?::\d{2}(?:\.\d*)?)?)(.*)\s*$/) {
153             # this is just a time
154 0         0 ($t, $tz) = ($1, $2);
155 0         0 $val = $t;
156             } else {
157 0         0 return undef;
158             }
159 127 100       384 if ($tz) {
160 21 50       218 $tz =~ /^(Z|[+-]\d{2}:\d{2})$/ or return undef;
161 21         78 $val .= $tz;
162             }
163 127         945 return $val;
164             }
165              
166             #------------------------------------------------------------------------------
167             # Check XMP values for validity and format accordingly
168             # Inputs: 0) ExifTool object ref, 1) tagInfo hash ref, 2) raw value ref, 3) conversion type
169             # Returns: error string or undef (and may change value) on success
170             # Note: copies structured information to avoid conflicts with calling code
171             sub CheckXMP($$$;$)
172             {
173 2545     2545 0 6507 my ($et, $tagInfo, $valPtr, $convType) = @_;
174              
175 2545 100       6545 if ($$tagInfo{Struct}) {
176 98         7902 require 'Image/ExifTool/XMPStruct.pl';
177 98         352 my ($item, $err, $w, $warn);
178 98 100       401 unless (ref $$valPtr) {
179 76         374 ($$valPtr, $warn) = InflateStruct($valPtr);
180             # expect a structure HASH ref or ARRAY of structures
181 76 100       392 unless (ref $$valPtr) {
182 64 50       258 $$valPtr eq '' and $$valPtr = { }, return undef; # allow empty structures
183 64         276 return 'Improperly formed structure';
184             }
185             }
186 34 100       127 if (ref $$valPtr eq 'ARRAY') {
187 1 50       8 return 'Not a list tag' unless $$tagInfo{List};
188 0         0 my @copy = ( @{$$valPtr} ); # copy the list for ExifTool to use
  0         0  
189 0         0 $$valPtr = \@copy; # return the copy
190 0         0 foreach $item (@copy) {
191 0 0       0 unless (ref $item eq 'HASH') {
192 0         0 ($item, $w) = InflateStruct(\$item); # deserialize structure
193 0 0       0 $w and $warn = $w;
194 0 0       0 next if ref $item eq 'HASH';
195 0         0 $err = 'Improperly formed structure';
196 0         0 last;
197             }
198 0         0 ($item, $err) = CheckStruct($et, $item, $$tagInfo{Struct});
199 0 0       0 last if $err;
200             }
201             } else {
202 33         167 ($$valPtr, $err) = CheckStruct($et, $$valPtr, $$tagInfo{Struct});
203             }
204 33 50       115 $warn and $$et{CHECK_WARN} = $warn;
205 33         120 return $err;
206             }
207 2447         4765 my $format = $$tagInfo{Writable};
208             # (if no format specified, value is a simple string)
209 2447 100 100     10807 if (not $format or $format eq 'string' or $format eq 'lang-alt') {
      100        
210             # convert value to UTF8 if necessary
211 1329 100       4294 if ($$et{OPTIONS}{Charset} ne 'UTF8') {
212 4 50       35 if ($$valPtr =~ /[\x80-\xff]/) {
213             # convert from Charset to UTF-8
214 4         18 $$valPtr = $et->Encode($$valPtr,'UTF8');
215             }
216             } else {
217             # translate invalid XML characters to "."
218 1325         3286 $$valPtr =~ tr/\0-\x08\x0b\x0c\x0e-\x1f/./;
219             # fix any malformed UTF-8 characters
220 1325 50 33     5085 if (FixUTF8($valPtr) and not $$et{WarnBadUTF8}) {
221 0         0 $et->Warn('Malformed UTF-8 character(s)');
222 0         0 $$et{WarnBadUTF8} = 1;
223             }
224             }
225 1329         4124 return undef; # success
226             }
227 1118 100 100     6129 if ($format eq 'rational' or $format eq 'real') {
    100          
    100          
    100          
    50          
228             # make sure the value is a valid floating point number
229 366 100 33     1439 unless (Image::ExifTool::IsFloat($$valPtr) or
      66        
      100        
230             # allow 'inf' and 'undef' rational values
231             ($format eq 'rational' and ($$valPtr eq 'inf' or
232             $$valPtr eq 'undef' or Image::ExifTool::IsRational($$valPtr))))
233             {
234 8         33 return 'Not a floating point number';
235             }
236 358 100       1294 if ($format eq 'rational') {
237 269         1266 $$valPtr = join('/', Image::ExifTool::Rationalize($$valPtr));
238             }
239             } elsif ($format eq 'integer') {
240             # make sure the value is integer
241 591 100       2013 if (Image::ExifTool::IsInt($$valPtr)) {
    50          
242             # no conversion required (converting to 'int' would remove leading '+')
243             } elsif (Image::ExifTool::IsHex($$valPtr)) {
244 0         0 $$valPtr = hex($$valPtr);
245             } else {
246 34         138 return 'Not an integer';
247             }
248             } elsif ($format eq 'date') {
249 92         396 my $newDate = FormatXMPDate($$valPtr);
250 92 50       336 return "Invalid date/time (use YYYY:mm:dd HH:MM:SS[.ss][+/-HH:MM|Z])" unless $newDate;
251 92         227 $$valPtr = $newDate;
252             } elsif ($format eq 'boolean') {
253             # (allow lower-case 'true' and 'false' if not setting PrintConv value)
254 68 100 66     636 if (not $$valPtr or $$valPtr =~ /false/i or $$valPtr =~ /^no$/i) {
    50 66        
      33        
      33        
255 43 0 33     295 if (not $$valPtr or $$valPtr ne 'false' or not $convType or $convType eq 'PrintConv') {
      33        
      33        
256 43         105 $$valPtr = 'False';
257             }
258             } elsif ($$valPtr ne 'true' or not $convType or $convType eq 'PrintConv') {
259 25         64 $$valPtr = 'True';
260             }
261             } elsif ($format eq '1') {
262             # this is the entire XMP data block
263 1 50       5 return 'Invalid XMP data' unless ValidateXMP($valPtr);
264             } else {
265 0         0 return "Unknown XMP format: $format";
266             }
267 1076         3517 return undef; # success!
268             }
269              
270             #------------------------------------------------------------------------------
271             # Get PropertyPath for specified tagInfo
272             # Inputs: 0) tagInfo reference
273             # Returns: PropertyPath string
274             sub GetPropertyPath($)
275             {
276 8778     8778 0 13508 my $tagInfo = shift;
277 8778 100       24547 SetPropertyPath($$tagInfo{Table}, $$tagInfo{TagID}) unless $$tagInfo{PropertyPath};
278 8778         22494 return $$tagInfo{PropertyPath};
279             }
280              
281             #------------------------------------------------------------------------------
282             # Set PropertyPath for specified tag (also for associated flattened tags and structure elements)
283             # Inputs: 0) tagTable reference, 1) tagID, 2) tagID of parent structure,
284             # 3) structure definition ref (or undef), 4) property list up to this point (or undef),
285             # 5) flag set if any containing structure has a TYPE
286             # Notes: also generates flattened tags if they don't already exist
287             sub SetPropertyPath($$;$$$$)
288             {
289 4042     4042 0 7704 my ($tagTablePtr, $tagID, $parentID, $structPtr, $propList, $isType) = @_;
290 4042   66     9753 my $table = $structPtr || $tagTablePtr;
291 4042         8220 my $tagInfo = $$table{$tagID};
292 4042         5535 my $flatInfo;
293              
294 4042 50       9747 return if ref($tagInfo) ne 'HASH'; # (shouldn't happen)
295              
296 4042 100       7200 if ($structPtr) {
297 2327         4466 my $flatID = $parentID . ucfirst($tagID);
298 2327         9343 $flatInfo = $$tagTablePtr{$flatID};
299 2327 100       3914 if ($flatInfo) {
    50          
300 2275 50       6377 return if $$flatInfo{PropertyPath};
301             } elsif (@$propList > 50) {
302 0         0 return; # avoid deep recursion
303             } else {
304             # flattened tag doesn't exist, so create it now
305             # (could happen if we were just writing a structure)
306 52         231 $flatInfo = { Name => ucfirst($flatID), Flat => 1 };
307 52         181 AddTagToTable($tagTablePtr, $flatID, $flatInfo);
308             }
309 2327 100       4327 $isType = 1 if $$structPtr{TYPE};
310             } else {
311             # don't override existing main table entry if already set by a Struct
312 1715 50       3732 return if $$tagInfo{PropertyPath};
313             # use property path from original tagInfo if this is an alternate-language tag
314 1715         2808 my $srcInfo = $$tagInfo{SrcTagInfo};
315 1715 100       3526 $$tagInfo{PropertyPath} = GetPropertyPath($srcInfo) if $srcInfo;
316 1715 100       3459 return if $$tagInfo{PropertyPath};
317             # set property path for all flattened tags in structure if necessary
318 1712 100       3830 if ($$tagInfo{RootTagInfo}) {
319 60         375 SetPropertyPath($tagTablePtr, $$tagInfo{RootTagInfo}{TagID});
320 60 50       262 return if $$tagInfo{PropertyPath};
321 0         0 warn "Internal Error: Didn't set path from root for $tagID\n";
322 0         0 warn "(Is the Struct NAMESPACE defined?)\n";
323             }
324             }
325 3979   66     14325 my $ns = $$tagInfo{Namespace} || $$table{NAMESPACE};
326 3979 50       7872 $ns or warn("No namespace for $tagID\n"), return;
327 3979         5741 my (@propList, $listType);
328 3979 100       10044 $propList and @propList = @$propList;
329 3979         9414 push @propList, "$ns:$tagID";
330             # lang-alt lists are handled specially, signified by Writable='lang-alt'
331 3979 100 100     12006 if ($$tagInfo{Writable} and $$tagInfo{Writable} eq 'lang-alt') {
332 102         227 $listType = 'Alt';
333             # remove language code from property path if it exists
334 102 50       286 $propList[-1] =~ s/-$$tagInfo{LangCode}$// if $$tagInfo{LangCode};
335             # handle lists of lang-alt lists (eg. XMP-plus:Custom tags)
336 102 100 66     386 if ($$tagInfo{List} and $$tagInfo{List} ne '1') {
337 3         11 push @propList, "rdf:$$tagInfo{List}", 'rdf:li 10';
338             }
339             } else {
340 3877         6340 $listType = $$tagInfo{List};
341             }
342             # add required properties if this is a list
343 3979 100 66     9170 push @propList, "rdf:$listType", 'rdf:li 10' if $listType and $listType ne '1';
344             # set PropertyPath for all flattened tags of this structure if necessary
345 3979         5859 my $strTable = $$tagInfo{Struct};
346 3979 100 100     9927 if ($strTable and not ($parentID and
      100        
347             # must test NoSubStruct flag to avoid infinite recursion
348             (($$tagTablePtr{$parentID} and $$tagTablePtr{$parentID}{NoSubStruct}) or
349             length $parentID > 500))) # avoid deep recursion
350             {
351             # make sure the structure namespace has been registered
352             # (user-defined namespaces may not have been)
353 231 100       872 RegisterNamespace($strTable) if ref $$strTable{NAMESPACE};
354 231         756 my $tag;
355 231         1623 foreach $tag (keys %$strTable) {
356             # ignore special fields and any lang-alt fields we may have added
357 2826 100 100     10285 next if $specialStruct{$tag} or $$strTable{$tag}{LangCode};
358 2327 100       4800 my $fullID = $parentID ? $parentID . ucfirst($tagID) : $tagID;
359 2327         4662 SetPropertyPath($tagTablePtr, $tag, $fullID, $strTable, \@propList, $isType);
360             }
361             }
362             # if this was a structure field and not a normal tag,
363             # we set PropertyPath in the corresponding flattened tag
364 3979 100       7434 if ($structPtr) {
365 2327         3580 $tagInfo = $flatInfo;
366             # set StructType flag if any containing structure has a TYPE
367 2327 100       4152 $$tagInfo{StructType} = 1 if $isType;
368             }
369             # set property path for tagInfo in main table
370 3979         21931 $$tagInfo{PropertyPath} = join '/', @propList;
371             }
372              
373             #------------------------------------------------------------------------------
374             # Save XMP property name/value for rewriting
375             # Inputs: 0) ExifTool object reference
376             # 1) reference to array of XMP property path (last is current property)
377             # 2) property value, 3) optional reference to hash of property attributes
378             sub CaptureXMP($$$;$)
379             {
380 1149     1149 0 2427 my ($et, $propList, $val, $attrs) = @_;
381 1149 50 33     3865 return unless defined $val and @$propList > 2;
382 1149 100 66     10621 if ($$propList[0] =~ /^x:x[ma]pmeta$/ and
    50 66        
      33        
383             $$propList[1] eq 'rdf:RDF' and
384             $$propList[2] =~ /$rdfDesc( |$)/)
385             {
386             # no properties to save yet if this is just the description
387 1148 100       2833 return unless @$propList > 3;
388             # ignore empty list properties
389 1140 50       2987 if ($$propList[-1] =~ /^rdf:(Bag|Seq|Alt)$/) {
390 0         0 $et->Warn("Ignored empty $$propList[-1] list for $$propList[-2]", 1);
391 0         0 return;
392             }
393             # save information about this property
394 1140         2020 my $capture = $$et{XMP_CAPTURE};
395 1140         3463 my $path = join('/', @$propList[3..$#$propList]);
396 1140 50       2624 if (defined $$capture{$path}) {
397 0         0 $$et{XMP_ERROR} = "Duplicate XMP property: $path";
398             } else {
399 1140   100     6940 $$capture{$path} = [$val, $attrs || { }];
400             }
401             } elsif ($$propList[0] eq 'rdf:RDF' and
402             $$propList[1] =~ /$rdfDesc( |$)/)
403             {
404             # set flag so we don't write x:xmpmeta element
405 1         6 $$et{XMP_NO_XMPMETA} = 1;
406             # add missing x:xmpmeta element and try again
407 1         6 unshift @$propList, 'x:xmpmeta';
408 1         10 CaptureXMP($et, $propList, $val, $attrs);
409             } else {
410 0         0 $$et{XMP_ERROR} = 'Improperly enclosed XMP property: ' . join('/',@$propList);
411             }
412             }
413              
414             #------------------------------------------------------------------------------
415             # Save information about resource containing blank node with nodeID
416             # Inputs: 0) reference to blank node information hash
417             # 1) reference to property list
418             # 2) property value
419             # 3) [optional] reference to attribute hash
420             # Notes: This routine and ProcessBlankInfo() are also used for reading information, but
421             # are uncommon so are put in this file to reduce compile time for the common case
422             sub SaveBlankInfo($$$;$)
423             {
424 30     30 0 82 my ($blankInfo, $propListPt, $val, $attrs) = @_;
425              
426 30         118 my $propPath = join '/', @$propListPt;
427 30         145 my @ids = ($propPath =~ m{ #([^ /]*)}g);
428 30         57 my $id;
429             # split the property path at each nodeID
430 30         58 foreach $id (@ids) {
431 30         405 my ($pre, $prop, $post) = ($propPath =~ m{^(.*?)/([^/]*) #$id((/.*)?)$});
432 30 50       103 defined $pre or warn("internal error parsing nodeID's"), next;
433             # the element with the nodeID should be in the path prefix for subject
434             # nodes and the path suffix for object nodes
435 30 100       80 unless ($prop eq $rdfDesc) {
436 12 100       33 if ($post) {
437 8         20 $post = "/$prop$post";
438             } else {
439 4         16 $pre = "$pre/$prop";
440             }
441             }
442 30         103 $$blankInfo{Prop}{$id}{Pre}{$pre} = 1;
443 30 100 66     177 if ((defined $post and length $post) or (defined $val and length $val)) {
      33        
      66        
444             # save the property value and attributes for each unique path suffix
445 26         186 $$blankInfo{Prop}{$id}{Post}{$post} = [ $val, $attrs, $propPath ];
446             }
447             }
448             }
449              
450             #------------------------------------------------------------------------------
451             # Process blank-node information
452             # Inputs: 0) ExifTool object ref, 1) tag table ref,
453             # 2) blank node information hash ref, 3) flag set for writing
454             sub ProcessBlankInfo($$$;$)
455             {
456 4     4 0 16 my ($et, $tagTablePtr, $blankInfo, $isWriting) = @_;
457 4 100       25 $et->VPrint(1, " [Elements with nodeID set:]\n") unless $isWriting;
458 4         10 my ($id, $pre, $post);
459             # handle each nodeID separately
460 4         12 foreach $id (sort keys %{$$blankInfo{Prop}}) {
  4         30  
461 8         24 my $path = $$blankInfo{Prop}{$id};
462             # flag all resource names so we can warn later if some are unused
463 8         16 my %unused;
464 8         13 foreach $post (keys %{$$path{Post}}) {
  8         34  
465 26         52 $unused{$post} = 1;
466             }
467             # combine property paths for all possible paths through this node
468 8         22 foreach $pre (sort keys %{$$path{Pre}}) {
  8         36  
469             # there will be no description for the object of a blank node
470 16 100       95 next unless $pre =~ m{/$rdfDesc/};
471 8         20 foreach $post (sort keys %{$$path{Post}}) {
  8         42  
472 38         140 my @propList = split m{/}, "$pre$post";
473 38         59 my ($val, $attrs) = @{$$path{Post}{$post}};
  38         103  
474 38 100       77 if ($isWriting) {
475 19         44 CaptureXMP($et, \@propList, $val, $attrs);
476             } else {
477 19         54 FoundXMP($et, $tagTablePtr, \@propList, $val);
478             }
479 38         124 delete $unused{$post};
480             }
481             }
482             # save information from unused properties (if RDF is malformed like f-spot output)
483 8 100       42 if (%unused) {
484 4 50       17 $et->Options('Verbose') and $et->Warn('An XMP resource is about nothing');
485 4         16 foreach $post (sort keys %unused) {
486 8         12 my ($val, $attrs, $propPath) = @{$$path{Post}{$post}};
  8         27  
487 8         29 my @propList = split m{/}, $propPath;
488 8 100       18 if ($isWriting) {
489 4         11 CaptureXMP($et, \@propList, $val, $attrs);
490             } else {
491 4         15 FoundXMP($et, $tagTablePtr, \@propList, $val);
492             }
493             }
494             }
495             }
496             }
497              
498             #------------------------------------------------------------------------------
499             # Convert path to namespace used in file (this is a pain, but the XMP
500             # spec only suggests 'preferred' namespace prefixes...)
501             # Inputs: 0) ExifTool object reference, 1) property path
502             # Returns: conforming property path
503             sub ConformPathToNamespace($$)
504             {
505 8860     8860 0 20121 my ($et, $path) = @_;
506 8860         27305 my @propList = split('/',$path);
507 8860         15913 my $nsUsed = $$et{XMP_NS};
508 8860         11883 my $prop;
509 8860         15093 foreach $prop (@propList) {
510 23910         99050 my ($ns, $tag) = $prop =~ /(.+?):(.*)/;
511 23910 100 66     85517 next if not defined $ns or $$nsUsed{$ns};
512 13786         28209 my $uri = $nsURI{$ns};
513 13786 50       23798 unless ($uri) {
514 0         0 warn "No URI for namespace prefix $ns!\n";
515 0         0 next;
516             }
517 13786         17726 my $ns2;
518 13786         41897 foreach $ns2 (keys %$nsUsed) {
519 74033 50       147291 next unless $$nsUsed{$ns2} eq $uri;
520             # use the existing namespace prefix instead of ours
521 0         0 $prop = "$ns2:$tag";
522 0         0 last;
523             }
524             }
525 8860         32230 return join('/',@propList);
526             }
527              
528             #------------------------------------------------------------------------------
529             # Add necessary rdf:type element when writing structure
530             # Inputs: 0) ExifTool ref, 1) tag table ref, 2) capture hash ref, 3) path string
531             # 4) optional base path (already conformed to namespace) for elements in
532             # variable-namespace structures
533             sub AddStructType($$$$;$)
534             {
535 1     1 0 4 my ($et, $tagTablePtr, $capture, $path, $basePath) = @_;
536 1         16 my @props = split '/', $path;
537 1         4 my %doneID;
538 1         3 for (;;) {
539 5         11 pop @props;
540 5 50       12 last unless @props;
541 5         17 my $tagID = GetXMPTagID(\@props);
542 5 100       16 next if $doneID{$tagID};
543 2         11 $doneID{$tagID} = 1;
544 2         5 my $tagInfo = $$tagTablePtr{$tagID};
545 2 50       7 last unless ref $tagInfo eq 'HASH';
546 2 100       9 if ($$tagInfo{Struct}) {
547 1         4 my $type = $$tagInfo{Struct}{TYPE};
548 1 50       3 if ($type) {
549 1         3 my $pat = $$tagInfo{PropertyPath};
550 1 50       4 $pat or warn("Missing PropertyPath in AddStructType\n"), last;
551 1         4 $pat = ConformPathToNamespace($et, $pat);
552 1         6 $pat =~ s/ \d+/ \\d\+/g;
553 1 50       29 $path =~ /^($pat)/ or warn("Wrong path in AddStructType\n"), last;
554 1         6 my $p = $1 . '/rdf:type';
555 1 50       7 $p = "$basePath/$p" if $basePath;
556 1 50       9 $$capture{$p} = [ '', { 'rdf:resource' => $type } ] unless $$capture{$p};
557             }
558             }
559 2 100       24 last unless $$tagInfo{StructType};
560             }
561             }
562              
563             #------------------------------------------------------------------------------
564             # Process SphericalVideoXML (see XMP-GSpherical tags documentation)
565             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
566             # Returns: SphericalVideoXML data
567             sub ProcessGSpherical($$$)
568             {
569 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
570             # extract SphericalVideoXML as a block if requested
571 0 0       0 if ($$et{REQ_TAG_LOOKUP}{sphericalvideoxml}) {
572 0         0 $et->FoundTag(SphericalVideoXML => substr(${$$dirInfo{DataPt}}, 16));
  0         0  
573             }
574 0         0 return Image::ExifTool::XMP::ProcessXMP($et, $dirInfo, $tagTablePtr);
575             }
576              
577             #------------------------------------------------------------------------------
578             # Hack to use XMP writer for SphericalVideoXML (see XMP-GSpherical tags documentation)
579             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
580             # Returns: SphericalVideoXML data
581             sub WriteGSpherical($$$)
582             {
583 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
584             $$dirInfo{Compact} = 1,
585 0         0 my $dataPt = $$dirInfo{DataPt};
586 0 0 0     0 if ($dataPt and $$dataPt) {
587             # make it look like XMP for writing
588 0         0 my $buff = $$dataPt;
589 0         0 $buff =~ s/\n
590 0         0 $buff =~ s/\s*xmlns:GSpherical/>\n
591 0         0 $buff =~ s/<\/rdf:SphericalVideo>/<\/rdf:Description>/;
592 0         0 $buff .= "";
593 0         0 $$dirInfo{DataPt} = \$buff;
594 0   0     0 $$dirInfo{DirLen} = length($buff) - ($$dirInfo{DirStart} || 0);
595             }
596 0         0 my $xmp = Image::ExifTool::XMP::WriteXMP($et, $dirInfo, $tagTablePtr);
597 0 0       0 if ($xmp) {
598             # change back to rdf:SphericalVideo structure
599 0         0 $xmp =~ s/^<\?xpacket begin.*?
600 0         0 $xmp =~ s/>\s*
601 0         0 $xmp =~ s/\s*<\/rdf:Description>\s*(<\/rdf:RDF>)/\n<\/rdf:SphericalVideo>$1/s;
602 0         0 $xmp =~ s/\s*<\/rdf:RDF>\s*<\/x:xmpmeta>.*//s;
603             }
604 0         0 return $xmp;
605             }
606              
607             #------------------------------------------------------------------------------
608             # Utility routine to encode data in base64
609             # Inputs: 0) binary data string, 1) flag to avoid inserting newlines
610             # Returns: base64-encoded string
611             sub EncodeBase64($;$)
612             {
613             # encode the data in 45-byte chunks
614 8     8 0 34 my $chunkSize = 45;
615 8         31 my $len = length $_[0];
616 8         33 my $str = '';
617 8         18 my $i;
618 8         50 for ($i=0; $i<$len; $i+=$chunkSize) {
619 42         70 my $n = $len - $i;
620 42 100       91 $n = $chunkSize if $n > $chunkSize;
621             # add uuencoded data to output (minus size byte, but including trailing newline)
622 42         196 $str .= substr(pack('u', substr($_[0], $i, $n)), 1);
623             }
624             # convert to base64 (remember that "\0" may be encoded as ' ' or '`')
625 8         43 $str =~ tr/` -_/AA-Za-z0-9+\//;
626             # convert pad characters at the end (remember to account for trailing newline)
627 8         40 my $pad = 3 - ($len % 3);
628 8 50       64 substr($str, -$pad-1, $pad) = ('=' x $pad) if $pad < 3;
629 8 50       40 $str =~ tr/\n//d if $_[1]; # remove newlines if specified
630 8         82 return $str;
631             }
632              
633             #------------------------------------------------------------------------------
634             # sort tagInfo hash references by tag name
635             sub ByTagName
636             {
637 131364     131364 0 206230 return $$a{Name} cmp $$b{Name};
638             }
639              
640             #------------------------------------------------------------------------------
641             # sort alphabetically, but with rdf:type first in the structure
642             sub TypeFirst
643             {
644 7884 100   7884 0 16078 if ($a =~ /rdf:type$/) {
    100          
645 8 100       35 return substr($a, 0, -8) cmp $b unless $b =~ /rdf:type$/;
646             } elsif ($b =~ /rdf:type$/) {
647 11         41 return $a cmp substr($b, 0, -8);
648             }
649 7866         11152 return $a cmp $b;
650             }
651              
652             #------------------------------------------------------------------------------
653             # Limit size of XMP
654             # Inputs: 0) ExifTool object ref, 1) XMP data ref (written up to start of $rdfClose),
655             # 2) max XMP len, 3) rdf:about string, 4) list ref for description start offsets
656             # 5) start offset of first description recommended for extended XMP
657             # Returns: 0) extended XMP ref, 1) GUID and updates $$dataPt (or undef if no extended XMP)
658             sub LimitXMPSize($$$$$$)
659             {
660 36     36 0 200 my ($et, $dataPt, $maxLen, $about, $startPt, $extStart) = @_;
661              
662             # return straight away if it isn't too big
663 36 50       217 return undef if length($$dataPt) < $maxLen;
664              
665 0         0 push @$startPt, length($$dataPt); # add end offset to list
666 0         0 my $newData = substr($$dataPt, 0, $$startPt[0]);
667 0         0 my $guid = '0' x 32;
668             # write the required xmpNote:HasExtendedXMP property
669 0         0 $newData .= "$nl$sp<$rdfDesc rdf:about='${about}'\n$sp${sp}xmlns:xmpNote='$nsURI{xmpNote}'";
670 0 0       0 if ($$et{OPTIONS}{Compact}{Shorthand}) {
671 0         0 $newData .= "\n$sp${sp}xmpNote:HasExtendedXMP='${guid}'/>\n";
672             } else {
673 0         0 $newData .= ">$nl$sp$sp$guid$nl$sp\n";
674             }
675              
676 0         0 my ($i, %descSize, $start);
677             # calculate all description block sizes
678 0         0 for ($i=1; $i<@$startPt; ++$i) {
679 0         0 $descSize{$$startPt[$i-1]} = $$startPt[$i] - $$startPt[$i-1];
680             }
681 0         0 pop @$startPt; # remove end offset
682             # write the descriptions from smallest to largest, as many in main XMP as possible
683 0         0 my @descStart = sort { $descSize{$a} <=> $descSize{$b} } @$startPt;
  0         0  
684 0         0 my $extData = XMPOpen($et) . $rdfOpen;
685 0         0 for ($i=0; $i<2; ++$i) {
686 0         0 foreach $start (@descStart) {
687             # write main XMP first (in order of size), then extended XMP afterwards (in order)
688 0 0 0     0 next if $i xor $start >= $extStart;
689 0 0       0 my $pt = (length($newData) + $descSize{$start} > $maxLen) ? \$extData : \$newData;
690 0         0 $$pt .= substr($$dataPt, $start, $descSize{$start});
691             }
692             }
693 0         0 $extData .= $rdfClose . $xmpClose; # close rdf:RDF and x:xmpmeta
694             # calculate GUID from MD5 of extended XMP data
695 0 0       0 if (eval { require Digest::MD5 }) {
  0         0  
696 0         0 $guid = uc unpack('H*', Digest::MD5::md5($extData));
697 0         0 $newData =~ s/0{32}/$guid/; # update GUID in main XMP segment
698             }
699 0         0 $et->VerboseValue('+ XMP-xmpNote:HasExtendedXMP', $guid);
700 0         0 $$dataPt = $newData; # return main XMP block
701 0         0 return (\$extData, $guid); # return extended XMP and its GUID
702             }
703              
704             #------------------------------------------------------------------------------
705             # Close out bottom-level property
706             # Inputs: 0) current property path list ref, 1) longhand properties at each resource
707             # level, 2) shorthand properties at each resource level, 3) resource flag for
708             # each property path level (set only if Shorthand is enabled)
709             sub CloseProperty($$$$)
710             {
711 1066     1066 0 2197 my ($curPropList, $long, $short, $resFlag) = @_;
712              
713 1066         1846 my $prop = pop @$curPropList;
714 1066         2285 $prop =~ s/ .*//; # remove list index if it exists
715 1066         2220 my $pad = $sp x (scalar(@$curPropList) + 1);
716 1066 100       2838 if ($$resFlag[@$curPropList]) {
    100          
717             # close this XMP structure with possible shorthand properties
718 1 50       9 if (length $$short[-1]) {
719 1 50       6 if (length $$long[-1]) {
720             # require a new Description if both longhand and shorthand properties
721 0         0 $$long[-2] .= ">$nl$pad<$rdfDesc";
722 0         0 $$short[-1] .= ">$nl";
723 0         0 $$long[-1] .= "$pad$nl";
724             } else {
725             # simply close empty property if all shorthand
726 1         4 $$short[-1] .= "/>$nl";
727             }
728             } else {
729             # use "parseType" instead of opening a new Description
730 0         0 $$long[-2] .= ' rdf:parseType="Resource"';
731 0 0       0 $$short[-1] = length $$long[-1] ? ">$nl" : "/>$nl";
732             }
733 1 50       12 $$long[-1] .= "$pad$nl" if length $$long[-1];
734 1         6 $$long[-2] .= $$short[-1] . $$long[-1];
735 1         3 pop @$short;
736 1         4 pop @$long;
737             } elsif (defined $$resFlag[@$curPropList]) {
738             # close this top level Description with possible shorthand values
739 6 100       23 if (length $$long[-1]) {
740 3         16 $$long[-2] .= $$short[-1] . ">$nl" . $$long[-1] . "$pad$nl";
741             } else {
742 3         12 $$long[-2] .= $$short[-1] . "/>$nl"; # empty element (ie. all shorthand)
743             }
744 6         13 $$short[-1] = $$long[-1] = '';
745             } else {
746             # close this property (no chance of shorthand)
747 1059         2567 $$long[-1] .= "$pad$nl";
748 1059 100       2271 unless (@$curPropList) {
749             # add properties now that this top-level Description is complete
750 301         1660 $$long[-2] .= ">$nl" . $$long[-1];
751 301         712 $$long[-1] = '';
752             }
753             }
754 1066         4230 $#$resFlag = $#$curPropList; # remove expired resource flags
755             }
756              
757             #------------------------------------------------------------------------------
758             # Write XMP information
759             # Inputs: 0) ExifTool ref, 1) source dirInfo ref (with optional WriteGroup),
760             # 2) [optional] tag table ref
761             # Returns: with tag table: new XMP data (may be empty if no XMP data) or undef on error
762             # without tag table: 1 on success, 0 if not valid XMP file, -1 on write error
763             # Notes: May set dirInfo InPlace flag to rewrite with specified DirLen (=2 to allow larger)
764             # May set dirInfo ReadOnly flag to write as read-only XMP ('r' mode and no padding)
765             # May set dirInfo Compact flag to force compact (drops 2kB of padding)
766             # May set dirInfo MaxDataLen to limit output data length -- this causes ExtendedXMP
767             # and ExtendedGUID to be returned in dirInfo if extended XMP was required
768             sub WriteXMP($$;$)
769             {
770 6182     6182 0 12727 my ($et, $dirInfo, $tagTablePtr) = @_;
771 6182 100       24739 $et or return 1; # allow dummy access to autoload this package
772 124         527 my $dataPt = $$dirInfo{DataPt};
773 124         408 my (%capture, %nsUsed, $xmpErr, $about);
774 124         270 my $changed = 0;
775 124         461 my $xmpFile = (not $tagTablePtr); # this is an XMP data file if no $tagTablePtr
776             # prefer XMP over other metadata formats in some types of files
777 124   100     865 my $preferred = $xmpFile || ($$et{PreferredGroup} and $$et{PreferredGroup} eq 'XMP');
778 124         399 my $verbose = $$et{OPTIONS}{Verbose};
779 124         289 my %compact = ( %{$$et{OPTIONS}{Compact}} ); # (make a copy so we can change settings)
  124         622  
780 124         371 my $dirLen = $$dirInfo{DirLen};
781 124 100 100     815 $dirLen = length($$dataPt) if not defined $dirLen and $dataPt;
782             #
783             # extract existing XMP information into %capture hash
784             #
785             # define hash in ExifTool object to capture XMP information (also causes
786             # CaptureXMP() instead of FoundXMP() to be called from ParseXMPElement())
787             #
788             # The %capture hash is keyed on the complete property path beginning after
789             # rdf:RDF/rdf:Description/. The values are array references with the
790             # following entries: 0) value, 1) attribute hash reference.
791 124         434 $$et{XMP_CAPTURE} = \%capture;
792 124         468 $$et{XMP_NS} = \%nsUsed;
793 124         347 delete $$et{XMP_NO_XMPMETA};
794 124         670 delete $$et{XMP_NO_XPACKET};
795 124         297 delete $$et{XMP_IS_XML};
796 124         296 delete $$et{XMP_IS_SVG};
797              
798             # set current padding characters
799 124 50       746 ($sp, $nl) = ($compact{NoIndent} ? '' : ' ', $compact{NoNewline} ? '' : "\n");
    50          
800              
801             # get value for new rdf:about
802 124         445 my $tagInfo = $Image::ExifTool::XMP::rdf{about};
803 124 100       584 if (defined $$et{NEW_VALUE}{$tagInfo}) {
804 1   50     7 $about = $et->GetNewValue($$et{NEW_VALUE}{$tagInfo}) || '';
805             }
806              
807 124 100 100     874 if ($xmpFile or $dirLen) {
    50          
808 72         184 delete $$et{XMP_ERROR};
809             # extract all existing XMP information (to the XMP_CAPTURE hash)
810 72         489 my $success = ProcessXMP($et, $dirInfo, $tagTablePtr);
811             # don't continue if there is nothing to parse or if we had a parsing error
812 72 100 66     636 unless ($success and not $$et{XMP_ERROR}) {
813 16   50     108 my $err = $$et{XMP_ERROR} || 'Error parsing XMP';
814             # may ignore this error only if we were successful
815 16 50       629 if ($xmpFile) {
816 16         50 my $raf = $$dirInfo{RAF};
817             # allow empty XMP data so we can create something from nothing
818 16 50 33     102 if ($success or not $raf->Seek(0,2) or $raf->Tell()) {
      33        
819             # no error message if not an XMP file
820 0 0       0 return 0 unless $$et{XMP_ERROR};
821 0 0       0 if ($et->Error($err, $success)) {
822 0         0 delete $$et{XMP_CAPTURE};
823 0         0 return 0;
824             }
825             }
826             } else {
827 0 0 0     0 $success = 2 if $success and $success eq '1';
828 0 0       0 if ($et->Warn($err, $success)) {
829 0         0 delete $$et{XMP_CAPTURE};
830 0         0 return undef;
831             }
832             }
833             }
834 72 100       285 if (defined $about) {
835 1 50       5 if ($verbose > 1) {
836 0         0 my $wasAbout = $$et{XmpAbout};
837 0 0       0 $et->VerboseValue('- XMP-rdf:About', UnescapeXML($wasAbout)) if defined $wasAbout;
838 0         0 $et->VerboseValue('+ XMP-rdf:About', $about);
839             }
840 1         5 $about = EscapeXML($about); # must escape for XML
841 1         4 ++$changed;
842             } else {
843 71   100     394 $about = $$et{XmpAbout} || '';
844             }
845 72         188 delete $$et{XMP_ERROR};
846              
847             # call InitWriteDirs to initialize FORCE_WRITE flags if necessary
848 72 50 66     653 $et->InitWriteDirs({}, 'XMP') if $xmpFile and $et->GetNewValue('ForceWrite');
849             # set changed if we are ForceWrite tag was set to "XMP"
850 72 50       341 ++$changed if $$et{FORCE_WRITE}{XMP};
851              
852             } elsif (defined $about) {
853 0         0 $et->VerboseValue('+ XMP-rdf:About', $about);
854 0         0 $about = EscapeXML($about); # must escape for XML
855             # (don't increment $changed here because we need another tag to be written)
856             } else {
857 52         165 $about = '';
858             }
859             #
860             # handle writing XMP as a block to XMP file
861             #
862 124 100       916 if ($xmpFile) {
863 35         117 $tagInfo = $Image::ExifTool::Extra{XMP};
864 35 50 33     266 if ($tagInfo and $$et{NEW_VALUE}{$tagInfo}) {
865 0         0 my $rtnVal = 1;
866 0         0 my $newVal = $et->GetNewValue($$et{NEW_VALUE}{$tagInfo});
867 0 0 0     0 if (defined $newVal and length $newVal) {
868 0         0 $et->VPrint(0, " Writing XMP as a block\n");
869 0         0 ++$$et{CHANGED};
870 0 0       0 Write($$dirInfo{OutFile}, $newVal) or $rtnVal = -1;
871             }
872 0         0 delete $$et{XMP_CAPTURE};
873 0         0 return $rtnVal;
874             }
875             }
876             #
877             # delete groups in family 1 if requested
878             #
879 124 100 66     266 if (%{$$et{DEL_GROUP}} and (grep /^XMP-.+$/, keys %{$$et{DEL_GROUP}} or
  124   66     608  
880             # (logic is a bit more complex for group names in exiftool XML files)
881             grep m{^http://ns.exiftool.(?:ca|org)/}, values %nsUsed))
882             {
883 12         45 my $del = $$et{DEL_GROUP};
884 12         31 my $path;
885 12         74 foreach $path (keys %capture) {
886 141         367 my @propList = split('/',$path); # get property list
887 141         349 my ($tag, $ns) = GetXMPTagID(\@propList);
888             # translate namespace if necessary
889 141 50       364 $ns = $stdXlatNS{$ns} if $stdXlatNS{$ns};
890 141         216 my ($grp, @g);
891             # no "XMP-" added to most groups in exiftool RDF/XML output file
892 141 100 66     722 if ($nsUsed{$ns} and (@g = ($nsUsed{$ns} =~ m{^http://ns.exiftool.(?:ca|org)/(.*?)/(.*?)/}))) {
893 78 100       190 if ($g[1] =~ /^\d/) {
894 20         49 $grp = "XML-$g[0]";
895             #(all XML-* groups stored as uppercase DEL_GROUP key)
896 20         39 my $ucg = uc $grp;
897 20 100 66     129 next unless $$del{$ucg} or ($$del{'XML-*'} and not $$del{"-$ucg"});
      66        
898             } else {
899 58         93 $grp = $g[1];
900 58 100 66     233 next unless $$del{$grp} or ($$del{$g[0]} and not $$del{"-$grp"});
      100        
901             }
902             } else {
903 63         111 $grp = "XMP-$ns";
904 63         110 my $ucg = uc $grp;
905 63 100 100     330 next unless $$del{$ucg} or ($$del{'XMP-*'} and not $$del{"-$ucg"});
      100        
906             }
907 91         459 $et->VerboseValue("- $grp:$tag", $capture{$path}->[0]);
908 91         277 delete $capture{$path};
909 91         246 ++$changed;
910             }
911             }
912             # delete HasExtendedXMP tag (we create it as needed)
913 124         435 my $hasExtTag = 'xmpNote:HasExtendedXMP';
914 124 100       505 if ($capture{$hasExtTag}) {
915 1         11 $et->VerboseValue("- XMP-$hasExtTag", $capture{$hasExtTag}->[0]);
916 1         4 delete $capture{$hasExtTag};
917             }
918             # set $xmpOpen now to to handle xmptk tag first
919 124 100       978 my $xmpOpen = $$et{XMP_NO_XMPMETA} ? '' : XMPOpen($et);
920             #
921             # add, delete or change information as specified
922             #
923             # get hash of all information we want to change
924             # (sorted by tag name so alternate languages come last, but with structures
925             # first so flattened tags may be used to override individual structure elements)
926 124         346 my (@tagInfoList, $delLangPath, %delLangPaths, %delAllLang, $firstNewPath);
927 124         375 my $writeGroup = $$dirInfo{WriteGroup};
928 124         723 foreach $tagInfo (sort ByTagName $et->GetNewTagInfoList()) {
929 15039 100       33020 next unless $et->GetGroup($tagInfo, 0) eq 'XMP';
930 4311 50       9746 next if $$tagInfo{Name} eq 'XMP'; # (ignore full XMP block if we didn't write it already)
931 4311 50 66     8149 next if $writeGroup and $writeGroup ne $$et{NEW_VALUE}{$tagInfo}{WriteGroup};
932 4311 100       7368 if ($$tagInfo{Struct}) {
933 196         602 unshift @tagInfoList, $tagInfo;
934             } else {
935 4115         8414 push @tagInfoList, $tagInfo;
936             }
937             }
938 124         2035 foreach $tagInfo (@tagInfoList) {
939 4311         7264 my @delPaths; # list of deleted paths
940 4311         22088 my $tag = $$tagInfo{TagID};
941 4311         10598 my $path = GetPropertyPath($tagInfo);
942 4311 50       9770 unless ($path) {
943 0         0 $et->Warn("Can't write XMP:$tag (namespace unknown)");
944 0         0 next;
945             }
946             # skip tags that were handled specially
947 4311 100 100     14393 if ($path eq 'rdf:about' or $path eq 'x:xmptk') {
948 2         4 ++$changed;
949 2         13 next;
950             }
951 4309         7730 my $isStruct = $$tagInfo{Struct};
952             # change our property path namespace prefixes to conform
953             # to the ones used in this file
954 4309         10257 $path = ConformPathToNamespace($et, $path);
955             # find existing property
956 4309         10013 my $cap = $capture{$path};
957             # MicrosoftPhoto screws up the case of some tags, and some other software,
958             # including Adobe software, has been known to write the wrong list type or
959             # not properly enclose properties in a list, so we check for this
960 4309         8676 until ($cap) {
961             # find and fix all incorrect property names if this is a structure or a flattened tag
962 4179         6599 my @fixInfo;
963 4179 100 100     16866 if ($isStruct or defined $$tagInfo{Flat}) {
964             # get tagInfo for all containing (possibly nested) structures
965 2173         6892 my @props = split '/', $path;
966 2173         5016 my $tbl = $$tagInfo{Table};
967 2173         5677 while (@props) {
968 12171         31996 my $info = $$tbl{GetXMPTagID(\@props)};
969             unshift @fixInfo, $info if ref $info eq 'HASH' and $$info{Struct} and
970 12171 100 66     78139 (not @fixInfo or $fixInfo[0] ne $info);
      100        
      100        
971 12171         31641 pop @props;
972             }
973 2173 50       5856 $et->WarnOnce("Error finding parent structure for $$tagInfo{Name}") unless @fixInfo;
974             }
975             # fix property path for this tag (last in the @fixInfo list)
976 4179 100 100     14136 push @fixInfo, $tagInfo unless @fixInfo and $isStruct;
977             # start from outermost containing structure, fixing incorrect list types, etc,
978             # finally fixing the actual tag properties after all containing structures
979 4179         7262 my $err;
980 4179         9191 while (@fixInfo) {
981 4454         7091 my $fixInfo = shift @fixInfo;
982 4454         11114 my $fixPath = ConformPathToNamespace($et, GetPropertyPath($fixInfo));
983 4454         11413 my $regex = quotemeta($fixPath);
984 4454         13384 $regex =~ s/ \d+/ \\d\+/g; # match any list index
985 4454         7769 my $ok = $regex;
986 4454         7355 my ($ok2, $match, $i, @fixed, %fixed, $fixed);
987             # check for incorrect list types
988 4454 100       16509 if ($regex =~ s{\\/rdf\\:(Bag|Seq|Alt)\\/}{/rdf:(Bag|Seq|Alt)/}g) {
    100          
989             # also look for missing bottom-level list
990 1971 100       7818 if ($regex =~ s{/rdf:\(Bag\|Seq\|Alt\)\/rdf\\:li\\ \\d\+$}{}) {
991 1958 100       4744 $regex .= '(/.*)?' unless @fixInfo;
992             }
993             } elsif (not @fixInfo) {
994 2005         3749 $ok2 = $regex;
995             # check for properties in lists that shouldn't be (ref forum4325)
996 2005         3773 $regex .= '(/rdf:(Bag|Seq|Alt)/rdf:li \d+)?';
997             }
998 4454 100       11580 if (@fixInfo) {
999 2083         3698 $regex .= '(/.*)?';
1000 2083         4049 $ok .= '(/.*)?';
1001             }
1002 4454         237584 my @matches = sort grep m{^$regex$}i, keys %capture;
1003 4454 100       23697 last unless @matches;
1004 279 100       2014 if ($matches[0] =~ m{^$ok$}) {
1005 274 50       673 unless (@fixInfo) {
1006 0         0 $path = $matches[0];
1007 0         0 $cap = $capture{$path};
1008             }
1009 274         1063 next;
1010             }
1011             # needs fixing...
1012 5         34 my @fixProps = split '/', $fixPath;
1013 5         21 foreach $match (@matches) {
1014 7         25 my @matchProps = split '/', $match;
1015             # remove superfluous list properties if necessary
1016 7 100 66     34 $#matchProps = $#fixProps if $ok2 and $#matchProps > $#fixProps;
1017 7         28 for ($i=0; $i<@fixProps; ++$i) {
1018 19 50       50 defined $matchProps[$i] or $matchProps[$i] = $fixProps[$i], next;
1019 19 100 100     106 next if $matchProps[$i] =~ / \d+$/ or $matchProps[$i] eq $fixProps[$i];
1020 4         10 $matchProps[$i] = $fixProps[$i];
1021             }
1022 7         27 $fixed = join '/', @matchProps;
1023 7 50 66     48 $err = 1 if $fixed{$fixed} or ($capture{$fixed} and $match ne $fixed);
      33        
1024 7         19 push @fixed, $fixed;
1025 7         25 $fixed{$fixed} = 1;
1026             }
1027 5         31 my $tg = $et->GetGroup($fixInfo, 1) . ':' . $$fixInfo{Name};
1028 5 100       29 my $wrn = lc($fixed[0]) eq lc($matches[0]) ? 'tag ID case' : 'list type';
1029 5 50       19 if ($err) {
1030 0         0 $et->Warn("Incorrect $wrn for existing $tg (not changed)");
1031             } else {
1032             # fix the incorrect property paths for all values of this tag
1033 5         13 my $didFix;
1034 5         12 foreach $fixed (@fixed) {
1035 7         19 my $match = shift @matches;
1036 7 100       31 next if $fixed eq $match;
1037 5         10 $capture{$fixed} = $capture{$match};
1038 5         11 delete $capture{$match};
1039             # remove xml:lang attribute from incorrect lang-alt list if necessary
1040 5 100 66     29 delete $capture{$fixed}[1]{'xml:lang'} if $ok2 and $match !~ /^$ok2$/;
1041 5         9 $didFix = 1;
1042             }
1043 5 100 66     35 $cap = $capture{$path} || $capture{$fixed[0]} unless @fixInfo;
1044 5 100       28 if ($didFix) {
1045 3         17 $et->Warn("Fixed incorrect $wrn for $tg", 1);
1046 3         16 ++$changed;
1047             }
1048             }
1049             }
1050 4179         8032 last;
1051             }
1052 4309         18684 my $nvHash = $et->GetNewValueHash($tagInfo);
1053 4309         12920 my $overwrite = $et->IsOverwriting($nvHash);
1054 4309   100     16952 my $writable = $$tagInfo{Writable} || '';
1055 4309         8661 my (%attrs, $deleted, $added, $existed, $newLang);
1056             # set up variables to save/restore paths of deleted lang-alt tags
1057 4309 100       9487 if ($writable eq 'lang-alt') {
1058 169   100     903 $newLang = lc($$tagInfo{LangCode} || 'x-default');
1059 169 100 100     726 if ($delLangPath and $delLangPath eq $path) {
1060             # restore paths of deleted entries for this language
1061 7 100       23 @delPaths = @{$delLangPaths{$newLang}} if $delLangPaths{$newLang};
  2         7  
1062             } else {
1063 162         398 undef %delLangPaths;
1064 162         337 $delLangPath = $path; # base path for deleted lang-alt tags
1065 162         292 undef %delAllLang;
1066 162         294 undef $firstNewPath; # reset first path for new lang-alt tag
1067             }
1068 169 100       458 if (%delAllLang) {
1069             # add missing paths to delete list for entries where all languages were deleted
1070 2         5 my ($prefix, $reSort);
1071 2         10 foreach $prefix (keys %delAllLang) {
1072 6 100       88 next if grep /^$prefix/, @delPaths;
1073 1         6 push @delPaths, "${prefix}10";
1074 1         5 $reSort = 1;
1075             }
1076 2 100       12 @delPaths = sort @delPaths if $reSort;
1077             }
1078             }
1079             # delete existing entry if necessary
1080 4309 100       11193 if ($isStruct) {
    100          
1081             # delete all structure (or pseudo-structure) elements
1082 196         1195 require 'Image/ExifTool/XMPStruct.pl';
1083 196         828 ($deleted, $added, $existed) = DeleteStruct($et, \%capture, \$path, $nvHash, \$changed);
1084 196 50 100     1200 next unless $deleted or $added or $et->IsOverwriting($nvHash);
      66        
1085 196 100 100     540 next if $existed and $$nvHash{CreateOnly};
1086             } elsif ($cap) {
1087 132 100       473 next if $$nvHash{CreateOnly}; # (necessary for List-type tags)
1088             # take attributes from old values if they exist
1089 130         247 %attrs = %{$$cap[1]};
  130         537  
1090 130 100       314 if ($overwrite) {
1091 126         268 my ($oldLang, $delLang, $addLang, @matchingPaths, $langPathPat, %langsHere);
1092             # check to see if this is an indexed list item
1093 126 100       433 if ($path =~ / /) {
1094 44         107 my $pp;
1095 44         295 ($pp = $path) =~ s/ \d+/ \\d\+/g;
1096 44         1640 @matchingPaths = sort grep(/^$pp$/, keys %capture);
1097             } else {
1098 82         183 push @matchingPaths, $path;
1099             }
1100 126         318 my $oldOverwrite = $overwrite;
1101 126         266 foreach $path (@matchingPaths) {
1102 181         297 my ($val, $attrs) = @{$capture{$path}};
  181         588  
1103 181 100       569 if ($writable eq 'lang-alt') {
    100          
1104             # get original language code (lc for comparisons)
1105 60   50     187 $oldLang = lc($$attrs{'xml:lang'} || 'x-default');
1106             # revert to original overwrite flag if this is in a different structure
1107 60 100 100     526 if (not $langPathPat or $path !~ /^$langPathPat$/) {
1108 38         73 $overwrite = $oldOverwrite;
1109 38         211 ($langPathPat = $path) =~ s/\d+$/\\d+/;
1110             }
1111             # remember languages in this lang-alt list
1112 60         232 $langsHere{$langPathPat}{$oldLang} = 1;
1113 60 100       172 unless (defined $addLang) {
1114             # add to lang-alt list by default if creating this tag from scratch
1115 24 100       78 $addLang = $$nvHash{IsCreating} ? 1 : 0;
1116             }
1117 60 100       139 if ($overwrite < 0) {
1118 13 100       36 next unless $oldLang eq $newLang;
1119             # only add new tag if we are overwriting this one
1120             # (note: this won't match if original XML contains CDATA!)
1121 8         35 $addLang = $et->IsOverwriting($nvHash, UnescapeXML($val));
1122 8 100       38 next unless $addLang;
1123             }
1124             # delete all if deleting "x-default" and writing with no LangCode
1125             # (XMP spec requires x-default language exist and be first in list)
1126 50 100 100     331 if ($oldLang eq 'x-default' and not $$tagInfo{LangCode}) {
    100 66        
1127 13         23 $delLang = 1; # delete all languages
1128 13         25 $overwrite = 1; # force overwrite
1129             } elsif ($$tagInfo{LangCode} and not $delLang) {
1130             # only overwrite specified language
1131 31 100       122 next unless lc($$tagInfo{LangCode}) eq $oldLang;
1132             }
1133             } elsif ($overwrite < 0) {
1134             # only overwrite specific values
1135 7 100       29 if ($$nvHash{Shift}) {
1136             # values to be shifted are checked (hence re-formatted) late,
1137             # so we must un-format the to-be-shifted value for IsOverwriting()
1138 3   50     14 my $fmt = $$tagInfo{Writable} || '';
1139 3 100       20 if ($fmt eq 'rational') {
    50          
1140 1         8 ConvertRational($val);
1141             } elsif ($fmt eq 'date') {
1142 2         16 $val = ConvertXMPDate($val);
1143             }
1144             }
1145             # (note: this won't match if original XML contains CDATA!)
1146 7 100       30 next unless $et->IsOverwriting($nvHash, UnescapeXML($val));
1147             }
1148 143 50       394 if ($verbose > 1) {
1149 0         0 my $grp = $et->GetGroup($tagInfo, 1);
1150 0         0 my $tagName = $$tagInfo{Name};
1151 0 0       0 $tagName =~ s/-$$tagInfo{LangCode}$// if $$tagInfo{LangCode};
1152 0 0       0 $tagName .= '-' . $$attrs{'xml:lang'} if $$attrs{'xml:lang'};
1153 0         0 $et->VerboseValue("- $grp:$tagName", $val);
1154             }
1155             # save attributes and path from first deleted property
1156             # so we can replace it exactly
1157 143 100       436 %attrs = %$attrs unless @delPaths;
1158 143 100       363 if ($writable eq 'lang-alt') {
1159 23         66 $langsHere{$langPathPat}{$oldLang} = 0; # (lang was deleted)
1160             }
1161             # save deleted paths so we can replace the same elements
1162             # (separately for each language of a lang-alt list)
1163 143 100 100     509 if ($writable ne 'lang-alt' or $oldLang eq $newLang) {
1164 137         288 push @delPaths, $path;
1165             } else {
1166 6 100       24 $delLangPaths{$oldLang} or $delLangPaths{$oldLang} = [ ];
1167 6         10 push @{$delLangPaths{$oldLang}}, $path;
  6         15  
1168             }
1169             # keep track of paths where we deleted all languages of a lang-alt tag
1170 143 100       324 if ($delLang) {
1171 19         37 my $p;
1172 19         96 ($p = $path) =~ s/\d+$//;
1173 19         68 $delAllLang{$p} = 1;
1174             }
1175             # delete this tag
1176 143         391 delete $capture{$path};
1177 143         681 ++$changed;
1178             # delete rdf:type tag if it is the only thing left in this structure
1179 143 50 66     904 if ($path =~ /^(.*)\// and $capture{"$1/rdf:type"}) {
1180 0         0 my $pp = $1;
1181 0         0 my @a = grep /^\Q$pp\E\/[^\/]+/, keys %capture;
1182 0 0       0 delete $capture{"$pp/rdf:type"} if @a == 1;
1183             }
1184             }
1185 126 100 100     463 next unless @delPaths or $$tagInfo{List} or $addLang;
      100        
1186 125 100       405 if (@delPaths) {
1187 118         239 $path = shift @delPaths;
1188             # make sure new path is unique
1189 118         333 while ($capture{$path}) {
1190 0 0       0 last unless $path =~ s/ \d(\d+)$/' '.length($1+1).($1+1)/e;
  0         0  
1191             }
1192 118         310 $deleted = 1;
1193             } else {
1194             # don't change tag if we couldn't delete old copy
1195             # unless this is a list or an lang-alt tag
1196 7 50 66     34 next unless $$tagInfo{List} or $oldLang;
1197             # avoid adding duplicate entry to lang-alt in a list
1198 7 50 33     43 if ($writable eq 'lang-alt' and %langsHere) {
1199 7         34 foreach (sort keys %langsHere) {
1200 9 50       131 next unless $path =~ /^$_$/;
1201 9 100       36 last unless $langsHere{$_}{$newLang};
1202 3 50       20 $path =~ /(.* )\d(\d+)(.*? \d+)$/ or $et->Error('Internal error writing lang-alt list'), last;
1203 3         11 my $nxt = $2 + 1;
1204 3         15 $path = $1 . length($nxt) . ($nxt) . $3; # step to next index
1205             }
1206             }
1207             # (match last index to put in same lang-alt list for Bag of lang-alt items)
1208 7 50       45 $path =~ m/.* (\d+)/g or warn "Internal error: no list index!\n", next;
1209 7         39 $added = $1;
1210             }
1211             } else {
1212             # we are never overwriting, so we must be adding to a list
1213             # match the last index unless this is a list of lang-alt lists
1214 4         15 my $pat = '.* (\d+)';
1215 4 100       13 if ($writable eq 'lang-alt') {
1216 2 100       9 if ($firstNewPath) {
1217 1         2 $path = $firstNewPath;
1218 1         3 $overwrite = 1; # necessary to put x-default entry first below
1219             } else {
1220 1         3 $pat = '.* (\d+)(.*? \d+)';
1221             }
1222             }
1223 4 50       67 if ($path =~ m/$pat/g) {
1224 4         14 $added = $1;
1225             # set position to end of matching index number
1226 4 100       19 pos($path) = pos($path) - length($2) if $2;
1227             }
1228             }
1229 129 100       364 if (defined $added) {
1230 11         24 my $len = length $added;
1231 11         28 my $pos = pos($path) - $len;
1232 11         37 my $nxt = substr($added, 1) + 1;
1233             # always insert x-default lang-alt entry first (as per XMP spec)
1234             # (need to test $overwrite because this will be a new lang-alt entry otherwise)
1235 11 100 66     100 if ($overwrite and $writable eq 'lang-alt' and (not $$tagInfo{LangCode} or
      100        
      100        
1236             $$tagInfo{LangCode} eq 'x-default'))
1237             {
1238 2         6 my $saveCap = $capture{$path};
1239 2         9 while ($saveCap) {
1240 1         3 my $p = $path;
1241 1         6 substr($p, $pos, $len) = length($nxt) . $nxt;
1242             # increment index in the path of the existing item
1243 1         5 my $nextCap = $capture{$p};
1244 1         4 $capture{$p} = $saveCap;
1245 1 50       6 last unless $nextCap;
1246 0         0 $saveCap = $nextCap;
1247 0         0 ++$nxt;
1248             }
1249             } else {
1250             # add to end of list
1251 9         34 while ($capture{$path}) {
1252 16         42 my $try = length($nxt) . $nxt;
1253 16         37 substr($path, $pos, $len) = $try;
1254 16         26 $len = length $try;
1255 16         43 ++$nxt;
1256             }
1257             }
1258             }
1259             }
1260             # check to see if we want to create this tag
1261             # (create non-avoided tags in XMP data files by default)
1262             my $isCreating = ($$nvHash{IsCreating} or (($isStruct or
1263             ($preferred and not $$tagInfo{Avoid} and
1264 4304   100     31313 not defined $$nvHash{Shift})) and not $$nvHash{EditOnly}));
1265              
1266             # don't add new values unless...
1267             # ...tag existed before and was deleted, or we added it to a list
1268 4304 100 100     24618 next unless $deleted or defined $added or
      66        
      100        
1269             # ...tag didn't exist before and we are creating it
1270             (not $cap and $isCreating);
1271              
1272             # get list of new values (all done if no new values specified)
1273 2660 100       9398 my @newValues = $et->GetNewValue($nvHash) or next;
1274              
1275             # set language attribute for lang-alt lists
1276 879 100       2222 if ($writable eq 'lang-alt') {
1277 73   100     432 $attrs{'xml:lang'} = $$tagInfo{LangCode} || 'x-default';
1278 73 100       234 $firstNewPath = $path if defined $added; # save path of first lang-alt tag added
1279             }
1280             # add new value(s) to %capture hash
1281 879         1632 my $subIdx;
1282 879         1333 for (;;) {
1283 1001         1879 my $newValue = shift @newValues;
1284 1001 100       2191 if ($isStruct) {
1285             ++$changed if AddNewStruct($et, $tagInfo, \%capture,
1286 30 50       213 $path, $newValue, $$tagInfo{Struct});
1287             } else {
1288 971         3095 $newValue = EscapeXML($newValue);
1289 971         1713 for (;;) { # (a cheap 'goto')
1290 971 100       2456 if ($$tagInfo{Resource}) {
1291             # only store as a resource if it doesn't contain any illegal characters
1292 3 50       17 if ($newValue !~ /[^a-z0-9\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\.\-\_\~]/i) {
1293 3         24 $capture{$path} = [ '', { %attrs, 'rdf:resource' => $newValue } ];
1294 3         9 last;
1295             }
1296 0         0 my $grp = $et->GetGroup($tagInfo, 1);
1297 0         0 $et->Warn("$grp:$$tagInfo{Name} written as a literal because value is not a valid URI", 1);
1298             # fall through to write as a string literal
1299             }
1300             # remove existing value and/or resource attribute if they exist
1301 968         1573 delete $attrs{'rdf:value'};
1302 968         1449 delete $attrs{'rdf:resource'};
1303 968         4420 $capture{$path} = [ $newValue, \%attrs ];
1304 968         1743 last;
1305             }
1306 971 100       2225 if ($verbose > 1) {
1307 1         7 my $grp = $et->GetGroup($tagInfo, 1);
1308 1         16 $et->VerboseValue("+ $grp:$$tagInfo{Name}", $newValue);
1309             }
1310 971         1470 ++$changed;
1311             # add rdf:type if necessary
1312 971 50       2306 if ($$tagInfo{StructType}) {
1313 0         0 AddStructType($et, $$tagInfo{Table}, \%capture, $path);
1314             }
1315             }
1316 1001 100       2481 last unless @newValues;
1317             # match last index except for lang-alt items where we want to put each
1318             # item in a different lang-alt list (so match the 2nd-last for these)
1319 122 100       460 my $pat = $writable eq 'lang-alt' ? '.* (\d+)(.*? \d+)' : '.* (\d+)';
1320 122         342 pos($path) = 0;
1321 122 50       929 $path =~ m/$pat/g or warn("Internal error: no list index for $tag ($path) ($pat)!\n"), next;
1322 122         374 my $idx = $1;
1323 122         300 my $len = length $1;
1324 122 100       430 my $pos = pos($path) - $len - ($2 ? length $2 : 0);
1325             # use sub-indices if necessary to store additional values in sequence
1326 122 100       353 if ($subIdx) {
    100          
1327 51         215 $idx = substr($idx, 0, -length($subIdx)); # remove old sub-index
1328 51         167 $subIdx = substr($subIdx, 1) + 1;
1329 51         133 $subIdx = length($subIdx) . $subIdx;
1330             } elsif (@delPaths) {
1331 19         45 $path = shift @delPaths;
1332             # make sure new path is unique
1333 19         72 while ($capture{$path}) {
1334 2 50       13 last unless $path =~ s/ \d(\d+)$/' '.length($1+1).($1+1)/e;
  2         19  
1335             }
1336 19         39 next;
1337             } else {
1338 52         123 $subIdx = '10';
1339             }
1340 103         389 substr($path, $pos, $len) = $idx . $subIdx;
1341             }
1342             # make sure any empty structures are deleted
1343             # (ExifTool shouldn't write these, but other software may)
1344 879 100       3200 if (defined $$tagInfo{Flat}) {
1345 318         602 my $p = $path;
1346 318         2093 while ($p =~ s/\/[^\/]+$//) {
1347 420 50       2570 next unless $capture{$p};
1348             # it is an error if this property has a value
1349 0 0       0 $et->Error("Improperly structured XMP ($p)",1) if $capture{$p}[0] =~ /\S/;
1350 0         0 delete $capture{$p}; # delete the (hopefully) empty structure
1351             }
1352             }
1353             }
1354             # remove the ExifTool members we created
1355 124         544 delete $$et{XMP_CAPTURE};
1356 124         366 delete $$et{XMP_NS};
1357              
1358 124         360 my $maxDataLen = $$dirInfo{MaxDataLen};
1359             # get DataPt again because it may have been set by ProcessXMP
1360 124         336 $dataPt = $$dirInfo{DataPt};
1361              
1362             # return now if we didn't change anything
1363 124 50 66     628 unless ($changed or ($maxDataLen and $dataPt and defined $$dataPt and
      66        
      33        
      66        
1364             length($$dataPt) > $maxDataLen))
1365             {
1366 16 50       194 return undef unless $xmpFile; # just rewrite original XMP
1367 0 0 0     0 Write($$dirInfo{OutFile}, $$dataPt) or return -1 if $dataPt and defined $$dataPt;
      0        
1368 0         0 return 1;
1369             }
1370             #
1371             # write out the new XMP information (serialize it)
1372             #
1373             # start writing the XMP data
1374 108         331 my (@long, @short, @resFlag);
1375 108         493 $long[0] = $long[1] = $short[0] = '';
1376 108 100       403 if ($$et{XMP_NO_XPACKET}) {
1377             # write BOM if flag is set
1378 1 50       7 $long[-2] .= "\xef\xbb\xbf" if $$et{XMP_NO_XPACKET} == 2;
1379             } else {
1380 107         424 $long[-2] .= $pktOpen;
1381             }
1382 108 100       438 $long[-2] .= $xmlOpen if $$et{XMP_IS_XML};
1383 108         496 $long[-2] .= $xmpOpen . $rdfOpen;
1384              
1385             # initialize current property path list
1386 108         480 my (@curPropList, @writeLast, @descStart, $extStart);
1387 108         0 my (%nsCur, $prop, $n, $path);
1388 108         1082 my @pathList = sort TypeFirst keys %capture;
1389             # order properties to write large values last if we have a MaxDataLen limit
1390 108 100 100     831 if ($maxDataLen and @pathList) {
1391 34         84 my @pathTmp;
1392 34         134 my ($lastProp, $lastNS, $propSize) = ('', '', 0);
1393 34         150 my @pathLoop = (@pathList, ''); # add empty path to end of list for loop
1394 34         119 undef @pathList;
1395 34         114 foreach $path (@pathLoop) {
1396 379         1106 $path =~ /^((\w*)[^\/]*)/; # get path element ($1) and ns ($2)
1397 379 100       877 if ($1 eq $lastProp) {
1398 95         210 push @pathTmp, $path; # accumulate all paths with same root
1399             } else {
1400             # put in list to write last if recommended or values are too large
1401 284 100 66     1394 if ($extendedRes{$lastProp} or $extendedRes{$lastNS} or
      66        
1402             $propSize > $newDescThresh)
1403             {
1404 14         34 push @writeLast, @pathTmp;
1405             } else {
1406 270         537 push @pathList, @pathTmp;
1407             }
1408 284 100       599 last unless $path; # all done if we hit empty path
1409 250         505 @pathTmp = ( $path );
1410 250         609 ($lastProp, $lastNS, $propSize) = ($1, $2, 0);
1411             }
1412 345         715 $propSize += length $capture{$path}->[0];
1413             }
1414             }
1415              
1416             # write out all properties
1417 108         324 for (;;) {
1418 1946         3030 my (%nsNew, $newDesc);
1419 1946 100       3973 unless (@pathList) {
1420 112 100       656 last unless @writeLast;
1421 4         19 @pathList = @writeLast;
1422 4         11 undef @writeLast;
1423 4         10 $newDesc = 2; # start with a new description for the extended data
1424             }
1425 1838         3126 $path = shift @pathList;
1426 1838         4765 my @propList = split('/',$path); # get property list
1427             # must open/close rdf:Description too
1428 1838         3728 unshift @propList, $rdfDesc;
1429             # make sure we have defined all necessary namespaces
1430 1838         3144 foreach $prop (@propList) {
1431 5539 50       15388 $prop =~ /(.*):/ or next;
1432 5539 100       12852 $1 eq 'rdf' and next; # rdf namespace already defined
1433 2407         4487 my $uri = $nsUsed{$1};
1434 2407 100       4486 unless ($uri) {
1435 1387         2611 $uri = $nsURI{$1}; # we must have added a namespace
1436 1387 50       2693 unless ($uri) {
1437             # (namespace prefix may be empty if trying to write empty XMP structure, forum12384)
1438 0 0       0 if (length $1) {
1439 0         0 my $err = "Undefined XMP namespace: $1";
1440 0 0 0     0 if (not $xmpErr or $err ne $xmpErr) {
1441 0 0       0 $xmpFile ? $et->Error($err) : $et->Warn($err);
1442 0         0 $xmpErr = $err;
1443             }
1444             }
1445 0         0 next;
1446             }
1447             }
1448 2407         4502 $nsNew{$1} = $uri;
1449             # need a new description if any new namespaces
1450 2407 100       5929 $newDesc = 1 unless $nsCur{$1};
1451             }
1452 1838         2766 my $closeTo = 0;
1453 1838 100       3193 if ($newDesc) {
1454             # look forward to see if we will want to also open other namespaces
1455             # at this level (this is necessary to keep lists and structures from
1456             # being broken if a property introduces a new namespace; plus it
1457             # improves formatting)
1458 307         543 my ($path2, $ns2);
1459 307         657 foreach $path2 (@pathList) {
1460 1730         8878 my @ns2s = ($path2 =~ m{(?:^|/)([^/]+?):}g);
1461 1730 50       3662 my $opening = $compact{OneDesc} ? 1 : 0;
1462 1730         2620 foreach $ns2 (@ns2s) {
1463 3265 100       6146 next if $ns2 eq 'rdf';
1464 2235 100       4829 $nsNew{$ns2} and ++$opening, next;
1465 211 100       572 last unless $opening;
1466             # get URI for this existing or new namespace
1467 12 50 66     73 my $uri = $nsUsed{$ns2} || $nsURI{$ns2} or last;
1468 12         35 $nsNew{$ns2} = $uri; # also open this namespace
1469             }
1470 1730 100       4098 last unless $opening;
1471             }
1472             } else {
1473             # find first property where the current path differs from the new path
1474 1531         3484 for ($closeTo=0; $closeTo<@curPropList; ++$closeTo) {
1475 2866 50       5299 last unless $closeTo < @propList;
1476 2866 100       7477 last unless $propList[$closeTo] eq $curPropList[$closeTo];
1477             }
1478             }
1479             # close out properties down to the common base path
1480 1838         4797 CloseProperty(\@curPropList, \@long, \@short, \@resFlag) while @curPropList > $closeTo;
1481              
1482             # open new description if necessary
1483 1838 100       3534 if ($newDesc) {
1484 307 50       766 $extStart = length($long[-2]) if $newDesc == 2; # extended data starts after this
1485             # save rdf:Description start positions so we can reorder them if necessary
1486 307 100       787 push @descStart, length($long[-2]) if $maxDataLen;
1487             # open the new description
1488 307         563 $prop = $rdfDesc;
1489 307         1067 %nsCur = %nsNew; # save current namespaces
1490 307         1020 my @ns = sort keys %nsCur;
1491 307         1305 $long[-2] .= "$nl$sp<$prop rdf:about='${about}'";
1492             # generate et:toolkit attribute if this is an exiftool RDF/XML output file
1493 307 100 66     1143 if ($$et{XMP_NO_XMPMETA} and @ns and $nsCur{$ns[0]} =~ m{^http://ns.exiftool.(?:ca|org)/}) {
      100        
1494 4         21 $long[-2] .= "\n$sp${sp}xmlns:et='http://ns.exiftool.org/1.0/'" .
1495             " et:toolkit='Image::ExifTool $Image::ExifTool::VERSION'";
1496             }
1497 307         1563 $long[-2] .= "\n$sp${sp}xmlns:$_='$nsCur{$_}'" foreach @ns;
1498 307         691 push @curPropList, $prop;
1499             # set resFlag to 0 to indicate base description when Shorthand enabled
1500 307 100       855 $resFlag[0] = 0 if $compact{Shorthand};
1501             }
1502 1838         2588 my ($val, $attrs) = @{$capture{$path}};
  1838         5548  
1503 1838 50       3699 $debug and print "$path = $val\n";
1504             # open new properties if necessary
1505 1838         2663 my ($attr, $dummy);
1506 1838         4212 for ($n=@curPropList; $n<$#propList; ++$n) {
1507 762         1407 $prop = $propList[$n];
1508 762         1290 push @curPropList, $prop;
1509 762         1611 $prop =~ s/ .*//; # remove list index if it exists
1510             # (we may add parseType and shorthand properties later,
1511             # so leave off the trailing ">" for now)
1512 762 50       2539 $long[-1] .= ($compact{NoIndent} ? '' : ' ' x scalar(@curPropList)) . "<$prop";
1513 762 100 100     4989 if ($prop ne $rdfDesc and ($propList[$n+1] !~ /^rdf:/ or
      66        
1514             ($propList[$n+1] eq 'rdf:type' and $n+1 == $#propList)))
1515             {
1516             # check for empty structure
1517 134 100       479 if ($propList[$n+1] =~ /:~dummy~$/) {
1518 3         10 $long[-1] .= " rdf:parseType='Resource'/>$nl";
1519 3         6 pop @curPropList;
1520 3         7 $dummy = 1;
1521 3         4 last;
1522             }
1523 131 100       338 if ($compact{Shorthand}) {
1524 1         5 $resFlag[$#curPropList] = 1;
1525 1         4 push @long, '';
1526 1         5 push @short, '';
1527             } else {
1528             # use rdf:parseType='Resource' to avoid new 'rdf:Description'
1529 130         493 $long[-1] .= " rdf:parseType='Resource'>$nl";
1530             }
1531             } else {
1532 628         1867 $long[-1] .= ">$nl"; # (will be no shorthand properties)
1533             }
1534             }
1535 1838         3104 my $prop2 = pop @propList; # get new property name
1536             # add element unless it was a dummy structure field
1537 1838 50 66     6236 unless ($dummy or ($val eq '' and $prop2 =~ /:~dummy~$/)) {
      66        
1538 1835         4119 $prop2 =~ s/ .*//; # remove list index if it exists
1539 1835 50       4615 my $pad = $compact{NoIndent} ? '' : ' ' x (scalar(@curPropList) + 1);
1540             # (can't write as shortcut if it has attributes or CDATA)
1541 1835 100 66     5157 if (defined $resFlag[$#curPropList] and not %$attrs and $val !~ /
      66        
1542 19         68 $short[-1] .= "\n$pad$prop2='${val}'";
1543             } else {
1544 1816         3946 $long[-1] .= "$pad<$prop2";
1545             # write out attributes
1546 1816         5142 foreach $attr (sort keys %$attrs) {
1547 211         522 my $attrVal = $$attrs{$attr};
1548 211 50       663 my $quot = ($attrVal =~ /'/) ? '"' : "'";
1549 211         659 $long[-1] .= " $attr=$quot$attrVal$quot";
1550             }
1551 1816 100       6895 $long[-1] .= length $val ? ">$val$nl" : "/>$nl";
1552             }
1553             }
1554             }
1555             # close out all open properties
1556 108         784 CloseProperty(\@curPropList, \@long, \@short, \@resFlag) while @curPropList;
1557              
1558             # limit XMP length and re-arrange if necessary to fit inside specified size
1559 108 100       549 if ($maxDataLen) {
1560             # adjust maxDataLen to allow room for closing elements
1561 36         150 $maxDataLen -= length($rdfClose) + length($xmpClose) + length($pktCloseW);
1562 36 50       168 $extStart or $extStart = length $long[-2];
1563 36         217 my @rtn = LimitXMPSize($et, \$long[-2], $maxDataLen, $about, \@descStart, $extStart);
1564             # return extended XMP information in $dirInfo
1565 36         140 $$dirInfo{ExtendedXMP} = $rtn[0];
1566 36         124 $$dirInfo{ExtendedGUID} = $rtn[1];
1567             # compact if necessary to fit
1568 36 50       201 $compact{NoPadding} = 1 if length($long[-2]) + 101 * $numPadLines > $maxDataLen;
1569             }
1570 108 50       492 $compact{NoPadding} = 1 if $$dirInfo{Compact};
1571             #
1572             # close out the XMP, clean up, and return our data
1573             #
1574 108         372 $long[-2] .= $rdfClose;
1575 108 100       477 $long[-2] .= $xmpClose unless $$et{XMP_NO_XMPMETA};
1576              
1577             # remove the ExifTool members we created
1578 108         276 delete $$et{XMP_CAPTURE};
1579 108         269 delete $$et{XMP_NS};
1580 108         272 delete $$et{XMP_NO_XMPMETA};
1581              
1582             # (the XMP standard recommends writing 2k-4k of white space before the
1583             # packet trailer, with a newline every 100 characters)
1584 108 100       416 unless ($$et{XMP_NO_XPACKET}) {
1585 107         387 my $pad = (' ' x 100) . "\n";
1586             # get current XMP length without padding
1587 107         343 my $len = length($long[-2]) + length($pktCloseW);
1588 107 50 0     1463 if ($$dirInfo{InPlace} and not ($$dirInfo{InPlace} == 2 and $len > $dirLen)) {
    100 33        
      100        
1589             # pad to specified DirLen
1590 0 0       0 if ($len > $dirLen) {
1591 0         0 my $str = 'Not enough room to edit XMP in place';
1592 0 0       0 $str .= '. Try Shorthand feature' unless $compact{Shorthand};
1593 0         0 $et->Warn($str);
1594 0         0 return undef;
1595             }
1596 0         0 my $num = int(($dirLen - $len) / length($pad));
1597 0 0       0 if ($num) {
1598 0         0 $long[-2] .= $pad x $num;
1599 0         0 $len += length($pad) * $num;
1600             }
1601 0 0       0 $len < $dirLen and $long[-2] .= (' ' x ($dirLen - $len - 1)) . "\n";
1602             } elsif (not $compact{NoPadding} and not $xmpFile and not $$dirInfo{ReadOnly}) {
1603 67         851 $long[-2] .= $pad x $numPadLines;
1604             }
1605 107 100       670 $long[-2] .= ($$dirInfo{ReadOnly} ? $pktCloseR : $pktCloseW);
1606             }
1607             # return empty data if no properties exist and this is allowed
1608 108 100 66     742 unless (%capture or $xmpFile or $$dirInfo{InPlace} or $$dirInfo{NoDelete}) {
      66        
      66        
1609 3         8 $long[-2] = '';
1610             }
1611 108 0       373 return($xmpFile ? -1 : undef) if $xmpErr;
    50          
1612 108         321 $$et{CHANGED} += $changed;
1613 108 0 33     437 $debug > 1 and $long[-2] and print $long[-2],"\n";
1614 108 100       1864 return $long[-2] unless $xmpFile;
1615 35 50       252 Write($$dirInfo{OutFile}, $long[-2]) or return -1;
1616 35         1839 return 1;
1617             }
1618              
1619              
1620             1; # end
1621              
1622             __END__