File Coverage

blib/lib/Image/ExifTool/XMPStruct.pl
Criterion Covered Total %
statement 397 536 74.0
branch 237 418 56.7
condition 63 120 52.5
subroutine 12 15 80.0
pod 0 11 0.0
total 709 1100 64.4


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: XMPStruct.pl
3             #
4             # Description: XMP structure support
5             #
6             # Revisions: 01/01/2011 - P. Harvey Created
7             #------------------------------------------------------------------------------
8              
9             package Image::ExifTool::XMP;
10              
11 15     15   82 use strict;
  15         28  
  15         620  
12 15     15   67 use vars qw(%specialStruct %stdXlatNS);
  15         30  
  15         746  
13              
14 15     15   67 use Image::ExifTool qw(:Utils);
  15         44  
  15         2015  
15 15     15   1093 use Image::ExifTool::XMP;
  15         28  
  15         94507  
16              
17             sub SerializeStruct($$;$);
18             sub InflateStruct($$;$);
19             sub DumpStruct($;$);
20             sub CheckStruct($$$);
21             sub AddNewStruct($$$$$$);
22             sub ConvertStruct($$$$;$);
23             sub EscapeJSON($;$);
24              
25             # lookups for JSON characters that we escape specially
26             my %jsonChar = ( '"'=>'"', '\\'=>'\\', "\b"=>'b', "\f"=>'f', "\n"=>'n', "\r"=>'r', "\t"=>'t' );
27             my %jsonEsc = ( '"'=>'"', '\\'=>'\\', 'b'=>"\b", 'f'=>"\f", 'n'=>"\n", 'r'=>"\r", 't'=>"\t" );
28              
29             #------------------------------------------------------------------------------
30             # Serialize a structure (or other object) into a simple string
31             # Inputs: 0) ExifTool ref, 1) HASH ref, ARRAY ref, or SCALAR, 2) closing bracket (or undef)
32             # Returns: serialized structure string (in format specified by StructFormat option)
33             # eg) "{field=text with {braces|}|, and a comma, field2=val2,field3={field4=[a,b]}}"
34             sub SerializeStruct($$;$)
35             {
36 0     0 0 0 my ($et, $obj, $ket) = @_;
37 0         0 my ($key, $val, @vals, $rtnVal);
38 0         0 my $sfmt = $et->Options('StructFormat');
39              
40 0 0       0 if (ref $obj eq 'HASH') {
    0          
    0          
    0          
41             # support hashes with ordered keys
42 0         0 foreach $key (Image::ExifTool::OrderedKeys($obj)) {
43 0 0       0 my $hdr = $sfmt ? EscapeJSON($key) . ':' : $key . '=';
44 0         0 push @vals, $hdr . SerializeStruct($et, $$obj{$key}, '}');
45             }
46 0         0 $rtnVal = '{' . join(',', @vals) . '}';
47             } elsif (ref $obj eq 'ARRAY') {
48 0         0 foreach $val (@$obj) {
49 0         0 push @vals, SerializeStruct($et, $val, ']');
50             }
51 0         0 $rtnVal = '[' . join(',', @vals) . ']';
52             } elsif (defined $obj) {
53 0 0       0 $obj = $$obj if ref $obj eq 'SCALAR';
54             # escape necessary characters in string (closing bracket plus "," and "|")
55 0 0       0 if ($sfmt) {
56 0         0 $rtnVal = EscapeJSON($obj, $sfmt eq 'JSONQ');
57             } else {
58 0 0       0 my $pat = $ket ? "\\$ket|,|\\|" : ',|\\|';
59 0         0 ($rtnVal = $obj) =~ s/($pat)/|$1/g;
60             # also must escape opening bracket or whitespace at start of string
61 0         0 $rtnVal =~ s/^([\s\[\{])/|$1/;
62             }
63             } elsif ($sfmt) {
64 0         0 $rtnVal = 'null';
65             } else {
66 0         0 $rtnVal = ''; # allow undefined list items
67             }
68 0         0 return $rtnVal;
69             }
70              
71             #------------------------------------------------------------------------------
72             # Inflate structure (or other object) from a serialized string
73             # Inputs: 0) ExifTool ref, 1) reference to object in string form
74             # (serialized using the '|' escape, or JSON)
75             # 2) extra delimiter for scalar values delimiters
76             # Returns: 0) object as a SCALAR, HASH ref, or ARRAY ref (or undef on error),
77             # 1) warning string (or undef)
78             # Notes: modifies input string to remove parsed objects
79             sub InflateStruct($$;$)
80             {
81 109     109 0 205 my ($et, $obj, $delim) = @_;
82 109         167 my ($val, $warn, $part);
83 109         7709 my $sfmt = $et->Options('StructFormat');
84              
85 109 100       530 if ($$obj =~ s/^\s*\{//) {
    100          
86 13         22 my %struct;
87 13         19 for (;;) {
88 20 50       124 last unless $sfmt ? $$obj =~ s/^\s*"(.*?)"\s*://s :
    100          
89             $$obj =~ s/^\s*([-\w:.]+#?)\s*=//s;
90 19         44 my $tag = $1;
91 19         75 my ($v, $w) = InflateStruct($et, $obj, '}');
92 19 50 33     88 $warn = $w if $w and not $warn;
93 19 50       37 return(undef, $warn) unless defined $v;
94 19         46 $struct{$tag} = $v;
95             # eat comma separator, or all done if there wasn't one
96 19 100       59 last unless $$obj =~ s/^\s*,//s;
97             }
98             # eat closing brace and warn if we didn't find one
99 13 50 33     56 unless ($$obj =~ s/^\s*\}//s or $warn) {
100 0 0       0 if (length $$obj) {
101 0         0 ($part = $$obj) =~ s/^\s*//s;
102 0         0 $part =~ s/[\x0d\x0a].*//s;
103 0 0       0 $part = substr($part,0,27) . '...' if length($part) > 30;
104 0         0 $warn = "Invalid structure field at '${part}'";
105             } else {
106 0         0 $warn = 'Missing closing brace for structure';
107             }
108             }
109 13         30 $val = \%struct;
110             } elsif ($$obj =~ s/^\s*\[//) {
111 5         10 my @list;
112 5         6 for (;;) {
113 9         25 my ($v, $w) = InflateStruct($et, $obj, ']');
114 9 50 33     23 $warn = $w if $w and not $warn;
115 9 50       15 return(undef, $warn) unless defined $v;
116 9         13 push @list, $v;
117 9 100       31 last unless $$obj =~ s/^\s*,//s;
118             }
119             # eat closing bracket and warn if we didn't find one
120 5 50 33     21 $$obj =~ s/^\s*\]//s or $warn or $warn = 'Missing closing bracket for list';
121 5         11 $val = \@list;
122             } else {
123 91         299 $$obj =~ s/^\s+//s; # remove leading whitespace
124 91 50       227 if ($sfmt) {
125 0 0       0 if ($$obj =~ s/^"//) {
    0          
    0          
126 0         0 $val = '';
127 0         0 while ($$obj =~ s/(.*?)"//) {
128 0         0 $val .= $1;
129 0 0 0     0 last unless $val =~ /([\\]+)$/ and length($1) & 0x01;
130 0         0 substr($val, -1, 1) = '"'; # (was an escaped quote)
131             }
132 0 0       0 if ($val =~ s/^base64://) {
133 0         0 $val = DecodeBase64($val);
134             } else {
135             # un-escape characters in JSON string
136 0 0       0 $val =~ s/\\(.)/$jsonEsc{$1}||'\\'.$1/egs;
  0         0  
137             }
138             } elsif ($$obj =~ s/^(true|false)\b//) {
139 0         0 $val = '"' . ucfirst($1) . '"';
140             } elsif ($$obj =~ s/^([+-]?(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?)//) {
141 0         0 $val = $1;
142             } else {
143 0 0       0 $warn or $warn = 'Unknown JSON object';
144 0         0 $val = '""';
145             }
146             } else {
147             # read scalar up to specified delimiter (or "," if not defined)
148 91 100       227 $delim = $delim ? "\\$delim|,|\\||\$" : ',|\\||$';
149 91         132 $val = '';
150 91         117 for (;;) {
151 91 50       1482 $$obj =~ s/^(.*?)($delim)//s or last;
152 91         283 $val .= $1;
153 91 100       238 last unless $2;
154 27 50       122 $2 eq '|' or $$obj = $2 . $$obj, last;
155 0 0       0 $$obj =~ s/^(.)//s and $val .= $1; # add escaped character
156             }
157             }
158             }
159 109         322 return($val, $warn);
160             }
161              
162             #------------------------------------------------------------------------------
163             # Escape string for JSON
164             # Inputs: 0) string, 1) flag to force numbers to be quoted too
165             # Returns: Escaped string (quoted if necessary)
166             sub EscapeJSON($;$)
167             {
168 0     0 0 0 my ($str, $quote) = @_;
169 0 0       0 unless ($quote) {
170 0 0       0 return 'null' unless defined $str;
171             # JSON boolean (true or false)
172 0 0       0 return lc($str) if $str =~ /^(true|false)$/i;
173             # JSON number (see json.org for numerical format)
174             # return $str if $str =~ /^-?(\d|[1-9]\d+)(\.\d+)?(e[-+]?\d+)?$/i;
175             # (these big numbers caused problems for some JSON parsers, so be more conservative)
176 0 0       0 return $str if $str =~ /^-?(\d|[1-9]\d{1,14})(\.\d{1,16})?(e[-+]?\d{1,3})?$/i;
177             }
178 0 0       0 return '""' unless defined $str;
179             # encode JSON string in base64 if necessary
180 0 0       0 return '"base64:' . EncodeBase64($str, 1) . '"' if Image::ExifTool::IsUTF8(\$str) < 0;
181             # escape special characters
182 0         0 $str =~ s/(["\t\n\r\\])/\\$jsonChar{$1}/sg;
183 0         0 $str =~ tr/\0//d; # remove all nulls
184             # escape other control characters with \u
185 0         0 $str =~ s/([\0-\x1f])/sprintf("\\u%.4X",ord $1)/sge;
  0         0  
186 0         0 return '"' . $str . '"'; # return the quoted string
187             }
188              
189             #------------------------------------------------------------------------------
190             # Get XMP language code from tag name string
191             # Inputs: 0) tag name string
192             # Returns: 0) separated tag name, 1) language code (in standard case), or '' if
193             # language code was 'x-default', or undef if the tag had no language code
194             sub GetLangCode($)
195             {
196 8     8 0 14 my $tag = shift;
197 8 50       38 if ($tag =~ /^(\w+)[-_]([a-z]{2,3}|[xi])([-_][a-z\d]{2,8}([-_][a-z\d]{1,8})*)?$/i) {
198             # normalize case of language codes
199 8         25 my ($tg, $langCode) = ($1, lc($2));
200 8 50       23 $langCode .= (length($3) == 3 ? uc($3) : lc($3)) if $3;
    100          
201 8         10 $langCode =~ tr/_/-/; # RFC 3066 specifies '-' as a separator
202 8 50       16 $langCode = '' if lc($langCode) eq 'x-default';
203 8         24 return($tg, $langCode);
204             } else {
205 0         0 return($tag, undef);
206             }
207             }
208              
209             #------------------------------------------------------------------------------
210             # Debugging routine to dump a structure, list or scalar
211             # Inputs: 0) scalar, ARRAY ref or HASH ref, 1) indent (or undef)
212             sub DumpStruct($;$)
213             {
214 0     0 0 0 local $_;
215 0         0 my ($obj, $indent) = @_;
216              
217 0 0       0 $indent or $indent = '';
218 0 0       0 if (ref $obj eq 'HASH') {
    0          
219 0         0 print "{\n";
220 0         0 foreach (Image::ExifTool::OrderedKeys($obj)) {
221 0         0 print "$indent $_ = ";
222 0         0 DumpStruct($$obj{$_}, "$indent ");
223             }
224 0         0 print $indent, "},\n";
225             } elsif (ref $obj eq 'ARRAY') {
226 0         0 print "[\n";
227 0         0 foreach (@$obj) {
228 0         0 print "$indent ";
229 0         0 DumpStruct($_, "$indent ");
230             }
231 0         0 print $indent, "],\n",
232             } else {
233 0         0 print "\"$obj\",\n";
234             }
235             }
236              
237             #------------------------------------------------------------------------------
238             # Recursively validate structure fields (tags)
239             # Inputs: 0) ExifTool ref, 1) Structure ref, 2) structure table definition ref
240             # Returns: 0) validated structure ref, 1) error string, or undef on success
241             # Notes:
242             # - fixes field names in structure and applies inverse conversions to values
243             # - copies structure to avoid interdependencies with calling code on referenced values
244             # - handles lang-alt tags, and '#' on field names
245             # - resets UTF-8 flag of SCALAR values
246             # - un-escapes for XML or HTML as per Escape option setting
247             sub CheckStruct($$$)
248             {
249 51     51 0 94 my ($et, $struct, $strTable) = @_;
250              
251 51   66     199 my $strName = $$strTable{STRUCT_NAME} || ('XMP ' . RegisterNamespace($strTable));
252 51 0       145 ref $struct eq 'HASH' or return wantarray ? (undef, "Expecting $strName structure") : undef;
    50          
253              
254 51         74 my ($key, $err, $warn, %copy, $rtnVal, $val);
255             # copy the ordered keys if they exist
256 51 50       112 $copy{_ordered_keys_} = [ ] if $$struct{_ordered_keys_};
257             Key:
258 51         152 foreach $key (Image::ExifTool::OrderedKeys($struct)) {
259 85         114 my $tag = $key;
260             # allow trailing '#' to disable print conversion on a per-field basis
261 85         131 my ($type, $fieldInfo);
262 85 100       199 $type = 'ValueConv' if $tag =~ s/#$//;
263 85 50       228 $fieldInfo = $$strTable{$tag} unless $specialStruct{$tag};
264             # fix case of field name if necessary
265 85 100       153 unless ($fieldInfo) {
266             # (sort in reverse to get lower case (not special) tags first)
267 44         962 my ($fix) = reverse sort grep /^$tag$/i, keys %$strTable;
268 44 100 66     205 $fieldInfo = $$strTable{$tag = $fix} if $fix and not $specialStruct{$fix};
269             }
270 85         177 until (ref $fieldInfo eq 'HASH') {
271             # generate wildcard fields on the fly (eg. mwg-rs:Extensions)
272 15 100       37 unless ($$strTable{NAMESPACE}) {
273 10         15 my ($grp, $tg, $langCode);
274 10 100       67 ($grp, $tg) = $tag =~ /^(.+):(.+)/ ? (lc $1, $2) : ('', $tag);
275 10 50       46 undef $grp if $grp eq 'XMP'; # (a group of 'XMP' is implied)
276 10         42 require Image::ExifTool::TagLookup;
277 10         35 my @matches = Image::ExifTool::TagLookup::FindTagInfo($tg);
278             # also look for lang-alt tags
279 10 100       23 unless (@matches) {
280 3         8 ($tg, $langCode) = GetLangCode($tg);
281 3 50       10 @matches = Image::ExifTool::TagLookup::FindTagInfo($tg) if defined $langCode;
282             }
283 10         17 my ($tagInfo, $priority, $ti, $g1);
284             # find best matching tag
285 10         19 foreach $ti (@matches) {
286 34         67 my @grps = $et->GetGroup($ti);
287 34 100       85 next unless $grps[0] eq 'XMP';
288 10 50 66     32 next if $grp and $grp ne lc $grps[1];
289             # must be lang-alt tag if we are writing an alternate language
290 10 50 33     38 next if defined $langCode and not ($$ti{Writable} and $$ti{Writable} eq 'lang-alt');
      66        
291 10   50     32 my $pri = $$ti{Priority} || 1;
292 10 50       21 $pri -= 10 if $$ti{Avoid};
293 10 50 33     24 next if defined $priority and $priority >= $pri;
294 10         11 $priority = $pri;
295 10         12 $tagInfo = $ti;
296 10         17 $g1 = $grps[1];
297             }
298 10 50       17 $tagInfo or $warn = "'${tag}' is not a writable XMP tag", next Key;
299 10         33 GetPropertyPath($tagInfo); # make sure property path is generated for this tag
300 10         17 $tag = $$tagInfo{Name};
301 10 100       24 $tag = "$g1:$tag" if $grp;
302 10 100       22 $tag .= "-$langCode" if $langCode;
303 10         15 $fieldInfo = $$strTable{$tag};
304             # create new structure field if necessary
305             $fieldInfo or $fieldInfo = $$strTable{$tag} = {
306             %$tagInfo, # (also copies the necessary TagID and PropertyPath)
307             Namespace => $$tagInfo{Namespace} || $$tagInfo{Table}{NAMESPACE},
308 10 50 33     124 LangCode => $langCode,
309             };
310             # delete stuff we don't need (shouldn't cause harm, but better safe than sorry)
311             # - need to keep StructType and Table in case we need to call AddStructType later
312 10         22 delete $$fieldInfo{Description};
313 10         16 delete $$fieldInfo{Groups};
314 10         22 last; # write this dynamically-generated field
315             }
316             # generate lang-alt fields on the fly (eg. Iptc4xmpExt:AOTitle)
317 5         11 my ($tg, $langCode) = GetLangCode($tag);
318 5 50       10 if (defined $langCode) {
319 5 50       15 $fieldInfo = $$strTable{$tg} unless $specialStruct{$tg};
320 5 100       10 unless ($fieldInfo) {
321 1         22 my ($fix) = reverse sort grep /^$tg$/i, keys %$strTable;
322 1 50 33     7 $fieldInfo = $$strTable{$tg = $fix} if $fix and not $specialStruct{$fix};
323             }
324 5 50 33     30 if (ref $fieldInfo eq 'HASH' and $$fieldInfo{Writable} and
      33        
325             $$fieldInfo{Writable} eq 'lang-alt')
326             {
327 5         8 my $srcInfo = $fieldInfo;
328 5 50       13 $tag = $tg . '-' . $langCode if $langCode;
329 5         6 $fieldInfo = $$strTable{$tag};
330             # create new structure field if necessary
331 5 50       26 $fieldInfo or $fieldInfo = $$strTable{$tag} = {
332             %$srcInfo,
333             TagID => $tg,
334             LangCode => $langCode,
335             };
336 5         9 last; # write this lang-alt field
337             }
338             }
339 0         0 $warn = "'${tag}' is not a field of $strName";
340 0         0 next Key;
341             }
342 85 100       275 if (ref $$struct{$key} eq 'HASH') {
    100          
    50          
343 10 50       27 $$fieldInfo{Struct} or $warn = "$tag is not a structure in $strName", next Key;
344             # recursively check this structure
345 10         65 ($val, $err) = CheckStruct($et, $$struct{$key}, $$fieldInfo{Struct});
346 10 50       23 $err and $warn = $err, next Key;
347 10         20 $copy{$tag} = $val;
348             } elsif (ref $$struct{$key} eq 'ARRAY') {
349 13 50       34 $$fieldInfo{List} or $warn = "$tag is not a list in $strName", next Key;
350             # check all items in the list
351 13         18 my ($item, @copy);
352 13         39 my $i = 0;
353 13         16 foreach $item (@{$$struct{$key}}) {
  13         41  
354 21 100       46 if (not ref $item) {
    50          
355 13 50       23 $item = '' unless defined $item; # use empty string for missing items
356 13 100       19 if ($$fieldInfo{Struct}) {
357             # (allow empty structures)
358 2 50       7 $item =~ /^\s*$/ or $warn = "$tag items are not valid structures", next Key;
359 2         4 $copy[$i] = { }; # create hash for empty structure
360             } else {
361 11         32 $et->Sanitize(\$item);
362 11         22 ($copy[$i],$err) = $et->ConvInv($item,$fieldInfo,$tag,$strName,$type,'');
363 11 50       20 $copy[$i] = '' unless defined $copy[$i]; # avoid undefined item
364 11 50       21 $err and $warn = $err, next Key;
365 11         22 $err = CheckXMP($et, $fieldInfo, \$copy[$i]);
366 11 50       18 $err and $warn = "$err in $strName $tag", next Key;
367             }
368             } elsif (ref $item eq 'HASH') {
369 8 50       18 $$fieldInfo{Struct} or $warn = "$tag is not a structure in $strName", next Key;
370 8         16 ($copy[$i], $err) = CheckStruct($et, $item, $$fieldInfo{Struct});
371 8 50       14 $err and $warn = $err, next Key;
372             } else {
373 0         0 $warn = "Invalid value for $tag in $strName";
374 0         0 next Key;
375             }
376 21         30 ++$i;
377             }
378 13         29 $copy{$tag} = \@copy;
379             } elsif ($$fieldInfo{Struct}) {
380 0         0 $warn = "Improperly formed structure in $strName $tag";
381 0         0 next;
382             } else {
383 62         220 $et->Sanitize(\$$struct{$key});
384 62         274 ($val,$err) = $et->ConvInv($$struct{$key},$fieldInfo,$tag,$strName,$type,'');
385 62 50       152 $err and $warn = $err, next Key;
386 62 50       114 next Key unless defined $val; # check for undefined
387 62         200 $err = CheckXMP($et, $fieldInfo, \$val);
388 62 50       102 $err and $warn = "$err in $strName $tag", next Key;
389             # turn this into a list if necessary
390 62 100       199 $copy{$tag} = $$fieldInfo{List} ? [ $val ] : $val;
391             }
392 85 50       188 push @{$copy{_ordered_keys_}}, $tag if $copy{_ordered_keys_}; # save ordered keys
  0         0  
393             }
394 51 50 66     157 if (%copy or not $warn) {
395 51         77 $rtnVal = \%copy;
396 51         65 undef $err;
397 51 50       93 $$et{CHECK_WARN} = $warn if $warn;
398             } else {
399 0         0 $err = $warn;
400             }
401 51 50       193 return wantarray ? ($rtnVal, $err) : $rtnVal;
402             }
403              
404             #------------------------------------------------------------------------------
405             # Delete matching structures from existing linearized XMP
406             # Inputs: 0) ExifTool ref, 1) capture hash ref, 2) structure path ref,
407             # 3) new value hash ref, 4) reference to change counter
408             # Returns: 0) delete flag, 1) list index of deleted structure if adding to list
409             # 2) flag set if structure existed
410             # Notes: updates path to new base path for structure to be added
411             sub DeleteStruct($$$$$)
412             {
413 218     218 0 436 my ($et, $capture, $pathPt, $nvHash, $changed) = @_;
414 218         577 my ($deleted, $added, $existed, $p, $pp, $val, $delPath);
415 218         0 my (@structPaths, @matchingPaths, @delPaths);
416              
417             # find all existing elements belonging to this structure
418 218         1028 ($pp = $$pathPt) =~ s/ \d+/ \\d\+/g;
419 218         14194 @structPaths = sort grep(/^$pp(\/|$)/, keys %$capture);
420 218 100       1034 $existed = 1 if @structPaths;
421             # delete only structures with matching fields if necessary
422 218 100       726 if ($$nvHash{DelValue}) {
    100          
423 4 50       8 if (@{$$nvHash{DelValue}}) {
  4         13  
424 4         10 my $strTable = $$nvHash{TagInfo}{Struct};
425             # all fields must match corresponding elements in the same
426             # root structure for it to be deleted
427 4         5 foreach $val (@{$$nvHash{DelValue}}) {
  4         10  
428 4 50       17 next unless ref $val eq 'HASH';
429 0         0 my (%cap, $p2, %match);
430 0 0       0 next unless AddNewStruct(undef, undef, \%cap, $$pathPt, $val, $strTable);
431 0         0 foreach $p (keys %cap) {
432 0 0       0 if ($p =~ / /) {
433 0         0 ($p2 = $p) =~ s/ \d+/ \\d\+/g;
434 0         0 @matchingPaths = sort grep(/^$p2$/, @structPaths);
435             } else {
436 0         0 push @matchingPaths, $p;
437             }
438 0         0 foreach $p2 (@matchingPaths) {
439 0 0       0 $p2 =~ /^($pp)/ or next;
440             # language attribute must also match if it exists
441 0         0 my $attr = $cap{$p}[1];
442 0 0       0 if ($$attr{'xml:lang'}) {
443 0         0 my $a2 = $$capture{$p2}[1];
444 0 0 0     0 next unless $$a2{'xml:lang'} and $$a2{'xml:lang'} eq $$attr{'xml:lang'};
445             }
446 0 0 0     0 if ($$capture{$p2} and $$capture{$p2}[0] eq $cap{$p}[0]) {
447             # ($1 contains root path for this structure)
448 0   0     0 $match{$1} = ($match{$1} || 0) + 1;
449             }
450             }
451             }
452 0         0 my $num = scalar(keys %cap);
453 0         0 foreach $p (keys %match) {
454             # do nothing unless all fields matched the same structure
455 0 0       0 next unless $match{$p} == $num;
456             # delete all elements of this structure
457 0         0 foreach $p2 (@structPaths) {
458 0 0       0 push @delPaths, $p2 if $p2 =~ /^$p/;
459             }
460             # remember path of first deleted structure
461 0 0 0     0 $delPath = $p if not $delPath or $delPath gt $p;
462             }
463             }
464             } # (else don't delete anything)
465             } elsif (@structPaths) {
466 2         4 @delPaths = @structPaths; # delete all
467 2         23 $structPaths[0] =~ /^($pp)/;
468 2         9 $delPath = $1;
469             }
470 218 100       902 if (@delPaths) {
    100          
471 2         25 my $verbose = $et->Options('Verbose');
472 2 50       7 @delPaths = sort @delPaths if $verbose > 1;
473 2         5 foreach $p (@delPaths) {
474 6 50       11 if ($verbose > 1) {
475 0         0 my $p2 = $p;
476 0 0       0 $p2 =~ s/^(\w+)/$stdXlatNS{$1} || $1/e;
  0         0  
477 0         0 $et->VerboseValue("- XMP-$p2", $$capture{$p}[0]);
478             }
479 6         18 delete $$capture{$p};
480 6         6 $deleted = 1;
481 6         10 ++$$changed;
482             }
483 2 50       7 $delPath or warn("Internal error 1 in DeleteStruct\n"), return(undef,undef,$existed);
484 2         6 $$pathPt = $delPath; # return path of first element deleted
485             } elsif ($$nvHash{TagInfo}{List}) {
486             # NOTE: we don't yet properly handle lang-alt elements!!!!
487 161 100       278 if (@structPaths) {
488 1 50       16 $structPaths[-1] =~ /^($pp)/ or warn("Internal error 2 in DeleteStruct\n"), return(undef,undef,$existed);
489 1         4 my $path = $1;
490             # delete any improperly formatted xmp
491 1 50       4 if ($$capture{$path}) {
492 0         0 my $cap = $$capture{$path};
493             # an error unless this was an empty structure
494 0 0 0     0 $et->Error("Improperly structured XMP ($path)",1) if ref $cap ne 'ARRAY' or $$cap[0];
495 0         0 delete $$capture{$path};
496             }
497             # (match last index to put in same lang-alt list for Bag of lang-alt items)
498 1 50       7 $path =~ m/.* (\d+)/g or warn("Internal error 3 in DeleteStruct\n"), return(undef,undef,$existed);
499 1         3 $added = $1;
500             # add after last item in list
501 1         3 my $len = length $added;
502 1         4 my $pos = pos($path) - $len;
503 1         4 my $nxt = substr($added, 1) + 1;
504 1         6 substr($path, $pos, $len) = length($nxt) . $nxt;
505 1         4 $$pathPt = $path;
506             } else {
507 160         244 $added = '10';
508             }
509             }
510 218         868 return($deleted, $added, $existed);
511             }
512              
513             #------------------------------------------------------------------------------
514             # Add new element to XMP capture hash
515             # Inputs: 0) ExifTool ref, 1) TagInfo ref, 2) capture hash ref,
516             # 3) resource path, 4) value ref, 5) hash ref for last used index numbers
517             sub AddNewTag($$$$$$)
518             {
519 71     71 0 144 my ($et, $tagInfo, $capture, $path, $valPtr, $langIdx) = @_;
520 71         167 my $val = EscapeXML($$valPtr);
521 71         109 my %attrs;
522             # support writing RDF "resource" values
523 71 100       282 if ($$tagInfo{Resource}) {
524 2         7 $attrs{'rdf:resource'} = $val;
525 2         5 $val = '';
526             }
527 71 100 100     214 if ($$tagInfo{Writable} and $$tagInfo{Writable} eq 'lang-alt') {
528             # write the lang-alt tag
529 20         30 my $langCode = $$tagInfo{LangCode};
530             # add indexed lang-alt list properties
531 20   100     44 my $i = $$langIdx{$path} || 0;
532 20         34 $$langIdx{$path} = $i + 1; # save next list index
533 20 100       34 if ($i) {
534 8         17 my $idx = length($i) . $i;
535 8         43 $path =~ s/(.*) \d+/$1 $idx/; # set list index
536             }
537 20   100     57 $attrs{'xml:lang'} = $langCode || 'x-default';
538             }
539 71         220 $$capture{$path} = [ $val, \%attrs ];
540             # print verbose message
541 71 50 33     269 if ($et and $et->Options('Verbose') > 1) {
542 0         0 my $p = $path;
543 0 0       0 $p =~ s/^(\w+)/$stdXlatNS{$1} || $1/e;
  0         0  
544 0         0 $et->VerboseValue("+ XMP-$p", $val);
545             }
546             }
547              
548             #------------------------------------------------------------------------------
549             # Add new structure to capture hash for writing
550             # Inputs: 0) ExifTool object ref (or undef for no warnings),
551             # 1) tagInfo ref (or undef if no ExifTool), 2) capture hash ref,
552             # 3) base path, 4) struct ref, 5) struct hash ref
553             # Returns: number of tags changed
554             # Notes: Escapes values for XML
555             sub AddNewStruct($$$$$$)
556             {
557 50     50 0 119 my ($et, $tagInfo, $capture, $basePath, $struct, $strTable) = @_;
558 50 50       182 my $verbose = $et ? $et->Options('Verbose') : 0;
559 50         69 my ($tag, %langIdx);
560              
561 50   100     140 my $ns = $$strTable{NAMESPACE} || '';
562 50         69 my $changed = 0;
563              
564             # add dummy field to allow empty structures (name starts with '~' so it will come
565             # after all valid structure fields, which is necessary when serializing the XMP later)
566 50 100       116 %$struct or $$struct{'~dummy~'} = '';
567              
568 50         117 foreach $tag (Image::ExifTool::OrderedKeys($struct)) {
569 85         155 my $fieldInfo = $$strTable{$tag};
570 85 100       138 unless ($fieldInfo) {
571 3 50       13 next unless $tag eq '~dummy~'; # check for dummy field
572 3         5 $fieldInfo = { }; # create dummy field info for dummy structure
573             }
574 85         160 my $val = $$struct{$tag};
575 85         166 my $propPath = $$fieldInfo{PropertyPath};
576 85 100       154 unless ($propPath) {
577 37   66     188 $propPath = ($$fieldInfo{Namespace} || $ns) . ':' . ($$fieldInfo{TagID} || $tag);
      66        
578 37 100       96 if ($$fieldInfo{List}) {
579 7         15 $propPath .= "/rdf:$$fieldInfo{List}/rdf:li 10";
580             }
581 37 100 100     107 if ($$fieldInfo{Writable} and $$fieldInfo{Writable} eq 'lang-alt') {
582 7         14 $propPath .= "/rdf:Alt/rdf:li 10";
583             }
584 37         93 $$fieldInfo{PropertyPath} = $propPath; # save for next time
585             }
586 85         202 my $path = $basePath . '/' . ConformPathToNamespace($et, $propPath);
587 85         117 my $addedTag;
588 85 100       235 if (ref $val eq 'HASH') {
    100          
589 10 50       23 my $subStruct = $$fieldInfo{Struct} or next;
590 10         55 $changed += AddNewStruct($et, $tagInfo, $capture, $path, $val, $subStruct);
591             } elsif (ref $val eq 'ARRAY') {
592 15 50       28 next unless $$fieldInfo{List};
593 15         21 my $i = 0;
594 15         17 my ($item, $p);
595 15         48 my $level = scalar(() = ($propPath =~ / \d+/g));
596             # loop through all list items (note: can't yet write multi-dimensional lists)
597 15         22 foreach $item (@{$val}) {
  15         26  
598 23 100       36 if ($i) {
599             # update first index in field property (may be list of lang-alt lists)
600 8         16 $p = ConformPathToNamespace($et, $propPath);
601 8         19 my $idx = length($i) . $i;
602 8         32 $p =~ s/ \d+/ $idx/;
603 8         17 $p = "$basePath/$p";
604             } else {
605 15         24 $p = $path;
606             }
607 23 100 33     76 if (ref $item eq 'HASH') {
    100 66        
608 10 50       22 my $subStruct = $$fieldInfo{Struct} or next;
609 10 50       25 AddNewStruct($et, $tagInfo, $capture, $p, $item, $subStruct) or next;
610             # don't write empty items in upper-level list
611             } elsif (length $item or (defined $item and $level == 1)) {
612 11         27 AddNewTag($et, $fieldInfo, $capture, $p, \$item, \%langIdx);
613 11         13 $addedTag = 1;
614             }
615 23         38 ++$changed;
616 23         37 ++$i;
617             }
618             } else {
619 60         159 AddNewTag($et, $fieldInfo, $capture, $path, \$val, \%langIdx);
620 60         89 $addedTag = 1;
621 60         75 ++$changed;
622             }
623             # this is tricky, but we must add the rdf:type for contained structures
624             # in the case that a whole hierarchy was added at once by writing a
625             # flattened tag inside a variable-namespace structure
626 85 50 100     291 if ($addedTag and $$fieldInfo{StructType} and $$fieldInfo{Table}) {
      66        
627 1         6 AddStructType($et, $$fieldInfo{Table}, $capture, $propPath, $basePath);
628             }
629             }
630             # add 'rdf:type' property if necessary
631 50 100 66     155 if ($$strTable{TYPE} and $changed) {
632 3         7 my $path = $basePath . '/' . ConformPathToNamespace($et, "rdf:type");
633 3 50       9 unless ($$capture{$path}) {
634 3         13 $$capture{$path} = [ '', { 'rdf:resource' => $$strTable{TYPE} } ];
635 3 50       8 if ($verbose > 1) {
636 0         0 my $p = $path;
637 0 0       0 $p =~ s/^(\w+)/$stdXlatNS{$1} || $1/e;
  0         0  
638 0         0 $et->VerboseValue("+ XMP-$p", $$strTable{TYPE});
639             }
640             }
641             }
642 50         162 return $changed;
643             }
644              
645             #------------------------------------------------------------------------------
646             # Convert structure field values for printing
647             # Inputs: 0) ExifTool ref, 1) tagInfo ref for structure tag, 2) value,
648             # 3) conversion type: PrintConv, ValueConv or Raw (Both not allowed)
649             # 4) tagID of parent structure (needed only if there was no flattened tag)
650             # Notes: Makes a copy of the hash so any applied escapes won't affect raw values
651             sub ConvertStruct($$$$;$)
652             {
653 204     204 0 380 my ($et, $tagInfo, $value, $type, $parentID) = @_;
654 204 100       446 if (ref $value eq 'HASH') {
    100          
655 108         122 my (%struct, $key);
656 108         161 my $table = $$tagInfo{Table};
657 108 100       198 $parentID = $$tagInfo{TagID} unless $parentID;
658 108 100       198 $struct{_ordered_keys_} = [ ] if $$value{_ordered_keys_};
659 108         225 foreach $key (Image::ExifTool::OrderedKeys($value)) {
660 212         311 my $tagID = $parentID . ucfirst($key);
661 212         374 my $flatInfo = $$table{$tagID};
662 212 100       310 unless ($flatInfo) {
663             # handle variable-namespace structures
664 16 100       65 if ($key =~ /^XMP-(.*?:)(.*)/) {
665 13         40 $tagID = $1 . $parentID . ucfirst($2);
666 13         20 $flatInfo = $$table{$tagID};
667             }
668 16 100       28 $flatInfo or $flatInfo = $tagInfo;
669             }
670 212         282 my $v = $$value{$key};
671 212 100       252 if (ref $v) {
672 48         103 $v = ConvertStruct($et, $flatInfo, $v, $type, $tagID);
673             } else {
674 164         314 $v = $et->GetValue($flatInfo, $type, $v);
675             }
676 212 50       344 if (defined $v) {
677 212         367 $struct{$key} = $v; # save the converted value
678             # maintain ordered keys if necessary
679 212 100       360 push @{$struct{_ordered_keys_}}, $key if $struct{_ordered_keys_};
  4         9  
680             }
681             }
682 108         267 return \%struct;
683             } elsif (ref $value eq 'ARRAY') {
684 66 50       158 if (defined $$et{OPTIONS}{ListItem}) {
685 0         0 my $li = $$et{OPTIONS}{ListItem};
686 0 0       0 return undef unless defined $$value[$li];
687 0         0 undef $$et{OPTIONS}{ListItem}; # only do top-level list
688 0         0 my $val = ConvertStruct($et, $tagInfo, $$value[$li], $type, $parentID);
689 0         0 $$et{OPTIONS}{ListItem} = $li;
690 0         0 return $val;
691             } else {
692 66         96 my (@list, $val);
693 66         110 foreach $val (@$value) {
694 98         185 my $v = ConvertStruct($et, $tagInfo, $val, $type, $parentID);
695 98 50       214 push @list, $v if defined $v;
696             }
697 66         138 return \@list;
698             }
699             } else {
700 30         60 return $et->GetValue($tagInfo, $type, $value);
701             }
702             }
703              
704             #------------------------------------------------------------------------------
705             # Restore XMP structures in extracted information
706             # Inputs: 0) ExifTool object ref, 1) flag to keep original flattened tags
707             # Notes: also restores lists (including multi-dimensional)
708             sub RestoreStruct($;$)
709             {
710 28     28 0 55 local $_;
711 28         80 my ($et, $keepFlat) = @_;
712 28         64 my ($key, %structs, %var, %lists, $si, %listKeys, @siList);
713 28         2011 my $valueHash = $$et{VALUE};
714 28         68 my $fileOrder = $$et{FILE_ORDER};
715 28         75 my $tagExtra = $$et{TAG_EXTRA};
716 28         50 foreach $key (keys %{$$et{TAG_INFO}}) {
  28         662  
717 2326 100       4179 my $structProps = $$tagExtra{$key}{Struct} or next;
718 329         430 delete $$tagExtra{$key}{Struct}; # (don't re-use)
719 329         438 my $tagInfo = $$et{TAG_INFO}{$key}; # tagInfo for flattened tag
720 329         468 my $table = $$tagInfo{Table};
721 329         418 my $prop = shift @$structProps;
722 329         497 my $tag = $$prop[0];
723             # get reference to structure tag (or normal list tag if not a structure)
724 329 100       529 my $strInfo = @$structProps ? $$table{$tag} : $tagInfo;
725 329 100       462 if ($strInfo) {
726 326 50       525 ref $strInfo eq 'HASH' or next; # (just to be safe)
727 326 50 66     717 if (@$structProps and not $$strInfo{Struct}) {
728             # this could happen for invalid XMP containing mixed lists
729             # (or for something like this -- what should we do here?:
730             # test)
731 0 0       0 $et->Warn("$$strInfo{Name} is not a structure!") unless $$et{NO_STRUCT_WARN};
732 0         0 next;
733             }
734             } else {
735             # create new entry in tag table for this structure
736 3   50     9 my $g1 = $$table{GROUPS}{0} || 'XMP';
737 3         3 my $name = $tag;
738             # tag keys will have a group 1 prefix when coming from import of XML from -X option
739 3 50       17 if ($tag =~ /(.+):(.+)/) {
740 3         5 my $ns;
741 3         8 ($ns, $name) = ($1, $2);
742 3         6 $ns =~ s/^XMP-//; # remove leading "XMP-" if it exists because we add it later
743 3 50       8 $ns = $stdXlatNS{$ns} if $stdXlatNS{$ns};
744 3         7 $g1 .= "-$ns";
745             }
746             $strInfo = {
747 3         13 Name => ucfirst $name,
748             Groups => { 1 => $g1 },
749             Struct => 'Unknown',
750             };
751             # add Struct entry if this is a structure
752 3 50       7 if (@$structProps) {
    0          
753             # this is a structure
754 3 50       11 $$strInfo{Struct} = { STRUCT_NAME => 'XMP Unknown' } if @$structProps;
755             } elsif ($$tagInfo{LangCode}) {
756             # this is lang-alt list
757 0         0 $tag = $tag . '-' . $$tagInfo{LangCode};
758 0         0 $$strInfo{LangCode} = $$tagInfo{LangCode};
759             }
760 3         10 AddTagToTable($table, $tag, $strInfo);
761             }
762             # use strInfo ref for base key to avoid collisions
763 329         386 $tag = $strInfo;
764 329         358 my $struct = \%structs;
765 329         496 my $oldStruct = $structs{$strInfo};
766             # (fyi: 'lang-alt' Writable type will be valid even if tag is not pre-defined)
767 329   100     728 my $writable = $$tagInfo{Writable} || '';
768             # walk through the stored structure property information
769             # to rebuild this structure
770 329         384 my ($err, $i);
771 329         327 for (;;) {
772 579         691 my $index = $$prop[1];
773 579 100 100     1104 if ($index and not @$structProps) {
774             # ignore this list if it is a simple lang-alt tag
775 216 100       337 if ($writable eq 'lang-alt') {
776 90         107 pop @$prop; # remove lang-alt index
777 90 100       187 undef $index if @$prop < 2;
778             }
779             # add language code if necessary
780 216 100 100     476 if ($$tagInfo{LangCode} and not ref $tag) {
781 24         38 $tag = $tag . '-' . $$tagInfo{LangCode};
782             }
783             }
784 579         707 my $nextStruct = $$struct{$tag};
785 579 100       698 if (defined $index) {
786             # the field is a list
787 276         350 $index = substr $index, 1; # remove digit count
788 276 100       455 if ($nextStruct) {
789 160 50       256 ref $nextStruct eq 'ARRAY' or $err = 2, last;
790 160         175 $struct = $nextStruct;
791             } else {
792 116         253 $struct = $$struct{$tag} = [ ];
793             }
794 276         360 $nextStruct = $$struct[$index];
795             # descend into multi-dimensional lists
796 276         450 for ($i=2; $$prop[$i]; ++$i) {
797 0 0       0 if ($nextStruct) {
798 0 0       0 ref $nextStruct eq 'ARRAY' or last;
799 0         0 $struct = $nextStruct;
800             } else {
801 0         0 $lists{$struct} = $struct;
802 0         0 $struct = $$struct[$index] = [ ];
803             }
804 0         0 $nextStruct = $$struct[$index];
805 0         0 $index = substr $$prop[$i], 1;
806             }
807 276 100       479 if (ref $nextStruct eq 'HASH') {
    100          
808 61         80 $struct = $nextStruct; # continue building sub-structure
809             } elsif (@$structProps) {
810 66         129 $lists{$struct} = $struct;
811 66         116 $struct = $$struct[$index] = { };
812             } else {
813 149         235 $lists{$struct} = $struct;
814 149         244 $$struct[$index] = $$valueHash{$key};
815 149         198 last;
816             }
817             } else {
818 303 100       483 if ($nextStruct) {
    100          
819 93 50       179 ref $nextStruct eq 'HASH' or $err = 3, last;
820 93         104 $struct = $nextStruct;
821             } elsif (@$structProps) {
822 30         68 $struct = $$struct{$tag} = { };
823             } else {
824 180         392 $$struct{$tag} = $$valueHash{$key};
825 180         222 last;
826             }
827             }
828 250 50       472 $prop = shift @$structProps or last;
829 250         358 $tag = $$prop[0];
830 250 100       469 if ($tag =~ /(.+):(.+)/) {
831             # tag in variable-namespace tables will have a leading
832             # XMP namespace on the tag name. In this case, add
833             # the corresponding group1 name to the tag ID.
834 17         51 my ($ns, $name) = ($1, $2);
835 17 100       44 $ns = $stdXlatNS{$ns} if $stdXlatNS{$ns};
836 17         54 $tag = "XMP-$ns:" . ucfirst $name;
837             } else {
838 233         375 $tag = ucfirst $tag;
839             }
840             }
841 329 50       611 if ($err) {
    100          
842             # this may happen if we have a structural error in the XMP
843             # (like an improperly contained list for example)
844 0 0       0 unless ($$et{NO_STRUCT_WARN}) {
845 0   0     0 my $ns = $$tagInfo{Namespace} || $$tagInfo{Table}{NAMESPACE} || '';
846 0         0 $et->Warn("Error $err placing $ns:$$tagInfo{TagID} in structure or list", 1);
847             }
848 0 0       0 delete $structs{$strInfo} unless $oldStruct;
849             } elsif ($tagInfo eq $strInfo) {
850             # just a regular list tag (or an empty structure)
851 178 100       262 if ($oldStruct) {
852             # keep tag with lowest numbered key (well, not exactly, since
853             # "Tag (10)" is lt "Tag (2)", but at least "Tag" is lt
854             # everything else, and this is really what we care about)
855 75         111 my $k = $listKeys{$oldStruct};
856 75 50       124 if ($k) { # ($k will be undef for an empty structure)
857 75 100       139 if ($k lt $key) {
858             # keep lowest file order
859 48 100       109 $$fileOrder{$k} = $$fileOrder{$key} if $$fileOrder{$k} > $$fileOrder{$key};
860 48         151 $et->DeleteTag($key);
861 48         118 next;
862             }
863 27 100       66 $$fileOrder{$key} = $$fileOrder{$k} if $$fileOrder{$key} > $$fileOrder{$k};
864 27         69 $et->DeleteTag($k); # remove tag with greater copy number
865             }
866             }
867             # replace existing value with new list
868 130         227 $$valueHash{$key} = $structs{$strInfo};
869 130         451 $listKeys{$structs{$strInfo}} = $key; # save key for this list tag
870             } else {
871             # save strInfo ref and file order
872 151 100       245 if ($var{$strInfo}) {
873             # set file order to just before the first associated flattened tag
874 104 100       234 if ($var{$strInfo}[1] > $$fileOrder{$key}) {
875 37         79 $var{$strInfo}[1] = $$fileOrder{$key} - 0.5;
876             }
877             } else {
878 47         144 $var{$strInfo} = [ $strInfo, $$fileOrder{$key} - 0.5 ];
879             }
880             # preserve original flattened tags if requested
881 151 100       229 if ($keepFlat) {
882 81         107 my $extra = $$tagExtra{$key};
883             # restore list behaviour of this flattened tag
884 81 100       229 if ($$extra{NoList}) {
    100          
885 6         10 $$valueHash{$key} = $$extra{NoList};
886 6         13 delete $$extra{NoList};
887             } elsif ($$extra{NoListDel}) {
888             # delete this tag since its value was included another list
889 8         31 $et->DeleteTag($key);
890             }
891             } else {
892 70         146 $et->DeleteTag($key); # delete the flattened tag
893             }
894             }
895             }
896             # fill in undefined items in lists. In theory, undefined list items should
897             # be fine, but in practice the calling code may not check for this (and
898             # historically this wasn't necessary, so do this for backward compatibility)
899 28         230 foreach $si (keys %lists) {
900 116   100     122 defined $_ or $_ = '' foreach @{$lists{$si}};
  116         335  
901             }
902             # make a list of all new structures we generated
903 28   66     227 $var{$_} and push @siList, $_ foreach keys %structs;
904             # save new structures in the same order they were read from file
905 28         142 foreach $si (sort { $var{$a}[1] <=> $var{$b}[1] } @siList) {
  40         70  
906             # test to see if a tag for this structure has already been generated
907             # (this could happen only if one of the structures in a list was empty)
908 47         112 $key = $var{$si}[0]{Name};
909 47         81 my $found;
910 47 50       95 if ($$valueHash{$key}) {
911 0         0 my @keys = grep /^$key( \(\d+\))?$/, keys %$valueHash;
912 0         0 foreach $key (@keys) {
913 0 0       0 next unless $$valueHash{$key} eq $structs{$si};
914 0         0 $found = 1;
915 0         0 last;
916             }
917             }
918 47 50       98 unless ($found) {
919             # otherwise, generate a new tag for this structure
920 47         165 $key = $et->FoundTag($var{$si}[0], '');
921 47         95 $$valueHash{$key} = $structs{$si};
922             }
923 47         241 $$fileOrder{$key} = $var{$si}[1];
924             }
925             }
926              
927              
928             1; #end
929              
930             __END__