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