File Coverage

blib/lib/Image/ExifTool/XMPStruct.pl
Criterion Covered Total %
statement 389 497 78.2
branch 230 378 60.8
condition 63 117 53.8
subroutine 12 14 85.7
pod 0 10 0.0
total 694 1016 68.3


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