source: gsdl/trunk/perllib/cpan/Image/ExifTool/WriteXMP.pl@ 16842

Last change on this file since 16842 was 16842, checked in by davidb, 16 years ago

ExifTool added to cpan area to support metadata extraction from files such as JPEG. Primarily targetted as Image files (hence the Image folder name decided upon by the ExifTool author) it also can handle video such as flash and audio such as Wav

File size: 39.0 KB
Line 
1#------------------------------------------------------------------------------
2# File: WriteXMP.pl
3#
4# Description: Write XMP meta information
5#
6# Revisions: 12/19/2004 - P. Harvey Created
7#------------------------------------------------------------------------------
8package Image::ExifTool::XMP;
9
10use strict;
11use Image::ExifTool qw(:DataAccess :Utils);
12
13sub CheckXMP($$$);
14sub SetPropertyPath($$;$$);
15sub CaptureXMP($$$;$);
16
17my $debug = 0;
18
19# XMP structures (each structure is similar to a tag table so we can
20# recurse through them in SetPropertyPath() as if they were tag tables)
21my %xmpStruct = (
22 ResourceRef => {
23 NAMESPACE => 'stRef',
24 InstanceID => { },
25 DocumentID => { },
26 VersionID => { },
27 RenditionClass => { },
28 RenditionParams => { },
29 Manager => { },
30 ManagerVariant => { },
31 ManageTo => { },
32 ManageUI => { },
33 },
34 ResourceEvent => {
35 NAMESPACE => 'stEvt',
36 action => { },
37 instanceID => { },
38 parameters => { },
39 softwareAgent => { },
40 when => { },
41 },
42 JobRef => {
43 NAMESPACE => 'stJob',
44 name => { },
45 id => { },
46 url => { },
47 },
48 Version => {
49 NAMESPACE => 'stVer',
50 comments => { },
51 event => { Struct => 'ResourceEvent' },
52 modifyDate => { },
53 modifier => { },
54 version => { },
55 },
56 Thumbnail => {
57 NAMESPACE => 'xapGImg',
58 height => { },
59 width => { },
60 'format' => { },
61 image => { },
62 },
63 IdentifierScheme => {
64 NAMESPACE => 'xmpidq',
65 Scheme => { }, # qualifier for xmp:Identifier only
66 },
67 Dimensions => {
68 NAMESPACE => 'stDim',
69 w => { },
70 h => { },
71 unit => { },
72 },
73 Colorant => {
74 NAMESPACE => 'xapG',
75 swatchName => { },
76 mode => { },
77 type => { },
78 cyan => { },
79 magenta => { },
80 yellow => { },
81 black => { },
82 red => { },
83 green => { },
84 blue => { },
85 L => { },
86 A => { },
87 B => { },
88 },
89 Font => {
90 NAMESPACE => 'stFnt',
91 fontName => { },
92 fontFamily => { },
93 fontFace => { },
94 fontType => { },
95 versionString => { },
96 composite => { },
97 fontFileName=> { },
98 childFontFiles=> { List => 'Seq' },
99 },
100 # the following stuctures are different: They don't have
101 # their own namespaces -- instead they use the parent namespace
102 Flash => {
103 NAMESPACE => 'exif',
104 Fired => { },
105 Return => { },
106 Mode => { },
107 Function => { },
108 RedEyeMode => { },
109 },
110 OECF => {
111 NAMESPACE => 'exif',
112 Columns => { },
113 Rows => { },
114 Names => { },
115 Values => { },
116 },
117 CFAPattern => {
118 NAMESPACE => 'exif',
119 Columns => { },
120 Rows => { },
121 Values => { },
122 },
123 DeviceSettings => {
124 NAMESPACE => 'exif',
125 Columns => { },
126 Rows => { },
127 Settings => { },
128 },
129 # Iptc4xmpCore structures
130 ContactInfo => {
131 NAMESPACE => 'Iptc4xmpCore',
132 CiAdrCity => { },
133 CiAdrCtry => { },
134 CiAdrExtadr => { },
135 CiAdrPcode => { },
136 CiAdrRegion => { },
137 CiEmailWork => { },
138 CiTelWork => { },
139 CiUrlWork => { },
140 },
141 # Dynamic Media structures
142 BeatSpliceStretch => {
143 NAMESPACE => 'xmpDM',
144 useFileBeatsMarker => { },
145 riseInDecibel => { },
146 riseInTimeDuration => { },
147 },
148 Marker => {
149 NAMESPACE => 'xmpDM',
150 startTime => { },
151 duration => { },
152 comment => { },
153 name => { },
154 location => { },
155 target => { },
156 type => { },
157 },
158 Media => {
159 NAMESPACE => 'xmpDM',
160 path => { },
161 track => { },
162 startTime => { },
163 duration => { },
164 managed => { },
165 webStatement=> { },
166 },
167 ProjectLink => {
168 NAMESPACE => 'xmpDM',
169 type => { },
170 path => { },
171 },
172 ResampleStretch => {
173 NAMESPACE => 'xmpDM',
174 quality => { },
175 },
176 Time => {
177 NAMESPACE => 'xmpDM',
178 value => { },
179 scale => { },
180 },
181 Timecode => {
182 NAMESPACE => 'xmpDM',
183 timeValue => { },
184 timeFormat => { },
185 },
186 TimeScaleStretch => {
187 NAMESPACE => 'xmpDM',
188 quality => { },
189 frameSize => { },
190 frameOverlappingPercentage => { },
191 },
192);
193
194# Lookup to translate our namespace prefixes into URI's. This list need
195# not be complete, but it must contain an entry for each namespace prefix
196# (NAMESPACE) for writable tags in the XMP tables or the table above
197my %nsURI = (
198 aux => 'http://ns.adobe.com/exif/1.0/aux/',
199 cc => 'http://web.resource.org/cc/',
200 crs => 'http://ns.adobe.com/camera-raw-settings/1.0/',
201 crss => 'http://ns.adobe.com/camera-raw-saved-settings/1.0/',
202 dc => 'http://purl.org/dc/elements/1.1/',
203 exif => 'http://ns.adobe.com/exif/1.0/',
204 iX => 'http://ns.adobe.com/iX/1.0/',
205 pdf => 'http://ns.adobe.com/pdf/1.3/',
206 pdfx => 'http://ns.adobe.com/pdfx/1.3/',
207 photoshop => 'http://ns.adobe.com/photoshop/1.0/',
208 rdf => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
209 rdfs => 'http://www.w3.org/2000/01/rdf-schema#',
210 stDim => 'http://ns.adobe.com/xap/1.0/sType/Dimensions#',
211 stEvt => 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#',
212 stFnt => 'http://ns.adobe.com/xap/1.0/sType/Font#',
213 stJob => 'http://ns.adobe.com/xap/1.0/sType/Job#',
214 stRef => 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#',
215 stVer => 'http://ns.adobe.com/xap/1.0/sType/Version#',
216 tiff => 'http://ns.adobe.com/tiff/1.0/',
217 'x' => 'adobe:ns:meta/',
218 xapG => 'http://ns.adobe.com/xap/1.0/g/',
219 xapGImg => 'http://ns.adobe.com/xap/1.0/g/img/',
220 xmp => 'http://ns.adobe.com/xap/1.0/',
221 xmpBJ => 'http://ns.adobe.com/xap/1.0/bj/',
222 xmpDM => 'http://ns.adobe.com/xmp/1.0/DynamicMedia/',
223 xmpMM => 'http://ns.adobe.com/xap/1.0/mm/',
224 xmpRights => 'http://ns.adobe.com/xap/1.0/rights/',
225 xmpTPg => 'http://ns.adobe.com/xap/1.0/t/pg/',
226 xmpidq => 'http://ns.adobe.com/xmp/Identifier/qual/1.0/',
227 xmpPLUS => 'http://ns.adobe.com/xap/1.0/PLUS/',
228 dex => 'http://ns.optimasc.com/dex/1.0/',
229 mediapro => 'http://ns.iview-multimedia.com/mediapro/1.0/',
230 Iptc4xmpCore => 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/',
231 MicrosoftPhoto => 'http://ns.microsoft.com/photo/1.0',
232 lr => 'http://ns.adobe.com/lightroom/1.0/',
233 DICOM => 'http://ns.adobe.com/DICOM/',
234);
235
236my $x_toolkit = "x:xmptk='Image::ExifTool $Image::ExifTool::VERSION'";
237my $rdfDesc = 'rdf:Description';
238#
239# packet/xmp/rdf headers and trailers
240#
241my $pktOpen = "<?xpacket begin='\xef\xbb\xbf' id='W5M0MpCehiHzreSzNTczkc9d'?>\n";
242my $xmlOpen = "<?xml version='1.0' encoding='UTF-8'?>\n";
243my $xmpOpen = "<x:xmpmeta xmlns:x='$nsURI{x}' $x_toolkit>\n";
244my $rdfOpen = "<rdf:RDF xmlns:rdf='$nsURI{rdf}'>\n";
245my $rdfClose = "</rdf:RDF>\n";
246my $xmpClose = "</x:xmpmeta>\n";
247my $pktCloseW = "<?xpacket end='w'?>"; # writable by default
248my $pktCloseR = "<?xpacket end='r'?>";
249
250# Update XMP tag tables when this library is loaded:
251# - generate all TagID's (required when writing)
252# - generate PropertyPath for structure elements
253# - add necessary inverse conversion routines
254{
255 my $mainTable = GetTagTable('Image::ExifTool::XMP::Main');
256 GenerateTagIDs($mainTable);
257 my ($mainTag, $ns, $tag);
258 foreach $mainTag (keys %$mainTable) {
259 my $mainInfo = $mainTable->{$mainTag};
260 next unless ref $mainInfo eq 'HASH' and $mainInfo->{SubDirectory};
261 my $table = GetTagTable($mainInfo->{SubDirectory}->{TagTable});
262 $$table{WRITE_PROC} = \&WriteXMP; # set WRITE_PROC for all tables
263 GenerateTagIDs($table);
264 $table->{CHECK_PROC} = \&CheckXMP; # add our write check routine
265 foreach $tag (TagTableKeys($table)) {
266 my $tagInfo = $$table{$tag};
267 next unless ref $tagInfo eq 'HASH';
268 # must set PropertyPath now for all tags that are Struct elements
269 # (normal tags will get set later if they are actually written)
270 SetPropertyPath($table, $tag) if $$tagInfo{Struct};
271 my $format = $$tagInfo{Writable};
272 next unless $format and $format eq 'date';
273 # add dummy conversion for dates (for now...)
274 $$tagInfo{PrintConvInv} = '$val' unless $$tagInfo{PrintConvInv};
275 $$tagInfo{ValueConvInv} = '$val' unless $$tagInfo{ValueConvInv};
276 }
277 # add new namespace if NAMESPACE is ns/uri pair
278 next unless ref $$table{NAMESPACE};
279 my $nsRef = $$table{NAMESPACE};
280 # recognize as either a list or hash
281 if (ref $nsRef eq 'ARRAY') {
282 $ns = $$nsRef[0];
283 $nsURI{$ns} = $$nsRef[1];
284 } else { # must be a hash
285 ($ns) = keys %$nsRef;
286 $nsURI{$ns} = $$nsRef{$ns};
287 }
288 $$table{NAMESPACE} = $ns;
289 }
290}
291
292#------------------------------------------------------------------------------
293# Validate XMP packet and set read or read/write mode
294# Inputs: 0) XMP data reference, 1) 'r' = read only, 'w' or undef = read/write
295# Returns: true if XMP is good (and adds packet header/trailer if necessary)
296sub ValidateXMP($;$)
297{
298 my ($xmpPt, $mode) = @_;
299 unless ($$xmpPt =~ /^\0*<\0*\?\0*x\0*p\0*a\0*c\0*k\0*e\0*t/) {
300 return '' unless $$xmpPt =~ /^<x(mp)?:xmpmeta/;
301 # add required xpacket header/trailer
302 $$xmpPt = $pktOpen . $$xmpPt . $pktCloseW;
303 }
304 $mode = 'w' unless $mode;
305 my $end = substr($$xmpPt, -32, 32);
306 # check for proper xpacket trailer and set r/w mode if necessary
307 return '' unless $end =~ s/(e\0*n\0*d\0*=\0*['"]\0*)([rw])(\0*['"]\0*\?\0*>)/$1$mode$3/;
308 substr($$xmpPt, -32, 32) = $end if $2 ne $mode;
309 return 1;
310}
311
312#------------------------------------------------------------------------------
313# Check XMP values for validity and format accordingly
314# Inputs: 0) ExifTool object reference, 1) tagInfo hash reference,
315# 2) raw value reference
316# Returns: error string or undef (and may change value) on success
317sub CheckXMP($$$)
318{
319 my ($exifTool, $tagInfo, $valPtr) = @_;
320 # convert value from Latin if necessary
321 if ($exifTool->{OPTIONS}->{Charset} eq 'Latin' and $$valPtr =~ /[\x80-\xff]/) {
322 # convert from Latin to UTF-8
323 my $val = Image::ExifTool::Latin2Unicode($$valPtr,'n');
324 $$valPtr = Image::ExifTool::Unicode2UTF8($val,'n');
325 }
326 my $format = $tagInfo->{Writable};
327 # if no format specified, value is a simple string
328 return undef unless $format and $format ne 'string';
329 if ($format eq 'rational' or $format eq 'real') {
330 # make sure the value is a valid floating point number
331 Image::ExifTool::IsFloat($$valPtr) or return 'Not a floating point number';
332 if ($format eq 'rational') {
333 $$valPtr = join('/', Image::ExifTool::Rationalize($$valPtr));
334 }
335 } elsif ($format eq 'integer') {
336 # make sure the value is integer
337 if (Image::ExifTool::IsInt($$valPtr)) {
338 # no conversion required (converting to 'int' would remove leading '+')
339 } elsif (Image::ExifTool::IsHex($$valPtr)) {
340 $$valPtr = hex($$valPtr);
341 } else {
342 return 'Not an integer';
343 }
344 } elsif ($format eq 'date') {
345 if ($$valPtr =~ /(\d{4}):(\d{2}):(\d{2}) (\d{2}:\d{2}(?::\d{2}(?:\.\d*)?)?)(.*)/) {
346 my ($y, $m, $d, $t, $tz) = ($1, $2, $3, $4, $5);
347 # use 'Z' for timezone unless otherwise specified
348 $tz = 'Z' unless $tz and $tz =~ /([+-]\d{2}:\d{2})/;
349 $$valPtr = "$y-$m-${d}T$t$tz";
350 } elsif ($$valPtr =~ /^\s*\d{4}(:\d{2}){0,2}\s*$/) {
351 # this is just a date (YYYY, YYYY-MM or YYYY-MM-DD)
352 $$valPtr =~ tr/:/-/;
353 } elsif ($$valPtr =~ /^\s*(\d{2}:\d{2}:\d{2})(.*)\s*$/) {
354 # this is just a time
355 my ($t, $tz) = ($1, $2);
356 $tz = 'Z' unless $tz and $tz =~ /([+-]\d{2}:\d{2})/;
357 $$valPtr = "$t$tz";
358 } else {
359 return "Invalid date or time format (should be YYYY:MM:DD HH:MM:SS[+/-HH:MM])";
360 }
361 } elsif ($format eq 'lang-alt') {
362 # nothing to do
363 } elsif ($format eq 'boolean') {
364 if (not $$valPtr or $$valPtr =~ /false/i or $$valPtr =~ /^no$/i) {
365 $$valPtr = 'False';
366 } else {
367 $$valPtr = 'True';
368 }
369 } elsif ($format eq '1') {
370 # this is the entire XMP data block
371 return 'Invalid XMP data' unless ValidateXMP($valPtr);
372 } else {
373 return "Unknown XMP format: $format";
374 }
375 return undef; # success!
376}
377
378#------------------------------------------------------------------------------
379# Get PropertyPath for specified tagInfo
380# Inputs: 0) tagInfo reference
381# Returns: PropertyPath string
382sub GetPropertyPath($)
383{
384 my $tagInfo = shift;
385 unless ($$tagInfo{PropertyPath}) {
386 SetPropertyPath($$tagInfo{Table}, $$tagInfo{TagID});
387 }
388 return $$tagInfo{PropertyPath};
389}
390
391#------------------------------------------------------------------------------
392# Set PropertyPath for specified tag (also for any structure elements)
393# Inputs: 0) tagTable reference, 1) tagID, 2) structure reference (or undef),
394# 3) property list up to this point (or undef), 4) true if tag is a list
395sub SetPropertyPath($$;$$)
396{
397 my ($tagTablePtr, $tagID, $structPtr, $propList) = @_;
398 my $table = $structPtr || $tagTablePtr;
399 my $tagInfo = $$table{$tagID};
400 my $ns = $$table{NAMESPACE};
401 # don't override existing main table entry if already set by a Struct
402 return if not $structPtr and $$tagInfo{PropertyPath};
403 $ns or warn("No namespace for $tagID\n"), return;
404 my (@propList, $listType);
405 $propList and @propList = @$propList;
406 push @propList, "$ns:$tagID";
407 # lang-alt lists are handled specially, signified by Writable='lang-alt'
408 if ($$tagInfo{Writable} and $$tagInfo{Writable} eq 'lang-alt') {
409 $listType = 'Alt';
410 # remove language code from property path if it exists
411 $propList[-1] =~ s/-$$tagInfo{LangCode}$// if $$tagInfo{LangCode};
412 } else {
413 $listType = $$tagInfo{List};
414 }
415 # add required properties if this is a list
416 push @propList, "rdf:$listType", 'rdf:li 000' if $listType and $listType ne '1';
417 # set PropertyPath for all elements of this structure if necessary
418 if ($$tagInfo{Struct}) {
419 my $struct = $xmpStruct{$$tagInfo{Struct}};
420 $struct or warn("No XMP $$tagInfo{Struct} structure!\n"), return;
421 my $tag;
422 foreach $tag (keys %$struct) {
423 next if $tag eq 'NAMESPACE';
424 SetPropertyPath($tagTablePtr, $tag, $struct, \@propList);
425 }
426 }
427 # use tagInfo for combined tag name if this was a Struct
428 if ($structPtr) {
429 my $tagName = GetXMPTagID(\@propList);
430 $$tagTablePtr{$tagName} or warn("Tag $tagName not found!\n"), return;
431 $tagInfo = $$tagTablePtr{$tagName};
432 # must check again for List's at this level
433 if ($$tagInfo{Writable} and $$tagInfo{Writable} eq 'lang-alt') {
434 $listType = 'Alt';
435 } else {
436 $listType = $$tagInfo{List};
437 }
438 push @propList, "rdf:$listType", 'rdf:li 000' if $listType and $listType ne '1';
439 }
440 # set property path for tagInfo in main table
441 $$tagInfo{PropertyPath} = join '/', @propList;
442}
443
444#------------------------------------------------------------------------------
445# Save XMP property name/value for rewriting
446# Inputs: 0) ExifTool object reference
447# 1) reference to array of XMP property path (last is current property)
448# 2) property value, 3) optional reference to hash of property attributes
449sub CaptureXMP($$$;$)
450{
451 my ($exifTool, $propList, $val, $attrs) = @_;
452 return unless defined $val and @$propList > 2;
453 if ($$propList[0] =~ /^x:x(a|m)pmeta$/ and
454 $$propList[1] eq 'rdf:RDF' and
455 $$propList[2] =~ /$rdfDesc( |$)/)
456 {
457 # no properties to save yet if this is just the description
458 return unless @$propList > 3;
459 # save information about this property
460 my $capture = $exifTool->{XMP_CAPTURE};
461 my $path = join('/', @$propList[3..$#$propList]);
462 if (defined $$capture{$path}) {
463 $exifTool->{XMP_ERROR} = "Duplicate XMP property: $path";
464 } else {
465 $$capture{$path} = [$val, $attrs || { }];
466 }
467 } else {
468 $exifTool->{XMP_ERROR} = 'Improperly enclosed XMP property: ' . join('/',@$propList);
469 }
470}
471
472#------------------------------------------------------------------------------
473# Save information about resource containing blank node with nodeID
474# Inputs: 0) reference to blank node information hash
475# 1) reference to property list
476# 2) property value
477# 3) [optional] reference to attribute hash
478# Notes: This routine and ProcessBlankInfo() are also used for reading information, but
479# are uncommon so are put in this file to reduce compile time for the common case
480sub SaveBlankInfo($$$;$)
481{
482 my ($blankInfo, $propListPt, $val, $attrs) = @_;
483
484 my $propPath = join '/', @$propListPt;
485 my @ids = ($propPath =~ m{ #([^ /]*)}g);
486 my $id;
487 # split the property path at each nodeID
488 foreach $id (@ids) {
489 my ($pre, $prop, $post) = ($propPath =~ m{^(.*?)/([^/]*) #$id((/.*)?)$});
490 defined $pre or warn("internal error parsing nodeID's"), next;
491 # the element with the nodeID should be in the path prefix for subject
492 # nodes and the path suffix for object nodes
493 unless ($prop eq $rdfDesc) {
494 if ($post) {
495 $post = "/$prop$post";
496 } else {
497 $pre = "$pre/$prop";
498 }
499 }
500 $blankInfo->{Prop}->{$id}->{Pre}->{$pre} = 1;
501 if ((defined $post and length $post) or (defined $val and length $val)) {
502 # save the property value and attributes for each unique path suffix
503 $blankInfo->{Prop}->{$id}->{Post}->{$post} = [ $val, $attrs, $propPath ];
504 }
505 }
506}
507
508#------------------------------------------------------------------------------
509# Process blank-node information
510# Inputs: 0) ExifTool object ref, 1) tag table ref,
511# 2) blank node information hash ref, 3) flag set for writing
512sub ProcessBlankInfo($$$;$)
513{
514 my ($exifTool, $tagTablePtr, $blankInfo, $isWriting) = @_;
515 $exifTool->VPrint(1, " [Elements with nodeID set:]\n") unless $isWriting;
516 my ($id, $pre, $post);
517 # handle each nodeID separately
518 foreach $id (sort keys %{$$blankInfo{Prop}}) {
519 my $path = $blankInfo->{Prop}->{$id};
520 # flag all resource names so we can warn later if some are unused
521 my %unused;
522 foreach $post (keys %{$path->{Post}}) {
523 $unused{$post} = 1;
524 }
525 # combine property paths for all possible paths through this node
526 foreach $pre (sort keys %{$path->{Pre}}) {
527 # there will be no description for the object of a blank node
528 next unless $pre =~ m{/$rdfDesc/};
529 foreach $post (sort keys %{$path->{Post}}) {
530 my @propList = split m{/}, "$pre$post";
531 my ($val, $attrs) = @{$path->{Post}->{$post}};
532 if ($isWriting) {
533 CaptureXMP($exifTool, \@propList, $val, $attrs);
534 } else {
535 FoundXMP($exifTool, $tagTablePtr, \@propList, $val);
536 }
537 delete $unused{$post};
538 }
539 }
540 # save information from unused properties (if RDF is malformed like f-spot output)
541 if (%unused) {
542 $exifTool->Options('Verbose') and $exifTool->Warn('An XMP resource is about nothing');
543 foreach $post (sort keys %unused) {
544 my ($val, $attrs, $propPath) = @{$path->{Post}->{$post}};
545 my @propList = split m{/}, $propPath;
546 if ($isWriting) {
547 CaptureXMP($exifTool, \@propList, $val, $attrs);
548 } else {
549 FoundXMP($exifTool, $tagTablePtr, \@propList, $val);
550 }
551 }
552 }
553 }
554}
555
556#------------------------------------------------------------------------------
557# Convert path to namespace used in file (this is a pain, but the XMP
558# spec only suggests 'preferred' namespace prefixes...)
559# Inputs: 0) ExifTool object reference, 1) property path
560# Returns: conforming property path
561sub ConformPathToNamespace($$)
562{
563 my ($exifTool, $path) = @_;
564 my @propList = split('/',$path);
565 my ($prop, $newKey);
566 my $nsUsed = $exifTool->{XMP_NS};
567 foreach $prop (@propList) {
568 my ($ns, $tag) = $prop =~ /(.+?):(.*)/;
569 next if $$nsUsed{$ns};
570 my $uri = $nsURI{$ns};
571 unless ($uri) {
572 warn "No URI for namepace prefix $ns!\n";
573 next;
574 }
575 my $ns2;
576 foreach $ns2 (keys %$nsUsed) {
577 next unless $$nsUsed{$ns2} eq $uri;
578 # use the existing namespace prefix instead of ours
579 $prop = "$ns2:$tag";
580 last;
581 }
582 }
583 return join('/',@propList);
584}
585
586#------------------------------------------------------------------------------
587# sort tagInfo hash references by tag name
588sub ByTagName
589{
590 return $$a{Name} cmp $$b{Name};
591}
592
593#------------------------------------------------------------------------------
594# Write XMP information
595# Inputs: 0) ExifTool object reference, 1) source dirInfo reference,
596# 2) [optional] tag table reference
597# Returns: with tag table: new XMP data (may be empty if no XMP data) or undef on error
598# without tag table: 1 on success, 0 if not valid XMP file, -1 on write error
599# Notes: May set dirInfo InPlace flag to rewrite with specified DirLen
600# May set dirInfo ReadOnly flag to write as read-only XMP ('r' mode and no padding)
601sub WriteXMP($$;$)
602{
603 my ($exifTool, $dirInfo, $tagTablePtr) = @_;
604 $exifTool or return 1; # allow dummy access to autoload this package
605 my $dataPt = $$dirInfo{DataPt};
606 my $dirStart = $$dirInfo{DirStart} || 0;
607 my (%capture, %nsUsed, $xmpErr, $uuid);
608 my $changed = 0;
609 my $xmpFile = (not $tagTablePtr); # this is an XMP data file if no $tagTablePtr
610 my $preferred = $xmpFile; # write XMP as preferred if this is an XMP file
611 my $verbose = $exifTool->Options('Verbose');
612#
613# extract existing XMP information into %capture hash
614#
615 # define hash in ExifTool object to capture XMP information (also causes
616 # CaptureXMP() instead of FoundXMP() to be called from ParseXMPElement())
617 #
618 # The %capture hash is keyed on the complete property path beginning after
619 # rdf:RDF/rdf:Description/. The values are array references with the
620 # following entries: 0) value, 1) attribute hash reference.
621 $exifTool->{XMP_CAPTURE} = \%capture;
622 $exifTool->{XMP_NS} = \%nsUsed;
623
624 if ($dataPt or $xmpFile) {
625 delete $exifTool->{XMP_ERROR};
626 delete $exifTool->{XMP_UUID};
627 # extract all existing XMP information (to the XMP_CAPTURE hash)
628 my $success = ProcessXMP($exifTool, $dirInfo, $tagTablePtr);
629 # don't continue if there is nothing to parse or if we had a parsing error
630 unless ($success and not $exifTool->{XMP_ERROR}) {
631 my $err = $exifTool->{XMP_ERROR} || 'Error parsing XMP';
632 # may ignore this error only if we were successful
633 if ($xmpFile) {
634 my $raf = $$dirInfo{RAF};
635 # allow empty XMP data so we can create something from nothing
636 if ($success or not $raf->Seek(0,2) or $raf->Tell()) {
637 # no error message if not an XMP file
638 return 0 unless $exifTool->{XMP_ERROR};
639 if ($exifTool->Error($err, $success)) {
640 delete $exifTool->{XMP_CAPTURE};
641 return 0;
642 }
643 }
644 } else {
645 if ($exifTool->Warn($err, $success)) {
646 delete $exifTool->{XMP_CAPTURE};
647 return undef;
648 }
649 }
650 }
651 $uuid = $exifTool->{XMP_UUID} || '';
652 delete $exifTool->{XMP_ERROR};
653 delete $exifTool->{XMP_UUID};
654 } else {
655 $uuid = '';
656 }
657#
658# add, delete or change information as specified
659#
660 # get hash of all information we want to change
661 # (sorted by tag name so alternate languages come last)
662 my @tagInfoList = sort ByTagName $exifTool->GetNewTagInfoList();
663 my $tagInfo;
664 foreach $tagInfo (@tagInfoList) {
665 next unless $exifTool->GetGroup($tagInfo, 0) eq 'XMP';
666 my $tag = $tagInfo->{TagID};
667 my $path = GetPropertyPath($tagInfo);
668 unless ($path) {
669 $exifTool->Warn("Can't write XMP:$tag (namespace unknown)");
670 next;
671 }
672 # change our property path namespace prefixes to conform
673 # to the ones used in this file
674 $path = ConformPathToNamespace($exifTool, $path);
675 # find existing property
676 my $capList = $capture{$path};
677 # MicrosoftPhoto screws up the case of some tags, so test for this
678 unless ($capList) {
679 my ($path2) = grep /^\Q$path\E$/i, keys %capture;
680 $path2 and $capList = $capture{$path = $path2};
681 }
682 my $newValueHash = $exifTool->GetNewValueHash($tagInfo);
683 my $overwrite = Image::ExifTool::IsOverwriting($newValueHash);
684 my $writable = $$tagInfo{Writable} || '';
685 my (%attrs, $deleted, $added);
686 # delete existing entry if necessary
687 if ($capList) {
688 # take attributes from old values if they exist
689 %attrs = %{$capList->[1]};
690 if ($overwrite) {
691 my ($delPath, @matchingPaths, $oldLang, $delLang);
692 # check to see if this is an indexed list item
693 if ($path =~ / /) {
694 my $pathPattern;
695 ($pathPattern = $path) =~ s/ 000/ \\d\{3\}/g;
696 @matchingPaths = sort grep(/^$pathPattern$/, keys %capture);
697 } else {
698 push @matchingPaths, $path;
699 }
700 foreach $path (@matchingPaths) {
701 my ($val, $attrs) = @{$capture{$path}};
702 if ($overwrite < 0) {
703 # only overwrite specific values
704 next unless Image::ExifTool::IsOverwriting($newValueHash, $val);
705 }
706 if ($writable eq 'lang-alt') {
707 # get original language code (lc for comparisons)
708 $oldLang = lc($$attrs{'xml:lang'} || 'x-default');
709 # delete all if deleting "x-default" or writing with no LangCode
710 # (XMP spec requires x-default language exist and be first in list)
711 if ($oldLang eq 'x-default' and not ($newValueHash->{Value} or
712 ($$tagInfo{LangCode} and $$tagInfo{LangCode} ne 'x-default')))
713 {
714 $delLang = 1; # delete all languages
715 $overwrite = 1; # force overwrite
716 }
717 if ($$tagInfo{LangCode} and not $delLang) {
718 # only overwrite specified language
719 next unless lc($$tagInfo{LangCode}) eq $oldLang;
720 }
721 }
722 if ($verbose > 1) {
723 my $grp = $exifTool->GetGroup($tagInfo, 1);
724 my $tagName = $$tagInfo{Name};
725 $tagName =~ s/-$$tagInfo{LangCode}$// if $$tagInfo{LangCode};
726 $tagName .= '-' . $$attrs{'xml:lang'} if $$attrs{'xml:lang'};
727 $exifTool->VPrint(1, " - $grp:$tagName = '$val'\n");
728 }
729 # save attributes and path from first deleted property
730 # so we can replace it exactly
731 unless ($delPath) {
732 %attrs = %$attrs;
733 $delPath = $path;
734 }
735 delete $capture{$path};
736 ++$changed;
737 }
738 next unless $delPath or $$tagInfo{List} or $oldLang;
739 if ($delPath) {
740 $path = $delPath;
741 $deleted = 1;
742 } else {
743 # don't change tag if we couldn't delete old copy
744 # unless this is a list or an lang-alt tag
745 next unless $$tagInfo{List} or $oldLang;
746 $path =~ m/ (\d{3})/g or warn "Internal error: no list index!\n", next;
747 $added = $1;
748 }
749 } elsif ($path =~ m/ (\d{3})/g) {
750 $added = $1;
751 }
752 if (defined $added) {
753 # add to end of list
754 my $pos = pos($path) - 3;
755 for (;;) {
756 substr($path, $pos, 3) = ++$added;
757 last unless $capture{$path};
758 }
759 }
760 }
761 # check to see if we want to create this tag
762 # (create non-avoided tags in XMP data files by default)
763 my $isCreating = (Image::ExifTool::IsCreating($newValueHash) or
764 ($preferred and not $$tagInfo{Avoid} and
765 not defined $$newValueHash{Shift}));
766
767 # don't add new values unless...
768 # ...tag existed before and was deleted, or we added it to a list
769 next unless $deleted or defined $added or
770 # ...tag didn't exist before and we are creating it
771 (not $capList and $isCreating);
772
773 # get list of new values (all done if no new values specified)
774 my @newValues = Image::ExifTool::GetNewValues($newValueHash) or next;
775
776 # set language attribute for lang-alt lists
777 if ($writable eq 'lang-alt') {
778 $attrs{'xml:lang'} = $$tagInfo{LangCode} || 'x-default';
779 # must generate x-default entry as first entry if it didn't exist
780 unless ($capList or lc($attrs{'xml:lang'}) eq 'x-default') {
781 my $newValue = EscapeXML($newValues[0]);
782 $capture{$path} = [ $newValue, { %attrs, 'xml:lang' => 'x-default' } ];
783 if ($verbose > 1) {
784 my $tagName = $$tagInfo{Name};
785 $tagName =~ s/-$$tagInfo{LangCode}$/-x-default/;
786 my $grp = $exifTool->GetGroup($tagInfo, 1);
787 $exifTool->VPrint(1, " + $grp:$tagName = '$newValue'\n");
788 }
789 $path =~ s/ 000/ 001/ or warn "Internal error: no list index!\n", next;
790 }
791 }
792
793 # add new value(s) to %capture hash
794 for (;;) {
795 my $newValue = EscapeXML(shift @newValues);
796 $capture{$path} = [ $newValue, \%attrs ];
797 if ($verbose > 1) {
798 my $grp = $exifTool->GetGroup($tagInfo, 1);
799 $exifTool->VPrint(1, " + $grp:$$tagInfo{Name} = '$newValue'\n");
800 }
801 ++$changed;
802 last unless @newValues;
803 $path =~ m/ (\d{3})/g or warn("Internal error: no list index!\n"), next;
804 my $listIndex = $1;
805 my $pos = pos($path) - 3;
806 for (;;) {
807 substr($path, $pos, 3) = ++$listIndex;
808 last unless $capture{$path};
809 }
810 $capture{$path} and warn("Too many entries in XMP list!\n"), next;
811 }
812 }
813 # remove the ExifTool members we created
814 delete $exifTool->{XMP_CAPTURE};
815 delete $exifTool->{XMP_NS};
816
817 # return now if we didn't change anything
818 unless ($changed) {
819 return undef unless $xmpFile; # just rewrite original XMP
820 # get DataPt again because it may have been set by ProcessXMP
821 $dataPt = $$dirInfo{DataPt};
822 unless (defined $dataPt) {
823 $exifTool->Error("Nothing to write");
824 return 1;
825 }
826 return 1 if Write($$dirInfo{OutFile}, $$dataPt);
827 return -1;
828 }
829#
830# write out the new XMP information
831#
832 # start writing the XMP data
833 my $newData = '';
834 if ($$exifTool{XMP_NO_XPACKET}) {
835 # write BOM if flag is set
836 $newData .= "\xef\xbb\xbf" if $$exifTool{XMP_NO_XPACKET} == 2;
837 } else {
838 $newData .= $pktOpen;
839 }
840 $newData .= $xmlOpen if $$exifTool{XMP_IS_XML};
841 $newData .= $xmpOpen . $rdfOpen;
842
843 # initialize current property path list
844 my @curPropList;
845 my (%nsCur, $prop, $n);
846 my @pathList = sort keys %capture;
847
848 while (@pathList) {
849 my $path = shift @pathList;
850 my @propList = split('/',$path); # get property list
851 # must open/close rdf:Description too
852 unshift @propList, $rdfDesc;
853 # make sure we have defined all necessary namespaces
854 my (%nsNew, $newDesc);
855 foreach $prop (@propList) {
856 $prop =~ /(.*):/ or next;
857 $1 eq 'rdf' and next; # rdf namespace already defined
858 my $nsNew = $nsUsed{$1};
859 unless ($nsNew) {
860 $nsNew = $nsURI{$1}; # we must have added a namespace
861 unless ($nsNew) {
862 $xmpErr = "Undefined XMP namespace: $1";
863 next;
864 }
865 }
866 $nsNew{$1} = $nsNew;
867 # need a new description if any new namespaces
868 $newDesc = 1 unless $nsCur{$1};
869 }
870 my $closeTo = 0;
871 if ($newDesc) {
872 # look forward to see if we will want to also open other namespaces
873 # (this is necessary to keep lists from being broken if a property
874 # introduces a new namespace; plus it improves formatting)
875 my ($path2, $ns2);
876 foreach $path2 (@pathList) {
877 my @ns2s = ($path2 =~ m{(?:^|/)([^/]+?):}g);
878 my $opening = 0;
879 foreach $ns2 (@ns2s) {
880 next if $ns2 eq 'rdf';
881 $nsNew{$ns2} and ++$opening, next;
882 last unless $opening and $nsURI{$ns2};
883 # also open this namespace
884 $nsNew{$ns2} = $nsURI{$ns2};
885 }
886 last unless $opening;
887 }
888 } else {
889 # find first property where the current path differs from the new path
890 for ($closeTo=0; $closeTo<@curPropList; ++$closeTo) {
891 last unless $closeTo < @propList;
892 last unless $propList[$closeTo] eq $curPropList[$closeTo];
893 }
894 }
895 # close out properties down to the common base path
896 while (@curPropList > $closeTo) {
897 ($prop = pop @curPropList) =~ s/ .*//;
898 $newData .= (' ' x scalar(@curPropList)) . " </$prop>\n";
899 }
900 if ($newDesc) {
901 # open the new description
902 $prop = $rdfDesc;
903 %nsCur = %nsNew; # save current namespaces
904 $newData .= "\n <$prop rdf:about='$uuid'";
905 foreach (sort keys %nsCur) {
906 $newData .= "\n xmlns:$_='$nsCur{$_}'";
907 }
908 $newData .= ">\n";
909 push @curPropList, $prop;
910 }
911 # loop over all values for this new property
912 my $capList = $capture{$path};
913 my ($val, $attrs) = @$capList;
914 $debug and print "$path = $val\n";
915 # open new properties
916 my $attr;
917 for ($n=@curPropList; $n<$#propList; ++$n) {
918 $prop = $propList[$n];
919 push @curPropList, $prop;
920 # remove list index if it exists
921 $prop =~ s/ .*//;
922 $attr = '';
923 if ($prop ne $rdfDesc and $propList[$n+1] !~ /^rdf:/) {
924 # need parseType='Resource' to avoid new 'rdf:Description'
925 $attr = " rdf:parseType='Resource'";
926 }
927 $newData .= (' ' x scalar(@curPropList)) . "<$prop$attr>\n";
928 }
929 my $prop2 = pop @propList; # get new property name
930 $prop2 =~ s/ .*//; # remove list index if it exists
931 $newData .= (' ' x scalar(@curPropList)) . " <$prop2";
932 # print out attributes
933 foreach $attr (sort keys %$attrs) {
934 my $attrVal = $$attrs{$attr};
935 my $quot = ($attrVal =~ /'/) ? '"' : "'";
936 $newData .= " $attr=$quot$attrVal$quot";
937 }
938 $newData .= ">$val</$prop2>\n";
939 }
940 # close off any open elements
941 while ($prop = pop @curPropList) {
942 $prop =~ s/ .*//; # remove list index if it exists
943 $newData .= (' ' x scalar(@curPropList)) . " </$prop>\n";
944 }
945#
946# clean up, close out the XMP, and return our data
947#
948 # remove the ExifTool members we created
949 delete $exifTool->{XMP_CAPTURE};
950 delete $exifTool->{XMP_NS};
951
952 $newData .= $rdfClose . $xmpClose;
953
954 # (the XMP standard recommends writing 2k-4k of white space before the
955 # packet trailer, with a newline every 100 characters)
956 unless ($$exifTool{XMP_NO_XPACKET}) {
957 my $pad = (' ' x 100) . "\n";
958 if ($$dirInfo{InPlace}) {
959 # pad to specified DirLen
960 my $dirLen = $$dirInfo{DirLen} || length $$dataPt;
961 my $len = length($newData) + length($pktCloseW);
962 if ($len > $dirLen) {
963 $exifTool->Warn('Not enough room to edit XMP in place');
964 return undef;
965 }
966 my $num = int(($dirLen - $len) / length($pad));
967 if ($num) {
968 $newData .= $pad x $num;
969 $len += length($pad) * $num;
970 }
971 $len < $dirLen and $newData .= (' ' x ($dirLen - $len - 1)) . "\n";
972 } elsif (not $exifTool->Options('Compact') and
973 not $xmpFile and not $$dirInfo{ReadOnly})
974 {
975 $newData .= $pad x 24;
976 }
977 $newData .= ($$dirInfo{ReadOnly} ? $pktCloseR : $pktCloseW);
978 }
979 # return empty data if no properties exist
980 $newData = '' unless %capture or $$dirInfo{InPlace};
981
982 if ($xmpErr) {
983 if ($xmpFile) {
984 $exifTool->Error($xmpErr);
985 return -1;
986 }
987 $exifTool->Warn($xmpErr);
988 return undef;
989 }
990 $exifTool->{CHANGED} += $changed;
991 $debug > 1 and $newData and print $newData,"\n";
992 return $newData unless $xmpFile;
993 return 1 if Write($$dirInfo{OutFile}, $newData);
994 return -1;
995}
996
997
9981; # end
999
1000__END__
1001
1002=head1 NAME
1003
1004Image::ExifTool::WriteXMP.pl - Write XMP meta information
1005
1006=head1 SYNOPSIS
1007
1008These routines are autoloaded by Image::ExifTool::XMP.
1009
1010=head1 DESCRIPTION
1011
1012This file contains routines to write XMP metadata.
1013
1014=head1 AUTHOR
1015
1016Copyright 2003-2007, Phil Harvey (phil at owl.phy.queensu.ca)
1017
1018This library is free software; you can redistribute it and/or modify it
1019under the same terms as Perl itself.
1020
1021=head1 SEE ALSO
1022
1023L<Image::ExifTool::XMP(3pm)|Image::ExifTool::XMP>,
1024L<Image::ExifTool(3pm)|Image::ExifTool>
1025
1026=cut
Note: See TracBrowser for help on using the repository browser.