summaryrefslogtreecommitdiff
path: root/lib/Image/ExifTool/XMPStruct.pl
blob: 570e98f0e35b864220dc3e2e334ba3e1051aae5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
#------------------------------------------------------------------------------
# File:         XMPStruct.pl
#
# Description:  XMP structure support
#
# Revisions:    01/01/2011 - P. Harvey Created
#------------------------------------------------------------------------------

package Image::ExifTool::XMP;

use strict;
use vars qw(%specialStruct %stdXlatNS);

use Image::ExifTool qw(:Utils);
use Image::ExifTool::XMP;

sub SerializeStruct($;$);
sub InflateStruct($;$);
sub DumpStruct($;$);
sub CheckStruct($$$);
sub AddNewStruct($$$$$$);
sub ConvertStruct($$$$;$);

#------------------------------------------------------------------------------
# Serialize a structure (or other object) into a simple string
# Inputs: 0) HASH ref, ARRAY ref, or SCALAR, 1) closing bracket (or undef)
# Returns: serialized structure string
# eg) "{field=text with {braces|}|, and a comma, field2=val2,field3={field4=[a,b]}}"
sub SerializeStruct($;$)
{
    my ($obj, $ket) = @_;
    my ($key, $val, @vals, $rtnVal);

    if (ref $obj eq 'HASH') {
        foreach $key (sort keys %$obj) {
            push @vals, $key . '=' . SerializeStruct($$obj{$key}, '}');
        }
        $rtnVal = '{' . join(',', @vals) . '}';
    } elsif (ref $obj eq 'ARRAY') {
        foreach $val (@$obj) {
            push @vals, SerializeStruct($val, ']');
        }
        $rtnVal = '[' . join(',', @vals) . ']';
    } elsif (defined $obj) {
        $obj = $$obj if ref $obj eq 'SCALAR';
        # escape necessary characters in string (closing bracket plus "," and "|")
        my $pat = $ket ? "\\$ket|,|\\|" : ',|\\|';
        ($rtnVal = $obj) =~  s/($pat)/|$1/g;
        # also must escape opening bracket or whitespace at start of string
        $rtnVal =~ s/^([\s\[\{])/|$1/;
    } else {
        $rtnVal = '';   # allow undefined list items
    }
    return $rtnVal;
}

#------------------------------------------------------------------------------
# Inflate structure (or other object) from a serialized string
# Inputs: 0) reference to object in string form (serialized using the '|' escape)
#         1) extra delimiter for scalar values delimiters
# Returns: 0) object as a SCALAR, HASH ref, or ARRAY ref (or undef on error),
#          1) warning string (or undef)
# Notes: modifies input string to remove parsed objects
sub InflateStruct($;$)
{
    my ($obj, $delim) = @_;
    my ($val, $warn, $part);

    if ($$obj =~ s/^\s*\{//) {
        my %struct;
        while ($$obj =~ s/^\s*([-\w:]+#?)\s*=//s) {
            my $tag = $1;
            my ($v, $w) = InflateStruct($obj, '}');
            $warn = $w if $w and not $warn;
            return(undef, $warn) unless defined $v;
            $struct{$tag} = $v;
            # eat comma separator, or all done if there wasn't one
            last unless $$obj =~ s/^\s*,//s;
        }
        # eat closing brace and warn if we didn't find one
        unless ($$obj =~ s/^\s*\}//s or $warn) {
            if (length $$obj) {
                ($part = $$obj) =~ s/^\s*//s;
                $part =~ s/[\x0d\x0a].*//s;
                $part = substr($part,0,27) . '...' if length($part) > 30;
                $warn = "Invalid structure field at '${part}'";
            } else {
                $warn = 'Missing closing brace for structure';
            }
        }
        $val = \%struct;
    } elsif ($$obj =~ s/^\s*\[//) {
        my @list;
        for (;;) {
            my ($v, $w) = InflateStruct($obj, ']');
            $warn = $w if $w and not $warn;
            return(undef, $warn) unless defined $v;
            push @list, $v;
            last unless $$obj =~ s/^\s*,//s;
        }
        # eat closing bracket and warn if we didn't find one
        $$obj =~ s/^\s*\]//s or $warn or $warn = 'Missing closing bracket for list';
        $val = \@list;
    } else {
        $$obj =~ s/^\s+//s; # remove leading whitespace
        # read scalar up to specified delimiter (or "," if not defined)
        $val = '';
        $delim = $delim ? "\\$delim|,|\\||\$" : ',|\\||$';
        for (;;) {
            $$obj =~ s/^(.*?)($delim)//s and $val .= $1;
            last unless $2;
            $2 eq '|' or $$obj = $2 . $$obj, last;
            $$obj =~ s/^(.)//s and $val .= $1;  # add escaped character
        }
    }
    return($val, $warn);
}

#------------------------------------------------------------------------------
# Get XMP language code from tag name string
# Inputs: 0) tag name string
# Returns: 0) separated tag name, 1) language code (in standard case), or '' if
#          language code was 'x-default', or undef if the tag had no language code
sub GetLangCode($)
{
    my $tag = shift;
    if ($tag =~ /^(\w+)[-_]([a-z]{2,3}|[xi])([-_][a-z\d]{2,8}([-_][a-z\d]{1,8})*)?$/i) {
        # normalize case of language codes
        my ($tg, $langCode) = ($1, lc($2));
        $langCode .= (length($3) == 3 ? uc($3) : lc($3)) if $3;
        $langCode =~ tr/_/-/;   # RFC 3066 specifies '-' as a separator
        $langCode = '' if lc($langCode) eq 'x-default';
        return($tg, $langCode);
    } else {
        return($tag, undef);
    }
}

#------------------------------------------------------------------------------
# Debugging routine to dump a structure, list or scalar
# Inputs: 0) scalar, ARRAY ref or HASH ref, 1) indent (or undef)
sub DumpStruct($;$)
{
    local $_;
    my ($obj, $indent) = @_;

    $indent or $indent = '';
    if (ref $obj eq 'HASH') {
        print "{\n";
        foreach (sort keys %$obj) {
            print "$indent  $_ = ";
            DumpStruct($$obj{$_}, "$indent  ");
        }
        print $indent, "},\n";
    } elsif (ref $obj eq 'ARRAY') {
        print "[\n";
        foreach (@$obj) {
            print "$indent  ";
            DumpStruct($_, "$indent  ");
        }
        print $indent, "],\n",
    } else {
        print "\"$obj\",\n";
    }
}

#------------------------------------------------------------------------------
# Recursively validate structure fields (tags)
# Inputs: 0) ExifTool ref, 1) Structure ref, 2) structure table definition ref
# Returns: 0) validated structure ref, 1) error string, or undef on success
# Notes:
# - fixes field names in structure and applies inverse conversions to values
# - copies structure to avoid interdependencies with calling code on referenced values
# - handles lang-alt tags, and '#' on field names
# - resets UTF-8 flag of SCALAR values
# - un-escapes for XML or HTML as per Escape option setting
sub CheckStruct($$$)
{
    my ($et, $struct, $strTable) = @_;

    my $strName = $$strTable{STRUCT_NAME} || ('XMP ' . RegisterNamespace($strTable));
    ref $struct eq 'HASH' or return wantarray ? (undef, "Expecting $strName structure") : undef;

    my ($key, $err, $warn, %copy, $rtnVal, $val);
Key:
    foreach $key (keys %$struct) {
        my $tag = $key;
        # allow trailing '#' to disable print conversion on a per-field basis
        my ($type, $fieldInfo);
        $type = 'ValueConv' if $tag =~ s/#$//;
        $fieldInfo = $$strTable{$tag} unless $specialStruct{$tag};
        # fix case of field name if necessary
        unless ($fieldInfo) {
            # (sort in reverse to get lower case (not special) tags first)
            my ($fix) = reverse sort grep /^$tag$/i, keys %$strTable;
            $fieldInfo = $$strTable{$tag = $fix} if $fix and not $specialStruct{$fix};
        }
        until (ref $fieldInfo eq 'HASH') {
            # generate wildcard fields on the fly (eg. mwg-rs:Extensions)
            unless ($$strTable{NAMESPACE}) {
                my ($grp, $tg, $langCode);
                ($grp, $tg) = $tag =~ /^(.+):(.+)/ ? (lc $1, $2) : ('', $tag);
                undef $grp if $grp eq 'XMP'; # (a group of 'XMP' is implied)
                require Image::ExifTool::TagLookup;
                my @matches = Image::ExifTool::TagLookup::FindTagInfo($tg);
                # also look for lang-alt tags
                unless (@matches) {
                    ($tg, $langCode) = GetLangCode($tg);
                    @matches = Image::ExifTool::TagLookup::FindTagInfo($tg) if defined $langCode;
                }
                my ($tagInfo, $priority, $ti, $g1);
                # find best matching tag
                foreach $ti (@matches) {
                    my @grps = $et->GetGroup($ti);
                    next unless $grps[0] eq 'XMP';
                    next if $grp and $grp ne lc $grps[1];
                    # must be lang-alt tag if we are writing an alternate language
                    next if defined $langCode and not ($$ti{Writable} and $$ti{Writable} eq 'lang-alt');
                    my $pri = $$ti{Priority} || 1;
                    $pri -= 10 if $$ti{Avoid};
                    next if defined $priority and $priority >= $pri;
                    $priority = $pri;
                    $tagInfo = $ti;
                    $g1 = $grps[1];
                }
                $tagInfo or $warn =  "'${tag}' is not a writable XMP tag", next Key;
                GetPropertyPath($tagInfo);  # make sure property path is generated for this tag
                $tag = $$tagInfo{Name};
                $tag = "$g1:$tag" if $grp;
                $tag .= "-$langCode" if $langCode;
                $fieldInfo = $$strTable{$tag};
                # create new structure field if necessary
                $fieldInfo or $fieldInfo = $$strTable{$tag} = {
                    %$tagInfo, # (also copies the necessary TagID and PropertyPath)
                    Namespace => $$tagInfo{Table}{NAMESPACE},
                    LangCode  => $langCode,
                };
                # delete stuff we don't need (shouldn't cause harm, but better safe than sorry)
                # - need to keep StructType and Table in case we need to call AddStructType later
                delete $$fieldInfo{Description};
                delete $$fieldInfo{Groups};
                last; # write this dynamically-generated field
            }
            # generate lang-alt fields on the fly (eg. Iptc4xmpExt:AOTitle)
            my ($tg, $langCode) = GetLangCode($tag);
            if (defined $langCode) {
                $fieldInfo = $$strTable{$tg} unless $specialStruct{$tg};
                unless ($fieldInfo) {
                    my ($fix) = reverse sort grep /^$tg$/i, keys %$strTable;
                    $fieldInfo = $$strTable{$tg = $fix} if $fix and not $specialStruct{$fix};
                }
                if (ref $fieldInfo eq 'HASH' and $$fieldInfo{Writable} and
                    $$fieldInfo{Writable} eq 'lang-alt')
                {
                    my $srcInfo = $fieldInfo;
                    $tag = $tg . '-' . $langCode if $langCode;
                    $fieldInfo = $$strTable{$tag};
                    # create new structure field if necessary
                    $fieldInfo or $fieldInfo = $$strTable{$tag} = {
                        %$srcInfo,
                        TagID    => $tg,
                        LangCode => $langCode,
                    };
                    last; # write this lang-alt field
                }
            }
            $warn = "'${tag}' is not a field of $strName";
            next Key;
        }
        if (ref $$struct{$key} eq 'HASH') {
            $$fieldInfo{Struct} or $warn = "$tag is not a structure in $strName", next Key;
            # recursively check this structure
            ($val, $err) = CheckStruct($et, $$struct{$key}, $$fieldInfo{Struct});
            $err and $warn = $err, next Key;
            $copy{$tag} = $val;
        } elsif (ref $$struct{$key} eq 'ARRAY') {
            $$fieldInfo{List} or $warn = "$tag is not a list in $strName", next Key;
            # check all items in the list
            my ($item, @copy);
            my $i = 0;
            foreach $item (@{$$struct{$key}}) {
                if (not ref $item) {
                    $item = '' unless defined $item; # use empty string for missing items
                    if ($$fieldInfo{Struct}) {
                        # (allow empty structures)
                        $item =~ /^\s*$/ or $warn = "$tag items are not valid structures", next Key;
                        $copy[$i] = { }; # create hash for empty structure
                    } else {
                        $et->Sanitize(\$item);
                        ($copy[$i],$err) = $et->ConvInv($item,$fieldInfo,$tag,$strName,$type,'');
                        $copy[$i] = '' unless defined $copy[$i];    # avoid undefined item
                        $err and $warn = $err, next Key;
                        $err = CheckXMP($et, $fieldInfo, \$copy[$i]);
                        $err and $warn = "$err in $strName $tag", next Key;
                    }
                } elsif (ref $item eq 'HASH') {
                    $$fieldInfo{Struct} or $warn = "$tag is not a structure in $strName", next Key;
                    ($copy[$i], $err) = CheckStruct($et, $item, $$fieldInfo{Struct});
                    $err and $warn = $err, next Key;
                } else {
                    $warn = "Invalid value for $tag in $strName";
                    next Key;
                }
                ++$i;
            }
            $copy{$tag} = \@copy;
        } elsif ($$fieldInfo{Struct}) {
            $warn = "Improperly formed structure in $strName $tag";
        } else {
            $et->Sanitize(\$$struct{$key});
            ($val,$err) = $et->ConvInv($$struct{$key},$fieldInfo,$tag,$strName,$type,'');
            $err and $warn = $err, next Key;
            next Key unless defined $val;   # check for undefined
            $err = CheckXMP($et, $fieldInfo, \$val);
            $err and $warn = "$err in $strName $tag", next Key;
            # turn this into a list if necessary
            $copy{$tag} = $$fieldInfo{List} ? [ $val ] : $val;
        }
    }
    if (%copy or not $warn) {
        $rtnVal = \%copy;
        undef $err;
        $$et{CHECK_WARN} = $warn if $warn;
    } else {
        $err = $warn;
    }
    return wantarray ? ($rtnVal, $err) : $rtnVal;
}

#------------------------------------------------------------------------------
# Delete matching structures from existing linearized XMP
# Inputs: 0) ExifTool ref, 1) capture hash ref, 2) structure path ref,
#         3) new value hash ref, 4) reference to change counter
# Returns: 0) delete flag, 1) list index of deleted structure if adding to list
#          2) flag set if structure existed
# Notes: updates path to new base path for structure to be added
sub DeleteStruct($$$$$)
{
    my ($et, $capture, $pathPt, $nvHash, $changed) = @_;
    my ($deleted, $added, $existed, $p, $pp, $val, $delPath);
    my (@structPaths, @matchingPaths, @delPaths);

    # find all existing elements belonging to this structure
    ($pp = $$pathPt) =~ s/ \d+/ \\d\+/g;
    @structPaths = sort grep(/^$pp(\/|$)/, keys %$capture);
    $existed = 1 if @structPaths;
    # delete only structures with matching fields if necessary
    if ($$nvHash{DelValue}) {
        if (@{$$nvHash{DelValue}}) {
            my $strTable = $$nvHash{TagInfo}{Struct};
            # all fields must match corresponding elements in the same
            # root structure for it to be deleted
            foreach $val (@{$$nvHash{DelValue}}) {
                next unless ref $val eq 'HASH';
                my (%cap, $p2, %match);
                next unless AddNewStruct(undef, undef, \%cap, $$pathPt, $val, $strTable);
                foreach $p (keys %cap) {
                    if ($p =~ / /) {
                        ($p2 = $p) =~ s/ \d+/ \\d\+/g;
                        @matchingPaths = sort grep(/^$p2$/, @structPaths);
                    } else {
                        push @matchingPaths, $p;
                    }
                    foreach $p2 (@matchingPaths) {
                        $p2 =~ /^($pp)/ or next;
                        # language attribute must also match if it exists
                        my $attr = $cap{$p}[1];
                        if ($$attr{'xml:lang'}) {
                            my $a2 = $$capture{$p2}[1];
                            next unless $$a2{'xml:lang'} and $$a2{'xml:lang'} eq $$attr{'xml:lang'};
                        }
                        if ($$capture{$p2} and $$capture{$p2}[0] eq $cap{$p}[0]) {
                            # ($1 contains root path for this structure)
                            $match{$1} = ($match{$1} || 0) + 1;
                        }
                    }
                }
                my $num = scalar(keys %cap);
                foreach $p (keys %match) {
                    # do nothing unless all fields matched the same structure
                    next unless $match{$p} == $num;
                    # delete all elements of this structure
                    foreach $p2 (@structPaths) {
                        push @delPaths, $p2 if $p2 =~ /^$p/;
                    }
                    # remember path of first deleted structure
                    $delPath = $p if not $delPath or $delPath gt $p;
                }
            }
        } # (else don't delete anything)
    } elsif (@structPaths) {
        @delPaths = @structPaths;   # delete all
        $structPaths[0] =~ /^($pp)/;
        $delPath = $1;
    }
    if (@delPaths) {
        my $verbose = $et->Options('Verbose');
        @delPaths = sort @delPaths if $verbose > 1;
        foreach $p (@delPaths) {
            $et->VerboseValue("- XMP-$p", $$capture{$p}[0]) if $verbose > 1;
            delete $$capture{$p};
            $deleted = 1;
            ++$$changed;
        }
        $delPath or warn("Internal error 1 in DeleteStruct\n"), return(undef,undef,$existed);
        $$pathPt = $delPath;    # return path of first element deleted
    } elsif ($$nvHash{TagInfo}{List}) {
        # NOTE: we don't yet properly handle lang-alt elements!!!!
        if (@structPaths) {
            $structPaths[-1] =~ /^($pp)/ or warn("Internal error 2 in DeleteStruct\n"), return(undef,undef,$existed);
            my $path = $1;
            # delete any improperly formatted xmp
            if ($$capture{$path}) {
                my $cap = $$capture{$path};
                # an error unless this was an empty structure
                $et->Error("Improperly structured XMP ($path)",1) if ref $cap ne 'ARRAY' or $$cap[0];
                delete $$capture{$path};
            }
            # (match last index to put in same lang-alt list for Bag of lang-alt items)
            $path =~ m/.* (\d+)/g or warn("Internal error 3 in DeleteStruct\n"), return(undef,undef,$existed);
            $added = $1;
            # add after last item in list
            my $len = length $added;
            my $pos = pos($path) - $len;
            my $nxt = substr($added, 1) + 1;
            substr($path, $pos, $len) = length($nxt) . $nxt;
            $$pathPt = $path;
        } else {
            $added = '10';
        }
    }
    return($deleted, $added, $existed);
}

#------------------------------------------------------------------------------
# Add new element to XMP capture hash
# Inputs: 0) ExifTool ref, 1) TagInfo ref, 2) capture hash ref,
#         3) resource path, 4) value ref, 5) hash ref for last used index numbers
sub AddNewTag($$$$$$)
{
    my ($et, $tagInfo, $capture, $path, $valPtr, $langIdx) = @_;
    my $val = EscapeXML($$valPtr);
    my %attrs;
    # support writing RDF "resource" values
    if ($$tagInfo{Resource}) {
        $attrs{'rdf:resource'} = $val;
        $val = '';
    }
    if ($$tagInfo{Writable} and $$tagInfo{Writable} eq 'lang-alt') {
        # write the lang-alt tag
        my $langCode = $$tagInfo{LangCode};
        # add indexed lang-alt list properties
        my $i = $$langIdx{$path} || 0;
        $$langIdx{$path} = $i + 1; # save next list index
        if ($i) {
            my $idx = length($i) . $i;
            $path =~ s/(.*) \d+/$1 $idx/;   # set list index
        }
        $attrs{'xml:lang'} = $langCode || 'x-default';
    }
    $$capture{$path} = [ $val, \%attrs ];
    # print verbose message
    if ($et and $et->Options('Verbose') > 1) {
        $et->VerboseValue("+ XMP-$path", $val);
    }
}

#------------------------------------------------------------------------------
# Add new structure to capture hash for writing
# Inputs: 0) ExifTool object ref (or undef for no warnings),
#         1) tagInfo ref (or undef if no ExifTool), 2) capture hash ref,
#         3) base path, 4) struct ref, 5) struct hash ref
# Returns: number of tags changed
# Notes: Escapes values for XML
sub AddNewStruct($$$$$$)
{
    my ($et, $tagInfo, $capture, $basePath, $struct, $strTable) = @_;
    my $verbose = $et ? $et->Options('Verbose') : 0;
    my ($tag, %langIdx);

    my $ns = $$strTable{NAMESPACE} || '';
    my $changed = 0;

    # add dummy field to allow empty structures (name starts with '~' so it will come
    # after all valid structure fields, which is necessary when serializing the XMP later)
    %$struct or $$struct{'~dummy~'} = '';

    foreach $tag (sort keys %$struct) {
        my $fieldInfo = $$strTable{$tag};
        unless ($fieldInfo) {
            next unless $tag eq '~dummy~'; # check for dummy field
            $fieldInfo = { }; # create dummy field info for dummy structure
        }
        my $val = $$struct{$tag};
        my $propPath = $$fieldInfo{PropertyPath};
        unless ($propPath) {
            $propPath = ($$fieldInfo{Namespace} || $ns) . ':' . ($$fieldInfo{TagID} || $tag);
            if ($$fieldInfo{List}) {
                $propPath .= "/rdf:$$fieldInfo{List}/rdf:li 10";
            }
            if ($$fieldInfo{Writable} and $$fieldInfo{Writable} eq 'lang-alt') {
                $propPath .= "/rdf:Alt/rdf:li 10";
            }
            $$fieldInfo{PropertyPath} = $propPath;  # save for next time
        }
        my $path = $basePath . '/' . ConformPathToNamespace($et, $propPath);
        my $addedTag;
        if (ref $val eq 'HASH') {
            my $subStruct = $$fieldInfo{Struct} or next;
            $changed += AddNewStruct($et, $tagInfo, $capture, $path, $val, $subStruct);
        } elsif (ref $val eq 'ARRAY') {
            next unless $$fieldInfo{List};
            my $i = 0;
            my ($item, $p);
            # loop through all list items (note: can't yet write multi-dimensional lists)
            foreach $item (@{$val}) {
                if ($i) {
                    # update first index in field property (may be list of lang-alt lists)
                    $p = ConformPathToNamespace($et, $propPath);
                    my $idx = length($i) . $i;
                    $p =~ s/ \d+/ $idx/;
                    $p = "$basePath/$p";
                } else {
                    $p = $path;
                }
                if (ref $item eq 'HASH') {
                    my $subStruct = $$fieldInfo{Struct} or next;
                    AddNewStruct($et, $tagInfo, $capture, $p, $item, $subStruct) or next;
                } elsif (length $item) { # don't write empty items in list
                    AddNewTag($et, $fieldInfo, $capture, $p, \$item, \%langIdx);
                    $addedTag = 1;
                }
                ++$changed;
                ++$i;
            }
        } else {
            AddNewTag($et, $fieldInfo, $capture, $path, \$val, \%langIdx);
            $addedTag = 1;
            ++$changed;
        }
        # this is tricky, but we must add the rdf:type for contained structures
        # in the case that a whole hierarchy was added at once by writing a
        # flattened tag inside a variable-namespace structure
        if ($addedTag and $$fieldInfo{StructType} and $$fieldInfo{Table}) {
            AddStructType($et, $$fieldInfo{Table}, $capture, $propPath, $basePath);
        }
    }
    # add 'rdf:type' property if necessary
    if ($$strTable{TYPE} and $changed) {
        my $path = $basePath . '/' . ConformPathToNamespace($et, "rdf:type");
        unless ($$capture{$path}) {
            $$capture{$path} = [ '', { 'rdf:resource' => $$strTable{TYPE} } ];
            $et->VerboseValue("+ XMP-$path", $$strTable{TYPE}) if $verbose > 1;
        }
    }
    return $changed;
}

#------------------------------------------------------------------------------
# Convert structure field values for printing
# Inputs: 0) ExifTool ref, 1) tagInfo ref for structure tag, 2) value,
#         3) conversion type: PrintConv, ValueConv or Raw (Both not allowed)
#         4) tagID of parent structure (needed only if there was no flattened tag)
# Notes: Makes a copy of the hash so any applied escapes won't affect raw values
sub ConvertStruct($$$$;$)
{
    my ($et, $tagInfo, $value, $type, $parentID) = @_;
    if (ref $value eq 'HASH') {
        my (%struct, $key);
        my $table = $$tagInfo{Table};
        $parentID = $$tagInfo{TagID} unless $parentID;
        foreach $key (keys %$value) {
            my $tagID = $parentID . ucfirst($key);
            my $flatInfo = $$table{$tagID};
            unless ($flatInfo) {
                # handle variable-namespace structures
                if ($key =~ /^XMP-(.*?:)(.*)/) {
                    $tagID = $1 . $parentID . ucfirst($2);
                    $flatInfo = $$table{$tagID};
                }
                $flatInfo or $flatInfo = $tagInfo;
            }
            my $v = $$value{$key};
            if (ref $v) {
                $v = ConvertStruct($et, $flatInfo, $v, $type, $tagID);
            } else {
                $v = $et->GetValue($flatInfo, $type, $v);
            }
            $struct{$key} = $v if defined $v;  # save the converted value
        }
        return \%struct;
    } elsif (ref $value eq 'ARRAY') {
        if (defined $$et{OPTIONS}{ListItem}) {
            my $li = $$et{OPTIONS}{ListItem};
            return undef unless defined $$value[$li];
            undef $$et{OPTIONS}{ListItem};      # only do top-level list
            my $val = ConvertStruct($et, $tagInfo, $$value[$li], $type, $parentID);
            $$et{OPTIONS}{ListItem} = $li;
            return $val;
        } else {
            my (@list, $val);
            foreach $val (@$value) {
                my $v = ConvertStruct($et, $tagInfo, $val, $type, $parentID);
                push @list, $v if defined $v;
            }
            return \@list;
        }
    } else {
        return $et->GetValue($tagInfo, $type, $value);
    }
}

#------------------------------------------------------------------------------
# Restore XMP structures in extracted information
# Inputs: 0) ExifTool object ref, 1) flag to keep original flattened tags
# Notes: also restores lists (including multi-dimensional)
sub RestoreStruct($;$)
{
    local $_;
    my ($et, $keepFlat) = @_;
    my ($key, %structs, %var, %lists, $si, %listKeys, @siList);
    my $ex = $$et{TAG_EXTRA};
    my $valueHash = $$et{VALUE};
    my $tagExtra = $$et{TAG_EXTRA};
    foreach $key (keys %{$$et{TAG_INFO}}) {
        $$ex{$key} or next;
        my $structProps = $$ex{$key}{Struct} or next;
        delete $$ex{$key}{Struct}; # (don't re-use)
        my $tagInfo = $$et{TAG_INFO}{$key};   # tagInfo for flattened tag
        my $table = $$tagInfo{Table};
        my $prop = shift @$structProps;
        my $tag = $$prop[0];
        # get reference to structure tag (or normal list tag if not a structure)
        my $strInfo = @$structProps ? $$table{$tag} : $tagInfo;
        if ($strInfo) {
            ref $strInfo eq 'HASH' or next; # (just to be safe)
            if (@$structProps and not $$strInfo{Struct}) {
                # this could happen for invalid XMP containing mixed lists
                # (or for something like this -- what should we do here?:
                # <meta:user-defined meta:name="License">test</meta:user-defined>)
                $et->Warn("$$strInfo{Name} is not a structure!") unless $$et{NO_STRUCT_WARN};
                next;
            }
        } else {
            # create new entry in tag table for this structure
            my $g1 = $$table{GROUPS}{0} || 'XMP';
            my $name = $tag;
            # tag keys will have a group 1 prefix when coming from import of XML from -X option
            if ($tag =~ /(.+):(.+)/) {
                my $ns;
                ($ns, $name) = ($1, $2);
                $ns =~ s/^XMP-//; # remove leading "XMP-" if it exists because we add it later
                $ns = $stdXlatNS{$ns} if $stdXlatNS{$ns};
                $g1 .= "-$ns";
            }
            $strInfo = {
                Name => ucfirst $name,
                Groups => { 1 => $g1 },
                Struct => 'Unknown',
            };
            # add Struct entry if this is a structure
            if (@$structProps) {
                # this is a structure
                $$strInfo{Struct} = { STRUCT_NAME => 'XMP Unknown' } if @$structProps;
            } elsif ($$tagInfo{LangCode}) {
                # this is lang-alt list
                $tag = $tag . '-' . $$tagInfo{LangCode};
                $$strInfo{LangCode} = $$tagInfo{LangCode};
            }
            AddTagToTable($table, $tag, $strInfo);
        }
        # use strInfo ref for base key to avoid collisions
        $tag = $strInfo;
        my $struct = \%structs;
        my $oldStruct = $structs{$strInfo};
        # (fyi: 'lang-alt' Writable type will be valid even if tag is not pre-defined)
        my $writable = $$tagInfo{Writable} || '';
        # walk through the stored structure property information
        # to rebuild this structure
        my ($err, $i);
        for (;;) {
            my $index = $$prop[1];
            if ($index and not @$structProps) {
                # ignore this list if it is a simple lang-alt tag
                if ($writable eq 'lang-alt') {
                    pop @$prop; # remove lang-alt index
                    undef $index if @$prop < 2;
                }
                # add language code if necessary
                if ($$tagInfo{LangCode} and not ref $tag) {
                    $tag = $tag . '-' . $$tagInfo{LangCode};
                }
            }
            my $nextStruct = $$struct{$tag};
            if (defined $index) {
                # the field is a list
                $index = substr $index, 1;  # remove digit count
                if ($nextStruct) {
                    ref $nextStruct eq 'ARRAY' or $err = 2, last;
                    $struct = $nextStruct;
                } else {
                    $struct = $$struct{$tag} = [ ];
                }
                $nextStruct = $$struct[$index];
                # descend into multi-dimensional lists
                for ($i=2; $$prop[$i]; ++$i) {
                    if ($nextStruct) {
                        ref $nextStruct eq 'ARRAY' or last;
                        $struct = $nextStruct;
                    } else {
                        $lists{$struct} = $struct;
                        $struct = $$struct[$index] = [ ];
                    }
                    $nextStruct = $$struct[$index];
                    $index = substr $$prop[$i], 1;
                }
                if (ref $nextStruct eq 'HASH') {
                    $struct = $nextStruct;  # continue building sub-structure
                } elsif (@$structProps) {
                    $lists{$struct} = $struct;
                    $struct = $$struct[$index] = { };
                } else {
                    $lists{$struct} = $struct;
                    $$struct[$index] = $$valueHash{$key};
                    last;
                }
            } else {
                if ($nextStruct) {
                    ref $nextStruct eq 'HASH' or $err = 3, last;
                    $struct = $nextStruct;
                } elsif (@$structProps) {
                    $struct = $$struct{$tag} = { };
                } else {
                    $$struct{$tag} = $$valueHash{$key};
                    last;
                }
            }
            $prop = shift @$structProps or last;
            $tag = $$prop[0];
            if ($tag =~ /(.+):(.+)/) {
                # tag in variable-namespace tables will have a leading
                # XMP namespace on the tag name.  In this case, add
                # the corresponding group1 name to the tag ID.
                my ($ns, $name) = ($1, $2);
                $ns = $stdXlatNS{$ns} if $stdXlatNS{$ns};
                $tag = "XMP-$ns:" . ucfirst $name;
            } else {
                $tag = ucfirst $tag;
            }
        }
        if ($err) {
            # this may happen if we have a structural error in the XMP
            # (like an improperly contained list for example)
            unless ($$et{NO_STRUCT_WARN}) {
                my $ns = $$tagInfo{Namespace} || $$tagInfo{Table}{NAMESPACE} || '';
                $et->Warn("Error $err placing $ns:$$tagInfo{TagID} in structure or list", 1);
            }
            delete $structs{$strInfo} unless $oldStruct;
        } elsif ($tagInfo eq $strInfo) {
            # just a regular list tag (or an empty structure)
            if ($oldStruct) {
                # keep tag with lowest numbered key (well, not exactly, since
                # "Tag (10)" is lt "Tag (2)", but at least "Tag" is lt
                # everything else, and this is really what we care about)
                my $k = $listKeys{$oldStruct};
                if ($k) {   # ($k will be undef for an empty structure)
                    $k lt $key and $et->DeleteTag($key), next;
                    $et->DeleteTag($k);   # remove tag with greater copy number
                }
            }
            # replace existing value with new list
            $$valueHash{$key} = $structs{$strInfo};
            $listKeys{$structs{$strInfo}} = $key;   # save key for this list tag
        } else {
            # save strInfo ref and file order
            if ($var{$strInfo}) {
                # set file order to just before the first associated flattened tag
                if ($var{$strInfo}[1] > $$et{FILE_ORDER}{$key}) {
                    $var{$strInfo}[1] = $$et{FILE_ORDER}{$key} - 0.5;
                }
            } else {
                $var{$strInfo} = [ $strInfo, $$et{FILE_ORDER}{$key} - 0.5 ];
            }
            # preserve original flattened tags if requested
            if ($keepFlat) {
                my $extra = $$tagExtra{$key} or next;
                # restore list behaviour of this flattened tag
                if ($$extra{NoList}) {
                    $$valueHash{$key} = $$extra{NoList};
                    delete $$extra{NoList};
                } elsif ($$extra{NoListDel}) {
                    # delete this tag since its value was included another list
                    $et->DeleteTag($key);
                }
            } else {
                $et->DeleteTag($key); # delete the flattened tag
            }
        }
    }
    # fill in undefined items in lists.  In theory, undefined list items should
    # be fine, but in practice the calling code may not check for this (and
    # historically this wasn't necessary, so do this for backward compatibility)
    foreach $si (keys %lists) {
        defined $_ or $_ = '' foreach @{$lists{$si}};
    }
    # make a list of all new structures we generated
    $var{$_} and push @siList, $_ foreach keys %structs;
    # save new structures in the same order they were read from file
    foreach $si (sort { $var{$a}[1] <=> $var{$b}[1] } @siList) {
        $key = $et->FoundTag($var{$si}[0], '');
        $$valueHash{$key} = $structs{$si};
        $$et{FILE_ORDER}{$key} = $var{$si}[1];
    }
}


1;  #end

__END__

=head1 NAME

Image::ExifTool::XMPStruct.pl - XMP structure support

=head1 SYNOPSIS

This module is loaded automatically by Image::ExifTool when required.

=head1 DESCRIPTION

This file contains routines to provide read/write support of structured XMP
information.

=head1 AUTHOR

Copyright 2003-2018, Phil Harvey (phil at owl.phy.queensu.ca)

This library is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=head1 SEE ALSO

L<Image::ExifTool::TagNames/XMP Tags>,
L<Image::ExifTool(3pm)|Image::ExifTool>

=cut