#------------------------------------------------------------------------------ # File: Writer.pl # # Description: ExifTool write routines # # Notes: Also contains some less used ExifTool functions # # URL: https://exiftool.org/ # # Revisions: 12/16/2004 - P. Harvey Created #------------------------------------------------------------------------------ package Image::ExifTool; use strict; use Image::ExifTool::TagLookup qw(FindTagInfo TagExists); use Image::ExifTool::Fixup; sub AssembleRational($$@); sub LastInList($); sub NextFreeTagKey($$); sub RemoveNewValueHash($$$); sub RemoveNewValuesForGroup($$); sub GetWriteGroup1($$); sub Sanitize($$); sub ConvInv($$$$$;$$); sub PushValue($$$;$); my $loadedAllTables; # flag indicating we loaded all tables # the following is a road map of where we write each directory # in the different types of files. my %tiffMap = ( IFD0 => 'TIFF', IFD1 => 'IFD0', XMP => 'IFD0', ICC_Profile => 'IFD0', ExifIFD => 'IFD0', GPS => 'IFD0', SubIFD => 'IFD0', GlobParamIFD => 'IFD0', PrintIM => 'IFD0', IPTC => 'IFD0', Photoshop => 'IFD0', SEAL => 'IFD0', InteropIFD => 'ExifIFD', MakerNotes => 'ExifIFD', CanonVRD => 'MakerNotes', # (so VRDOffset will get updated) NikonCapture => 'MakerNotes', # (to allow delete by group) PhaseOne => 'MakerNotes', # (for editing PhaseOne SensorCalibration tags) ); my %exifMap = ( IFD1 => 'IFD0', EXIF => 'IFD0', # to write EXIF as a block ExifIFD => 'IFD0', GPS => 'IFD0', SubIFD => 'IFD0', GlobParamIFD => 'IFD0', PrintIM => 'IFD0', InteropIFD => 'ExifIFD', MakerNotes => 'ExifIFD', NikonCapture => 'MakerNotes', # (to allow delete by group) # (no CanonVRD trailer allowed) ); my %jpegMap = ( %exifMap, # covers all JPEG EXIF mappings JFIF => 'APP0', CIFF => 'APP0', IFD0 => 'APP1', XMP => 'APP1', ICC_Profile => 'APP2', FlashPix => 'APP2', MPF => 'APP2', Meta => 'APP3', MetaIFD => 'Meta', RMETA => 'APP5', SEAL => ['APP8','APP9'], # (note: add 'IFD0' if this is a possibility) Ducky => 'APP12', Photoshop => 'APP13', Adobe => 'APP14', IPTC => 'Photoshop', MakerNotes => ['ExifIFD', 'CIFF'], # (first parent is the default) CanonVRD => 'MakerNotes', # (so VRDOffset will get updated) NikonCapture => 'MakerNotes', # (to allow delete by group) Comment => 'COM', ); my %dirMap = ( JPEG => \%jpegMap, EXV => \%jpegMap, TIFF => \%tiffMap, ORF => \%tiffMap, RAW => \%tiffMap, EXIF => \%exifMap, ); # module names and write functions for each writable file type # (defaults to "$type" and "Process$type" if not defined) # - types that are handled specially will not appear in this list my %writableType = ( CRW => [ 'CanonRaw', 'WriteCRW' ], DR4 => 'CanonVRD', EPS => [ 'PostScript', 'WritePS' ], FLIF=> [ undef, 'WriteFLIF'], GIF => undef, ICC => [ 'ICC_Profile', 'WriteICC' ], IND => 'InDesign', JP2 => 'Jpeg2000', JXL => 'Jpeg2000', MIE => undef, MOV => [ 'QuickTime', 'WriteMOV' ], MRW => 'MinoltaRaw', PDF => [ undef, 'WritePDF' ], PNG => undef, PPM => undef, PS => [ 'PostScript', 'WritePS' ], PSD => 'Photoshop', RAF => [ 'FujiFilm', 'WriteRAF' ], RIFF=> [ 'RIFF', 'WriteRIFF'], VRD => 'CanonVRD', WEBP=> [ 'RIFF', 'WriteRIFF'], X3F => 'SigmaRaw', XMP => [ undef, 'WriteXMP' ], ); # RAW file types (2 = raw file where we can delete maker notes from ExifIFD) my %rawType = ( '3FR'=> 1, CR3 => 2, IIQ => 1, NEF => 1, RW2 => 1, ARQ => 1, CRW => 1, K25 => 1, NRW => 1, RWL => 1, ARW => 1, DCR => 1, KDC => 1, ORF => 1, SR2 => 1, ARW => 1, ERF => 1, MEF => 1, PEF => 1, SRF => 1, CR2 => 1, FFF => 1, MOS => 1, RAW => 1, SRW => 1, ); # groups we are allowed to delete # Notes: # 1) these names must either exist in %dirMap, or be translated in InitWriteDirs()) # 2) any dependencies must be added to %excludeGroups my @delGroups = qw( Adobe AFCP APP0 APP1 APP2 APP3 APP4 APP5 APP6 APP7 APP8 APP9 APP10 APP11 APP12 APP13 APP14 APP15 CanonVRD CIFF Ducky EXIF ExifIFD File FlashPix FotoStation GlobParamIFD GPS ICC_Profile IFD0 IFD1 Insta360 InteropIFD IPTC ItemList JFIF Jpeg2000 JUMBF Keys MakerNotes Meta MetaIFD Microsoft MIE MPF Nextbase NikonApp NikonCapture PDF PDF-update PhotoMechanic Photoshop PNG PNG-pHYs PrintIM QuickTime RMETA RSRC SEAL SubIFD Trailer UserData XML XML-* XMP XMP-* ); # family 2 group names that we can delete my @delGroup2 = qw( Audio Author Camera Document ExifTool Image Location Other Preview Printing Time Video ); # Extra groups to delete when deleting another group my %delMore = ( QuickTime => [ qw(ItemList UserData Keys) ], XMP => [ 'XMP-*' ], XML => [ 'XML-*' ], SEAL => [ 'XMP-SEAL' ], ); # family 0 groups where directories should never be deleted my %permanentDir = ( QuickTime => 1, Jpeg2000 => 1 ); # lookup for all valid family 2 groups (lower case) my %family2groups = map { lc $_ => 1 } @delGroup2, 'Unknown'; # groups we don't delete when deleting all information my $protectedGroups = '(IFD1|SubIFD|InteropIFD|GlobParamIFD|PDF-update|Adobe)'; # other group names of new tag values to remove when deleting an entire group my %removeGroups = ( IFD0 => [ 'EXIF', 'MakerNotes' ], EXIF => [ 'MakerNotes' ], ExifIFD => [ 'MakerNotes', 'InteropIFD' ], Trailer => [ 'CanonVRD' ], #(because we can add back CanonVRD as a block) ); # related family 0/1 groups in @delGroups (and not already in %jpegMap) # that must be removed from delete list when excluding a group my %excludeGroups = ( EXIF => [ qw(IFD0 IFD1 ExifIFD GPS MakerNotes GlobParamIFD InteropIFD PrintIM SubIFD) ], IFD0 => [ 'EXIF' ], IFD1 => [ 'EXIF' ], ExifIFD => [ 'EXIF' ], GPS => [ 'EXIF' ], MakerNotes => [ 'EXIF' ], InteropIFD => [ 'EXIF' ], GlobParamIFD => [ 'EXIF' ], PrintIM => [ 'EXIF' ], CIFF => [ 'MakerNotes' ], # technically correct, but very uncommon and not a good reason to avoid deleting trailer # IPTC => [ qw(AFCP FotoStation Trailer) ], AFCP => [ 'Trailer' ], FotoStation => [ 'Trailer' ], CanonVRD => [ 'Trailer' ], PhotoMechanic=> [ 'Trailer' ], MIE => [ 'Trailer' ], QuickTime => [ qw(ItemList UserData Keys) ], ); # translate (lower case) wanted group when writing for tags where group name may change my %translateWantGroup = ( ciff => 'canonraw', ); # group names to translate for writing my %translateWriteGroup = ( EXIF => 'ExifIFD', Meta => 'MetaIFD', File => 'Comment', # any entry in this table causes the write group to be set from the # tag information instead of whatever the user specified... MIE => 'MIE', APP14 => 'APP14', ); # names of valid EXIF and Meta directories (lower case keys): my %exifDirs = ( gps => 'GPS', exififd => 'ExifIFD', subifd => 'SubIFD', globparamifd => 'GlobParamIFD', interopifd => 'InteropIFD', previewifd => 'PreviewIFD', # (in MakerNotes) metaifd => 'MetaIFD', # Kodak APP3 Meta makernotes => 'MakerNotes', ); # valid family 0 groups when WriteGroup is set to "All" my %allFam0 = ( exif => 1, makernotes => 1, ); my @writableMacOSTags = qw( FileCreateDate MDItemFinderComment MDItemFSCreationDate MDItemFSLabel MDItemUserTags XAttrQuarantine XAttrMDItemWhereFroms ); # min/max values for integer formats my %intRange = ( 'int8u' => [0, 0xff], 'int8s' => [-0x80, 0x7f], 'int16u' => [0, 0xffff], 'int16uRev' => [0, 0xffff], 'int16s' => [-0x8000, 0x7fff], 'int32u' => [0, 0xffffffff], 'int32s' => [-0x80000000, 0x7fffffff], 'int64u' => [0, 18446744073709551615], 'int64s' => [-9223372036854775808, 9223372036854775807], ); # lookup for file types with block-writable EXIF my %blockExifTypes = map { $_ => 1 } qw(JPEG PNG JP2 JXL MIE EXIF FLIF MOV MP4 RIFF); my $maxSegmentLen = 0xfffd; # maximum length of data in a JPEG segment my $maxXMPLen = $maxSegmentLen; # maximum length of XMP data in JPEG # value separators when conversion list is used (in SetNewValue) my %listSep = ( PrintConv => '; ?', ValueConv => ' ' ); # printConv hash keys to ignore when doing reverse lookup my %ignorePrintConv = map { $_ => 1 } qw(OTHER BITMASK Notes); #------------------------------------------------------------------------------ # Set tag value # Inputs: 0) ExifTool object reference # 1) tag key, tag name, or '*' (optionally prefixed by group name), # or undef to reset all previous SetNewValue() calls # 2) new value (scalar, scalar ref or list ref), or undef to delete tag # 3-N) Options: # Type => PrintConv, ValueConv or Raw - specifies value type # AddValue => true to add to list of existing values instead of overwriting # DelValue => true to delete this existing value value from a list, or # or doing a conditional delete, or to shift a time value # Group => family 0 or 1 group name (case insensitive) # Replace => 0, 1 or 2 - overwrite previous new values (2=reset) # Protected => bitmask to write tags with specified protections # EditOnly => true to only edit existing tags (don't create new tag) # EditGroup => true to only edit existing groups (don't create new group) # Shift => undef, 0, +1 or -1 - shift value if possible # NoFlat => treat flattened tags as 'unsafe' # NoShortcut => true to prevent looking up shortcut tags # ProtectSaved => protect existing new values with a save count greater than this # IgnorePermanent => ignore attempts to delete a permanent tag # CreateGroups => [internal use] createGroups hash ref from related tags # ListOnly => [internal use] set only list or non-list tags # SetTags => [internal use] hash ref to return tagInfo refs of set tags # Sanitized => [internal use] set to avoid double-sanitizing the value # Fixup => [internal use] fixup information when writing maker notes # Returns: number of tags set (plus error string in list context) # Notes: For tag lists (like Keywords), call repeatedly with the same tag name for # each value in the list. Internally, the new information is stored in # the following members of the $$self{NEW_VALUE}{$tagInfo} hash: # TagInfo - tag info ref # DelValue - list ref for raw values to delete # Value - list ref for raw values to add (not defined if deleting the tag) # IsCreating - must be set for the tag to be added for the standard file types, # otherwise just changed if it already exists. This may be # overridden for file types with a PREFERRED metadata type. # Set to 2 to create individual tags but not new groups # EditOnly - flag set if tag should never be created (regardless of file type). # If this is set, then IsCreating must be false # CreateOnly - flag set if creating only (never edit existing tag) # CreateGroups - hash of all family 0 group names where tag may be created # WriteGroup - group name where information is being written (correct case) # WantGroup - group name as specified in call to function (case insensitive) # Next - pointer to next new value hash (if more than one for this tag) # NoReplace - set if value was created with Replace=0 # AddBefore - number of list items added by a subsequent Replace=0 call # IsNVH - flag indicating this is a new value hash # Order - counter to indicate the order that new value hashes were created # Shift - shift value # Save - counter used by SaveNewValues()/RestoreNewValues() # MAKER_NOTE_FIXUP - pointer to fixup if necessary for a maker note value sub SetNewValue($;$$%) { local $_; my ($self, $tag, $value, %options) = @_; my ($err, $tagInfo, $family); my $verbose = $$self{OPTIONS}{Verbose}; my $out = $$self{OPTIONS}{TextOut}; my $protected = $options{Protected} || 0; my $listOnly = $options{ListOnly}; my $setTags = $options{SetTags}; my $noFlat = $options{NoFlat}; my $numSet = 0; unless (defined $tag) { delete $$self{NEW_VALUE}; $$self{SAVE_COUNT} = $$self{NV_COUNT} = 0; $$self{DEL_GROUP} = { }; return 1; } # allow value to be scalar or list reference if (ref $value) { if (ref $value eq 'ARRAY') { # value is an ARRAY so it may have more than one entry # - set values both separately and as a combined string if there are more than one if (@$value > 1) { # set all list-type tags first my $replace = $options{Replace}; my $noJoin; foreach (@$value) { $noJoin = 1 if ref $_; my ($n, $e) = SetNewValue($self, $tag, $_, %options, ListOnly => 1); $err = $e if $e; $numSet += $n; delete $options{Replace}; # don't replace earlier values in list } return $numSet if $noJoin; # don't join if list contains objects # and now set only non-list tags $value = join $$self{OPTIONS}{ListSep}, @$value; $options{Replace} = $replace; $listOnly = $options{ListOnly} = 0; } else { $value = $$value[0]; $value = $$value if ref $value eq 'SCALAR'; # (handle single scalar ref in a list) } } elsif (ref $value eq 'SCALAR') { $value = $$value; } } # un-escape as necessary and make sure the Perl UTF-8 flag is OFF for the value # if perl is 5.6 or greater (otherwise our byte manipulations get corrupted!!) $self->Sanitize(\$value) if defined $value and not ref $value and not $options{Sanitized}; # set group name in options if specified ($options{Group}, $tag) = ($1, $2) if $tag =~ /(.*):(.+)/; # allow trailing '#' for ValueConv value $options{Type} = 'ValueConv' if $tag =~ s/#$//; my $convType = $options{Type} || ($$self{OPTIONS}{PrintConv} ? 'PrintConv' : 'ValueConv'); # filter value if necessary $self->Filter($$self{OPTIONS}{FilterW}, \$value) or return 0 if $convType eq 'PrintConv'; my (@wantGroup, $family2); my $wantGroup = $options{Group}; if ($wantGroup) { foreach (split /:/, $wantGroup) { next unless length($_) and /^(\d+)?(.*)/; # separate family number and group name my ($f, $g) = ($1, $2); my $lcg = lc $g; # save group/family unless '*' or 'all' push @wantGroup, [ $f, $lcg ] unless $lcg eq '*' or $lcg eq 'all'; if ($g =~ s/^ID-//i) { # family 7 is a tag ID return 0 if defined $f and $f ne 7; $wantGroup[-1] = [ 7, $g ]; # group name with 'ID-' removed and case preserved } elsif (defined $f) { $f > 2 and return 0; # only allow family 0, 1 or 2 $family2 = 1 if $f == 2; # set flag indicating family 2 was used } else { $family2 = 1 if $family2groups{$lcg}; } } undef $wantGroup unless @wantGroup; } $tag =~ s/ .*//; # convert from tag key to tag name if necessary $tag = '*' if lc($tag) eq 'all'; # use '*' instead of 'all' # # handle group delete # while ($tag eq '*' and not defined $value and not $family2 and @wantGroup < 2) { # set groups to delete my (@del, $grp); my $remove = ($options{Replace} and $options{Replace} > 1); if ($wantGroup) { @del = grep /^$wantGroup$/i, @delGroups unless $wantGroup =~ /^XM[LP]-\*$/i; # remove associated groups when excluding from mass delete if (@del and $remove) { # remove associated groups in other family push @del, @{$excludeGroups{$del[0]}} if $excludeGroups{$del[0]}; # remove upstream groups according to JPEG map my $dirName = $del[0]; my @dirNames; for (;;) { my $parent = $jpegMap{$dirName}; if (ref $parent) { push @dirNames, @$parent; $parent = pop @dirNames; } $dirName = $parent || shift @dirNames or last; push @del, $dirName; # exclude this too } } # allow MIE groups to be deleted by number, # and allow any XMP family 1 group to be deleted push @del, uc($wantGroup) if $wantGroup =~ /^(MIE\d+|XM[LP]-[-\w]*\w)$/i; } else { # push all groups plus '*', except the protected groups push @del, (grep !/^$protectedGroups$/, @delGroups), '*'; } if (@del) { ++$numSet; my @donegrps; my $delGroup = $$self{DEL_GROUP}; foreach $grp (@del) { if ($remove) { my $didExcl; if ($grp =~ /^(XM[LP])(-.*)?$/) { my $x = $1; if ($grp eq $x) { # exclude all related family 1 groups too foreach (keys %$delGroup) { next unless /^(-?)$x-/; push @donegrps, $_ unless $1; delete $$delGroup{$_}; } } elsif ($$delGroup{"$x-*"} and not $$delGroup{"-$grp"}) { # must also exclude XMP or XML to prevent bulk delete if ($$delGroup{$x}) { push @donegrps, $x; delete $$delGroup{$x}; } # flag XMP/XML family 1 group for exclusion with leading '-' $$delGroup{"-$grp"} = 1; $didExcl = 1; } } if (exists $$delGroup{$grp}) { delete $$delGroup{$grp}; } else { next unless $didExcl; } } else { $$delGroup{$grp} = 1; # add extra groups to delete if necessary if ($delMore{$grp}) { $$delGroup{$_} = 1, push @donegrps, $_ foreach @{$delMore{$grp}}; } # remove all of this group from previous new values $self->RemoveNewValuesForGroup($grp); } push @donegrps, $grp; } if ($verbose > 1 and @donegrps) { @donegrps = sort @donegrps; my $msg = $remove ? 'Excluding from deletion' : 'Deleting tags in'; print $out " $msg: @donegrps\n"; } } elsif (grep /^$wantGroup$/i, @delGroup2) { last; # allow tags to be deleted by group2 name } else { $err = "Not a deletable group: $wantGroup"; } # all done return ($numSet, $err) if wantarray; $err and warn "$err\n"; return $numSet; } # initialize write/create flags my $createOnly; my $editOnly = $options{EditOnly}; my $editGroup = $options{EditGroup}; my $writeMode = $$self{OPTIONS}{WriteMode}; if ($writeMode ne 'wcg') { $createOnly = 1 if $writeMode !~ /w/i; # don't write existing tags if ($writeMode !~ /c/i) { return 0 if $createOnly; # nothing to do unless writing existing tags $editOnly = 1; # don't create new tags } elsif ($writeMode !~ /g/i) { $editGroup = 1; # don't create new groups } } my ($ifdName, $mieGroup, $movGroup, $fg); # set family 1 group names foreach $fg (@wantGroup) { next if defined $$fg[0] and $$fg[0] != 1; $_ = $$fg[1]; # set $ifdName if this group is a valid IFD or SubIFD name my $grpName; if (/^IFD(\d+)$/i) { $grpName = $ifdName = "IFD$1"; } elsif (/^SubIFD(\d+)$/i) { $grpName = $ifdName = "SubIFD$1"; } elsif (/^Version(\d+)$/i) { $grpName = $ifdName = "Version$1"; # Sony IDC VersionIFD } elsif ($exifDirs{$_}) { $grpName = $exifDirs{$_}; $ifdName = $grpName unless $ifdName and $allFam0{$_}; } elsif ($allFam0{$_}) { $grpName = $allFam0{$_}; } elsif (/^Track(\d+)$/i) { $grpName = $movGroup = "Track$1"; # QuickTime track } elsif (/^MIE(\d*-?)(\w+)$/i) { $grpName = $mieGroup = "MIE$1" . ucfirst(lc($2)); } elsif (not $ifdName and /^XMP\b/i) { # must load XMP table to set group1 names my $table = GetTagTable('Image::ExifTool::XMP::Main'); my $writeProc = $$table{WRITE_PROC}; if ($writeProc) { no strict 'refs'; &$writeProc(); } } # fix case for known groups $wantGroup =~ s/$grpName/$grpName/i if $grpName and $grpName ne $_; } # # get list of tags we want to set # my $origTag = $tag; my @matchingTags = FindTagInfo($tag); until (@matchingTags) { my $langCode; # allow language suffix of form "-en_CA" or "-" on tag name if ($tag =~ /^([?*\w]+)-([a-z]{2})(_[a-z]{2})$/i or # MIE $tag =~ /^([?*\w]+)-([a-z]{2,3}|[xi])(-[a-z\d]{2,8}(-[a-z\d]{1,8})*)?$/i) # XMP/PNG/QuickTime { $tag = $1; # normalize case of language codes $langCode = lc($2); $langCode .= (length($3) == 3 ? uc($3) : lc($3)) if $3; my @newMatches = FindTagInfo($tag); foreach $tagInfo (@newMatches) { # only allow language codes in tables which support them next unless $$tagInfo{Table}; my $langInfoProc = $$tagInfo{Table}{LANG_INFO} or next; my $langInfo = &$langInfoProc($tagInfo, $langCode); push @matchingTags, $langInfo if $langInfo; } last if @matchingTags; } elsif (not $options{NoShortcut}) { # look for a shortcut or alias require Image::ExifTool::Shortcuts; my ($match) = grep /^\Q$tag\E$/i, keys %Image::ExifTool::Shortcuts::Main; undef $err; if ($match) { $options{NoShortcut} = $options{Sanitized} = 1; foreach $tag (@{$Image::ExifTool::Shortcuts::Main{$match}}) { my ($n, $e) = $self->SetNewValue($tag, $value, %options); $numSet += $n; $e and $err = $e; } undef $err if $numSet; # no error if any set successfully return ($numSet, $err) if wantarray; $err and warn "$err\n"; return $numSet; } } unless ($listOnly) { if (not TagExists($tag)) { if ($tag =~ /^[-\w*?]+$/) { my $pre = $wantGroup ? $wantGroup . ':' : ''; $err = "Tag '$pre${origTag}' is not defined"; $err .= ' or has a bad language code' if $origTag =~ /-/; if (not $pre and uc($origTag) eq 'TAG') { $err .= " (specify a writable tag name, not '${origTag}' literally)" } } else { $err = "Invalid tag name '${tag}'"; $err .= " (remove the leading '\$')" if $tag =~ /^\$/; } } elsif ($langCode) { $err = "Tag '${tag}' does not support alternate languages"; } elsif ($wantGroup) { $err = "Sorry, $wantGroup:$origTag doesn't exist or isn't writable"; } else { $err = "Sorry, $origTag is not writable"; } $verbose > 2 and print $out "$err\n"; } # all done return ($numSet, $err) if wantarray; $err and warn "$err\n"; return $numSet; } # get group name that we're looking for my $foundMatch = 0; # # determine the groups for all tags found, and the tag with # the highest priority group # my (@tagInfoList, @writeAlsoList, %writeGroup, %preferred, %tagPriority); my (%avoid, $wasProtected, $noCreate, %highestPriority, %highestQT); TAG: foreach $tagInfo (@matchingTags) { $tag = $$tagInfo{Name}; # get tag name for warnings my $lcTag = lc $tag; # get lower-case tag name for use in variables # initialize highest priority if we are starting a new tag $highestPriority{$lcTag} = -999 unless defined $highestPriority{$lcTag}; my ($priority, $writeGroup); my $prfTag = defined $$tagInfo{Preferred} ? $$tagInfo{Preferred} : $$tagInfo{Table}{PREFERRED}; if ($wantGroup) { # a WriteGroup of All is special my $wgAll = ($$tagInfo{WriteGroup} and $$tagInfo{WriteGroup} eq 'All'); my @grp = $self->GetGroup($tagInfo); my $hiPri = 1000; foreach $fg (@wantGroup) { my ($fam, $lcWant) = @$fg; $lcWant = $translateWantGroup{$lcWant} if $translateWantGroup{$lcWant}; # only set tag in specified group # bump priority of preferred tag $hiPri += $prfTag if $prfTag; if (not defined $fam) { if ($lcWant eq lc $grp[0]) { # don't go to more general write group of "All" # if something more specific was wanted $writeGroup = $grp[0] if $wgAll and not $writeGroup; next; } next if $lcWant eq lc $grp[2]; } elsif ($fam == 7) { next if IsSameID($$tagInfo{TagID}, $lcWant); } elsif ($fam != 1 and not $$tagInfo{AllowGroup}) { next if $lcWant eq lc $grp[$fam]; if ($wgAll and not $fam and $allFam0{$lcWant}) { $writeGroup or $writeGroup = $allFam0{$lcWant}; next; } next TAG; # wrong group } # handle family 1 groups specially if ($grp[0] eq 'EXIF' or $grp[0] eq 'SonyIDC' or $wgAll) { unless ($ifdName and $lcWant eq lc $ifdName) { next TAG unless $wgAll and not $fam and $allFam0{$lcWant}; $writeGroup = $allFam0{$lcWant} unless $writeGroup; next; } next TAG if $wgAll and $allFam0{$lcWant} and $fam; # can't yet write PreviewIFD tags (except for image) $lcWant eq 'PreviewIFD' and ++$foundMatch, next TAG; $writeGroup = $ifdName; # write to the specified IFD } elsif ($grp[0] eq 'QuickTime') { if ($grp[1] eq 'Track#') { next TAG unless $movGroup and $lcWant eq lc($movGroup); $writeGroup = $movGroup; } else { my $grp = $$tagInfo{Table}{WRITE_GROUP}; next TAG unless $grp and $lcWant eq lc $grp; $writeGroup = $grp; } } elsif ($grp[0] eq 'MIE') { next TAG unless $mieGroup and $lcWant eq lc($mieGroup); $writeGroup = $mieGroup; # write to specific MIE group # set specific write group with document number if specified if ($writeGroup =~ /^MIE\d+$/ and $$tagInfo{Table}{WRITE_GROUP}) { $writeGroup = $$tagInfo{Table}{WRITE_GROUP}; $writeGroup =~ s/^MIE/$mieGroup/; } } elsif (not $$tagInfo{AllowGroup} or $lcWant !~ /^$$tagInfo{AllowGroup}$/i) { # allow group1 name to be specified next TAG unless $lcWant eq lc $grp[1]; } } $writeGroup or $writeGroup = ($$tagInfo{WriteGroup} || $$tagInfo{Table}{WRITE_GROUP} || $grp[0]); $priority = $hiPri; # highest priority since group was specified } ++$foundMatch; # must do a dummy call to the write proc to autoload write package # before checking Writable flag my $table = $$tagInfo{Table}; my $writeProc = $$table{WRITE_PROC}; # load source table if this was a user-defined table if ($$table{SRC_TABLE}) { my $src = GetTagTable($$table{SRC_TABLE}); $writeProc = $$src{WRITE_PROC} unless $writeProc; } { # make sure module is loaded if the writeProc is a string unless (ref $writeProc) { my $module = $writeProc; $module =~ s/::\w+$//; eval "require $module"; } no strict 'refs'; next unless $writeProc and &$writeProc(); } # must still check writable flags in case of UserDefined tags my $writable = $$tagInfo{Writable}; next unless $writable or ($$table{WRITABLE} and not defined $writable and not $$tagInfo{SubDirectory}); # set specific write group (if we didn't already) if (not $writeGroup or ($translateWriteGroup{$writeGroup} and (not $$tagInfo{WriteGroup} or $$tagInfo{WriteGroup} ne 'All'))) { # use default write group $writeGroup = $$tagInfo{WriteGroup} || $$tagInfo{Table}{WRITE_GROUP}; # use group 0 name if no WriteGroup specified my $group0 = $self->GetGroup($tagInfo, 0); $writeGroup or $writeGroup = $group0; # get priority for this group unless ($priority) { if ($$tagInfo{Avoid} and $$tagInfo{WriteAlso}) { $priority = 0; } else { $priority = $$self{WRITE_PRIORITY}{lc($writeGroup)}; unless ($priority) { $priority = $$self{WRITE_PRIORITY}{lc($group0)} || 0; } } } # adjust priority based on Preferred level for this tag $priority += $prfTag if $prfTag; } # don't write tag if protected my $prot = $$tagInfo{Protected}; $prot = 1 if $noFlat and defined $$tagInfo{Flat}; if ($prot) { $prot &= ~$protected; if ($prot) { my %lkup = ( 1=>'unsafe', 2=>'protected', 3=>'unsafe and protected'); $wasProtected = $lkup{$prot}; if ($verbose > 1) { my $wgrp1 = $self->GetWriteGroup1($tagInfo, $writeGroup); print $out "Sorry, $wgrp1:$tag is $wasProtected for writing\n"; } next; } } # set priority for this tag $tagPriority{$tagInfo} = $priority; # keep track of highest priority QuickTime tag $highestQT{$lcTag} = $priority if $$table{GROUPS}{0} eq 'QuickTime' and (not defined $highestQT{$lcTag} or $highestQT{$lcTag} < $priority); if ($priority > $highestPriority{$lcTag}) { $highestPriority{$lcTag} = $priority; $preferred{$lcTag} = { $tagInfo => 1 }; $avoid{$lcTag} = $$tagInfo{Avoid} ? 1 : 0; } elsif ($priority == $highestPriority{$lcTag}) { # create all tags with highest priority $preferred{$lcTag}{$tagInfo} = 1; ++$avoid{$lcTag} if $$tagInfo{Avoid}; } if ($$tagInfo{WriteAlso}) { # store WriteAlso tags separately so we can set them first push @writeAlsoList, $tagInfo; } else { push @tagInfoList, $tagInfo; } # special case to allow override of XMP WriteGroup if ($writeGroup eq 'XMP') { my $wg = $$tagInfo{WriteGroup} || $$table{WRITE_GROUP}; $writeGroup = $wg if $wg; } $writeGroup{$tagInfo} = $writeGroup; } # sort tag info list in reverse order of priority (highest number last) # so we get the highest priority error message in the end @tagInfoList = sort { $tagPriority{$a} <=> $tagPriority{$b} } @tagInfoList; # must write any tags which also write other tags first unshift @tagInfoList, @writeAlsoList if @writeAlsoList; # check priorities for each set of tags we are writing my $lcTag; foreach $lcTag (keys %preferred) { # don't create tags with priority 0 if group priorities are set if ($preferred{$lcTag} and $highestPriority{$lcTag} == 0 and %{$$self{WRITE_PRIORITY}}) { delete $preferred{$lcTag} } # avoid creating tags with 'Avoid' flag set if there are other alternatives if ($avoid{$lcTag} and $preferred{$lcTag}) { if ($avoid{$lcTag} < scalar(keys %{$preferred{$lcTag}})) { # just remove the 'Avoid' tags since there are other preferred tags foreach $tagInfo (@tagInfoList) { next unless $lcTag eq lc $$tagInfo{Name}; delete $preferred{$lcTag}{$tagInfo} if $$tagInfo{Avoid}; } } elsif ($highestPriority{$lcTag} < 1000) { # look for another priority tag to create instead my $nextHighest = 0; my @nextBestTags; foreach $tagInfo (@tagInfoList) { next unless $lcTag eq lc $$tagInfo{Name}; my $priority = $tagPriority{$tagInfo} or next; next if $priority == $highestPriority{$lcTag}; next if $priority < $nextHighest; my $permanent = $$tagInfo{Permanent}; $permanent = $$tagInfo{Table}{PERMANENT} unless defined $permanent; next if $$tagInfo{Avoid} or $permanent; next if $writeGroup{$tagInfo} eq 'MakerNotes'; if ($nextHighest < $priority) { $nextHighest = $priority; undef @nextBestTags; } push @nextBestTags, $tagInfo; } if (@nextBestTags) { # change our preferred tags to the next best tags delete $preferred{$lcTag}; foreach $tagInfo (@nextBestTags) { $preferred{$lcTag}{$tagInfo} = 1; } } } } } # # generate new value hash for each tag # my ($prioritySet, $createGroups, %alsoWrote); delete $$self{CHECK_WARN}; # reset CHECK_PROC warnings # loop through all valid tags to find the one(s) to write foreach $tagInfo (@tagInfoList) { next if $alsoWrote{$tagInfo}; # don't rewrite tags we already wrote # only process List or non-List tags if specified next if defined $listOnly and ($listOnly xor $$tagInfo{List}); my $noConv; my $writeGroup = $writeGroup{$tagInfo}; my $permanent = $$tagInfo{Permanent}; $permanent = $$tagInfo{Table}{PERMANENT} unless defined $permanent; $writeGroup eq 'MakerNotes' and $permanent = 1 unless defined $permanent; my $wgrp1 = $self->GetWriteGroup1($tagInfo, $writeGroup); $tag = $$tagInfo{Name}; # get tag name for warnings my $lcTag = lc $tag; my $pref = $preferred{$lcTag} || { }; # don't write Avoid-ed tags with side effect unless preferred next if not $$pref{$tagInfo} and $$tagInfo{Avoid} and $$tagInfo{WriteAlso}; my $shift = $options{Shift}; my $addValue = $options{AddValue}; if (defined $shift) { # (can't currently shift list-type tags) my $shiftable; if ($$tagInfo{List}) { $shiftable = ''; # can add/delete but not shift } else { $shiftable = $$tagInfo{Shift}; unless ($shift) { # set shift according to AddValue/DelValue $shift = 1 if $addValue; # can shift a date/time with -=, but this is # a conditional delete operation for other tags $shift = -1 if $options{DelValue} and defined $shiftable and $shiftable eq 'Time'; } if ($shift and (not defined $value or not length $value)) { # (now allow -= to be used for shiftable tag - v8.05) #$err = "No value for time shift of $wgrp1:$tag"; #$verbose > 2 and print $out "$err\n"; #next; undef $shift; } } # can't shift List-type tag if ((defined $shiftable and not $shiftable) and # and don't try to conditionally delete if Shift is "0" ($shift or ($shiftable eq '0' and $options{DelValue}))) { $err = "$wgrp1:$tag is not shiftable"; $verbose and print $out "$err\n"; next; } } my $val = $value; if (defined $val) { # check to make sure this is a List or Shift tag if adding if ($addValue and not ($shift or $$tagInfo{List})) { if ($addValue eq '2') { undef $addValue; # quietly reset this option } else { $err = "Can't add $wgrp1:$tag (not a List type)"; $verbose > 2 and print $out "$err\n"; next; } } if ($shift) { if ($$tagInfo{Shift} and $$tagInfo{Shift} eq 'Time') { # add '+' or '-' prefix to indicate shift direction $val = ($shift > 0 ? '+' : '-') . $val; # check the shift for validity require 'Image/ExifTool/Shift.pl'; my $err2 = CheckShift($$tagInfo{Shift}, $val); if ($err2) { $err = "$err2 for $wgrp1:$tag"; $verbose > 2 and print $out "$err\n"; next; } } elsif (IsFloat($val)) { $val *= $shift; } else { $err = "Shift value for $wgrp1:$tag is not a number"; $verbose > 2 and print $out "$err\n"; next; } $noConv = 1; # no conversions if shifting tag } elsif (not length $val and $options{DelValue}) { $noConv = 1; # no conversions for deleting empty value } elsif (ref $val eq 'HASH' and not $$tagInfo{Struct}) { $err = "Can't write a structure to $wgrp1:$tag"; $verbose > 2 and print $out "$err\n"; next; } } elsif ($permanent) { return 0 if $options{IgnorePermanent}; # can't delete permanent tags, so set them to DelValue or empty string instead if (defined $$tagInfo{DelValue}) { $val = $$tagInfo{DelValue}; $noConv = 1; # DelValue is the raw value, so no conversion necessary } else { $val = ''; } } elsif ($addValue or $options{DelValue}) { $err = "No value to add or delete in $wgrp1:$tag"; $verbose > 2 and print $out "$err\n"; next; } else { if ($$tagInfo{DelCheck}) { #### eval DelCheck ($self, $tagInfo, $wantGroup) my $err2 = eval $$tagInfo{DelCheck}; $@ and warn($@), $err2 = 'Error evaluating DelCheck'; if (defined $err2) { # (allow other tags to be set using DelCheck as a hook) $err2 or goto WriteAlso; # GOTO! $err2 .= ' for' unless $err2 =~ /delete$/; $err = "$err2 $wgrp1:$tag"; $verbose > 2 and print $out "$err\n"; next; } } # set group delete flag if this tag represents an entire group if ($$tagInfo{DelGroup} and not $options{DelValue}) { my @del = ( $tag ); $$self{DEL_GROUP}{$tag} = 1; # delete extra groups if necessary if ($delMore{$tag}) { $$self{DEL_GROUP}{$_} = 1, push(@del,$_) foreach @{$delMore{$tag}}; } # remove all of this group from previous new values $self->RemoveNewValuesForGroup($tag); $verbose and print $out " Deleting tags in: @del\n"; ++$numSet; next; } $noConv = 1; # value is not defined, so don't do conversion } # apply inverse PrintConv and ValueConv conversions # save ValueConv setting for use in ConvInv() unless ($noConv) { # set default conversion type used by ConvInv() and CHECK_PROC routines $$self{ConvType} = $convType; my $e; ($val,$e) = $self->ConvInv($val,$tagInfo,$tag,$wgrp1,$$self{ConvType},$wantGroup); if (defined $e) { # empty error string causes error to be ignored without setting the value $e or goto WriteAlso; # GOTO! $err = $e; } } if (not defined $val and defined $value) { # if value conversion failed, we must still add a NEW_VALUE # entry for this tag it it was a DelValue next unless $options{DelValue}; $val = 'xxx never delete xxx'; } $$self{NEW_VALUE} or $$self{NEW_VALUE} = { }; if ($options{Replace}) { # delete the previous new value $self->GetNewValueHash($tagInfo, $writeGroup, 'delete', $options{ProtectSaved}); # also delete related tag previous new values if ($$tagInfo{WriteAlso}) { $$self{INDENT2} = '+'; my ($wgrp, $wtag); if ($$tagInfo{WriteGroup} and $$tagInfo{WriteGroup} eq 'All' and $writeGroup) { $wgrp = $writeGroup . ':'; } else { $wgrp = ''; } foreach $wtag (sort keys %{$$tagInfo{WriteAlso}}) { my ($n,$e) = $self->SetNewValue($wgrp . $wtag, undef, Replace=>2); $numSet += $n; } $$self{INDENT2} = ''; } $options{Replace} == 2 and ++$numSet, next; } if (defined $val) { # we are editing this tag, so create a NEW_VALUE hash entry my $nvHash = $self->GetNewValueHash($tagInfo, $writeGroup, 'create', $options{ProtectSaved}, ($options{DelValue} and not $shift)); # ignore new values protected with ProtectSaved $nvHash or ++$numSet, next; # (increment $numSet to avoid warning) $$nvHash{NoReplace} = 1 if $$tagInfo{List} and not $options{Replace}; $$nvHash{WantGroup} = $wantGroup; $$nvHash{EditOnly} = 1 if $editOnly; # save maker note fixup information if writing maker notes $$nvHash{MAKER_NOTE_FIXUP} = $options{Fixup} if $$tagInfo{MakerNotes}; if ($createOnly) { # create only (never edit) # empty item in DelValue list to never edit existing value $$nvHash{DelValue} = [ '' ]; $$nvHash{CreateOnly} = 1; } elsif ($options{DelValue} or $addValue or $shift) { # flag any AddValue or DelValue by creating the DelValue list $$nvHash{DelValue} or $$nvHash{DelValue} = [ ]; if ($shift) { # add shift value to list $$nvHash{Shift} = $val; } elsif ($options{DelValue}) { # don't create if we are replacing a specific value $$nvHash{IsCreating} = 0 unless $val eq '' or $$tagInfo{List}; # add delete value to list push @{$$nvHash{DelValue}}, ref $val eq 'ARRAY' ? @$val : $val; if ($verbose > 1) { my $verb = $permanent ? 'Replacing' : 'Deleting'; my $fromList = $$tagInfo{List} ? ' from list' : ''; my @vals = (ref $val eq 'ARRAY' ? @$val : $val); foreach (@vals) { if (ref $_ eq 'HASH') { require 'Image/ExifTool/XMPStruct.pl'; $_ = Image::ExifTool::XMP::SerializeStruct($self, $_); } print $out "$$self{INDENT2}$verb $wgrp1:$tag$fromList if value is '${_}'\n"; } } } } # set priority flag to add only the high priority info # (will only create the priority tag if it doesn't exist, # others get changed only if they already exist) my $prf = defined $$tagInfo{Preferred} ? $$tagInfo{Preferred} : $$tagInfo{Table}{PREFERRED}; # hack to prefer only a single tag in the QuickTime group if ($$tagInfo{Table}{GROUPS}{0} eq 'QuickTime') { $prf = 0 if $tagPriority{$tagInfo} < $highestQT{$lcTag}; } if ($$pref{$tagInfo} or $prf) { if ($permanent or $shift) { # don't create permanent or Shift-ed tag but define IsCreating # so we know that it is the preferred tag $$nvHash{IsCreating} = 0; } elsif (($$tagInfo{List} and not $options{DelValue}) or not ($$nvHash{DelValue} and @{$$nvHash{DelValue}}) or # also create tag if any DelValue value is empty ('') grep(/^$/,@{$$nvHash{DelValue}})) { $$nvHash{IsCreating} = $editOnly ? 0 : ($editGroup ? 2 : 1); # add to hash of groups where this tag is being created $createGroups or $createGroups = $options{CreateGroups} || { }; $$createGroups{$self->GetGroup($tagInfo, 0)} = 1; $$nvHash{CreateGroups} = $createGroups; } } if ($$nvHash{IsCreating}) { if (%{$$self{DEL_GROUP}}) { my ($grp, @grps); foreach $grp (keys %{$$self{DEL_GROUP}}) { next if $$self{DEL_GROUP}{$grp} == 2; # set flag indicating tags were written after this group was deleted $$self{DEL_GROUP}{$grp} = 2; push @grps, $grp; } if ($verbose > 1 and @grps) { @grps = sort @grps; print $out " Writing new tags after deleting groups: @grps\n"; } } } elsif ($createOnly) { $noCreate = $permanent ? 'permanent' : ($$tagInfo{Avoid} ? 'avoided' : ''); $noCreate or $noCreate = $shift ? 'shifting' : 'not preferred'; $verbose > 2 and print $out "Not creating $wgrp1:$tag ($noCreate)\n"; next; # nothing to do (not creating and not editing) } if ($shift or not $options{DelValue}) { $$nvHash{Value} or $$nvHash{Value} = [ ]; if (not $$tagInfo{List}) { # not a List tag -- overwrite existing value $$nvHash{Value}[0] = $val; } elsif (defined $$nvHash{AddBefore} and @{$$nvHash{Value}} >= $$nvHash{AddBefore}) { # values from a later argument have been added (ie. Replace=0) # to this list, so the new values should come before these splice @{$$nvHash{Value}}, -$$nvHash{AddBefore}, 0, ref $val eq 'ARRAY' ? @$val : $val; } else { # add at end of existing list push @{$$nvHash{Value}}, ref $val eq 'ARRAY' ? @$val : $val; } if ($verbose > 1) { my $ifExists; if ($$tagInfo{IsComposite}) { # (composite tags don't technically exist in the file) if ($$tagInfo{WriteAlso}) { $ifExists = ' (+' . join(',+',sort keys %{$$tagInfo{WriteAlso}}) . '):'; } else { $ifExists = ''; } } else { $ifExists = $$nvHash{IsCreating} ? ( $createOnly ? ($$nvHash{IsCreating} == 2 ? " if $writeGroup exists and tag doesn't" : " if tag doesn't exist") : ($$nvHash{IsCreating} == 2 ? " if $writeGroup exists" : '')) : (($$nvHash{DelValue} and @{$$nvHash{DelValue}}) ? ' if tag was deleted' : ' if tag exists'); } my $verb = ($shift ? 'Shifting' : ($addValue ? 'Adding' : 'Writing')); print $out "$$self{INDENT2}$verb $wgrp1:$tag$ifExists\n"; } } } elsif ($permanent) { $err = "Can't delete Permanent tag $wgrp1:$tag"; $verbose > 1 and print $out "$err\n"; next; } elsif ($addValue or $options{DelValue}) { $verbose > 1 and print $out "Adding/Deleting nothing does nothing\n"; next; } else { # create empty new value hash entry to delete this tag $self->GetNewValueHash($tagInfo, $writeGroup, 'delete'); my $nvHash = $self->GetNewValueHash($tagInfo, $writeGroup, 'create'); $$nvHash{WantGroup} = $wantGroup; $verbose > 1 and print $out "$$self{INDENT2}Deleting $wgrp1:$tag\n"; } $$setTags{$tagInfo} = 1 if $setTags; $prioritySet = 1 if $$pref{$tagInfo}; WriteAlso: ++$numSet; # also write related tags my $writeAlso = $$tagInfo{WriteAlso}; if ($writeAlso) { $$self{INDENT2} = '+'; # indicate related tag with a leading "+" my ($wgrp, $wtag, $n); if ($$tagInfo{WriteGroup} and $$tagInfo{WriteGroup} eq 'All' and $writeGroup) { $wgrp = $writeGroup . ':'; } else { $wgrp = ''; } local $SIG{'__WARN__'} = \&SetWarning; foreach $wtag (sort keys %$writeAlso) { my %opts = ( Type => 'ValueConv', Protected => $protected | 0x02, AddValue => $addValue, DelValue => $options{DelValue}, Shift => $options{Shift}, Replace => $options{Replace}, # handle lists properly CreateGroups=> $createGroups, SetTags => \%alsoWrote, # remember tags already written ); undef $evalWarning; #### eval WriteAlso ($val,%opts) my $v = eval $$writeAlso{$wtag}; # we wanted to do the eval in case there are side effect, but we # don't want to write a value for a tag that is being deleted: undef $v unless defined $val; $@ and $evalWarning = $@; unless ($evalWarning) { ($n,$evalWarning) = $self->SetNewValue($wgrp . $wtag, $v, %opts); $numSet += $n; # count this as being set if any related tag is set $prioritySet = 1 if $n and $$pref{$tagInfo}; } if ($evalWarning and (not $err or $verbose > 2)) { my $str = CleanWarning(); if ($str) { $str .= " for $wtag" unless $str =~ / for [-\w:]+$/; $str .= " in $wgrp1:$tag (WriteAlso)"; $err or $err = $str; print $out "$str\n" if $verbose > 2; } } } $$self{INDENT2} = ''; } } # print warning if we couldn't set our priority tag if (defined $err and not $prioritySet) { warn "$err\n" if $err and not wantarray; } elsif (not $numSet) { my $pre = $wantGroup ? $wantGroup . ':' : ''; if ($wasProtected) { $verbose = 0; # we already printed this verbose message unless ($options{Replace} and $options{Replace} == 2) { $err = "Sorry, $pre$tag is $wasProtected for writing"; } } elsif (not $listOnly) { if ($origTag =~ /[?*]/) { if ($noCreate) { $err = "No tags matching 'pre${origTag}' will be created"; $verbose = 0; # (already printed) } elsif ($foundMatch) { $err = "Sorry, no writable tags matching '$pre${origTag}'"; } else { $err = "No matching tags for '$pre${origTag}'"; } } elsif ($noCreate) { $err = "Not creating $pre$tag"; $verbose = 0; # (already printed) } elsif ($foundMatch) { $err = "Sorry, $pre$tag is not writable"; } elsif ($wantGroup and @matchingTags) { $err = "Sorry, $pre$tag doesn't exist or isn't writable"; } else { $err = "Tag '$pre${tag}' is not defined"; } } if ($err) { $verbose > 2 and print $out "$err\n"; warn "$err\n" unless wantarray; } } elsif ($$self{CHECK_WARN}) { $err = $$self{CHECK_WARN}; $verbose > 2 and print $out "$err\n"; } elsif ($err and not $verbose) { undef $err; } return ($numSet, $err) if wantarray; return $numSet; } #------------------------------------------------------------------------------ # set new values from information in specified file # Inputs: 0) ExifTool object reference, 1) source file name or reference, etc, # or ExifTool ref to use already-extracted tags from an ExifTool object, # 2-N) List of tags to set (or all if none specified), or reference(s) to # hash for options to pass to SetNewValue. The Replace option defaults # to 1 for SetNewValuesFromFile -- set this to 0 to allow multiple tags # to be copied to a list # Returns: Hash of information set successfully (includes Warning or Error messages) # Notes: Tag names may contain a group prefix, a leading '-' to exclude from copy, # and/or a trailing '#' to copy the ValueConv value. The tag name '*' may # be used to represent all tags in a group. An optional destination tag # may be specified with '>DSTTAG' ('DSTTAG 1 ); # replace existing list items by default while (ref $setTags[0] eq 'HASH') { $_ = shift @setTags; foreach $key (keys %$_) { $opts{$key} = $$_{$key}; } } # expand shortcuts @setTags and ExpandShortcuts(\@setTags); # set options for our extraction tool my $options = $$self{OPTIONS}; my $printConv = $$options{PrintConv}; if ($opts{Type}) { # save source type separately because it may be different than dst Type $opts{SrcType} = $opts{Type}; # override PrintConv option with initial Type if given $printConv = ($opts{Type} eq 'PrintConv' ? 1 : 0); } my $srcType = $printConv ? 'PrintConv' : 'ValueConv'; my $structOpt = defined $$options{Struct} ? $$options{Struct} : 2; if (ref $srcFile and UNIVERSAL::isa($srcFile,'Image::ExifTool')) { $srcExifTool = $srcFile; $info = $srcExifTool->GetInfo({ PrintConv => $printConv }); } else { $srcExifTool = Image::ExifTool->new; $srcExifTool->Options(PrintConv => $printConv); # set flag to indicate we are being called from inside SetNewValuesFromFile() $$srcExifTool{TAGS_FROM_FILE} = 1; # synchronize and increment the file sequence number $$srcExifTool{FILE_SEQUENCE} = $$self{FILE_SEQUENCE}++; # copy both structured and flattened tags by default (but flattened tags are "unsafe") # copy structures only if no tags specified (since flattened tags are "unsafe") $structOpt = 1 if $structOpt eq '2' and not @setTags; # +------------------------------------------+ # ! DON'T FORGET!! Must consider each new ! # ! option to decide how it is handled here. ! # +------------------------------------------+ foreach (qw(ByteUnit Charset CharsetEXIF CharsetFileName CharsetID3 CharsetIPTC CharsetPhotoshop Composite DateFormat Debug EncodeHangs Escape ExtendedXMP ExtractEmbedded FastScan Filter FixBase Geolocation GeolocAltNames GeolocFeature GeolocMinPop GeolocMaxDist GlobalTimeShift HexTagIDs IgnoreGroups IgnoreMinorErrors IgnoreTags ImageHashType Lang LargeFileSupport LigoGPSScale ListItem ListSep MDItemTags MissingTagValue NoPDFList NoWarning Password PrintConv QuickTimeUTC RequestTags SaveFormat SavePath ScanForXMP StructFormat SystemTags TimeZone Unknown UserParam Validate WindowsLongPath WindowsWideFile XAttrTags XMPAutoConv)) { $srcExifTool->Options($_ => $$options{$_}); } $srcExifTool->Options( Binary => 1, CoordFormat => $$options{CoordFormat} || '%d %d %.8f', # copy coordinates at high resolution unless otherwise specified Duplicates => 1, # Exclude (set below) LimitLongValues => 10000000, # (10 MB) List => 1, MakerNotes => $$options{FastScan} && $$options{FastScan} > 1 ? undef : 1, RequestAll => $$options{RequestAll} || 1, # (must request all because reqTags doesn't cover wildcards) StrictDate => defined $$options{StrictDate} ? $$options{StrictDate} : 1, Struct => $structOpt, ); # reset Geolocation option if we aren't copying any geolocation tags if ($$options{Geolocation} and not grep /\bGeolocation/i, @setTags) { $self->VPrint(0, '(resetting unnecessary Geolocation option)'); $$srcExifTool{OPTIONS}{Geolocation} = undef; } $$srcExifTool{GLOBAL_TIME_OFFSET} = $$self{GLOBAL_TIME_OFFSET}; $$srcExifTool{ALT_EXIFTOOL} = $$self{ALT_EXIFTOOL}; foreach $tag (@setTags) { next if ref $tag; # avoid extracting tags that are excluded $tag =~ /^-(.*)/ and push(@exclude, $1), next; # add specified tags to list of requested tags $_ = $tag; if (/(.+?)\s*(>|<)\s*(.+)/) { if ($2 eq '>') { $_ = $1; } else { $_ = $3; /\$/ and push(@reqTags, /\$\{?(?:[-\w]+:)*([-\w?*]+)/g), next; } } push @reqTags, $2 if /(^|:)([-\w?*]+)#?$/; } if (@exclude) { ExpandShortcuts(\@exclude, 1); $srcExifTool->Options(Exclude => \@exclude); } $srcExifTool->Options(RequestTags => \@reqTags) if @reqTags; # get all tags from source file (including MakerNotes block) $info = $srcExifTool->ImageInfo($srcFile); } return $info if $$info{Error} and $$info{Error} eq 'Error opening file'; delete $$srcExifTool{VALUE}{Error}; # delete so we can check this later # sort tags in file order with priority tags last my (@tags, @prio); foreach (sort { $$srcExifTool{FILE_ORDER}{$a} <=> $$srcExifTool{FILE_ORDER}{$b} } keys %$info) { if (/ /) { push @tags, $_; } else { push @prio, $_; } } push @tags, @prio; # # simply transfer all tags from source image if no tags specified # unless (@setTags) { # transfer maker note information to this object $$self{MAKER_NOTE_BYTE_ORDER} = $$srcExifTool{MAKER_NOTE_BYTE_ORDER}; my $tagExtra = $$srcExifTool{TAG_EXTRA}; foreach $tag (@tags) { # don't try to set errors or warnings next if $tag =~ /^(Error|Warning)\b/; # get appropriate value type if necessary if ($opts{SrcType} and $opts{SrcType} ne $srcType) { $$info{$tag} = $srcExifTool->GetValue($tag, $opts{SrcType}); } my $fixup = $$tagExtra{$tag}{Fixup}; $opts{Fixup} = $fixup if $fixup; # set value for this tag my ($n, $e) = $self->SetNewValue($tag, $$info{$tag}, %opts); # delete this tag if we couldn't set it $n or delete $$info{$tag}; delete $opts{Fixup} if $fixup; } return $info; } # # transfer specified tags in the proper order # # 1) loop through input list of tags to set, and build @setList my (@setList, $set, %setMatches, $t, %altFiles); my $assign = 0; foreach $t (@setTags) { if (ref $t eq 'HASH') { # update current options foreach $key (keys %$t) { $opts{$key} = $$t{$key}; } next; } # make a copy of the current options for this setTag # (also use this hash to store expression and wildcard flags, EXPR and WILD) my $opts = { %opts }; $tag = lc $t; # change tag/group names to all lower case my (@fg, $grp, $dst, $dstGrp, $dstTag, $isExclude); # handle redirection to another tag if ($tag =~ /(.+?)\s*(>|<|=)(\s*)(.*)/) { $dstGrp = ''; my ($opt, $op, $spc); if ($2 eq '>') { ($tag, $dstTag) = ($1, $4); # flag add and delete (eg. '+<' and '-<') redirections $opt = $1 if $tag =~ s/\s*([-+])$// or $dstTag =~ s/^([-+])\s*//; } else { ($dstTag, $op, $spc, $tag) = ($1, $2, $3, $4); $opt = $1 if $dstTag =~ s/\s*([-+])$//; if ($op eq '=') { # simple assignment ($tag will be the new value) $tag = $spc . $tag; undef $tag unless $dstTag =~ s/\^$// or length $tag; $$opts{ASSIGN} = ++$assign; } elsif ($tag =~ /\$/) { # handle expressions $tag = $t; # restore original case # recover leading whitespace (except for initial single space) $tag =~ s/(.+?)\s*(>|<) ?//; $$opts{EXPR} = 1; # flag this expression } else { # (not sure why this is here because sign should be before '<') # (--> allows "<+" or "<-", which is an undocumented feature) $opt = $1 if $tag =~ s/^([-+])\s*//; } } $$opts{Replace} = 0 if $dstTag =~ s/^\+//; # validate tag name(s) unless ($$opts{EXPR} or $$opts{ASSIGN} or ValidTagName($tag)) { $self->Warn("Invalid tag name '${tag}'. Use '=' not '<' to assign a tag value"); next; } ValidTagName($dstTag) or $self->Warn("Invalid tag name '${dstTag}'"), next; # translate '+' and '-' to appropriate SetNewValue option if ($opt) { $$opts{{ '+' => 'AddValue', '-' => 'DelValue' }->{$opt}} = 1; $$opts{Shift} = 0; # shift if shiftable } ($dstGrp, $dstTag) = ($1, $2) if $dstTag =~ /(.*):(.+)/; # ValueConv may be specified separately on the destination with '#' $$opts{Type} = 'ValueConv' if $dstTag =~ s/#$//; # replace tag name of 'all' with '*' $dstTag = '*' if $dstTag eq 'all'; } else { $$opts{Replace} = 0 if $tag =~ s/^\+//; } unless ($$opts{EXPR} or $$opts{ASSIGN}) { $isExclude = ($tag =~ s/^-//); if ($tag =~ /(.*):(.+)/) { ($grp, $tag) = ($1, $2); foreach (split /:/, $grp) { # save family/groups in list (ignoring 'all' and '*') next unless length($_) and /^(\d+)?(.*)/; my ($f, $g) = ($1, $2); $f = 7 if (not $f or $f eq '7') and $g =~ s/^ID-//i; if ($g =~ /^file\d+$/i and (not $f or $f eq '8')) { $f = 8; my $g8 = ucfirst $g; if ($$srcExifTool{ALT_EXIFTOOL}{$g8}) { $$opts{GROUP8} = $g8; $altFiles{$g8} or $altFiles{$g8} = [ ]; # save list of requested tags for this alternate ExifTool object push @{$altFiles{$g8}}, "$grp:$tag"; } } push @fg, [ $f, $g ] unless $g eq '*' or $g eq 'all'; } } # allow ValueConv to be specified by a '#' on the tag name if ($tag =~ s/#$//) { $$opts{SrcType} = 'ValueConv'; $$opts{Type} = 'ValueConv' unless $dstTag; } # replace 'all' with '*' in tag and group names $tag = '*' if $tag eq 'all'; # allow wildcards in tag names (handle differently from all tags: '*') if ($tag =~ /[?*]/ and $tag ne '*') { $$opts{WILD} = 1; # set flag indicating wildcards were used in source tag $tag =~ s/\*/[-\\w]*/g; $tag =~ s/\?/[-\\w]/g; } } # redirect, exclude or set this tag (Note: @fg is empty if we don't care about the group) if ($dstTag) { # redirect this tag $isExclude and return { Error => "Can't redirect excluded tag" }; # set destination group the same as source if necessary # (removed in 7.72 so '-*:*GetInfo($altFiles{$g8}); # add to tags list after dummy entry to signify start of tags for this alt file if (%$altInfo) { push @tags, 'Warning DUMMY', reverse sort keys %$altInfo; $$info{$_} = $$altInfo{$_} foreach keys %$altInfo; } } # 2) initialize lists of matching tags for each setTag foreach $set (@setList) { $$set[2] and $setMatches{$set} = [ ]; } # no need to search source tags if doing only assignments undef @tags if $assign == @setList; # 3) loop through all tags in source image and save tags matching each setTag my (%rtnInfo, $isAlt); foreach $tag (@tags) { # don't try to set errors or warnings if ($tag =~ /^(Error|Warning)( |$)/) { if ($tag eq 'Warning DUMMY') { $isAlt = 1; # start of the alt tags } else { $rtnInfo{$tag} = $$info{$tag}; } next; } # only set specified tags my $lcTag = lc(GetTagName($tag)); my (@grp, %grp); SET: foreach $set (@setList) { my $opts = $$set[3]; next if $$opts{EXPR}; # (expressions handled in step 4) next if $$opts{GROUP8} xor $isAlt; # check first for matching tag unless ($$set[1] eq $lcTag or $$set[1] eq '*') { # handle wildcards next unless $$opts{WILD} and $lcTag =~ /^$$set[1]$/; } # then check for matching group if (@{$$set[0]}) { # get lower case group names if not done already unless (@grp) { @grp = map(lc, $srcExifTool->GetGroup($tag)); $grp{$_} = 1 foreach @grp; } foreach (@{$$set[0]}) { my ($f, $g) = @$_; if (not defined $f) { next SET unless $grp{$g}; } elsif ($f == 7) { next SET unless IsSameID($srcExifTool->GetTagID($tag), $g); } else { next SET unless defined $grp[$f] and $g eq $grp[$f]; } } } last unless $$set[2]; # all done if we hit an exclude # add to the list of tags matching this setTag push @{$setMatches{$set}}, $tag; } } # 4) loop through each setTag in original order, setting new tag values foreach $set (reverse @setList) { # get options for SetNewValue my $opts = $$set[3]; # handle expressions if ($$opts{EXPR} or $$opts{ASSIGN}) { my $val; if ($$opts{EXPR}) { $val = $srcExifTool->InsertTagValues($$set[1], \@tags, 'Error'); my $err = $$srcExifTool{VALUE}{Error}; if ($err) { # pass on any error as a warning unless it is suppressed my $noWarn = $$srcExifTool{OPTIONS}{NoWarning}; unless ($noWarn and (eval { $err =~ /$noWarn/ } or # (also apply expression to warning without "[minor] " prefix) ($err =~ s/^\[minor\] //i and eval { $err =~ /$noWarn/ }))) { $tag = NextFreeTagKey(\%rtnInfo, 'Warning'); $rtnInfo{$tag} = $$srcExifTool{VALUE}{Error}; } delete $$srcExifTool{VALUE}{Error}; next unless defined $val; } } else { $val = $$set[1]; } my ($dstGrp, $dstTag) = @{$$set[2]}; $$opts{Protected} = 1 unless $dstTag =~ /[?*]/ and $dstTag ne '*'; $$opts{Group} = $dstGrp if $dstGrp; my @rtnVals = $self->SetNewValue($dstTag, $val, %$opts); $rtnInfo{$dstTag} = $val if $rtnVals[0]; # tag was set successfully next; } foreach $tag (@{$setMatches{$set}}) { my ($val, $noWarn); if ($$opts{SrcType} and $$opts{SrcType} ne $srcType) { $val = $srcExifTool->GetValue($tag, $$opts{SrcType}); } else { $val = $$info{$tag}; } my ($dstGrp, $dstTag) = @{$$set[2]}; if ($dstGrp) { my @dstGrp = split /:/, $dstGrp; # destination group of '*' writes to same group as source tag # (family 1 unless otherwise specified) foreach (@dstGrp) { next unless /^(\d*)(all|\*)$/i; $_ = $1 . $srcExifTool->GetGroup($tag, length $1 ? $1 : 1); $noWarn = 1; # don't warn on wildcard destinations } $$opts{Group} = join ':', @dstGrp; } else { delete $$opts{Group}; } # transfer maker note information if setting this tag if ($$srcExifTool{TAG_INFO}{$tag}{MakerNotes}) { $$opts{Fixup} = $$srcExifTool{TAG_EXTRA}{$tag}{Fixup}; $$self{MAKER_NOTE_BYTE_ORDER} = $$srcExifTool{MAKER_NOTE_BYTE_ORDER}; } if ($dstTag eq '*') { $dstTag = $tag; $noWarn = 1; } if ($$set[1] eq '*' or $$set[3]{WILD}) { # don't copy from protected binary tags when using wildcards next if $$srcExifTool{TAG_INFO}{$tag}{Protected} and $$srcExifTool{TAG_INFO}{$tag}{Binary}; # don't copy to protected tags when using wildcards delete $$opts{Protected}; # don't copy flattened tags if copying structures too when copying all $$opts{NoFlat} = $structOpt eq '2' ? 1 : 0; } else { # allow protected tags to be copied if specified explicitly $$opts{Protected} = 1 unless $dstTag =~ /[?*]/; delete $$opts{NoFlat}; } # set value(s) for this tag my ($rtn, $wrn) = $self->SetNewValue($dstTag, $val, %$opts); # this was added in version 9.14, and allowed actions like "-subjectGetNewValueHash($tag); } elsif (defined($tagInfo = $Image::ExifTool::Extra{$tag}) and $$tagInfo{Writable}) { $nvHash = $self->GetNewValueHash($tagInfo); } else { # separate group from tag name my @groups; @groups = split ':', $1 if $tag =~ s/(.*)://; my @tagInfoList = FindTagInfo($tag); # decide which tag we want GNV_TagInfo: foreach $tagInfo (@tagInfoList) { my $nvh = $self->GetNewValueHash($tagInfo) or next; # select tag in specified group(s) if necessary foreach (@groups) { next if $_ eq $$nvh{WriteGroup}; my @grps = $self->GetGroup($tagInfo); if ($grps[0] eq $$nvh{WriteGroup}) { # check family 1 group only if WriteGroup is not specific next if $_ eq $grps[1]; } else { # otherwise check family 0 group next if $_ eq $grps[0]; } # also check family 7 next if /^ID-(.*)/i and IsSameID($$tagInfo{TagID}, $1); # step to next entry in list $nvh = $$nvh{Next} or next GNV_TagInfo; } $nvHash = $nvh; # give priority to the one we are creating last if defined $$nvHash{IsCreating}; } } } # return new value hash if requested $newValueHashPt and $$newValueHashPt = $nvHash; } unless ($nvHash and $$nvHash{Value}) { return () if wantarray; # return empty list return undef; } my $vals = $$nvHash{Value}; # do inverse raw conversion if necessary # - must also check after doing a Shift if ($$nvHash{TagInfo}{RawConvInv} or $$nvHash{Shift}) { my @copyVals = @$vals; # modify a copy of the values $vals = \@copyVals; my $tagInfo = $$nvHash{TagInfo}; my $conv = $$tagInfo{RawConvInv}; my $table = $$tagInfo{Table}; my ($val, $checkProc); $checkProc = $$table{CHECK_PROC} if $$nvHash{Shift} and $table; local $SIG{'__WARN__'} = \&SetWarning; undef $evalWarning; foreach $val (@$vals) { # must check value now if it was shifted if ($checkProc) { my $err = &$checkProc($self, $tagInfo, \$val); if ($err or not defined $val) { $err or $err = 'Error generating raw value'; $self->Warn("$err for $$tagInfo{Name}"); @$vals = (); last; } next unless $conv; } else { last unless $conv; } # do inverse raw conversion if (ref($conv) eq 'CODE') { $val = &$conv($val, $self); } else { #### eval RawConvInv ($self, $val, $tagInfo) $val = eval $conv; $@ and $evalWarning = $@; } if ($evalWarning) { # an empty warning ("\n") ignores tag with no error if ($evalWarning ne "\n") { my $err = CleanWarning() . " in $$tagInfo{Name} (RawConvInv)"; $self->Warn($err); } @$vals = (); last; } } } # return our value(s) if (wantarray) { # remove duplicates if requested if (@$vals > 1 and $self->Options('NoDups')) { my %seen; @$vals = grep { !$seen{$_}++ } @$vals; } return @$vals; } return $$vals[0]; } #------------------------------------------------------------------------------ # Return the total number of new values set # Inputs: 0) ExifTool object reference # Returns: Scalar context) Number of new values that have been set (incl pseudo) # List context) Number of new values (incl pseudo), number of "pseudo" values # ("pseudo" values are those which don't require rewriting the file to change) sub CountNewValues($) { my $self = shift; my $newVal = $$self{NEW_VALUE}; my ($num, $pseudo) = (0, 0); if ($newVal) { $num = scalar keys %$newVal; my $nv; foreach $nv (values %$newVal) { my $tagInfo = $$nv{TagInfo}; # don't count tags that don't write anything $$tagInfo{WriteNothing} and --$num, next; # count the number of pseudo tags included $$tagInfo{WritePseudo} and ++$pseudo; } } $num += scalar keys %{$$self{DEL_GROUP}}; return $num unless wantarray; return ($num, $pseudo); } #------------------------------------------------------------------------------ # Save new values for subsequent restore # Inputs: 0) ExifTool object reference # Returns: Number of times new values have been saved # Notes: increments SAVE_COUNT flag each time routine is called sub SaveNewValues($) { my $self = shift; my $newValues = $$self{NEW_VALUE}; my $saveCount = ++$$self{SAVE_COUNT}; my $key; foreach $key (keys %$newValues) { my $nvHash = $$newValues{$key}; while ($nvHash) { # set Save count if not done already $$nvHash{Save} or $$nvHash{Save} = $saveCount; $nvHash = $$nvHash{Next}; } } # initialize hash for saving overwritten new values $$self{SAVE_NEW_VALUE} = { }; # make a copy of the delete group hash my %delGrp = %{$$self{DEL_GROUP}}; $$self{SAVE_DEL_GROUP} = \%delGrp; return $saveCount; } #------------------------------------------------------------------------------ # Restore new values to last saved state # Inputs: 0) ExifTool object reference # Notes: Restores saved new values, but currently doesn't restore them in the # original order, so there may be some minor side-effects when restoring tags # with overlapping groups. eg) XMP:Identifier, XMP-dc:Identifier # Also, this doesn't do the right thing for list-type tags which accumulate # values across a save point sub RestoreNewValues($) { my $self = shift; my $newValues = $$self{NEW_VALUE}; my $savedValues = $$self{SAVE_NEW_VALUE}; my $key; # 1) remove any new values which don't have the Save flag set if ($newValues) { my @keys = keys %$newValues; foreach $key (@keys) { my $lastHash; my $nvHash = $$newValues{$key}; while ($nvHash) { if ($$nvHash{Save}) { $lastHash = $nvHash; } else { # remove this entry from the list if ($lastHash) { $$lastHash{Next} = $$nvHash{Next}; } elsif ($$nvHash{Next}) { $$newValues{$key} = $$nvHash{Next}; } else { delete $$newValues{$key}; } } $nvHash = $$nvHash{Next}; } } } # 2) restore saved new values if ($savedValues) { $newValues or $newValues = $$self{NEW_VALUE} = { }; foreach $key (keys %$savedValues) { if ($$newValues{$key}) { # add saved values to end of list my $nvHash = LastInList($$newValues{$key}); $$nvHash{Next} = $$savedValues{$key}; } else { $$newValues{$key} = $$savedValues{$key}; } } $$self{SAVE_NEW_VALUE} = { }; # reset saved new values } # 3) restore delete groups my %delGrp = %{$$self{SAVE_DEL_GROUP}}; $$self{DEL_GROUP} = \%delGrp; } #------------------------------------------------------------------------------ # Set alternate file for extracting information # Inputs: 0) ExifTool ref, 1) family 8 group name (of the form "File#" where # is any number) # 2) alternate file name, or undef to reset # Returns: 1 on success, or 0 on invalid group name sub SetAlternateFile($$$) { my ($self, $g8, $file) = @_; $g8 = ucfirst lc $g8; return 0 unless $g8 =~ /^File\d+$/; # keep the same file if already initialized (possibly has metadata extracted) if (not defined $file) { delete $$self{ALT_EXIFTOOL}{$g8}; } elsif (not ($$self{ALT_EXIFTOOL}{$g8} and $$self{ALT_EXIFTOOL}{$g8}{ALT_FILE} eq $file)) { my $altExifTool = Image::ExifTool->new; $$altExifTool{ALT_FILE} = $file; $$self{ALT_EXIFTOOL}{$g8} = $altExifTool; } return 1; } #------------------------------------------------------------------------------ # Set filesystem time from from FileModifyDate or FileCreateDate tag # Inputs: 0) ExifTool object reference, 1) file name or file ref # 2) time (-M or -C) of original file (used for shift; obtained from file if not given) # 3) tag name to write (undef for 'FileModifyDate') # 4) flag set if argument 2 has already been converted to Unix seconds # Returns: 1=time changed OK, 0=nothing done, -1=error setting time # (increments CHANGED flag and sets corresponding WRITTEN tag) sub SetFileModifyDate($$;$$$) { my ($self, $file, $originalTime, $tag, $isUnixTime) = @_; my $nvHash; $tag = 'FileModifyDate' unless defined $tag; my $val = $self->GetNewValue($tag, \$nvHash); return 0 unless defined $val; my $isOverwriting = $self->IsOverwriting($nvHash); return 0 unless $isOverwriting; # can currently only set creation date on Windows systems # (and Mac now too, but that is handled with the MacOS tags) return 0 if $tag eq 'FileCreateDate' and $^O ne 'MSWin32'; if ($isOverwriting < 0) { # are we shifting time? # use original time of this file if not specified unless (defined $originalTime) { my ($aTime, $mTime, $cTime) = $self->GetFileTime($file); $originalTime = ($tag eq 'FileCreateDate') ? $cTime : $mTime; return 0 unless defined $originalTime; $isUnixTime = 1; } $originalTime = int($^T - $originalTime*(24*3600) + 0.5) unless $isUnixTime; return 0 unless $self->IsOverwriting($nvHash, $originalTime); $val = $$nvHash{Value}[0]; # get shifted value } my ($aTime, $mTime, $cTime); if ($tag eq 'FileCreateDate') { eval { require Win32::API } or $self->Warn("Install Win32::API to set $tag"), return -1; eval { require Win32API::File } or $self->Warn("Install Win32API::File to set $tag"), return -1; $cTime = $val; } else { $aTime = $mTime = $val; } $self->SetFileTime($file, $aTime, $mTime, $cTime, 1) or $self->Warn("Error setting $tag"), return -1; ++$$self{CHANGED}; $$self{WRITTEN}{$tag} = $val; # remember that we wrote this tag $self->VerboseValue("+ $tag", $val); return 1; } #------------------------------------------------------------------------------ # Change file name and/or directory from FileName and Directory tags # Inputs: 0) ExifTool object reference, 1) current file name (including path) # 2) new name (or undef to build from FileName and Directory tags) # 3) option: 'HardLink'/'SymLink' to create hard/symbolic link instead of renaming # 'Test' to only print new file name # 4) 0 to indicate that a file will no longer exist (used for 'Test' only) # Returns: 1=name changed OK, 0=nothing changed, -1=error changing name # (and increments CHANGED flag if filename changed) # Notes: Will not overwrite existing file. Creates directories as necessary. sub SetFileName($$;$$$) { my ($self, $file, $newName, $opt, $usedFlag) = @_; my ($nvHash, $doName, $doDir); $opt or $opt = ''; # determine the new file name unless (defined $newName) { if ($opt) { if ($opt eq 'HardLink' or $opt eq 'Link') { $newName = $self->GetNewValue('HardLink'); } elsif ($opt eq 'SymLink') { $newName = $self->GetNewValue('SymLink'); } elsif ($opt eq 'Test') { $newName = $self->GetNewValue('TestName'); } return 0 unless defined $newName; } else { my $filename = $self->GetNewValue('FileName', \$nvHash); $doName = 1 if defined $filename and $self->IsOverwriting($nvHash, $file); my $dir = $self->GetNewValue('Directory', \$nvHash); $doDir = 1 if defined $dir and $self->IsOverwriting($nvHash, $file); return 0 unless $doName or $doDir; # nothing to do if ($doName) { $newName = GetNewFileName($file, $filename); $newName = GetNewFileName($newName, $dir) if $doDir; } else { $newName = GetNewFileName($file, $dir); } } } # validate new file name in Windows if ($^O eq 'MSWin32') { if ($newName =~ /[\0-\x1f<>"|*]/) { $self->Warn('New file name not allowed in Windows (contains reserved characters)'); return -1; } if ($newName =~ /:/ and $newName !~ /^[A-Z]:[^:]*$/i) { $self->Warn("New file name not allowed in Windows (contains ':')"); return -1; } if ($newName =~ /\?/ and $newName !~ m{^[\\/]{2}\?[\\/][^?]*$}) { $self->Warn("New file name not allowed in Windows (contains '?')"); return -1; } if ($newName =~ m{(^|[\\/])(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.[^.]*)?$}i) { $self->Warn('New file name not allowed in Windows (reserved device name)'); return -1; } if ($newName =~ /([. ])$/) { $self->Warn("New file name not recommended for Windows (ends with '${1}')", 2) and return -1; } if (length $newName > 259 and $newName !~ /\?/) { $self->Warn('New file name not recommended for Windows (exceeds 260 chars)', 2) and return -1; } } else { $newName =~ tr/\0//d; # make sure name doesn't contain nulls } # protect against empty file name length $newName or $self->Warn('New file name is empty'), return -1; # don't replace existing file if ($self->Exists($newName, 1) and (not defined $usedFlag or $usedFlag)) { if ($file ne $newName or $opt =~ /Link$/) { # allow for case-insensitive filesystem if ($opt =~ /Link$/ or not $self->IsSameFile($file, $newName)) { $self->Warn("File '${newName}' already exists"); return -1; } } else { $self->Warn('File name is unchanged'); return 0; } } if ($opt eq 'Test') { my $out = $$self{OPTIONS}{TextOut}; print $out "'${file}' --> '${newName}'\n"; return 1; } # create directory for new file if necessary my $err = $self->CreateDirectory($newName); if (defined $err) { if ($err) { $self->Warn($err) unless $err =~ /^Error creating/; $self->Warn("Error creating directory for '${newName}'"); return -1; } $self->VPrint(0, "Created directory for '${newName}'\n"); } if ($opt eq 'HardLink' or $opt eq 'Link') { unless (link $file, $newName) { $self->Warn("Error creating hard link '${newName}'"); return -1; } ++$$self{CHANGED}; $self->VerboseValue('+ HardLink', $newName); return 1; } elsif ($opt eq 'SymLink') { $^O eq 'MSWin32' and $self->Warn('SymLink not supported in Windows'), return -1; $newName =~ s(^\./)(); # remove leading "./" from link name if it exists # path to linked file must be relative to the $newName directory, but $file # is relative to the current directory, so convert it to an absolute path # if using a relative directory and $newName isn't in the current directory if ($file !~ m(^/) and $newName =~ m(/)) { unless (eval { require Cwd }) { $self->Warn('Install Cwd to make symlinks to other directories'); return -1; } $file = eval { Cwd::abs_path($file) }; unless (defined $file) { $self->Warn('Error in Cwd::abs_path when creating symlink'); return -1; } } unless (eval { symlink $file, $newName } ) { $self->Warn("Error creating symbolic link '${newName}'"); return -1; } ++$$self{CHANGED}; $self->VerboseValue('+ SymLink', $newName); return 1; } # attempt to rename the file unless ($self->Rename($file, $newName)) { local (*EXIFTOOL_SFN_IN, *EXIFTOOL_SFN_OUT); # renaming didn't work, so copy the file instead unless ($self->Open(\*EXIFTOOL_SFN_IN, $file)) { $self->Error("Error opening '${file}'"); return -1; } unless ($self->Open(\*EXIFTOOL_SFN_OUT, $newName, '>')) { close EXIFTOOL_SFN_IN; $self->Error("Error creating '${newName}'"); return -1; } binmode EXIFTOOL_SFN_IN; binmode EXIFTOOL_SFN_OUT; my ($buff, $err); while (read EXIFTOOL_SFN_IN, $buff, 65536) { print EXIFTOOL_SFN_OUT $buff or $err = 1; } close EXIFTOOL_SFN_OUT or $err = 1; close EXIFTOOL_SFN_IN; if ($err) { $self->Unlink($newName); # erase bad output file $self->Error("Error writing '${newName}'"); return -1; } # preserve modification time my ($aTime, $mTime, $cTime) = $self->GetFileTime($file); $self->SetFileTime($newName, $aTime, $mTime, $cTime); # remove the original file $self->Unlink($file) or $self->Warn('Error removing old file'); } $$self{NewName} = $newName; # remember new file name ++$$self{CHANGED}; $self->VerboseValue('+ FileName', $newName); return 1; } #------------------------------------------------------------------------------ # Set file permissions, group/user id and various MDItem tags from new tag values # Inputs: 0) ExifTool ref, 1) file name or glob (must be a name for MDItem tags) # Returns: 1=something was set OK, 0=didn't try, -1=error (and warning set) # Notes: There may be errors even if 1 is returned sub SetSystemTags($$) { my ($self, $file) = @_; my $result = 0; my $perm = $self->GetNewValue('FilePermissions'); if (defined $perm) { if (eval { chmod($perm & 07777, $file) }) { $self->VerboseValue('+ FilePermissions', $perm); $result = 1; } else { $self->Warn('Error setting FilePermissions'); $result = -1; } } my $uid = $self->GetNewValue('FileUserID'); my $gid = $self->GetNewValue('FileGroupID'); if (defined $uid or defined $gid) { defined $uid or $uid = -1; defined $gid or $gid = -1; if (eval { chown($uid, $gid, $file) }) { $self->VerboseValue('+ FileUserID', $uid) if $uid >= 0; $self->VerboseValue('+ FileGroupID', $gid) if $gid >= 0; $result = 1; } else { $self->Warn('Error setting FileGroup/UserID'); $result = -1 unless $result; } } my $tag; foreach $tag (@writableMacOSTags) { my $nvHash; my $val = $self->GetNewValue($tag, \$nvHash); next unless $nvHash; if ($^O eq 'darwin') { ref $file and $self->Warn('Setting MDItem tags requires a file name'), last; require Image::ExifTool::MacOS; my $res = Image::ExifTool::MacOS::SetMacOSTags($self, $file, \@writableMacOSTags); $result = $res if $res == 1 or not $result; last; } elsif ($tag ne 'FileCreateDate') { $self->Warn('Can only set MDItem tags on MacOS'); last; } } # delete Windows Zone.Identifier if specified my $zhash = $self->GetNewValueHash($Image::ExifTool::Extra{ZoneIdentifier}); if ($zhash) { my $res = -1; if ($^O ne 'MSWin32') { $self->Warn('ZoneIdentifer is a Windows-only tag'); } elsif (ref $file) { $self->Warn('Writing ZoneIdentifer requires a file name'); } elsif (defined $self->GetNewValue('ZoneIdentifier', \$zhash)) { $self->Warn('ZoneIndentifier may only be deleted'); } elsif (not eval { require Win32API::File }) { $self->Warn('Install Win32API::File to write ZoneIdentifier'); } else { my ($wattr, $wide); my $zfile = "${file}:Zone.Identifier"; if ($self->EncodeFileName($zfile)) { $wide = 1; $wattr = eval { Win32API::File::GetFileAttributesW($zfile) }; } else { $wattr = eval { Win32API::File::GetFileAttributes($zfile) }; } if ($wattr == Win32API::File::INVALID_FILE_ATTRIBUTES()) { $res = 0; # file doesn't exist, nothing to do } elsif ($wattr & Win32API::File::FILE_ATTRIBUTE_READONLY()) { $self->Warn('Zone.Identifier stream is read-only'); } else { if ($wide) { $res = 1 if eval { Win32API::File::DeleteFileW($zfile) }; } else { $res = 1 if eval { Win32API::File::DeleteFile($zfile) }; } if ($res > 0) { $self->VPrint(0, " Deleting Zone.Identifier stream\n"); } else { $self->Warn('Error deleting Zone.Identifier stream'); } } } $result = $res if $res == 1 or not $result; } return $result; } #------------------------------------------------------------------------------ # Write information back to file # Inputs: 0) ExifTool object reference, # 1) input filename, file ref, RAF ref, or scalar ref (or '' or undef to create from scratch) # 2) output filename, file ref, or scalar ref (or undef to overwrite) # 3) optional output file type (required only if input file is not specified # and output file is a reference) # Returns: 1=file written OK, 2=file written but no changes made, 0=file write error sub WriteInfo($$;$$) { local ($_, *EXIFTOOL_FILE2, *EXIFTOOL_OUTFILE); my ($self, $infile, $outfile, $outType) = @_; my (@fileTypeList, $fileType, $tiffType, $hdr, $seekErr, $type, $tmpfile); my ($inRef, $outRef, $closeIn, $closeOut, $outPos, $outBuff, $eraseIn, $raf, $fileExt); my ($hardLink, $symLink, $testName); my $oldRaf = $$self{RAF}; my $rtnVal = 0; # initialize member variables $self->Init(); $$self{IsWriting} = 1; # first, save original file modify date if necessary # (do this now in case we are modifying file in place and shifting date) my ($nvHash, $nvHash2, $originalTime, $createTime); my $setModDate = defined $self->GetNewValue('FileModifyDate', \$nvHash); my $setCreateDate = defined $self->GetNewValue('FileCreateDate', \$nvHash2); my ($aTime, $mTime, $cTime); if ($setModDate and $self->IsOverwriting($nvHash) < 0 and defined $infile and ref $infile ne 'SCALAR') { ($aTime, $mTime, $cTime) = $self->GetFileTime($infile); $originalTime = $mTime; } if ($setCreateDate and $self->IsOverwriting($nvHash2) < 0 and defined $infile and ref $infile ne 'SCALAR') { ($aTime, $mTime, $cTime) = $self->GetFileTime($infile) unless defined $cTime; $createTime = $cTime; } # # do quick in-place change of file dir/name or date if that is all we are doing # my ($numNew, $numPseudo) = $self->CountNewValues(); if (not defined $outfile and defined $infile) { $hardLink = $self->GetNewValue('HardLink'); $symLink = $self->GetNewValue('SymLink'); $testName = $self->GetNewValue('TestName'); undef $hardLink if defined $hardLink and not length $hardLink; undef $symLink if defined $symLink and not length $symLink; undef $testName if defined $testName and not length $testName; my $newFileName = $self->GetNewValue('FileName', \$nvHash); my $newDir = $self->GetNewValue('Directory'); if (defined $newDir and length $newDir) { $newDir .= '/' unless $newDir =~ m{/$}; } else { undef $newDir; } if ($numNew == $numPseudo) { $rtnVal = 2; if ((defined $newFileName or defined $newDir) and not ref $infile) { my $result = $self->SetFileName($infile); if ($result > 0) { $infile = $$self{NewName}; # file name changed $rtnVal = 1; } elsif ($result < 0) { return 0; # don't try to do anything else } } if (not ref $infile or UNIVERSAL::isa($infile,'GLOB')) { $self->SetFileModifyDate($infile) > 0 and $rtnVal = 1 if $setModDate; $self->SetFileModifyDate($infile, undef, 'FileCreateDate') > 0 and $rtnVal = 1 if $setCreateDate; $self->SetSystemTags($infile) > 0 and $rtnVal = 1; } if (defined $hardLink or defined $symLink or defined $testName) { $hardLink and $self->SetFileName($infile, $hardLink, 'HardLink') and $rtnVal = 1; $symLink and $self->SetFileName($infile, $symLink, 'SymLink') and $rtnVal = 1; $testName and $self->SetFileName($infile, $testName, 'Test') and $rtnVal = 1; } return $rtnVal; } elsif (defined $newFileName and length $newFileName) { # can't simply rename file, so just set the output name if new FileName # --> in this case, must erase original copy if (ref $infile) { $outfile = $newFileName; # can't delete original } elsif ($self->IsOverwriting($nvHash, $infile)) { $outfile = GetNewFileName($infile, $newFileName); $eraseIn = 1; # delete original } } # set new directory if specified if (defined $newDir) { $outfile = $infile unless defined $outfile or ref $infile; if (defined $outfile) { $outfile = GetNewFileName($outfile, $newDir); $eraseIn = 1 unless ref $infile; } } } # # set up input file # if (ref $infile) { $inRef = $infile; if (UNIVERSAL::isa($inRef,'GLOB')) { seek($inRef, 0, 0); # make sure we are at the start of the file } elsif (UNIVERSAL::isa($inRef,'File::RandomAccess')) { $inRef->Seek(0); $raf = $inRef; } elsif ($] >= 5.006 and ($$self{OPTIONS}{EncodeHangs} or eval { require Encode; Encode::is_utf8($$inRef) } or $@)) { local $SIG{'__WARN__'} = \&SetWarning; # convert image data from UTF-8 to character stream if necessary my $buff = ($$self{OPTIONS}{EncodeHangs} or $@) ? pack('C*', unpack($] < 5.010000 ? 'U0C*' : 'C0C*', $$inRef)) : Encode::encode('utf8', $$inRef); if (defined $outfile) { $inRef = \$buff; } else { $$inRef = $buff; } } } elsif (defined $infile and $infile ne '') { # write to a temporary file if no output file given $outfile = $tmpfile = "${infile}_exiftool_tmp" unless defined $outfile; if ($self->Open(\*EXIFTOOL_FILE2, $infile)) { $fileExt = GetFileExtension($infile); $fileType = GetFileType($infile); @fileTypeList = GetFileType($infile); $tiffType = $$self{FILE_EXT} = GetFileExtension($infile); $self->VPrint(0, "Rewriting $infile...\n"); $inRef = \*EXIFTOOL_FILE2; $closeIn = 1; # we must close the file since we opened it } else { $self->Error('Error opening file'); return 0; } } elsif (not defined $outfile) { $self->Error("WriteInfo(): Must specify infile or outfile\n"); return 0; } else { # create file from scratch $outType = GetFileExtension($outfile) unless $outType or ref $outfile; if (CanCreate($outType)) { if ($$self{OPTIONS}{WriteMode} =~ /g/i) { $fileType = $tiffType = $outType; # use output file type if no input file $infile = "$fileType file"; # make bogus file name $self->VPrint(0, "Creating $infile...\n"); $inRef = \ ''; # set $inRef to reference to empty data } else { $self->Error("Not creating new $outType file (disallowed by WriteMode)"); return 0; } } elsif ($outType) { $self->Error("Can't create $outType files"); return 0; } else { $self->Error("Can't create file (unknown type)"); return 0; } } unless (@fileTypeList) { if ($fileType) { @fileTypeList = ( $fileType ); } else { @fileTypeList = @fileTypes; $tiffType = 'TIFF'; } } # # set up output file # if (ref $outfile) { $outRef = $outfile; if (UNIVERSAL::isa($outRef,'GLOB')) { binmode($outRef); $outPos = tell($outRef); } else { # initialize our output buffer if necessary defined $$outRef or $$outRef = ''; $outPos = length($$outRef); } } elsif (not defined $outfile) { # editing in place, so write to memory first # (only when infile is a file ref or scalar ref) if ($raf) { $self->Error("Can't edit File::RandomAccess object in place"); return 0; } $outBuff = ''; $outRef = \$outBuff; $outPos = 0; } elsif ($self->Exists($outfile, 1)) { $self->Error("File already exists: $outfile"); } elsif ($self->Open(\*EXIFTOOL_OUTFILE, $outfile, '>')) { $outRef = \*EXIFTOOL_OUTFILE; $closeOut = 1; # we must close $outRef binmode($outRef); $outPos = 0; } else { my $tmp = $tmpfile ? ' temporary' : ''; $self->Error("Error creating$tmp file: $outfile"); } # # write the file # until ($$self{VALUE}{Error}) { # create random access file object (disable seek test in case of straight copy) $raf or $raf = File::RandomAccess->new($inRef, 1); $raf->BinMode(); if ($numNew == $numPseudo) { $rtnVal = 1; # just do a straight copy of the file (no "real" tags are being changed) my $buff; while ($raf->Read($buff, 65536)) { Write($outRef, $buff) or $rtnVal = -1, last; } last; } elsif (not ref $infile and ($infile eq '-' or $infile =~ /\|$/)) { # patch for Windows command shell pipe $$raf{TESTED} = -1; # force buffering } else { $raf->SeekTest(); } # $raf->Debug() and warn " RAF debugging enabled!\n"; my $inPos = $raf->Tell(); $$self{RAF} = $raf; my %dirInfo = ( RAF => $raf, OutFile => $outRef, ); $raf->Read($hdr, 1024) or $hdr = ''; $raf->Seek($inPos, 0) or $seekErr = 1; my $wrongType; until ($seekErr) { $type = shift @fileTypeList; # do quick test to see if this is the right file type if ($magicNumber{$type} and length($hdr) and $hdr !~ /^$magicNumber{$type}/s) { next if @fileTypeList; $wrongType = 1; last; } # save file type in member variable $dirInfo{Parent} = $$self{FILE_TYPE} = $$self{PATH}[0] = $type; # determine which directories we must write for this file type $self->InitWriteDirs($type); if ($type eq 'JPEG' or $type eq 'EXV') { $rtnVal = $self->WriteJPEG(\%dirInfo); } elsif ($type eq 'TIFF') { # disallow writing of some TIFF-based RAW images: if (grep /^$tiffType$/, @{$noWriteFile{TIFF}}) { $fileType = $tiffType; undef $rtnVal; } else { if ($tiffType eq 'FFF') { # (see https://exiftool.org/forum/index.php?topic=10848.0) $self->Error('Phocus may not properly update previews of edited FFF images', 1); } $dirInfo{Parent} = $tiffType; $rtnVal = $self->ProcessTIFF(\%dirInfo); } } elsif (exists $writableType{$type}) { my ($module, $func); if (ref $writableType{$type} eq 'ARRAY') { $module = $writableType{$type}[0] || $type; $func = $writableType{$type}[1]; } else { $module = $writableType{$type} || $type; } require "Image/ExifTool/$module.pm"; $func = "Image::ExifTool::${module}::" . ($func || "Process$type"); no strict 'refs'; $rtnVal = &$func($self, \%dirInfo); use strict 'refs'; } elsif ($type eq 'ORF' or $type eq 'RAW') { $rtnVal = $self->ProcessTIFF(\%dirInfo); } elsif ($type eq 'EXIF') { # go through WriteDirectory so block writes, etc are handled my $tagTablePtr = GetTagTable('Image::ExifTool::Exif::Main'); my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr, \&WriteTIFF); if (defined $buff) { $rtnVal = Write($outRef, $buff) ? 1 : -1; } else { $rtnVal = 0; } } else { undef $rtnVal; # flag that we don't write this type of file } # all done unless we got the wrong type last if $rtnVal; last unless @fileTypeList; # seek back to original position in files for next try $raf->Seek($inPos, 0) or $seekErr = 1, last; if (UNIVERSAL::isa($outRef,'GLOB')) { seek($outRef, 0, $outPos); } else { $$outRef = substr($$outRef, 0, $outPos); } } # print file format errors unless ($rtnVal) { my $err; if ($seekErr) { $err = 'Error seeking in file'; } elsif ($fileType and defined $rtnVal) { if ($$self{VALUE}{Error}) { # existing error message will do } elsif ($fileType eq 'RAW') { $err = 'Writing this type of RAW file is not supported'; } else { if ($wrongType) { my $type = $fileExt || ($fileType eq 'TIFF' ? $tiffType : $fileType); $err = "Not a valid $type"; # do a quick check to see what this file looks like foreach $type (@fileTypes) { next unless $magicNumber{$type}; next unless $hdr =~ /^$magicNumber{$type}/s; $err .= " (looks more like a $type)"; last; } } else { $err = 'Format error in file'; } } } elsif ($fileType) { # get specific type of file from extension $fileType = GetFileExtension($infile) if $infile and GetFileType($infile); $err = "Writing of $fileType files is not yet supported"; } else { $err = 'Writing of this type of file is not supported'; } $self->Error($err) if $err; $rtnVal = 0; # (in case it was undef) } # $raf->Close(); # only used to force debug output last; # (didn't really want to loop) } # don't return success code if any error occurred if ($rtnVal > 0) { if ($outType and $type and $outType ne $type) { my @types = GetFileType($outType); unless (grep /^$type$/, @types) { $self->Error("Can't create $outType file from $type"); $rtnVal = 0; } } if ($rtnVal > 0 and not Tell($outRef) and not $$self{VALUE}{Error}) { # don't write a file with zero length if (defined $hdr and length $hdr) { $type = '' unless defined $type; $self->Error("Can't delete all meta information from $type file"); } else { $self->Error('Nothing to write'); } } $rtnVal = 0 if $$self{VALUE}{Error}; } # rewrite original file in place if required if (defined $outBuff) { if ($rtnVal <= 0 or not $$self{CHANGED}) { # nothing changed, so no need to write $outBuff } elsif (UNIVERSAL::isa($inRef,'GLOB')) { my $len = length($outBuff); my $size; $rtnVal = -1 unless seek($inRef, 0, 2) and # seek to the end of file ($size = tell $inRef) >= 0 and # get the file size seek($inRef, 0, 0) and # seek back to the start print $inRef $outBuff and # write the new data ($len >= $size or # if necessary: eval { truncate($inRef, $len) }); # shorten output file } else { $$inRef = $outBuff; # replace original data } $outBuff = ''; # free memory but leave $outBuff defined } # close input file if we opened it if ($closeIn) { # errors on input file are significant if we edited the file in place $rtnVal and $rtnVal = -1 unless close($inRef) or not defined $outBuff; if ($rtnVal > 0) { # copy Mac OS resource fork if it exists if ($^O eq 'darwin' and -s "$infile/..namedfork/rsrc") { if ($$self{DEL_GROUP}{RSRC}) { $self->VPrint(0,"Deleting Mac OS resource fork\n"); ++$$self{CHANGED}; } else { $self->VPrint(0,"Copying Mac OS resource fork\n"); my ($buf, $err); local (*SRC, *DST); if ($self->Open(\*SRC, "$infile/..namedfork/rsrc")) { if ($self->Open(\*DST, "$outfile/..namedfork/rsrc", '>')) { binmode SRC; # (not necessary for Darwin, but let's be thorough) binmode DST; while (read SRC, $buf, 65536) { print DST $buf or $err = 'copying', last; } close DST or $err or $err = 'closing'; } else { # (this is normal if the destination filesystem isn't Mac OS) $self->Warn('Error creating Mac OS resource fork'); } close SRC; } else { $err = 'opening'; } $rtnVal = 0 if $err and $self->Error("Error $err Mac OS resource fork", 2); } } # erase input file if renaming while editing information in place $self->Unlink($infile) or $self->Warn('Error erasing original file') if $eraseIn; } } # close output file if we created it if ($closeOut) { # close file and set $rtnVal to -1 if there was an error $rtnVal and $rtnVal = -1 unless close($outRef); # erase the output file if we weren't successful if ($rtnVal <= 0) { $self->Unlink($outfile); # else rename temporary file if necessary } elsif ($tmpfile) { $self->CopyFileAttrs($infile, $tmpfile); # copy attributes to new file unless ($self->Rename($tmpfile, $infile)) { # some filesystems won't overwrite with 'rename', so try erasing original if (not $self->Unlink($infile)) { $self->Unlink($tmpfile); $self->Error('Error renaming temporary file'); $rtnVal = 0; } elsif (not $self->Rename($tmpfile, $infile)) { $self->Error('Error renaming temporary file after deleting original'); $rtnVal = 0; } } # the output file should now have the name of the original infile $outfile = $infile if $rtnVal > 0; } } # set filesystem attributes if requested (and if possible!) if ($rtnVal > 0 and ($closeOut or (defined $outBuff and ($closeIn or UNIVERSAL::isa($infile,'GLOB'))))) { my $target = $closeOut ? $outfile : $infile; # set file permissions if requested ++$$self{CHANGED} if $self->SetSystemTags($target) > 0; if ($closeIn) { # (no use setting file times unless the input file is closed) ++$$self{CHANGED} if $setModDate and $self->SetFileModifyDate($target, $originalTime, undef, 1) > 0; # set FileCreateDate if requested (and if possible!) ++$$self{CHANGED} if $setCreateDate and $self->SetFileModifyDate($target, $createTime, 'FileCreateDate', 1) > 0; # create hard link if requested and no output filename specified (and if possible!) ++$$self{CHANGED} if defined $hardLink and $self->SetFileName($target, $hardLink, 'HardLink'); ++$$self{CHANGED} if defined $symLink and $self->SetFileName($target, $symLink, 'SymLink'); defined $testName and $self->SetFileName($target, $testName, 'Test'); } } # check for write error and set appropriate error message and return value if ($rtnVal < 0) { $self->Error('Error writing output file') unless $$self{VALUE}{Error}; $rtnVal = 0; # return 0 on failure } elsif ($rtnVal > 0) { ++$rtnVal unless $$self{CHANGED}; } # set things back to the way they were $$self{RAF} = $oldRaf; return $rtnVal; } #------------------------------------------------------------------------------ # Get list of all available tags for specified group # Inputs: 0) optional group name (or string of names separated by colons) # Returns: tag list (sorted alphabetically) # Notes: Can't get tags for specific IFD sub GetAllTags(;$) { local $_; my $group = shift; my (%allTags, @groups); @groups = split ':', $group if $group; my $et = Image::ExifTool->new; LoadAllTables(); # first load all our tables my @tableNames = keys %allTables; # loop through all tables and save tag names to %allTags hash while (@tableNames) { my $table = GetTagTable(pop @tableNames); # generate flattened tag names for structure fields if this is an XMP table if ($$table{GROUPS} and $$table{GROUPS}{0} eq 'XMP') { Image::ExifTool::XMP::AddFlattenedTags($table); } my $tagID; foreach $tagID (TagTableKeys($table)) { my @infoArray = GetTagInfoList($table,$tagID); my $tagInfo; GATInfo: foreach $tagInfo (@infoArray) { my $tag = $$tagInfo{Name}; $tag or warn("no name for tag!\n"), next; # don't list subdirectories unless they are writable next if $$tagInfo{SubDirectory} and not $$tagInfo{Writable}; next if $$tagInfo{Hidden}; # ignore hidden tags if (@groups) { my @tg = $et->GetGroup($tagInfo); foreach $group (@groups) { next GATInfo unless grep /^$group$/i, @tg; } } $allTags{$tag} = 1; } } } return sort keys %allTags; } #------------------------------------------------------------------------------ # Get list of all writable tags # Inputs: 0) optional group name (or names separated by colons) # Returns: tag list (sorted alphabetically) sub GetWritableTags(;$) { local $_; my $group = shift; my (%writableTags, @groups); @groups = split ':', $group if $group; my $et = Image::ExifTool->new; LoadAllTables(); my @tableNames = keys %allTables; while (@tableNames) { my $tableName = pop @tableNames; my $table = GetTagTable($tableName); # generate flattened tag names for structure fields if this is an XMP table if ($$table{GROUPS} and $$table{GROUPS}{0} eq 'XMP') { Image::ExifTool::XMP::AddFlattenedTags($table); } # attempt to load Write tables if autoloaded my @parts = split(/::/,$tableName); if (@parts > 3) { my $i = $#parts - 1; $parts[$i] = "Write$parts[$i]"; # add 'Write' before class name my $module = join('::',@parts[0..$i]); eval { require $module }; # (fails silently if nothing loaded) } my $tagID; foreach $tagID (TagTableKeys($table)) { my @infoArray = GetTagInfoList($table,$tagID); my $tagInfo; GWTInfo: foreach $tagInfo (@infoArray) { my $tag = $$tagInfo{Name}; $tag or warn("no name for tag!\n"), next; my $writable = $$tagInfo{Writable}; next unless $writable or ($$table{WRITABLE} and not defined $writable and not $$tagInfo{SubDirectory}); next if $$tagInfo{Hidden}; # ignore hidden tags if (@groups) { my @tg = $et->GetGroup($tagInfo); foreach $group (@groups) { next GWTInfo unless grep /^$group$/i, @tg; } } $writableTags{$tag} = 1; } } } return sort keys %writableTags; } #------------------------------------------------------------------------------ # Get list of all group names # Inputs: 0) [optional] ExifTool ref, 1) Group family number # Returns: List of group names (sorted alphabetically) sub GetAllGroups($;$) { local $_; my $family = shift || 0; my $self; ref $family and $self = $family, $family = shift || 0; $family == 3 and return('Doc#', 'Main'); $family == 4 and return('Copy#'); $family == 5 and return('[too many possibilities to list]'); if ($family == 6) { my $fn = \%Image::ExifTool::Exif::formatNumber; return(sort { $$fn{$a} <=> $$fn{$b} } keys %$fn); } $family == 8 and return('File#'); LoadAllTables(); # first load all our tables my @tableNames = keys %allTables; my %allGroups; # add family 1 groups not in tables $family == 1 and map { $allGroups{$_} = 1 } qw(Garmin); # loop through all tag tables and get all group names while (@tableNames) { my $table = GetTagTable(pop @tableNames); my ($grps, $grp, $tag, $tagInfo); $allGroups{$grp} = 1 if ($grps = $$table{GROUPS}) and ($grp = $$grps{$family}); foreach $tag (TagTableKeys($table)) { my @infoArray = GetTagInfoList($table, $tag); if ($family == 7) { foreach $tagInfo (@infoArray) { my $id = $$tagInfo{TagID}; if (not defined $id) { $id = ''; # (just to be safe) } elsif ($id =~ /^\d+$/) { $id = sprintf('0x%x', $id) if $self and $$self{OPTIONS}{HexTagIDs}; } else { $id =~ s/([^-_A-Za-z0-9])/sprintf('%.2x',ord $1)/ge; } $allGroups{'ID-' . $id} = 1; } } else { foreach $tagInfo (@infoArray) { next unless ($grps = $$tagInfo{Groups}) and ($grp = $$grps{$family}); $allGroups{$grp} = 1; } } } } delete $allGroups{'*'}; # (not a real group) return sort keys %allGroups; } #------------------------------------------------------------------------------ # get priority group list for new values # Inputs: 0) ExifTool object reference # Returns: List of group names sub GetNewGroups($) { my $self = shift; return @{$$self{WRITE_GROUPS}}; } #------------------------------------------------------------------------------ # Get list of all deletable group names # Returns: List of group names (sorted alphabetically) sub GetDeleteGroups() { return sort @delGroups, @delGroup2; } #------------------------------------------------------------------------------ # Add user-defined tags at run time # Inputs: 0) destination table name, 1) tagID/tagInfo pairs for tags to add # Returns: number of tags added # Notes: will replace existing tags sub AddUserDefinedTags($%) { local $_; my ($tableName, %addTags) = @_; my $table = GetTagTable($tableName) or return 0; # add tags to writer lookup Image::ExifTool::TagLookup::AddTags(\%addTags, $tableName); my $tagID; my $num = 0; foreach $tagID (keys %addTags) { next if $specialTags{$tagID}; delete $$table{$tagID}; # delete old entry if it existed AddTagToTable($table, $tagID, $addTags{$tagID}, 1); ++$num; } return $num; } #============================================================================== # Functions below this are not part of the public API #------------------------------------------------------------------------------ # Maintain backward compatibility for old GetNewValues function name sub GetNewValues($$;$) { my ($self, $tag, $nvHashPt) = @_; return $self->GetNewValue($tag, $nvHashPt); } #------------------------------------------------------------------------------ # Un-escape string according to options settings and clear UTF-8 flag # Inputs: 0) ExifTool ref, 1) string ref or string ref ref # Notes: also de-references SCALAR values sub Sanitize($$) { my ($self, $valPt) = @_; # de-reference SCALAR references $$valPt = $$$valPt if ref $$valPt eq 'SCALAR'; # make sure the Perl UTF-8 flag is OFF for the value if perl 5.6 or greater # (otherwise our byte manipulations get corrupted!!) # NOTE: Don't use Encode on Windows becase "require Encode" on Windows hangs if cwd is a long path name!! if ($] >= 5.006 and ($$self{OPTIONS}{EncodeHangs} or eval { require Encode; Encode::is_utf8($$valPt) } or $@)) { # (SIG handling was added in 10.39. Not sure why, but I've added this to other similar code for 13.02) local $SIG{'__WARN__'} = \&SetWarning; # repack by hand if Encode isn't available $$valPt = ($$self{OPTIONS}{EncodeHangs} or $@) ? pack('C*', unpack($] < 5.010000 ? 'U0C*' : 'C0C*', $$valPt)) : Encode::encode('utf8', $$valPt); } # un-escape value if necessary if ($$self{OPTIONS}{Escape}) { # (XMP.pm and HTML.pm were require'd as necessary when option was set) if ($$self{OPTIONS}{Escape} eq 'XML') { $$valPt = Image::ExifTool::XMP::UnescapeXML($$valPt); } elsif ($$self{OPTIONS}{Escape} eq 'HTML') { $$valPt = Image::ExifTool::HTML::UnescapeHTML($$valPt, $$self{OPTIONS}{Charset}); } } } #------------------------------------------------------------------------------ # Apply inverse conversions # Inputs: 0) ExifTool ref, 1) value, 2) tagInfo (or Struct item) ref, # 3) tag name, 4) group 1 name, 5) conversion type (or undef), # 6) [optional] want group ("" for structure field) # Returns: 0) converted value, 1) error string (or undef on success) # Notes: # - uses ExifTool "ConvType" member when conversion type is undef # - conversion types other than 'ValueConv' and 'PrintConv' are treated as 'Raw' sub ConvInv($$$$$;$$) { my ($self, $val, $tagInfo, $tag, $wgrp1, $convType, $wantGroup) = @_; my ($err, $type); $convType or $convType = $$self{ConvType} || 'PrintConv'; Conv: for (;;) { if (not defined $type) { # split value into list if necessary if ($$tagInfo{List}) { my $listSplit = $$tagInfo{AutoSplit} || $$self{OPTIONS}{ListSplit}; if (defined $listSplit and not $$tagInfo{Struct} and ($wantGroup or not defined $wantGroup)) { $listSplit = ',?\s+' if $listSplit eq '1' and $$tagInfo{AutoSplit}; my @splitVal = split /$listSplit/, $val, -1; $val = @splitVal > 1 ? \@splitVal : @splitVal ? $splitVal[0] : ''; } } $type = $convType; } elsif ($type eq 'PrintConv') { $type = 'ValueConv'; } else { # split raw value if necessary if ($$tagInfo{RawJoin} and $$tagInfo{List} and not ref $val) { my @splitVal = split ' ', $val; $val = \@splitVal if @splitVal > 1; } # finally, do our value check my ($err2, $v); if ($$tagInfo{WriteCheck}) { #### eval WriteCheck ($self, $tagInfo, $val) $err2 = eval $$tagInfo{WriteCheck}; $@ and warn($@), $err2 = 'Error evaluating WriteCheck'; } unless (defined $err2) { my $table = $$tagInfo{Table}; if ($table and $$table{CHECK_PROC} and not $$tagInfo{RawConvInv}) { my $checkProc = $$table{CHECK_PROC}; if (ref $val eq 'ARRAY') { # loop through array values foreach $v (@$val) { $err2 = &$checkProc($self, $tagInfo, \$v, $convType); last if $err2; } } else { $err2 = &$checkProc($self, $tagInfo, \$val, $convType); } } } if (defined $err2) { if ($err2) { $err = "$err2 for $wgrp1:$tag"; $self->VPrint(2, "$err\n"); undef $val; # value was invalid } else { $err = $err2; # empty error (quietly don't write tag) } } last; } my $conv = $$tagInfo{$type}; my $convInv = $$tagInfo{"${type}Inv"}; # nothing to do at this level if no conversion defined next unless defined $conv or defined $convInv; my (@valList, $index, $convList, $convInvList); if (ref $val eq 'ARRAY') { # handle ValueConv of ListSplit and AutoSplit values @valList = @$val; $val = $valList[$index = 0]; } elsif (ref $conv eq 'ARRAY' or ref $convInv eq 'ARRAY') { # handle conversion lists @valList = split /$listSep{$type}/, $val; $val = $valList[$index = 0]; if (ref $conv eq 'ARRAY') { $convList = $conv; $conv = $$conv[0]; } if (ref $convInv eq 'ARRAY') { $convInvList = $convInv; $convInv = $$convInv[0]; } } # loop through multiple values if necessary for (;;) { if ($convInv) { # capture eval warnings too local $SIG{'__WARN__'} = \&SetWarning; undef $evalWarning; if (ref($convInv) eq 'CODE') { $val = &$convInv($val, $self); } else { #### eval PrintConvInv/ValueConvInv ($val, $self, $wantGroup) $val = eval $convInv; $@ and $evalWarning = $@; } if ($evalWarning) { # an empty warning ("\n") ignores tag with no error if ($evalWarning eq "\n") { $err = '' unless defined $err; } else { $err = CleanWarning() . " in $wgrp1:$tag (${type}Inv)"; $self->VPrint(2, "$err\n"); } undef $val; last Conv; } elsif (not defined $val) { $err = "Error converting value for $wgrp1:$tag (${type}Inv)"; $self->VPrint(2, "$err\n"); last Conv; } } elsif ($conv) { if (ref $conv eq 'HASH' and (not exists $$tagInfo{"${type}Inv"} or $convInvList)) { my ($multi, $lc); # insert alternate language print conversions if required if ($$self{CUR_LANG} and $type eq 'PrintConv' and ref($lc = $$self{CUR_LANG}{$tag}) eq 'HASH' and ($lc = $$lc{PrintConv})) { my %newConv; foreach (keys %$conv) { my $val = $$conv{$_}; defined $$lc{$val} or $newConv{$_} = $val, next; $newConv{$_} = $self->Decode($$lc{$val}, 'UTF8'); } if ($$conv{BITMASK}) { foreach (keys %{$$conv{BITMASK}}) { my $val = $$conv{BITMASK}{$_}; defined $$lc{$val} or $newConv{BITMASK}{$_} = $val, next; $newConv{BITMASK}{$_} = $self->Decode($$lc{$val}, 'UTF8'); } } $conv = \%newConv; } undef $evalWarning; if ($$conv{BITMASK}) { my $lookupBits = $$conv{BITMASK}; my ($wbits, $tbits) = @$tagInfo{'BitsPerWord','BitsTotal'}; my ($val2, $err2) = EncodeBits($val, $lookupBits, $wbits, $tbits); if ($err2) { # ok, try matching a straight value ($val, $multi) = ReverseLookup($val, $conv); unless (defined $val) { $err = "Can't encode $wgrp1:$tag ($err2)"; $self->VPrint(2, "$err\n"); last Conv; } } elsif (defined $val2) { $val = $val2; } else { delete $$conv{BITMASK}; ($val, $multi) = ReverseLookup($val, $conv); $$conv{BITMASK} = $lookupBits; } } else { ($val, $multi) = ReverseLookup($val, $conv); } if (not defined $val) { my $prob = $evalWarning ? lcfirst CleanWarning() : ($multi ? 'matches more than one ' : 'not in ') . $type; $err = "Can't convert $wgrp1:$tag ($prob)"; $self->VPrint(2, "$err\n"); last Conv; } elsif ($evalWarning) { $self->VPrint(2, CleanWarning() . " for $wgrp1:$tag\n"); } } elsif (not $$tagInfo{WriteAlso}) { $err = "Can't convert value for $wgrp1:$tag (no ${type}Inv)"; $self->VPrint(2, "$err\n"); undef $val; last Conv; } } last unless @valList; $valList[$index] = $val; if (++$index >= @valList) { # leave AutoSplit lists in ARRAY form, or join conversion lists $val = $$tagInfo{List} ? \@valList : join ' ', @valList; last; } $conv = $$convList[$index] if $convList; $convInv = $$convInvList[$index] if $convInvList; $val = $valList[$index]; } } # end ValueConv/PrintConv loop return($val, $err); } #------------------------------------------------------------------------------ # Dereference value and push onto list # Inputs: 0) ExifTool ref, 1) value, 2) list ref, 3) flag to push MissingTagValue for undef value sub PushValue($$$;$) { local $_; my ($self, $val, $list, $missing) = @_; if (ref $val eq 'ARRAY' and ref $$val[0] ne 'HASH') { $self->PushValue($_, $list, $missing) foreach @$val; } elsif (ref $val eq 'SCALAR') { if ($$self{OPTIONS}{Binary} or $$val =~ /^Binary data/) { push @$list, $$val; } else { push @$list, 'Binary data ' . length($$val) . ' bytes'; } } elsif (ref $val eq 'HASH' or ref $val eq 'ARRAY') { require 'Image/ExifTool/XMPStruct.pl'; push @$list, Image::ExifTool::XMP::SerializeStruct($self, $val); } elsif (not defined $val) { my $mval = $$self{OPTIONS}{MissingTagValue}; push @$list, $mval if $missing and defined $mval; } else { push @$list, $val; } } #------------------------------------------------------------------------------ # Convert tag names to values or variables in a string # (eg. '${EXIF:ISO}x $$' --> '100x $' without hash ref, or "$info{'EXIF:ISO'}x $" with) # Inputs: 0) ExifTool object ref, 1) string with embedded tag names, # 2) reference to list of found tags or undef to use FOUND_TAGS, 3) Options: # undef - set missing tags to '' # 'Error' - issue minor error on missing tag (and return undef if error sent) # 'Warn' - issue minor warning on missing tag (and return undef if warning sent) # 'Silent' - just return undef on missing tag (no errors/warnings) # Hash ref - defined to interpolate as variables in string instead of values # --> receives tag/value pairs for interpolation of the variables # 4) document group name if extracting from a specific document # 5) hash ref to cache tag keys for subsequent calls in document loop # Returns: string with embedded tag values (or '$info{TAGNAME}' entries with Hash ref option) # Notes: # - tag names are not case sensitive and may end with '#' for ValueConv value # - uses MissingTagValue option if set # - '$GROUP:all' evaluates to 1 if any tag from GROUP exists, or 0 otherwise # - advanced feature allows Perl expressions inside braces (eg. '${model;tr/ //d}') # - an error/warning in an advanced expression ("${TAG;EXPR}") generates an error # if option set to 'Error', or a warning otherwise sub InsertTagValues($$;$$$$) { local $_; my ($self, $line, $foundTags, $opt, $docGrp, $cache) = @_; my $rtnStr = ''; my ($docNum, $tag); if ($docGrp) { $docNum = $docGrp =~ /(\d+)$/ ? $1 : 0; } else { undef $cache; # no cache if no document groups } $foundTags or $foundTags = $$self{FOUND_TAGS} || []; while ($line =~ s/(.*?)\$(\{\s*)?([-\w]*\w|\$|\/)//s) { my ($pre, $bra, $var) = ($1, $2, $3); my (@tags, $tg, $val, @val, $type, $expr, $didExpr, $level, $asList); # "$$" represents a "$" symbol, and "$/" is a newline if ($var eq '$' or $var eq '/') { $line =~ s/^\s*\}// if $bra; if ($var eq '/') { $var = "\n"; } elsif ($line =~ /^self\b/ and not $rtnStr =~ /\$$/) { $var = '$$'; # ("$$self{var}" in string) } $rtnStr .= "$pre$var"; next; } # allow multiple group names while ($line =~ /^:([-\w]*\w)(.*)/s) { my $group = $var; ($var, $line) = ($1, $2); $var = "$group:$var"; } # allow trailing '#' to indicate ValueConv value $type = 'ValueConv' if $line =~ s/^#//; # special advanced formatting '@' feature to evaluate list values separately if ($bra and $line =~ s/^\@(#)?//) { $asList = 1; $type = 'ValueConv' if $1; } # remove trailing bracket if there was a leading one # and extract Perl expression from inside brackets if it exists if ($bra and $line !~ s/^\s*\}// and $line =~ s/^\s*;\s*(.*?)\s*\}//s) { my $part = $1; $expr = ''; for ($level=0; ; --$level) { # increase nesting level for each opening brace ++$level while $part =~ /\{/g; $expr .= $part; last unless $level and $line =~ s/^(.*?)\s*\}//s; # get next part $part = $1; $expr .= '}'; # this brace was part of the expression } # use default Windows filename filter if expression is empty $expr = 'tr(/\\\\?*:|"<>\\0)()d' unless length $expr; } push @tags, $var; ExpandShortcuts(\@tags); @tags or $rtnStr .= $pre, next; # save advanced formatting expression to allow access by user-defined ValueConv $$self{FMT_EXPR} = $expr; for (;;) { # temporarily reset ListJoin option if evaluating list values separately my $oldListJoin; $oldListJoin = $self->Options(ListJoin => undef) if $asList; $tag = shift @tags; my $lcTag = lc $tag; if ($cache and $lcTag !~ /(^|:)all$/) { # remove group from tag name (but not lower-case version) my $group; $tag =~ s/^(.*):// and $group = $1; # cache tag keys to speed processing for a large number of sub-documents # (similar to code in BuildCompositeTags(), but this is case-insensitive) my $cacheTag = $$cache{$lcTag}; unless ($cacheTag) { $cacheTag = $$cache{$lcTag} = [ ]; # find all matching keys, organize into groups, and store in cache my $ex = $$self{TAG_EXTRA}; my @matches = grep /^$tag(\s|$)/i, @$foundTags; @matches = $self->GroupMatches($group, \@matches) if defined $group; foreach (@matches) { my $doc = $$ex{$_}{G3} || 0; if (defined $$cacheTag[$doc]) { next unless $$cacheTag[$doc] =~ / \((\d+)\)$/; my $cur = $1; # keep the most recently extracted tag next if / \((\d+)\)$/ and $1 < $cur; } $$cacheTag[$doc] = $_; } } my $doc = $lcTag =~ /\b(main|doc(\d+)):/ ? ($2 || 0) : $docNum; if ($$cacheTag[$doc]) { $tag = $$cacheTag[$doc]; $val = $self->GetValue($tag, $type); } } else { # add document number to tag if specified and it doesn't already exist if ($docGrp and $lcTag !~ /\b(main|doc\d+):/) { $tag = $docGrp . ':' . $tag; $lcTag = lc $tag; } my ($et, $fileTags) = ($self, $foundTags); if ($tag =~ s/(\bfile\d+)://i) { $et = $$self{ALT_EXIFTOOL}{ucfirst lc $1}; if ($et) { $fileTags = $$et{FoundTags}; } else { $et = $self; $tag = 'no_alt_file'; } } if ($lcTag eq 'all') { $val = 1; # always some tag available } elsif (defined $$et{OPTIONS}{UserParam}{$lcTag}) { $val = $$et{OPTIONS}{UserParam}{$lcTag}; } elsif ($tag =~ /(.*):(.+)/) { my ($group, @matches); ($group, $tag) = ($1, $2); # join values of all matching tags if "All" group is used # (and remove "All" from group prefix) if ($group =~ s/(^|:)(all|\*)(:|$)/$1 and $3/ei) { if (lc $tag eq 'all') { @matches = $group ? $et->GroupMatches($group, $fileTags) : @$fileTags; } else { @matches = grep /^$tag(\s|$)/i, @$fileTags; @matches = $et->GroupMatches($group, \@matches) if $group; } $self->PushValue(scalar $et->GetValue($_, $type), \@val) foreach @matches; } elsif (lc $tag eq 'all') { # return "1" if any tag from the specified group exists $val = $et->GroupMatches($group, $fileTags) ? 1 : 0; } else { # find the specified tag @matches = grep /^$tag(\s|$)/i, @$fileTags; @matches = $et->GroupMatches($group, \@matches); foreach $tg (@matches) { if (defined $val and $tg =~ / \((\d+)\)$/) { # take the most recently extracted tag my $tagNum = $1; next if $tag !~ / \((\d+)\)$/ or $1 > $tagNum; } $val = $et->GetValue($tg, $type); $tag = $tg; last unless $tag =~ / /; # all done if we got our best match } } } elsif ($tag eq 'self') { $val = $et; # ("$self{var}" or "$file1:self{var}" in string) } else { # get the tag value $val = $et->GetValue($tag, $type); unless (defined $val) { # check for tag name with different case ($tg) = grep /^$tag$/i, @$fileTags; if (defined $tg) { $val = $et->GetValue($tg, $type); $tag = $tg; } } } } $self->Options(ListJoin => $oldListJoin) if $asList; $self->PushValue($val, \@val, $asList); undef $val; last unless @tags; } if (@val) { $self->PushValue($val, \@val) if defined $val; $val = join $$self{OPTIONS}{ListSep}, @val; } elsif (defined $val) { $self->PushValue($val, \@val); # (so the eval has access to @val if required) } # evaluate advanced formatting expression if given (eg. "${TAG;EXPR}") if (defined $expr and defined $val) { local $SIG{'__WARN__'} = \&SetWarning; undef $evalWarning; $advFmtSelf = $self; # set variable for access to $self in helper functions if ($asList) { foreach (@val) { #### eval advanced formatting expression ($_, $self, @val, $tag, $advFmtSelf) eval $expr; $@ and $evalWarning = $@; } # join back together if any values are still defined @val = grep defined, @val; $val = @val ? join $$self{OPTIONS}{ListSep}, @val : undef; } else { $_ = $val; #### eval advanced formatting expression ($_, $self, @val, $tag, $advFmtSelf) eval $expr; $@ and $evalWarning = $@; $val = ref $_ eq 'ARRAY' ? join($$self{OPTIONS}{ListSep}, @$_): $_; } if ($evalWarning) { my $g3 = ($docGrp and $var !~ /\b(main|doc\d+):/i) ? $docGrp . ':' : ''; my $str = CleanWarning() . " for '$g3${var}'"; if ($opt) { if ($opt eq 'Error') { $self->Error($str); } elsif ($opt ne 'Silent') { $self->Warn($str); } } } undef $advFmtSelf; $didExpr = 1; # set flag indicating an expression was evaluated } unless (defined $val) { $val = $$self{OPTIONS}{MissingTagValue}; unless (defined $val) { my $g3 = ($docGrp and $var !~ /\b(main|doc\d+):/i) ? $docGrp . ':' : ''; my $msg = $didExpr ? "Advanced formatting expression returned undef for '$g3${var}'" : "Tag '$g3${var}' not defined"; if (ref $opt) { $self->Warn($msg,2) or $val = ''; } elsif ($opt) { no strict 'refs'; ($opt eq 'Silent' or &$opt($self, $msg, 2)) and return $$self{FMT_EXPR} = undef; $val = ''; } } } if (ref $opt eq 'HASH') { $var .= '#' if $type; if (defined $expr) { # generate unique variable name for this modified tag value my $i = 1; ++$i while exists $$opt{"$var.expr$i"}; $var .= '.expr' . $i; } $rtnStr .= "$pre\$info{'${var}'}"; $$opt{$var} = $val; } else { $rtnStr .= "$pre$val"; } } $$self{FMT_EXPR} = undef; return $rtnStr . $line; } #------------------------------------------------------------------------------ # Reformat date/time value in $_ based on specified format string # Inputs: 0) date/time format string # Returns: Reformatted date/time string sub DateFmt($) { my $et = bless { OPTIONS => { DateFormat => shift, StrictDate => 1 } }; my $shift; if ($advFmtSelf and defined($shift = $$advFmtSelf{OPTIONS}{GlobalTimeShift})) { $$et{OPTIONS}{GlobalTimeShift} = $shift; $$et{GLOBAL_TIME_OFFSET} = $$advFmtSelf{GLOBAL_TIME_OFFSET}; } $_ = $et->ConvertDateTime($_); defined $_ or warn "Error converting date/time\n"; $$advFmtSelf{GLOBAL_TIME_OFFSET} = $$et{GLOBAL_TIME_OFFSET} if $shift; return $_; } #------------------------------------------------------------------------------ # Utility routine to remove duplicate items from default input string # Inputs: 0) true to set $_ to undef if not changed # Notes: - for use only in advanced formatting expressions sub NoDups { my %seen; my $sep = $advFmtSelf ? $$advFmtSelf{OPTIONS}{ListSep} : ', '; my $new = join $sep, grep { !$seen{$_}++ } split /\Q$sep\E/, $_; $_ = ($_[0] and $new eq $_) ? undef : $new; } #------------------------------------------------------------------------------ # Utility routine to set in $_ image from current object # Inputs: 0-N) list of tags to copy # Returns: Return value from WriteInfo # Notes: - for use only in advanced formatting expressions sub SetTags(@) { my $self = $advFmtSelf; my $et = Image::ExifTool->new; $et->SetNewValuesFromFile($self, @_); return $et->WriteInfo(\$_); } #------------------------------------------------------------------------------ # Is specified tag writable # Inputs: 0) tag name, case insensitive (optional group name currently ignored) # Returns: 0=exists but not writable, 1=writable, undef=doesn't exist sub IsWritable($) { my $tag = shift; $tag =~ s/^(.*)://; # ignore group name my @tagInfo = FindTagInfo($tag); unless (@tagInfo) { return 0 if TagExists($tag); return undef; } my $tagInfo; foreach $tagInfo (@tagInfo) { return $$tagInfo{Writable} ? 1 : 0 if defined $$tagInfo{Writable}; return 1 if $$tagInfo{Table}{WRITABLE}; # must call WRITE_PROC to autoload writer because this may set the writable tag my $writeProc = $$tagInfo{Table}{WRITE_PROC}; if ($writeProc) { no strict 'refs'; &$writeProc(); # dummy call to autoload writer return 1 if $$tagInfo{Writable}; } } return 0; } #------------------------------------------------------------------------------ # Check to see if these are the same file # Inputs: 0) ExifTool ref, 1) first file name, 2) second file name # Returns: true if file names reference the same file sub IsSameFile($$$) { my ($self, $file, $file2) = @_; return 0 unless lc $file eq lc $file2; # (only looking for differences in case) my ($isSame, $interrupted); my $tmp1 = "${file}_ExifTool_tmp_$$"; my $tmp2 = "${file2}_ExifTool_tmp_$$"; { local *TMP1; local $SIG{INT} = sub { $interrupted = 1 }; if ($self->Open(\*TMP1, $tmp1, '>')) { close TMP1; $isSame = 1 if $self->Exists($tmp2); $self->Unlink($tmp1); } } if ($interrupted and $SIG{INT}) { no strict 'refs'; &{$SIG{INT}}(); } return $isSame; } #------------------------------------------------------------------------------ # Is this a raw file type? # Inputs: 0) ExifTool ref # Returns: true if FileType is a type of RAW image sub IsRawType($) { my $self = shift; return $rawType{$$self{FileType}}; } #------------------------------------------------------------------------------ # Copy file attributes from one file to another # Inputs: 0) ExifTool ref, 1) source file name, 2) destination file name # Notes: eventually add support for extended attributes? sub CopyFileAttrs($$$) { my ($self, $src, $dst) = @_; my ($mode, $uid, $gid) = (stat($src))[2, 4, 5]; # copy file attributes unless we already set them if (defined $mode and not defined $self->GetNewValue('FilePermissions')) { eval { chmod($mode & 07777, $dst) }; } my $newUid = $self->GetNewValue('FileUserID'); my $newGid = $self->GetNewValue('FileGroupID'); if (defined $uid and defined $gid and (not defined $newUid or not defined $newGid)) { defined $newGid and $gid = $newGid; defined $newUid and $uid = $newUid; eval { chown($uid, $gid, $dst) }; } } #------------------------------------------------------------------------------ # Get new file path name # Inputs: 0) existing name (may contain directory), # 1) new file name, new directory, or new path (dir+name) # Returns: new file path name sub GetNewFileName($$) { my ($oldName, $newName) = @_; my ($dir, $name) = ($oldName =~ m{(.*/)(.*)}); ($dir, $name) = ('', $oldName) unless defined $dir; if ($newName =~ m{/$}) { $newName = "$newName$name"; # change dir only } elsif ($newName !~ m{/}) { $newName = "$dir$newName"; # change name only if newname doesn't specify dir } # else change dir and name return $newName; } #------------------------------------------------------------------------------ # Get next available tag key # Inputs: 0) hash reference (keys are tag keys), 1) tag name # Returns: next available tag key sub NextFreeTagKey($$) { my ($info, $tag) = @_; return $tag unless exists $$info{$tag}; my $i; for ($i=1; ; ++$i) { my $key = "$tag ($i)"; return $key unless exists $$info{$key}; } } #------------------------------------------------------------------------------ # Reverse hash lookup # Inputs: 0) value, 1) hash reference # Returns: Hash key or undef if not found (plus flag for multiple matches in list context) sub ReverseLookup($$) { my ($val, $conv) = @_; return undef unless defined $val; my $multi; if ($val =~ /^Unknown\s*\((.*)\)$/i) { $val = $1; # was unknown if ($val =~ /^0x([\da-fA-F]+)$/) { # disable "Hexadecimal number > 0xffffffff non-portable" warning local $SIG{'__WARN__'} = sub { }; $val = hex($val); # convert hex value } } else { my $qval = $val; $qval =~ s/\s+$//; # remove trailing whitespace $qval = quotemeta $qval; my @patterns = ( "^$qval\$", # exact match "^(?i)$qval\$", # case-insensitive "^(?i)$qval", # beginning of string "(?i)$qval", # substring ); # hash entries to ignore in reverse lookup my ($pattern, $found, $matches); PAT: foreach $pattern (@patterns) { $matches = scalar grep /$pattern/, values(%$conv); next unless $matches; # multiple matches are bad unless they were exact if ($matches > 1 and $pattern !~ /\$$/) { # don't match entries that we should ignore foreach (keys %ignorePrintConv) { --$matches if defined $$conv{$_} and $$conv{$_} =~ /$pattern/; } last if $matches > 1; } foreach (sort keys %$conv) { next if $$conv{$_} !~ /$pattern/ or $ignorePrintConv{$_}; $val = $_; $found = 1; last PAT; } } unless ($found) { # call OTHER conversion routine if available if ($$conv{OTHER}) { local $SIG{'__WARN__'} = \&SetWarning; undef $evalWarning; $val = &{$$conv{OTHER}}($val,1,$conv); } else { $val = undef; } $multi = 1 if $matches > 1; } } return ($val, $multi) if wantarray; return $val; } #------------------------------------------------------------------------------ # Return true if we are deleting or overwriting the specified tag # Inputs: 0) ExifTool object ref, 1) new value hash reference # 2) optional tag value (before RawConv) if deleting specific values # Returns: >0 - tag should be overwritten # =0 - the tag should be preserved # <0 - not sure, we need the old value to tell (if there is no old value # then the tag should be written if $$nvHash{IsCreating} is true) # Notes: $$nvHash{Value} is updated with the new value when shifting a value sub IsOverwriting($$;$) { my ($self, $nvHash, $val) = @_; return 0 unless $nvHash; # overwrite regardless if no DelValues specified return 1 unless $$nvHash{DelValue}; # never overwrite if DelValue list exists but is empty my $shift = $$nvHash{Shift}; return 0 unless @{$$nvHash{DelValue}} or defined $shift; # return "don't know" if we don't have a value to test return -1 unless defined $val; # apply raw conversion if necessary my $tagInfo = $$nvHash{TagInfo}; my $conv = $$tagInfo{RawConv}; if ($conv) { local $SIG{'__WARN__'} = \&SetWarning; undef $evalWarning; if (ref $conv eq 'CODE') { $val = &$conv($val, $self); } else { my ($priority, @grps); my $tag = $$tagInfo{Name}; #### eval RawConv ($self, $val, $tag, $tagInfo, $priority, @grps) $val = eval $conv; $@ and $evalWarning = $@; } return -1 unless defined $val; } # do not overwrite if only creating return 0 if $$nvHash{CreateOnly}; # apply time/number shift if necessary if (defined $shift) { my $shiftType = $$tagInfo{Shift}; unless ($shiftType and $shiftType eq 'Time') { unless (IsFloat($val)) { # do the ValueConv to try to get a number my $conv = $$tagInfo{ValueConv}; if (defined $conv) { local $SIG{'__WARN__'} = \&SetWarning; undef $evalWarning; if (ref $conv eq 'CODE') { $val = &$conv($val, $self); } elsif (not ref $conv) { #### eval ValueConv ($val, $self) $val = eval $conv; $@ and $evalWarning = $@; } if ($evalWarning) { $self->Warn("ValueConv $$tagInfo{Name}: " . CleanWarning()); return 0; } } unless (defined $val and IsFloat($val)) { $self->Warn("Can't shift $$tagInfo{Name} (not a number)"); return 0; } } $shiftType = 'Number'; # allow any number to be shifted } require 'Image/ExifTool/Shift.pl'; my $err = $self->ApplyShift($shiftType, $shift, $val, $nvHash); if ($err) { $self->Warn("$err when shifting $$tagInfo{Name}"); return 0; } # ensure that the shifted value is valid and reformat if necessary my $checkVal = $self->GetNewValue($nvHash); return 0 unless defined $checkVal; # don't bother overwriting if value is the same return 0 if $val eq $$nvHash{Value}[0]; return 1; } # return 1 if value matches a DelValue my $delVal; foreach $delVal (@{$$nvHash{DelValue}}) { return 1 if $val eq $delVal; } return 0; } #------------------------------------------------------------------------------ # Get write group for specified tag # Inputs: 0) new value hash reference # Returns: Write group name sub GetWriteGroup($) { return $_[0]{WriteGroup}; } #------------------------------------------------------------------------------ # Get name of write group or family 1 group # Inputs: 0) ExifTool ref, 1) tagInfo ref, 2) write group name # Returns: Name of group for verbose message sub GetWriteGroup1($$) { my ($self, $tagInfo, $writeGroup) = @_; return $writeGroup unless $writeGroup =~ /^(MakerNotes|XMP|Composite|QuickTime)$/; return $self->GetGroup($tagInfo, 1); } #------------------------------------------------------------------------------ # Get list of tags to write for Geolocate feature # Inputs: 0) ExifTool ref, 1) group name(s), # 2) 0=prefer writing City, 1=prefer writing GPS, undef=deleting tags # Returns: list of tags to write/delete sub GetGeolocateTags($$;$) { my ($self, $wantGroup, $writeGPS) = @_; my @grps = $wantGroup ? map lc, split(/:/, $wantGroup) : (); my %grps = map { $_ => $_ } @grps; # lookup for specified groups $grps{exif} and not $grps{gps} and $grps{gps} = 'gps', push(@grps, 'gps'); my %tagGroups = ( 'xmp-iptcext' => [ qw(LocationShownCity LocationShownProvinceState LocationShownCountryCode LocationShownCountryName LocationShownGPSLatitude LocationShownGPSLongitude) ], 'xmp-photoshop' => [ qw(City State Country) ], 'xmp-iptccore' => [ 'CountryCode' ], 'iptc' => [ qw(City Province-State Country-PrimaryLocationCode Country-PrimaryLocationName) ], 'gps' => [ qw(GPSLatitude GPSLongitude GPSLatitudeRef GPSLongitudeRef) ], 'xmp-exif' => [ qw(GPSLatitude GPSLongitude) ], 'itemlist' => [ 'GPSCoordinates' ], 'userdata' => [ 'GPSCoordinates' ], # more general groups not in this lookup: XMP and QuickTime ); my (@tags, $grp); # set specific City and GPS tags foreach $grp (@grps) { $tagGroups{$grp} and push @tags, map("$grp:$_", @{$tagGroups{$grp}}); } # set default XMP City tags if necessary if (not $writeGPS) { push @tags, 'Keys:LocationName' if $grps{'keys'}; if ($grps{xmp} or (not @tags and not $grps{quicktime})) { push @tags, qw(XMP:City XMP:State XMP:CountryCode XMP:Country Keys:LocationName); } } $writeGPS = 1 unless defined $writeGPS; # (delete both City and GPS) push @tags, 'Keys:GPSCoordinates' if $writeGPS and $grps{'keys'}; # set default QuickTime tag if necessary my $didQT = grep /GPSCoordinates$/, @tags; if (($grps{quicktime} and not $didQT) or ($writeGPS and not @tags and not $grps{xmp})) { push @tags, 'QuickTime:GPSCoordinates'; } # set default GPS tags if necessary if ($writeGPS) { push @tags, qw(XMP:GPSLatitude XMP:GPSLongitude) if $grps{xmp} and not $grps{'xmp-exif'}; push @tags, qw(GPSLatitude GPSLongitude GPSLatitudeRef GPSLongitudeRef) if not $wantGroup; } return @tags; } #------------------------------------------------------------------------------ # Get new value hash for specified tagInfo/writeGroup # Inputs: 0) ExifTool object reference, 1) reference to tag info hash # 2) Write group name, 3) Options: 'delete' or 'create' new value hash # 4) optional ProtectSaved value, 5) true if we are deleting a value # Returns: new value hash reference for specified write group # (or first new value hash in linked list if write group not specified) # Notes: May return undef when 'create' is used with ProtectSaved sub GetNewValueHash($$;$$$$) { my ($self, $tagInfo, $writeGroup, $opts) = @_; return undef unless $tagInfo; my $nvHash = $$self{NEW_VALUE}{$tagInfo}; my %opts; # quick lookup for options $opts and $opts{$opts} = 1; $writeGroup = '' unless defined $writeGroup; if ($writeGroup) { # find the new value in the list with the specified write group while ($nvHash and $$nvHash{WriteGroup} ne $writeGroup) { # QuickTime and All are special cases because all group1 tags may be updated at once last if $$nvHash{WriteGroup} =~ /^(QuickTime|All)$/; # replace existing entry if WriteGroup is 'All' (avoids confusion of forum10349) last if $$tagInfo{WriteGroup} and $$tagInfo{WriteGroup} eq 'All'; $nvHash = $$nvHash{Next}; } } # remove this entry if deleting, or if creating a new entry and # this entry is marked with "Save" flag if (defined $nvHash and ($opts{'delete'} or ($opts{'create'} and $$nvHash{Save}))) { my $protect = (defined $_[4] and defined $$nvHash{Save} and $$nvHash{Save} > $_[4]); # this is a bit tricky: we want to add to a protected nvHash only if we # are adding a conditional delete ($_[5] true or DelValue with no Shift) # or accumulating List items (NoReplace true) # (NOTE: this should be looked into --> lists may be accumulated instead of being replaced # as expected when copying to the same list from different dynamic -tagsFromFile source files) if ($protect and not ($opts{create} and ($$nvHash{NoReplace} or $_[5] or ($$nvHash{DelValue} and not defined $$nvHash{Shift})))) { return undef; # honour ProtectSaved value by not writing this tag } elsif ($opts{'delete'}) { $self->RemoveNewValueHash($nvHash, $tagInfo); undef $nvHash; } else { # save a copy of this new value hash my %copy = %$nvHash; # make copy of Value and DelValue lists my $key; foreach $key (keys %copy) { next unless ref $copy{$key} eq 'ARRAY'; $copy{$key} = [ @{$copy{$key}} ]; } my $saveHash = $$self{SAVE_NEW_VALUE}; # add to linked list of saved new value hashes $copy{Next} = $$saveHash{$tagInfo}; $$saveHash{$tagInfo} = \%copy; delete $$nvHash{Save}; # don't save it again $$nvHash{AddBefore} = scalar @{$$nvHash{Value}} if $protect and $$nvHash{Value}; } } if (not defined $nvHash and $opts{'create'}) { # create a new entry $nvHash = { TagInfo => $tagInfo, WriteGroup => $writeGroup, IsNVH => 1, # set flag so we can recognize a new value hash Order => $$self{NV_COUNT}++, }; # add entry to our NEW_VALUE hash if ($$self{NEW_VALUE}{$tagInfo}) { # add to end of linked list my $lastHash = LastInList($$self{NEW_VALUE}{$tagInfo}); $$lastHash{Next} = $nvHash; } else { $$self{NEW_VALUE}{$tagInfo} = $nvHash; } } return $nvHash; } #------------------------------------------------------------------------------ # Load all tag tables sub LoadAllTables() { return if $loadedAllTables; # load all of our non-referenced tables (first our modules) my $table; foreach $table (@loadAllTables) { my $tableName = "Image::ExifTool::$table"; $tableName .= '::Main' unless $table =~ /:/; GetTagTable($tableName); } # (then our special tables) GetTagTable('Image::ExifTool::Extra'); GetTagTable('Image::ExifTool::Composite'); # recursively load all tables referenced by the current tables my @tableNames = keys %allTables; my %pushedTables; while (@tableNames) { $table = GetTagTable(shift @tableNames); # call write proc if it exists in case it adds tags to the table my $writeProc = $$table{WRITE_PROC}; if ($writeProc) { no strict 'refs'; &$writeProc(); } # recursively scan through tables in subdirectories foreach (TagTableKeys($table)) { my @infoArray = GetTagInfoList($table,$_); my $tagInfo; foreach $tagInfo (@infoArray) { my $subdir = $$tagInfo{SubDirectory} or next; my $tableName = $$subdir{TagTable} or next; # next if table already loaded or queued for loading next if $allTables{$tableName} or $pushedTables{$tableName}; push @tableNames, $tableName; # must scan this one too $pushedTables{$tableName} = 1; } } } $loadedAllTables = 1; } #------------------------------------------------------------------------------ # Remove new value hash from linked list (and save if necessary) # Inputs: 0) ExifTool object reference, 1) new value hash ref, 2) tagInfo ref sub RemoveNewValueHash($$$) { my ($self, $nvHash, $tagInfo) = @_; my $firstHash = $$self{NEW_VALUE}{$tagInfo}; if ($nvHash eq $firstHash) { # remove first entry from linked list if ($$nvHash{Next}) { $$self{NEW_VALUE}{$tagInfo} = $$nvHash{Next}; } else { delete $$self{NEW_VALUE}{$tagInfo}; } } else { # find the list element pointing to this hash $firstHash = $$firstHash{Next} while $$firstHash{Next} ne $nvHash; # remove from linked list $$firstHash{Next} = $$nvHash{Next}; } # save the existing entry if necessary if ($$nvHash{Save}) { my $saveHash = $$self{SAVE_NEW_VALUE}; # add to linked list of saved new value hashes $$nvHash{Next} = $$saveHash{$tagInfo}; $$saveHash{$tagInfo} = $nvHash; } } #------------------------------------------------------------------------------ # Remove all new value entries for specified group # Inputs: 0) ExifTool object reference, 1) group name sub RemoveNewValuesForGroup($$) { my ($self, $group) = @_; return unless $$self{NEW_VALUE}; # make list of all groups we must remove my @groups = ( $group ); push @groups, @{$removeGroups{$group}} if $removeGroups{$group}; my ($out, @keys, $hashKey); $out = $$self{OPTIONS}{TextOut} if $$self{OPTIONS}{Verbose} > 1; # loop though all new values, and remove any in this group @keys = keys %{$$self{NEW_VALUE}}; foreach $hashKey (@keys) { my $nvHash = $$self{NEW_VALUE}{$hashKey}; # loop through each entry in linked list for (;;) { my $nextHash = $$nvHash{Next}; my $tagInfo = $$nvHash{TagInfo}; my ($grp0,$grp1) = $self->GetGroup($tagInfo); my $wgrp = $$nvHash{WriteGroup}; # use group1 if write group is not specific $wgrp = $grp1 if $wgrp eq $grp0; if ($grp0 eq '*' or $wgrp eq '*' or grep /^($grp0|$wgrp)$/i, @groups) { $out and print $out "Removed new value for $wgrp:$$tagInfo{Name}\n"; # remove from linked list $self->RemoveNewValueHash($nvHash, $tagInfo); } $nvHash = $nextHash or last; } } } #------------------------------------------------------------------------------ # Get list of tagInfo hashes for all new data # Inputs: 0) ExifTool object reference, 1) optional tag table pointer # Returns: list of tagInfo hashes in no particular order sub GetNewTagInfoList($;$) { my ($self, $tagTablePtr) = @_; my @tagInfoList; my $nv = $$self{NEW_VALUE}; if ($nv) { my $hashKey; foreach $hashKey (keys %$nv) { my $tagInfo = $$nv{$hashKey}{TagInfo}; next if $tagTablePtr and $tagTablePtr ne $$tagInfo{Table}; push @tagInfoList, $tagInfo; } } return @tagInfoList; } #------------------------------------------------------------------------------ # Get hash of tagInfo references keyed on tagID for a specific table # Inputs: 0) ExifTool object reference, 1-N) tag table pointers # Returns: hash reference # Notes: returns only one tagInfo ref for each conditional list sub GetNewTagInfoHash($@) { my $self = shift; my (%tagInfoHash, $hashKey); my $nv = $$self{NEW_VALUE}; while ($nv) { my $tagTablePtr = shift || last; foreach $hashKey (keys %$nv) { my $tagInfo = $$nv{$hashKey}{TagInfo}; next if $tagTablePtr and $tagTablePtr ne $$tagInfo{Table}; $tagInfoHash{$$tagInfo{TagID}} = $tagInfo; } } return \%tagInfoHash; } #------------------------------------------------------------------------------ # Get a tagInfo/tagID hash for subdirectories we need to add # Inputs: 0) ExifTool object reference, 1) parent tag table reference # 2) parent directory name (taken from GROUP0 of tag table if not defined) # Returns: Reference to Hash of subdirectory tagInfo references keyed by tagID # (plus Reference to edit directory hash in list context) sub GetAddDirHash($$;$) { my ($self, $tagTablePtr, $parent) = @_; $parent or $parent = $$tagTablePtr{GROUPS}{0}; my $tagID; my %addDirHash; my %editDirHash; my $addDirs = $$self{ADD_DIRS}; my $editDirs = $$self{EDIT_DIRS}; foreach $tagID (TagTableKeys($tagTablePtr)) { my @infoArray = GetTagInfoList($tagTablePtr,$tagID); my $tagInfo; foreach $tagInfo (@infoArray) { next unless $$tagInfo{SubDirectory}; # get name for this sub directory # (take directory name from SubDirectory DirName if it exists, # otherwise Group0 name of SubDirectory TagTable or tag Group1 name) my $dirName = $$tagInfo{SubDirectory}{DirName}; unless ($dirName) { # use tag name for directory name and save for next time $dirName = $$tagInfo{Name}; $$tagInfo{SubDirectory}{DirName} = $dirName; } # save this directory information if we are writing it if ($$editDirs{$dirName} and $$editDirs{$dirName} eq $parent) { $editDirHash{$tagID} = $tagInfo; $addDirHash{$tagID} = $tagInfo if $$addDirs{$dirName}; } } } return (\%addDirHash, \%editDirHash) if wantarray; return \%addDirHash; } #------------------------------------------------------------------------------ # Get localized version of tagInfo hash (used by MIE, XMP, PNG and QuickTime) # Inputs: 0) tagInfo hash ref, 1) locale code (eg. "en_CA" for MIE) # Returns: new tagInfo hash ref, or undef if invalid # - sets LangCode member in new tagInfo sub GetLangInfo($$) { my ($tagInfo, $langCode) = @_; # make a new tagInfo hash for this locale my $table = $$tagInfo{Table}; my $tagID = $$tagInfo{TagID} . '-' . $langCode; my $langInfo = $$table{$tagID}; unless ($langInfo) { # make a new tagInfo entry for this locale $langInfo = { %$tagInfo, Name => $$tagInfo{Name} . '-' . $langCode, Description => Image::ExifTool::MakeDescription($$tagInfo{Name}) . " ($langCode)", LangCode => $langCode, SrcTagInfo => $tagInfo, # save reference to original tagInfo }; AddTagToTable($table, $tagID, $langInfo); } return $langInfo; } #------------------------------------------------------------------------------ # initialize ADD_DIRS and EDIT_DIRS hashes for all directories that need # to be created or will have tags changed in them # Inputs: 0) ExifTool object reference, 1) file type string (or map hash ref) # 2) preferred family 0 group for creating tags, 3) alternate preferred group # Notes: # - the ADD_DIRS and EDIT_DIRS keys are the directory names, and the values # are the names of the parent directories (undefined for a top-level directory) # - also initializes FORCE_WRITE lookup sub InitWriteDirs($$;$$) { my ($self, $fileType, $preferredGroup, $altGroup) = @_; my $editDirs = $$self{EDIT_DIRS} = { }; my $addDirs = $$self{ADD_DIRS} = { }; my $fileDirs = $dirMap{$fileType}; unless ($fileDirs) { return unless ref $fileType eq 'HASH'; $fileDirs = $fileType; } my @tagInfoList = $self->GetNewTagInfoList(); my ($tagInfo, $nvHash); # save the preferred group $$self{PreferredGroup} = $preferredGroup; foreach $tagInfo (@tagInfoList) { # cycle through all hashes in linked list for ($nvHash=$self->GetNewValueHash($tagInfo); $nvHash; $nvHash=$$nvHash{Next}) { # are we creating this tag? (otherwise just deleting or editing it) my $isCreating = $$nvHash{IsCreating}; if ($preferredGroup) { my $g0 = $self->GetGroup($tagInfo, 0); if ($isCreating) { # if another group is taking priority, only create # directory if specifically adding tags to this group # or if this tag isn't being added to the priority group $isCreating = 0 if $preferredGroup ne $g0 and $$nvHash{CreateGroups}{$preferredGroup} and (not $altGroup or $altGroup ne $g0); } else { # create this directory if any tag is preferred and has a value # (unless group creation is disabled via the WriteMode option) $isCreating = 1 if $$nvHash{Value} and $preferredGroup eq $g0 and not $$nvHash{EditOnly} and $$self{OPTIONS}{WriteMode} =~ /g/; } } # tag belongs to directory specified by WriteGroup, or by # the Group0 name if WriteGroup not defined my $dirName = $$nvHash{WriteGroup}; # remove MIE copy number(s) if they exist if ($dirName =~ /^MIE\d*(-[a-z]+)?\d*$/i) { $dirName = 'MIE' . ($1 || ''); } my @dirNames; # allow a group name of '*' to force writing EXIF/IPTC/XMP/PNG (ForceWrite tag) if ($dirName eq '*' and $$nvHash{Value}) { my $val = $$nvHash{Value}[0]; if ($val) { foreach (qw(EXIF IPTC XMP PNG FixBase)) { next unless $val =~ /\b($_|All)\b/i; push @dirNames, $_; push @dirNames, 'EXIF' if $_ eq 'FixBase'; $$self{FORCE_WRITE}{$_} = 1; } } $dirName = shift @dirNames; } elsif ($dirName eq 'QuickTime') { # write to specific QuickTime group $dirName = $self->GetGroup($tagInfo, 1); } while ($dirName) { my $parent = $$fileDirs{$dirName}; if (ref $parent) { push @dirNames, reverse @$parent; $parent = pop @dirNames; } $$editDirs{$dirName} = $parent; $$addDirs{$dirName} = $parent if $isCreating and $isCreating != 2; $dirName = $parent || shift @dirNames } } } if (%{$$self{DEL_GROUP}}) { # add delete groups to list of edited groups foreach (keys %{$$self{DEL_GROUP}}) { next if /^-/; # ignore excluded groups my $dirName = $_; # translate necessary group 0 names $dirName = $translateWriteGroup{$dirName} if $translateWriteGroup{$dirName}; # convert XMP group 1 names $dirName = 'XMP' if $dirName =~ /^XMP-/; my @dirNames; while ($dirName) { my $parent = $$fileDirs{$dirName}; if (ref $parent) { push @dirNames, reverse @$parent; $parent = pop @dirNames; } $$editDirs{$dirName} = $parent; $dirName = $parent || shift @dirNames } } } # special case to edit JFIF to get resolutions if editing EXIF information if ($$editDirs{IFD0} and $$fileDirs{JFIF}) { $$editDirs{JFIF} = 'IFD1'; $$editDirs{APP0} = undef; } if ($$self{OPTIONS}{Verbose}) { my $out = $$self{OPTIONS}{TextOut}; print $out " Editing tags in: "; foreach (sort keys %$editDirs) { print $out "$_ "; } print $out "\n"; return unless $$self{OPTIONS}{Verbose} > 1; print $out " Creating tags in: "; foreach (sort keys %$addDirs) { print $out "$_ "; } print $out "\n"; } } #------------------------------------------------------------------------------ # Write an image directory # Inputs: 0) ExifTool object reference, 1) source directory information reference # 2) tag table reference, 3) optional reference to writing procedure # Returns: New directory data or undefined on error (or empty string to delete directory) sub WriteDirectory($$$;$) { my ($self, $dirInfo, $tagTablePtr, $writeProc) = @_; my ($out, $nvHash, $delFlag); $tagTablePtr or return undef; $out = $$self{OPTIONS}{TextOut} if $$self{OPTIONS}{Verbose}; # set directory name from default group0 name if not done already my $dirName = $$dirInfo{DirName}; my $parent = $$dirInfo{Parent} || ''; my $dataPt = $$dirInfo{DataPt}; my $grp0 = $$tagTablePtr{GROUPS}{0}; $dirName or $dirName = $$dirInfo{DirName} = $grp0; if (%{$$self{DEL_GROUP}}) { my $delGroup = $$self{DEL_GROUP}; # delete entire directory if specified my $grp1 = $dirName; $delFlag = ($$delGroup{$grp0} or $$delGroup{$grp1}); if ($permanentDir{$grp0} and not ($$dirInfo{TagInfo} and $$dirInfo{TagInfo}{Deletable})) { undef $delFlag; } # (never delete an entire QuickTime group) if ($delFlag) { if (($grp0 =~ /^(MakerNotes)$/ or $grp1 =~ /^(IFD0|ExifIFD|MakerNotes)$/) and $self->IsRawType() and # allow non-permanent MakerNote directories to be deleted (ie. NikonCapture) (not $$dirInfo{TagInfo} or not defined $$dirInfo{TagInfo}{Permanent} or $$dirInfo{TagInfo}{Permanent}) and # allow MakerNotes to be deleted from ExifIFD of CR3 file not ($self->IsRawType() == 2 and $parent eq 'ExifIFD')) { $self->Warn("Can't delete $1 from $$self{FileType}",1); undef $grp1; } elsif (not $blockExifTypes{$$self{FILE_TYPE}}) { # restrict delete logic to prevent entire tiff image from being killed # (don't allow IFD0 to be deleted, and delete only ExifIFD if EXIF specified) if ($$self{FILE_TYPE} eq 'PSD') { # don't delete Photoshop directories from PSD image undef $grp1 if $grp0 eq 'Photoshop'; } elsif ($$self{FILE_TYPE} =~ /^(EPS|PS)$/) { # allow anything to be deleted from PostScript files } elsif ($grp1 eq 'IFD0') { my $type = $$self{TIFF_TYPE} || $$self{FILE_TYPE}; $$delGroup{IFD0} and $self->Warn("Can't delete IFD0 from $type",1); undef $grp1; } elsif ($grp0 eq 'EXIF' and $$delGroup{$grp0}) { undef $grp1 unless $$delGroup{$grp1} or $grp1 eq 'ExifIFD'; } } if ($grp1) { if ($dataPt or $$dirInfo{RAF}) { ++$$self{CHANGED}; $out and print $out " Deleting $grp1\n"; $self->Warn('ICC_Profile deleted. Image colors may be affected') if $grp1 eq 'ICC_Profile'; # can no longer validate TIFF_END if deleting an entire IFD delete $$self{TIFF_END} if $dirName =~ /IFD/; } # don't add back into the wrong location my $right = $$self{ADD_DIRS}{$grp1}; # (take care because EXIF directory name may be either EXIF or IFD0, # but IFD0 will be the one that appears in the directory map) $right = $$self{ADD_DIRS}{IFD0} if not $right and $grp1 eq 'EXIF'; if ($delFlag == 2 and $right) { # also check grandparent because some routines create 2 levels in 1 my $right2 = $$self{ADD_DIRS}{$right} || ''; if (not $parent or $parent eq $right or $parent eq $right2) { # prevent duplicate directories from being recreated at the same path my $path = join '-', @{$$self{PATH}}, $dirName; $$self{Recreated} or $$self{Recreated} = { }; if ($$self{Recreated}{$path}) { my $p = $parent ? " in $parent" : ''; $self->Warn("Not recreating duplicate $grp1$p",1); return ''; } $$self{Recreated}{$path} = 1; # empty the directory my $data = ''; $$dirInfo{DataPt} = \$data; $$dirInfo{DataLen} = 0; $$dirInfo{DirStart} = 0; $$dirInfo{DirLen} = 0; delete $$dirInfo{RAF}; delete $$dirInfo{Base}; delete $$dirInfo{DataPos}; } else { $self->Warn("Not recreating $grp1 in $parent (should be in $right)",1); return ''; } } else { return '' unless $$dirInfo{NoDelete}; } } } } # use default proc from tag table if no proc specified $writeProc or $writeProc = $$tagTablePtr{WRITE_PROC} or return undef; # are we rewriting a pre-existing directory? my $isRewriting = ($$dirInfo{DirLen} or (defined $dataPt and length $$dataPt) or $$dirInfo{RAF}); # copy or delete new directory as a block if specified my $blockName = $dirName; $blockName = 'EXIF' if $blockName eq 'IFD0'; my $tagInfo = $Image::ExifTool::Extra{$blockName} || $$dirInfo{TagInfo}; while ($tagInfo and ($nvHash = $$self{NEW_VALUE}{$tagInfo}) and $self->IsOverwriting($nvHash) and not ($$nvHash{CreateOnly} and $isRewriting)) { # protect against writing EXIF to wrong file types, etc if ($blockName eq 'EXIF') { unless ($blockExifTypes{$$self{FILE_TYPE}}) { $self->Warn("Can't write EXIF as a block to $$self{FILE_TYPE} file"); last; } # this can happen if we call WriteDirectory for an EXIF directory without going # through WriteTIFF as the WriteProc (which happens if conditionally replacing # the EXIF block and the condition fails), but we never want to do a block write # in this case because the EXIF block would end up with two TIFF headers last unless $writeProc eq \&Image::ExifTool::WriteTIFF; } last unless $self->IsOverwriting($nvHash, $dataPt ? $$dataPt : ''); my $verb = 'Writing'; my $newVal = $self->GetNewValue($nvHash); if (defined $newVal and length $newVal) { # hack to add back TIFF header when writing MakerNoteCanon to CMT3 in CR3 images if ($$tagInfo{Name} eq 'MakerNoteCanon') { require Image::ExifTool::Canon; if ($tagInfo eq $Image::ExifTool::Canon::uuid{CMT3}) { my $hdr; if (substr($newVal, 0, 1) eq "\0") { $hdr = "MM\0\x2a" . pack('N', 8); } else { $hdr = "II\x2a\0" . pack('V', 8); } $newVal = $hdr . $newVal; } } } else { return '' unless $dataPt or $$dirInfo{RAF}; # nothing to do if block never existed # don't allow MakerNotes to be removed from RAW files if ($blockName eq 'MakerNotes' and $self->IsRawType() and # but allow MakerNotes to be deleted from ExifIFD of CR3 image (shouldn't be there) not ($self->IsRawType() == 2 and $parent eq 'ExifIFD')) { $self->Warn("Can't delete MakerNotes from $$self{FileType}",1); return undef; } $verb = 'Deleting'; $newVal = ''; } $$dirInfo{BlockWrite} = 1; # set flag indicating we did a block write $out and print $out " $verb $blockName as a block\n"; ++$$self{CHANGED}; return $newVal; } # guard against writing the same directory twice if (defined $dataPt and defined $$dirInfo{DirStart} and defined $$dirInfo{DataPos} and not $$dirInfo{NoRefTest}) { my $addr = $$dirInfo{DirStart} + $$dirInfo{DataPos} + ($$dirInfo{Base}||0) + $$self{BASE}; # (Phase One P25 IIQ files have ICC_Profile duplicated in IFD0 and IFD1) if ($$self{PROCESSED}{$addr} and ($dirName ne 'ICC_Profile' or $$self{TIFF_TYPE} ne 'IIQ')) { if (defined $$dirInfo{DirLen} and not $$dirInfo{DirLen} and $dirName ne $$self{PROCESSED}{$addr}) { # it is hypothetically possible to have 2 different directories # with the same address if one has a length of zero } elsif ($self->Error("$dirName pointer references previous $$self{PROCESSED}{$addr} directory", 2)) { return undef; } else { $self->Warn("Deleting duplicate $dirName directory"); $out and print $out " Deleting $dirName\n"; # delete the duplicate directory (don't recreate it when writing new # tags to prevent propagating a duplicate IFD in cases like when the # same ExifIFD exists in both IFD0 and IFD1) return ''; } } else { $$self{PROCESSED}{$addr} = $dirName; } } my $oldDir = $$self{DIR_NAME}; my @save = @$self{'Compression','SubfileType'}; my $name; if ($out) { $name = ($dirName eq 'MakerNotes' and $$dirInfo{TagInfo}) ? $$dirInfo{TagInfo}{Name} : $dirName; if (not defined $oldDir or $oldDir ne $name) { my $verb = $isRewriting ? 'Rewriting' : 'Creating'; print $out " $verb $name\n"; } } my $saveOrder = GetByteOrder(); my $oldChanged = $$self{CHANGED}; $$self{DIR_NAME} = $dirName; push @{$$self{PATH}}, $dirName; $$dirInfo{IsWriting} = 1; my $newData; { no strict 'refs'; $newData = &$writeProc($self, $dirInfo, $tagTablePtr); } pop @{$$self{PATH}}; # nothing changed if error occurred or nothing was created $$self{CHANGED} = $oldChanged unless defined $newData and (length($newData) or $isRewriting); $$self{DIR_NAME} = $oldDir; @$self{'Compression','SubfileType'} = @save; SetByteOrder($saveOrder); if ($out) { print $out " Deleting $name\n" if defined $newData and not length $newData; if ($$self{CHANGED} == $oldChanged and $$self{OPTIONS}{Verbose} > 2) { print $out "$$self{INDENT} [nothing changed in $name]\n"; } } return $newData; } #------------------------------------------------------------------------------ # Uncommon utility routines to for reading binary data values # Inputs: 0) data reference, 1) offset into data sub Get64s($$) { my ($dataPt, $pos) = @_; my $pt = GetByteOrder() eq 'MM' ? 0 : 4; # get position of high word my $hi = Get32s($dataPt, $pos + $pt); # preserve sign bit of high word my $lo = Get32u($dataPt, $pos + 4 - $pt); return $hi * 4294967296 + $lo; } sub Get64u($$) { my ($dataPt, $pos) = @_; my $pt = GetByteOrder() eq 'MM' ? 0 : 4; # get position of high word my $hi = Get32u($dataPt, $pos + $pt); # (unsigned this time) my $lo = Get32u($dataPt, $pos + 4 - $pt); return $hi * 4294967296 + $lo; } sub GetFixed64s($$) { my ($dataPt, $pos) = @_; my $val = Get64s($dataPt, $pos) / 4294967296; # remove insignificant digits return int($val * 1e10 + ($val>0 ? 0.5 : -0.5)) / 1e10; } # Decode extended 80-bit float used by Apple SANE and Intel 8087 # (note: different than the IEEE standard 80-bit float) sub GetExtended($$) { my ($dataPt, $pos) = @_; my $pt = GetByteOrder() eq 'MM' ? 0 : 2; # get position of exponent my $exp = Get16u($dataPt, $pos + $pt); my $sig = Get64u($dataPt, $pos + 2 - $pt); # get significand as int64u my $sign = $exp & 0x8000 ? -1 : 1; $exp = ($exp & 0x7fff) - 16383 - 63; # (-63 to fractionalize significand) return $sign * $sig * 2 ** $exp; } #------------------------------------------------------------------------------ # Dump data in hex and ASCII to console # Inputs: 0) data reference, 1) length or undef, 2-N) Options: # Options: Start => offset to start of data (default=0) # Addr => address to print for data start (default=DataPos+Base+Start) # DataPos => position of data within block (relative to Base) # Base => base offset for pointers from start of file # Width => width of printout (bytes, default=16) # Prefix => prefix to print at start of line (default='') # MaxLen => maximum length to dump # Out => output file reference # Len => data length sub HexDump($;$%) { my $dataPt = shift; my $len = shift; my %opts = @_; my $start = $opts{Start} || 0; my $addr = $opts{Addr}; my $wid = $opts{Width} || 16; my $prefix = $opts{Prefix} || ''; my $out = $opts{Out} || \*STDOUT; my $maxLen = $opts{MaxLen}; my $datLen = length($$dataPt) - $start; my $more; $len = $opts{Len} if defined $opts{Len}; $addr = $start + ($opts{DataPos} || 0) + ($opts{Base} || 0) unless defined $addr; $len = $datLen unless defined $len; if ($maxLen and $len > $maxLen) { # print one line less to allow for $more line below $maxLen = int(($maxLen - 1) / $wid) * $wid; $more = $len - $maxLen; $len = $maxLen; } if ($len > $datLen) { print $out "$prefix Warning: Attempted dump outside data\n"; print $out "$prefix ($len bytes specified, but only $datLen available)\n"; $len = $datLen; } my $format = sprintf("%%-%ds", $wid * 3); my $tmpl = 'H2' x $wid; # ('(H2)*' would have been nice, but older perl versions don't support it) my $i; for ($i=0; $i<$len; $i+=$wid) { $wid > $len-$i and $wid = $len-$i, $tmpl = 'H2' x $wid; printf $out "$prefix%8.4x: ", $addr+$i; my $dat = substr($$dataPt, $i+$start, $wid); my $s = join(' ', unpack($tmpl, $dat)); printf $out $format, $s; $dat =~ tr /\x00-\x1f\x7f-\xff/./; print $out "[$dat]\n"; } $more and print $out "$prefix [snip $more bytes]\n"; } #------------------------------------------------------------------------------ # Print verbose tag information # Inputs: 0) ExifTool object reference, 1) tag ID # 2) tag info reference (or undef) # 3-N) extra parms: # Parms: Index => Index of tag in menu (starting at 0) # Value => Tag value # DataPt => reference to value data block # DataPos => location of data block in file # Base => base added to all offsets # Size => length of value data within block # Format => value format string # Count => number of values # Extra => Extra Verbose=2 information to put after tag number # Table => Reference to tag table # Name => Name to use for unknown tag # --> plus any of these HexDump() options: Start, Addr, Width sub VerboseInfo($$$%) { my ($self, $tagID, $tagInfo, %parms) = @_; my $verbose = $$self{OPTIONS}{Verbose}; my $out = $$self{OPTIONS}{TextOut}; my ($tag, $line, $hexID); # generate hex number if tagID is numerical if (defined $tagID) { $tagID =~ /^\d+$/ and $hexID = sprintf("0x%.4x", $tagID); } else { $tagID = 'Unknown'; } # get tag name if ($tagInfo and $$tagInfo{Name}) { $tag = $$tagInfo{Name}; } elsif ($parms{Name}) { # (used for PNG Plus FPX tags) $tag = $parms{Name}; undef $hexID; } else { my $prefix; $prefix = $parms{Table}{TAG_PREFIX} if $parms{Table}; if ($prefix or $hexID) { $prefix = 'Unknown' unless $prefix; $tag = $prefix . '_' . ($hexID ? $hexID : $tagID); } else { $tag = $tagID; } } my $dataPt = $parms{DataPt}; my $size = $parms{Size}; $size = length $$dataPt unless defined $size or not $dataPt; my $indent = $$self{INDENT}; # Level 1: print tag/value information $line = $indent; my $index = $parms{Index}; if (defined $index) { $line .= $index . ') '; $line .= ' ' if length($index) < 2; $indent .= ' '; # indent everything else to align with tag name } $line .= $tag; if ($tagInfo and $$tagInfo{SubDirectory}) { $line .= ' (SubDirectory) -->'; } else { my $maxLen = 90 - length($line); my $val = $parms{Value}; if (defined $val) { $val = '[' . join(',',@$val) . ']' if ref $val eq 'ARRAY'; $line .= ' = ' . $self->Printable($val, $maxLen); } elsif ($dataPt) { my $start = $parms{Start} || 0; $line .= ' = ' . $self->Printable(substr($$dataPt,$start,$size), $maxLen); } } print $out "$line\n"; # Level 2: print detailed information about the tag if ($verbose > 1 and ($parms{Extra} or $parms{Format} or $parms{DataPt} or defined $size or $tagID =~ /\//)) { $line = $indent . '- Tag '; if ($hexID) { $line .= $hexID; } else { $tagID =~ s/([\0-\x1f\x7f-\xff])/sprintf('\\x%.2x',ord $1)/ge; $line .= "'${tagID}'"; } $line .= $parms{Extra} if defined $parms{Extra}; my $format = $parms{Format}; if ($format or defined $size) { $line .= ' ('; if (defined $size) { $line .= "$size bytes"; $line .= ', ' if $format; } if ($format) { $line .= $format; $line .= '['.$parms{Count}.']' if $parms{Count}; } $line .= ')'; } $line .= ':' if $verbose > 2 and $parms{DataPt}; print $out "$line\n"; } # Level 3: do hex dump of value if ($verbose > 2 and $parms{DataPt} and (not $tagInfo or not $$tagInfo{ReadFromRAF})) { $parms{Out} = $out; $parms{Prefix} = $indent; # limit dump length if Verbose < 5 $parms{MaxLen} = $verbose == 3 ? 96 : 2048 if $verbose < 5; HexDump($dataPt, $size, %parms); } } #------------------------------------------------------------------------------ # Dump trailer information # Inputs: 0) ExifTool object ref, 1) dirInfo hash (RAF, DirName, DataPos, DirLen) # Notes: Restores current file position before returning sub DumpTrailer($$) { my ($self, $dirInfo) = @_; my $raf = $$dirInfo{RAF}; my $curPos = $raf->Tell(); my $trailer = $$dirInfo{DirName} || 'Unknown'; my $pos = $$dirInfo{DataPos}; my $verbose = $$self{OPTIONS}{Verbose}; my $htmlDump = $$self{HTML_DUMP}; my ($buff, $buf2); my $size = $$dirInfo{DirLen}; $pos = $curPos unless defined $pos; # get full trailer size if not specified for (;;) { unless ($size) { $raf->Seek(0, 2) or last; $size = $raf->Tell() - $pos; last unless $size; } $raf->Seek($pos, 0) or last; if ($htmlDump) { my $num = $raf->Read($buff, $size) or return; my $desc = "$trailer trailer"; $desc = "[$desc]" if $trailer eq 'Unknown'; $self->HDump($pos, $num, $desc, undef, 0x08); last; } my $out = $$self{OPTIONS}{TextOut}; printf $out "$trailer trailer (%d bytes at offset 0x%.4x):\n", $size, $pos; last unless $verbose > 2; my $num = $size; # number of bytes to read # limit size if not very verbose if ($verbose < 5) { my $limit = $verbose < 4 ? 96 : 512; $num = $limit if $num > $limit; } $raf->Read($buff, $num) == $num or return; # read the end of the trailer too if not done already if ($size > 2 * $num) { $raf->Seek($pos + $size - $num, 0); $raf->Read($buf2, $num); } elsif ($size > $num) { $raf->Seek($pos + $num, 0); $raf->Read($buf2, $size - $num); $buff .= $buf2; undef $buf2; } HexDump(\$buff, undef, Addr => $pos, Out => $out); if (defined $buf2) { print $out " [snip ", $size - $num * 2, " bytes]\n"; HexDump(\$buf2, undef, Addr => $pos + $size - $num, Out => $out); } last; } $raf->Seek($curPos, 0); } #------------------------------------------------------------------------------ # Dump unknown trailer information # Inputs: 0) ExifTool ref, 1) dirInfo ref (with RAF, DataPos and DirLen defined) # Notes: changes dirInfo elements sub DumpUnknownTrailer($$) { my ($self, $dirInfo) = @_; my $pos = $$dirInfo{DataPos}; my $endPos = $pos + $$dirInfo{DirLen}; # account for preview/MPF image trailer my $prePos = $$self{VALUE}{PreviewImageStart} || $$self{PreviewImageStart}; my $preLen = $$self{VALUE}{PreviewImageLength} || $$self{PreviewImageLength}; my $hidPos = $$self{VALUE}{HiddenDataOffset}; my $hidLen = $$self{VALUE}{HiddenDataLength}; my $tag = 'PreviewImage'; my $mpImageNum = 0; my (%image, $lastOne); # add HiddenData to list of known trailer blocks if ($hidPos and $hidLen) { # call ReadHiddenData to validate hidden data and fix offset if necessary require Image::ExifTool::Sony; my $datPt = Image::ExifTool::Sony::ReadHiddenData($self, $hidPos, $hidLen); $image{$hidPos} = ['HiddenData', $hidLen] if $datPt; } for (;;) { # add to Preview block list if valid and in the trailer $image{$prePos} = [$tag, $preLen] if $prePos and $preLen and $prePos+$preLen > $pos; last if $lastOne; # checked all images # look for MPF images (in the proper order) ++$mpImageNum; $prePos = $$self{VALUE}{"MPImageStart ($mpImageNum)"}; if (defined $prePos) { $preLen = $$self{VALUE}{"MPImageLength ($mpImageNum)"}; } else { $prePos = $$self{VALUE}{'MPImageStart'}; $preLen = $$self{VALUE}{'MPImageLength'}; $lastOne = 1; } $tag = "MPImage$mpImageNum"; } # dump trailer sections in order $image{$endPos} = [ '', 0 ]; # add terminator "image" foreach $prePos (sort { $a <=> $b } keys %image) { if ($pos < $prePos) { # dump unknown trailer data $$dirInfo{DirName} = 'Unknown'; $$dirInfo{DataPos} = $pos; $$dirInfo{DirLen} = $prePos - $pos; $self->DumpTrailer($dirInfo); } ($tag, $preLen) = @{$image{$prePos}}; last unless $preLen; # dump image if verbose (it is htmlDump'd by ExtractImage) if ($$self{OPTIONS}{Verbose}) { $$dirInfo{DirName} = $tag; $$dirInfo{DataPos} = $prePos; $$dirInfo{DirLen} = $preLen; $self->DumpTrailer($dirInfo); } $pos = $prePos + $preLen; } } #------------------------------------------------------------------------------ # Find last element in linked list # Inputs: 0) element in list # Returns: Last element in list sub LastInList($) { my $element = shift; while ($$element{Next}) { $element = $$element{Next}; } return $element; } #------------------------------------------------------------------------------ # Print verbose value while writing # Inputs: 0) ExifTool object ref, 1) heading "eg. '+ IPTC:Keywords', # 2) value, 3) [optional] extra text after value sub VerboseValue($$$;$) { return unless $_[0]{OPTIONS}{Verbose} > 1; my ($self, $str, $val, $xtra) = @_; my $out = $$self{OPTIONS}{TextOut}; $xtra or $xtra = ''; my $maxLen = 81 - length($str) - length($xtra); $val = $self->Printable($val, $maxLen); print $out " $str = '${val}'$xtra\n"; } #------------------------------------------------------------------------------ # Pack Unicode numbers into UTF8 string # Inputs: 0-N) list of Unicode numbers # Returns: Packed UTF-8 string sub PackUTF8(@) { my @out; while (@_) { my $ch = pop; unshift(@out, $ch), next if $ch < 0x80; unshift(@out, 0x80 | ($ch & 0x3f)); $ch >>= 6; unshift(@out, 0xc0 | $ch), next if $ch < 0x20; unshift(@out, 0x80 | ($ch & 0x3f)); $ch >>= 6; unshift(@out, 0xe0 | $ch), next if $ch < 0x10; unshift(@out, 0x80 | ($ch & 0x3f)); $ch >>= 6; unshift(@out, 0xf0 | ($ch & 0x07)); } return pack('C*', @out); } #------------------------------------------------------------------------------ # Unpack numbers from UTF8 string # Inputs: 0) UTF-8 string # Returns: List of Unicode numbers (sets $evalWarning on error) sub UnpackUTF8($) { my (@out, $pos); pos($_[0]) = $pos = 0; # start at beginning of string for (;;) { my ($ch, $newPos, $val, $byte); if ($_[0] =~ /([\x80-\xff])/g) { $ch = ord($1); $newPos = pos($_[0]) - 1; } else { $newPos = length $_[0]; } # unpack 7-bit characters my $len = $newPos - $pos; push @out, unpack("x${pos}C$len",$_[0]) if $len; last unless defined $ch; $pos = $newPos + 1; # minimum lead byte for 2-byte sequence is 0xc2 (overlong sequences # not allowed), 0xf8-0xfd are restricted by RFC 3629 (no 5 or 6 byte # sequences), and 0xfe and 0xff are not valid in UTF-8 strings if ($ch < 0xc2 or $ch >= 0xf8) { push @out, ord('?'); # invalid UTF-8 $evalWarning = 'Bad UTF-8'; next; } # decode 2, 3 and 4-byte sequences my $n = 1; if ($ch < 0xe0) { $val = $ch & 0x1f; # 2-byte sequence } elsif ($ch < 0xf0) { $val = $ch & 0x0f; # 3-byte sequence ++$n; } else { $val = $ch & 0x07; # 4-byte sequence $n += 2; } unless ($_[0] =~ /\G([\x80-\xbf]{$n})/g) { pos($_[0]) = $pos; # restore position push @out, ord('?'); # invalid UTF-8 $evalWarning = 'Bad UTF-8'; next; } foreach $byte (unpack 'C*', $1) { $val = ($val << 6) | ($byte & 0x3f); } push @out, $val; # save Unicode character value $pos += $n; # position at end of UTF-8 character } return @out; } #------------------------------------------------------------------------------ # Generate a new, random GUID # Inputs: # Returns: GUID string my $guidCount; sub NewGUID() { my @tm = localtime time; $guidCount = 0 unless defined $guidCount and ++$guidCount < 0x100; return sprintf('%.4d%.2d%.2d%.2d%.2d%.2d%.2X%.4X%.4X%.4X%.4X', $tm[5]+1900, $tm[4]+1, $tm[3], $tm[2], $tm[1], $tm[0], $guidCount, $$ & 0xffff, rand(0x10000), rand(0x10000), rand(0x10000)); } #------------------------------------------------------------------------------ # Make TIFF header for raw data # Inputs: 0) width, 1) height, 2) num colour components, 3) bits, 4) resolution # 5) color-map data for palette-color image (8 or 16 bit) # Returns: TIFF header # Notes: Multi-byte data must be little-endian sub MakeTiffHeader($$$$;$$) { my ($w, $h, $cols, $bits, $res, $cmap) = @_; $res or $res = 72; my $saveOrder = GetByteOrder(); SetByteOrder('II'); if (not $cmap) { $cmap = ''; } elsif (length $cmap == 3 * 2**$bits) { # convert to short $cmap = pack 'v*', map { $_ | ($_<<8) } unpack 'C*', $cmap; } elsif (length $cmap != 6 * 2**$bits) { $cmap = ''; } my $cmo = $cmap ? 12 : 0; # offset due to ColorMap IFD entry my $hdr = "\x49\x49\x2a\0\x08\0\0\0\x0e\0" . # 0x00 14 menu entries: "\xfe\x00\x04\0\x01\0\0\0\x00\0\0\0" . # 0x0a SubfileType = 0 "\x00\x01\x04\0\x01\0\0\0" . Set32u($w) . # 0x16 ImageWidth "\x01\x01\x04\0\x01\0\0\0" . Set32u($h) . # 0x22 ImageHeight "\x02\x01\x03\0" . Set32u($cols) . # 0x2e BitsPerSample Set32u($cols == 1 ? $bits : 0xb6 + $cmo) . "\x03\x01\x03\0\x01\0\0\0\x01\0\0\0" . # 0x3a Compression = 1 "\x06\x01\x03\0\x01\0\0\0" . # 0x46 PhotometricInterpretation Set32u($cmap ? 3 : $cols == 1 ? 1 : 2) . "\x11\x01\x04\0\x01\0\0\0" . # 0x52 StripOffsets Set32u(0xcc + $cmo + length($cmap)) . "\x15\x01\x03\0\x01\0\0\0" . Set32u($cols) . # 0x5e SamplesPerPixel "\x16\x01\x04\0\x01\0\0\0" . Set32u($h) . # 0x6a RowsPerStrip "\x17\x01\x04\0\x01\0\0\0" . # 0x76 StripByteCounts Set32u($w * $h * $cols * int(($bits+7)/8)) . "\x1a\x01\x05\0\x01\0\0\0" . Set32u(0xbc + $cmo) . # 0x82 XResolution "\x1b\x01\x05\0\x01\0\0\0" . Set32u(0xc4 + $cmo) . # 0x8e YResolution "\x1c\x01\x03\0\x01\0\0\0\x01\0\0\0" . # 0x9a PlanarConfiguration = 1 "\x28\x01\x03\0\x01\0\0\0\x02\0\0\0" . # 0xa6 ResolutionUnit = 2 ($cmap ? # 0xb2 ColorMap [optional] "\x40\x01\x03\0" . Set32u(3 * 2**$bits) . "\xd8\0\0\0" : '') . "\0\0\0\0" . # 0xb2+$cmo (no IFD1) (Set16u($bits) x 3) . # 0xb6+$cmo BitsPerSample value Set32u($res) . "\x01\0\0\0" . # 0xbc+$cmo XResolution = 72 Set32u($res) . "\x01\0\0\0" . # 0xc4+$cmo YResolution = 72 $cmap; # 0xcc or 0xd8 (cmap and data go here) SetByteOrder($saveOrder); return $hdr; } #------------------------------------------------------------------------------ # Return current time in EXIF format # Inputs: 0) [optional] ExifTool ref, 1) flag to include timezone (0 to disable, # undef or 1 to include) # Returns: time string # - a consistent value is returned for each processed file sub TimeNow(;$$) { my ($self, $tzFlag) = @_; my $timeNow; ref $self or $tzFlag = $self, $self = { }; if ($$self{Now}) { $timeNow = $$self{Now}[0]; } else { my $time = time(); my @tm = localtime $time; my $tz = TimeZoneString(\@tm, $time); $timeNow = sprintf("%4d:%.2d:%.2d %.2d:%.2d:%.2d", $tm[5]+1900, $tm[4]+1, $tm[3], $tm[2], $tm[1], $tm[0]); $$self{Now} = [ $timeNow, $tz ]; } $timeNow .= $$self{Now}[1] if $tzFlag or not defined $tzFlag; return $timeNow; } #------------------------------------------------------------------------------ # Inverse date/time print conversion (reformat to YYYY:mm:dd HH:MM:SS[.ss][+-HH:MM|Z]) # Inputs: 0) ExifTool object ref, 1) Date/Time string, 2) timezone flag: # 0 - remove timezone and sub-seconds if they exist # 1 - add timezone if it doesn't exist # undef - leave timezone alone # 3) flag to allow date-only (YYYY, YYYY:mm or YYYY:mm:dd) or time without seconds # Returns: formatted date/time string (or undef and issues warning on error) # Notes: currently accepts different separators, but doesn't use DateFormat yet my $strptimeLib; # strptime library name if available sub InverseDateTime($$;$$) { my ($self, $val, $tzFlag, $dateOnly) = @_; my ($rtnVal, $tz); my $fmt = $$self{OPTIONS}{DateFormat}; # strip off timezone first if it exists if (not $fmt and $val =~ s/([-+])(\d{1,2}):?(\d{2})\s*(DST)?$//i) { $tz = sprintf("$1%.2d:$3", $2); } elsif (not $fmt and $val =~ s/Z$//i) { $tz = 'Z'; } else { $tz = ''; # allow special value of 'now' return $self->TimeNow($tzFlag) if lc($val) eq 'now'; } # only convert date if a format was specified and the date is recognizable if ($fmt) { unless (defined $strptimeLib) { if (eval { require POSIX::strptime }) { $strptimeLib = 'POSIX::strptime'; } elsif (eval { require Time::Piece }) { $strptimeLib = 'Time::Piece'; # (call use_locale() to convert localized date/time, # only available in Time::Piece 1.32 and later) eval { Time::Piece->use_locale() }; } else { $strptimeLib = ''; } } # handle factional seconds (%f), but only at the end of the string my $fs = ($fmt =~ s/%f$// and $val =~ s/(\.\d+)\s*$//) ? $1 : ''; my ($lib, $wrn, @a); TryLib: for ($lib=$strptimeLib; ; $lib='') { # handle %s format ourself (not supported in Fedora, see forum15032) if ($fmt eq '%s') { $val = ConvertUnixTime($val, 1); last; } if (not $lib) { last unless $$self{OPTIONS}{StrictDate}; warn $wrn || "Install POSIX::strptime or Time::Piece for inverse date/time conversions\n"; return undef; } elsif ($lib eq 'POSIX::strptime') { @a = eval { POSIX::strptime($val, $fmt) }; } else { # protect against a negative epoch time, it can cause a hard crash in Windows if ($^O eq 'MSWin32' and $fmt =~ /%s/ and $val =~ /-\d/) { warn "Can't convert negative epoch time\n"; return undef; } @a = eval { my $t = Time::Piece->strptime($val, $fmt); return ($t->sec, $t->min, $t->hour, $t->mday, $t->_mon, $t->_year); }; } if (defined $a[5] and length $a[5]) { $a[5] += 1900; # add 1900 to year } else { $wrn = "Invalid date/time (no year) using $lib\n"; next; } ++$a[4] if defined $a[4] and length $a[4]; # add 1 to month my $i; foreach $i (0..4) { if (not defined $a[$i] or not length $a[$i]) { if ($i < 2 or $dateOnly) { # (allow missing minutes/seconds) $a[$i] = ' '; } else { $wrn = "Incomplete date/time specification using $lib\n"; next TryLib; } } elsif (length($a[$i]) < 2) { $a[$i] = "0$a[$i]"; # pad to 2 digits if necessary } } $val = join(':', @a[5,4,3]) . ' ' . join(':', @a[2,1,0]) . $fs; last; } } if ($val =~ /(\d{4})/g) { # get YYYY my $yr = $1; my @a = ($val =~ /\d{1,2}/g); # get mm, dd, HH, and maybe MM, SS length($_) < 2 and $_ = "0$_" foreach @a; # pad to 2 digits if necessary if (@a >= 3) { my $ss = $a[4]; # get SS push @a, '00' while @a < 5; # add MM, SS if not given # get sub-seconds if they exist (must be after SS, and have leading ".") my $fs = (@a > 5 and $val =~ /(\.\d+)\s*$/) ? $1 : ''; # add/remove timezone if necessary if ($tzFlag) { if (not $tz) { if (eval { require Time::Local }) { # determine timezone offset for this time my @args = ($a[4],$a[3],$a[2],$a[1],$a[0]-1,$yr); my $diff = Time::Local::timegm(@args) - TimeLocal(@args); $tz = TimeZoneString($diff / 60); } else { $tz = 'Z'; # don't know time zone } } } elsif (defined $tzFlag) { $tz = $fs = ''; # remove timezone and sub-seconds } if (defined $ss and $ss < 60) { $ss = ":$ss"; } elsif ($dateOnly) { $ss = ''; } else { $ss = ':00'; } # construct properly formatted date/time string if ($a[0] < 1 or $a[0] > 12) { warn "Month '$a[0]' out of range 1..12\n"; return undef; } if ($a[1] < 1 or $a[1] > 31) { warn "Day '$a[1]' out of range 1..31\n"; return undef; } $a[2] > 24 and warn("Hour '$a[2]' out of range 0..24\n"), return undef; $a[3] > 59 and warn("Minutes '$a[3]' out of range 0..59\n"), return undef; $rtnVal = "$yr:$a[0]:$a[1] $a[2]:$a[3]$ss$fs$tz"; } elsif ($dateOnly) { $rtnVal = join ':', $yr, @a; } } $rtnVal or warn "Invalid date/time (use YYYY:mm:dd HH:MM:SS[.ss][+/-HH:MM|Z])\n"; return $rtnVal; } #------------------------------------------------------------------------------ # Set byte order according to our current preferences # Inputs: 0) ExifTool object ref, 1) default byte order # Returns: new byte order ('II' or 'MM') and sets current byte order # Notes: takes the first of the following that is valid: # 1) ByteOrder option # 2) new value for ExifByteOrder # 3) default byte order passed to this routine # 4) makenote byte order from last file read # 5) big endian sub SetPreferredByteOrder($;$) { my ($self, $default) = @_; my $byteOrder = $self->Options('ByteOrder') || $self->GetNewValue('ExifByteOrder') || $default || $$self{MAKER_NOTE_BYTE_ORDER} || 'MM'; unless (SetByteOrder($byteOrder)) { warn "Invalid byte order '${byteOrder}'\n" if $self->Options('Verbose'); $byteOrder = $$self{MAKER_NOTE_BYTE_ORDER} || 'MM'; SetByteOrder($byteOrder); } return GetByteOrder(); } #------------------------------------------------------------------------------ # Assemble a continuing fraction into a rational value # Inputs: 0) numerator, 1) denominator # 2-N) list of fraction denominators, deepest first # Returns: numerator, denominator (in list context) sub AssembleRational($$@) { @_ < 3 and return @_; my ($num, $denom, $frac) = splice(@_, 0, 3); return AssembleRational($frac*$num+$denom, $num, @_); } #------------------------------------------------------------------------------ # Convert a floating point number (or 'inf' or 'undef' or a fraction) into a rational # Inputs: 0) floating point number, 1) optional maximum value (defaults to 0x7fffffff) # Returns: numerator, denominator (in list context) # Notes: # - the returned rational will be accurate to at least 8 significant figures if possible # - eg. an input of 3.14159265358979 returns a rational of 104348/33215, # which equals 3.14159265392142 and is accurate to 10 significant figures # - the returned rational will be reduced to the lowest common denominator except when # the input is a fraction in which case the input is returned unchanged # - these routines were a bit tricky, but fun to write! sub Rationalize($;$) { my $val = shift; return (1, 0) if $val eq 'inf'; return (0, 0) if $val eq 'undef'; return ($1,$2) if $val =~ m{^([-+]?\d+)/(\d+)$}; # accept fractional values # Note: Just testing "if $val" doesn't work because '0.0' is true! (ugghh!) return (0, 1) if $val == 0; my $sign = $val < 0 ? ($val = -$val, -1) : 1; my ($num, $denom, @fracs); my $frac = $val; my $maxInt = shift || 0x7fffffff; for (;;) { my ($n, $d) = AssembleRational(int($frac + 0.5), 1, @fracs); if ($n > $maxInt or $d > $maxInt) { last if defined $num; return ($sign, $maxInt) if $val < 1; return ($sign * $maxInt, 1); } ($num, $denom) = ($n, $d); # save last good values my $err = ($n/$d-$val) / $val; # get error of this rational last if abs($err) < 1e-8; # all done if error is small my $int = int($frac); unshift @fracs, $int; last unless $frac -= $int; $frac = 1 / $frac; } return ($num * $sign, $denom); } #------------------------------------------------------------------------------ # Utility routines to for writing binary data values # Inputs: 0) value, 1) data ref, 2) offset # Notes: prototype is (@) so values can be passed from list if desired sub Set16s(@) { my $val = shift; $val < 0 and $val += 0x10000; return Set16u($val, @_); } sub Set32s(@) { my $val = shift; $val < 0 and $val += 0xffffffff, ++$val; return Set32u($val, @_); } sub Set64u(@) { my $val = $_[0]; my $hi = int($val / 4294967296); my $lo = Set32u($val - $hi * 4294967296); $hi = Set32u($hi); $val = GetByteOrder() eq 'MM' ? $hi . $lo : $lo . $hi; $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } sub Set64s(@) { my $val = shift; $val < 0 and $val += 4294967296 * 4294967296; # (temporary hack won't really work due to round-off errors) return Set64u($val, @_); } sub SetRational64u(@) { my ($numer,$denom) = Rationalize($_[0],0xffffffff); my $val = Set32u($numer) . Set32u($denom); $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } sub SetRational64s(@) { my ($numer,$denom) = Rationalize($_[0]); my $val = Set32s($numer) . Set32u($denom); $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } sub SetRational32u(@) { my ($numer,$denom) = Rationalize($_[0],0xffff); my $val = Set16u($numer) . Set16u($denom); $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } sub SetRational32s(@) { my ($numer,$denom) = Rationalize($_[0],0x7fff); my $val = Set16s($numer) . Set16u($denom); $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } sub SetFixed16u(@) { my $val = int(shift() * 0x100 + 0.5); return Set16u($val, @_); } sub SetFixed16s(@) { my $val = shift; return Set16s(int($val * 0x100 + ($val < 0 ? -0.5 : 0.5)), @_); } sub SetFixed32u(@) { my $val = int(shift() * 0x10000 + 0.5); return Set32u($val, @_); } sub SetFixed32s(@) { my $val = shift; return Set32s(int($val * 0x10000 + ($val < 0 ? -0.5 : 0.5)), @_); } sub SetFloat(@) { my $val = SwapBytes(pack('f',$_[0]), 4); $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } sub SetDouble(@) { # swap 32-bit words (ARM quirk) and bytes if necessary my $val = SwapBytes(SwapWords(pack('d',$_[0])), 8); $_[1] and substr(${$_[1]}, $_[2], length($val)) = $val; return $val; } #------------------------------------------------------------------------------ # hash lookups for writing binary data values my %writeValueProc = ( int8s => \&Set8s, int8u => \&Set8u, int16s => \&Set16s, int16u => \&Set16u, int16uRev => \&Set16uRev, int32s => \&Set32s, int32u => \&Set32u, int64s => \&Set64s, int64u => \&Set64u, rational32s => \&SetRational32s, rational32u => \&SetRational32u, rational64s => \&SetRational64s, rational64u => \&SetRational64u, fixed16u => \&SetFixed16u, fixed16s => \&SetFixed16s, fixed32u => \&SetFixed32u, fixed32s => \&SetFixed32s, float => \&SetFloat, double => \&SetDouble, ifd => \&Set32u, ); # verify that we can write floats on this platform { my %writeTest = ( float => [ -3.14159, 'c0490fd0' ], double => [ -3.14159, 'c00921f9f01b866e' ], ); my $format; my $oldOrder = GetByteOrder(); SetByteOrder('MM'); foreach $format (keys %writeTest) { my ($val, $hex) = @{$writeTest{$format}}; # add floating point entries if we can write them next if unpack('H*', &{$writeValueProc{$format}}($val)) eq $hex; delete $writeValueProc{$format}; # we can't write them } SetByteOrder($oldOrder); } #------------------------------------------------------------------------------ # write binary data value (with current byte ordering) # Inputs: 0) value, 1) format string # 2) number of values: # undef = 1 for numerical types, or data length for string/undef types # -1 = number of space-delimited values in the input string # 3) optional data reference, 4) value offset (may be negative for bytes from end) # Returns: packed value (and sets value in data) or undef on error # Notes: May modify input value to round for integer formats sub WriteValue($$;$$$$) { my ($val, $format, $count, $dataPt, $offset) = @_; my $proc = $writeValueProc{$format}; my $packed; if ($proc) { my @vals = split(' ',$val); if ($count) { $count = @vals if $count < 0; } else { $count = 1; # assume 1 if count not specified } $packed = ''; while ($count--) { $val = shift @vals; return undef unless defined $val; # validate numerical formats if ($format =~ /^int/) { unless (IsInt($val) or IsHex($val)) { return undef unless IsFloat($val); # round to nearest integer $val = int($val + ($val < 0 ? -0.5 : 0.5)); $_[0] = $val; } } elsif (not IsFloat($val)) { return undef unless $format =~ /^rational/ and ($val eq 'inf' or $val eq 'undef' or IsRational($val)); } $packed .= &$proc($val); } } elsif ($format eq 'string' or $format eq 'undef') { $format eq 'string' and $val .= "\0"; # null-terminate strings if ($count and $count > 0) { my $diff = $count - length($val); if ($diff) { #warn "wrong string length!\n"; # adjust length of string to match specified count if ($diff < 0) { if ($format eq 'string') { return undef unless $count; $val = substr($val, 0, $count - 1) . "\0"; } else { $val = substr($val, 0, $count); } } else { $val .= "\0" x $diff; } } } else { $count = length($val); } $dataPt and substr($$dataPt, $offset, $count) = $val; return $val; } else { warn "Sorry, Can't write $format values on this platform\n"; return undef; } $dataPt and substr($$dataPt, $offset, length($packed)) = $packed; return $packed; } #------------------------------------------------------------------------------ # Encode bit mask (the inverse of DecodeBits()) # Inputs: 0) value to encode, 1) Reference to hash for encoding (or undef) # 2) optional number of bits per word (defaults to 32), 3) total bits # Returns: bit mask or undef on error (plus error string in list context) sub EncodeBits($$;$$) { my ($val, $lookup, $bits, $num) = @_; $bits or $bits = 32; $num or $num = $bits; my $words = int(($num + $bits - 1) / $bits); my @outVal = (0) x $words; if ($val ne '(none)') { my @vals = split /\s*,\s*/, $val; foreach $val (@vals) { my $bit; if ($lookup) { $bit = ReverseLookup($val, $lookup); # (Note: may get non-numerical $bit values from Unknown() tags) unless (defined $bit) { if ($val =~ /\[(\d+)\]/) { # numerical bit specification $bit = $1; } else { # don't return error string unless more than one value return undef unless @vals > 1 and wantarray; return (undef, "no match for '${val}'"); } } } else { $bit = $val; } unless (IsInt($bit) and $bit < $num) { return undef unless wantarray; return (undef, IsInt($bit) ? 'bit number too high' : 'not an integer'); } my $word = int($bit / $bits); $outVal[$word] |= (1 << ($bit - $word * $bits)); } } return "@outVal"; } #------------------------------------------------------------------------------ # get current position in output file (or end of file if a scalar reference) # Inputs: 0) file or scalar reference # Returns: Current position or -1 on error sub Tell($) { my $outfile = shift; if (UNIVERSAL::isa($outfile,'GLOB')) { return tell($outfile); } else { return length($$outfile); } } #------------------------------------------------------------------------------ # write to file or memory # Inputs: 0) file or scalar reference, 1-N) list of stuff to write # Returns: true on success sub Write($@) { my $outfile = shift; if (UNIVERSAL::isa($outfile,'GLOB')) { return print $outfile @_; } elsif (ref $outfile eq 'SCALAR') { $$outfile .= join('', @_); return 1; } return 0; } #------------------------------------------------------------------------------ # Write trailer buffer to file (applying fixups if necessary) # Inputs: 0) ExifTool object ref, 1) trailer dirInfo ref, 2) output file ref # Returns: 1 on success sub WriteTrailerBuffer($$$) { my ($self, $trailInfo, $outfile) = @_; if ($$self{DEL_GROUP}{Trailer}) { $self->VPrint(0, " Deleting trailer ($$trailInfo{Offset} bytes)\n"); ++$$self{CHANGED}; return 1; } my $pos = Tell($outfile); my $trailPt = $$trailInfo{OutFile}; # apply fixup if necessary (AFCP requires this) if ($$trailInfo{Fixup}) { if ($pos > 0) { # shift offsets to final AFCP location and write it out $$trailInfo{Fixup}{Shift} += $pos; $$trailInfo{Fixup}->ApplyFixup($trailPt); } else { $self->Error("Can't get file position for trailer offset fixup",1); } } return Write($outfile, $$trailPt); } #------------------------------------------------------------------------------ # Add trailers as a block # Inputs: 0) ExifTool object ref, 1) [optional] trailer data raf, # 1 or 2-N) trailer types to add (or none to add all) # Returns: new trailer ref, or undef # - increments CHANGED if trailer was added sub AddNewTrailers($;@) { my ($self, @types) = @_; my $trailPt; ref $types[0] and $trailPt = shift @types; $types[0] or shift @types; # (in case undef data ref is passed) # add all possible trailers if none specified (currently only CanonVRD) @types or @types = qw(CanonVRD CanonDR4); # add trailers as a block (if not done already) my $type; foreach $type (@types) { next unless $$self{NEW_VALUE}{$Image::ExifTool::Extra{$type}}; next if $$self{"Did$type"}; my $val = $self->GetNewValue($type) or next; # DR4 record must be wrapped in VRD trailer package if ($type eq 'CanonDR4') { next if $$self{DidCanonVRD}; # (only allow one VRD trailer) require Image::ExifTool::CanonVRD; $val = Image::ExifTool::CanonVRD::WrapDR4($val); $$self{DidCanonVRD} = 1; } my $verb = $trailPt ? 'Writing' : 'Adding'; $self->VPrint(0, " $verb $type as a block\n"); if ($trailPt) { $$trailPt .= $val; } else { $trailPt = \$val; } $$self{"Did$type"} = 1; ++$$self{CHANGED}; } return $trailPt; } #------------------------------------------------------------------------------ # Write segment, splitting up into multiple segments if necessary # Inputs: 0) file or scalar reference, 1) segment marker # 2) segment header, 3) segment data ref, 4) segment type # Returns: number of segments written, or 0 on error # Notes: Writes a single empty segment if data is empty sub WriteMultiSegment($$$$;$) { my ($outfile, $marker, $header, $dataPt, $type) = @_; $type or $type = ''; my $len = length($$dataPt); my $hdr = "\xff" . chr($marker); my $count = 0; my $maxLen = $maxSegmentLen - length($header); $maxLen -= 2 if $type eq 'ICC'; # leave room for segment counters my $num = int(($len + $maxLen - 1) / $maxLen); # number of segments to write my $n = 0; # write data, splitting into multiple segments if necessary # (each segment gets its own header) for (;;) { ++$count; my $size = $len - $n; if ($size > $maxLen) { $size = $maxLen; # avoid starting an Extended EXIF segment with a valid TIFF header # (because we would interpret that as a separate EXIF segment) --$size if $type eq 'EXIF' and $n+$maxLen <= $len-4 and substr($$dataPt, $n+$maxLen, 4) =~ /^(MM\0\x2a|II\x2a\0)/; } my $buff = substr($$dataPt,$n,$size); $n += $size; $size += length($header); if ($type eq 'ICC') { $buff = pack('CC', $count, $num) . $buff; $size += 2; } # write the new segment with appropriate header my $segHdr = $hdr . pack('n', $size + 2); Write($outfile, $segHdr, $header, $buff) or return 0; last if $n >= $len; } return $count; } #------------------------------------------------------------------------------ # Write XMP segment(s) to JPEG file # Inputs: 0) ExifTool object ref, 1) outfile ref, 2) XMP data ref, # 3) extended XMP data ref, 4) 32-char extended XMP GUID (or undef if no extended data) # Returns: true on success, false on write error sub WriteMultiXMP($$$$$) { my ($self, $outfile, $dataPt, $extPt, $guid) = @_; my $success = 1; # write main XMP segment my $size = length($$dataPt) + length($xmpAPP1hdr); if ($size > $maxXMPLen) { $self->Error("XMP block too large for JPEG segment! ($size bytes)", 1); return 1; } my $app1hdr = "\xff\xe1" . pack('n', $size + 2); Write($outfile, $app1hdr, $xmpAPP1hdr, $$dataPt) or $success = 0; # write extended XMP segment(s) if necessary if (defined $guid) { $size = length($$extPt); my $maxLen = $maxXMPLen - 75; # maximum size without 75-byte header my $off; for ($off=0; $off<$size; $off+=$maxLen) { # header(75) = signature(35) + guid(32) + size(4) + offset(4) my $len = $size - $off; $len = $maxLen if $len > $maxLen; $app1hdr = "\xff\xe1" . pack('n', $len + 75 + 2); $self->VPrint(0, "Writing extended XMP segment ($len bytes)\n"); Write($outfile, $app1hdr, $xmpExtAPP1hdr, $guid, pack('N2', $size, $off), substr($$extPt, $off, $len)) or $success = 0; } } return $success; } #------------------------------------------------------------------------------ # WriteJPEG : Write JPEG image # Inputs: 0) ExifTool object reference, 1) dirInfo reference # Returns: 1 on success, 0 if this wasn't a valid JPEG file, or -1 if # an output file was specified and a write error occurred sub WriteJPEG($$) { my ($self, $dirInfo) = @_; my $outfile = $$dirInfo{OutFile}; my $raf = $$dirInfo{RAF}; my ($ch, $s, $length,$err, %doneDir, $isEXV, $creatingEXV); my $verbose = $$self{OPTIONS}{Verbose}; my $out = $$self{OPTIONS}{TextOut}; my $rtnVal = 0; my ($writeBuffer, $oldOutfile); # used to buffer writing until PreviewImage position is known # check to be sure this is a valid JPG or EXV file unless ($raf->Read($s,2) == 2 and $s eq "\xff\xd8") { if (defined $s and length $s) { return 0 unless $s eq "\xff\x01" and $raf->Read($s,5) == 5 and $s eq 'Exiv2'; } else { return 0 unless $$self{FILE_TYPE} eq 'EXV'; $s = 'Exiv2'; $creatingEXV = 1; } Write($outfile,"\xff\x01") or $err = 1; $isEXV = 1; } delete $$self{PREVIEW_INFO}; # reset preview information delete $$self{DEL_PREVIEW}; # reset flag to delete preview Write($outfile, $s) or $err = 1; # figure out what segments we need to write for the tags we have set my $addDirs = $$self{ADD_DIRS}; my $editDirs = $$self{EDIT_DIRS}; my $delGroup = $$self{DEL_GROUP}; my $path = $$self{PATH}; my $pn = scalar @$path; # set input record separator to 0xff (the JPEG marker) to make reading quicker local $/ = "\xff"; # # pre-scan image to determine if any create-able segment already exists # my $pos = $raf->Tell(); my ($marker, @dirOrder, %dirCount); Prescan: for (;;) { # read up to next marker (JPEG markers begin with 0xff) $raf->ReadLine($s) or last; # JPEG markers can be padded with unlimited 0xff's for (;;) { $raf->Read($ch, 1) or last Prescan; $marker = ord($ch); last unless $marker == 0xff; } my $dirName; # stop pre-scan at SOS (end of meta information) or EOI (end of image) if ($marker == 0xda or $marker == 0xd9) { $dirName = $jpegMarker{$marker}; push(@dirOrder, $dirName); $dirCount{$dirName} = 1; last; } # handle SOF markers: SOF0-SOF15, except DHT(0xc4), JPGA(0xc8) and DAC(0xcc) if (($marker & 0xf0) == 0xc0 and ($marker == 0xc0 or $marker & 0x03)) { last unless $raf->Seek(7, 1); # read data for all markers except stand-alone # markers 0x00, 0x01 and 0xd0-0xd7 (NULL, TEM, RST0-RST7) } elsif ($marker!=0x00 and $marker!=0x01 and ($marker<0xd0 or $marker>0xd7)) { # read record length word last unless $raf->Read($s, 2) == 2; my $len = unpack('n',$s); # get data length last unless defined($len) and $len >= 2; $len -= 2; # subtract size of length word if (($marker & 0xf0) == 0xe0) { # is this an APP segment? my $n = $len < 64 ? $len : 64; $raf->Read($s, $n) == $n or last; $len -= $n; # Note: only necessary to recognize APP segments that we can create, # or delete as a group (and the names below should match @delGroups) if ($marker == 0xe0) { $s =~ /^JFIF\0/ and $dirName = 'JFIF'; $s =~ /^JFXX\0\x10/ and $dirName = 'JFXX'; $s =~ /^(II|MM).{4}HEAPJPGM/s and $dirName = 'CIFF'; } elsif ($marker == 0xe1) { if ($s =~ /^(.{0,4})Exif\0.(.{1,4})/is) { $dirName = 'IFD0'; my ($junk, $bytes) = ($1, $2); # support multi-segment EXIF if (@dirOrder and $dirOrder[-1] =~ /^(IFD0|ExtendedEXIF)$/ and not length $junk and $bytes !~ /^(MM\0\x2a|II\x2a\0)/) { $dirName = 'ExtendedEXIF'; } } $s =~ /^$xmpAPP1hdr/ and $dirName = 'XMP'; $s =~ /^$xmpExtAPP1hdr/ and $dirName = 'XMP'; } elsif ($marker == 0xe2) { $s =~ /^ICC_PROFILE\0/ and $dirName = 'ICC_Profile'; $s =~ /^FPXR\0/ and $dirName = 'FlashPix'; $s =~ /^MPF\0/ and $dirName = 'MPF'; } elsif ($marker == 0xe3) { $s =~ /^(Meta|META|Exif)\0\0/ and $dirName = 'Meta'; } elsif ($marker == 0xe5) { $s =~ /^RMETA\0/ and $dirName = 'RMETA'; } elsif ($marker == 0xeb) { $s =~ /^JP/ and $dirName = 'JUMBF'; } elsif ($marker == 0xec) { $s =~ /^Ducky/ and $dirName = 'Ducky'; } elsif ($marker == 0xed) { $s =~ /^$psAPP13hdr/ and $dirName = 'Photoshop'; } elsif ($marker == 0xee) { $s =~ /^Adobe/ and $dirName = 'Adobe'; } # initialize doneDir as a flag that the directory exists # (unless we are deleting it anyway) $doneDir{$dirName} = 0 if defined $dirName and not $$delGroup{$dirName}; } $raf->Seek($len, 1) or last; } $dirName or $dirName = JpegMarkerName($marker); $dirCount{$dirName} = ($dirCount{$dirName} || 0) + 1; push @dirOrder, $dirName; } unless ($marker and $marker == 0xda) { $isEXV or $self->Error('Corrupted JPEG image'), return 1; $marker and $marker != 0xd9 and $self->Error('Corrupted EXV file'), return 1; } $raf->Seek($pos, 0) or $self->Error('Seek error'), return 1; # # re-write the image # my ($combinedSegData, $segPos, $firstSegPos, %extendedXMP); my (@iccChunk, $iccChunkCount, $iccChunksTotal); # read through each segment in the JPEG file Marker: for (;;) { # read up to next marker (JPEG markers begin with 0xff) my $segJunk; $raf->ReadLine($segJunk) or $segJunk = ''; # remove the 0xff but write the rest of the junk up to this point # (this will handle the data after the first 7 bytes of SOF segments) chomp($segJunk); Write($outfile, $segJunk) if length $segJunk; # JPEG markers can be padded with unlimited 0xff's for (;;) { if ($raf->Read($ch, 1)) { $marker = ord($ch); last unless $marker == 0xff; } elsif ($creatingEXV) { # create EXV from scratch $marker = 0xd9; # EOI push @dirOrder, 'EOI'; $dirCount{EOI} = 1; last; } else { $self->Error('Format error'); return 1; } } # read the segment data my $segData; # handle SOF markers: SOF0-SOF15, except DHT(0xc4), JPGA(0xc8) and DAC(0xcc) if (($marker & 0xf0) == 0xc0 and ($marker == 0xc0 or $marker & 0x03)) { last unless $raf->Read($segData, 7) == 7; # read data for all markers except stand-alone # markers 0x00, 0x01 and 0xd0-0xd7 (NULL, TEM, EOI, RST0-RST7) } elsif ($marker!=0x00 and $marker!=0x01 and $marker!=0xd9 and ($marker<0xd0 or $marker>0xd7)) { # read record length word last unless $raf->Read($s, 2) == 2; my $len = unpack('n',$s); # get data length last unless defined($len) and $len >= 2; $segPos = $raf->Tell(); $len -= 2; # subtract size of length word last unless $raf->Read($segData, $len) == $len; } # initialize variables for this segment my $hdr = "\xff" . chr($marker); # segment header my $markerName = JpegMarkerName($marker); my $dirName = shift @dirOrder; # get directory name # # create all segments that must come before this one # (nothing comes before SOI or after SOS) # while ($markerName ne 'SOI') { if (exists $$addDirs{JFIF} and not defined $doneDir{JFIF}) { $doneDir{JFIF} = 1; if (defined $doneDir{Adobe}) { # JFIF overrides Adobe APP14 colour components, so don't allow this # (ref https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html) $self->Warn('Not creating JFIF in JPEG with Adobe APP14'); } else { if ($verbose) { print $out "Creating APP0:\n"; print $out " Creating JFIF with default values\n"; } my $jfif = "\x01\x02\x01\0\x48\0\x48\0\0"; SetByteOrder('MM'); my $tagTablePtr = GetTagTable('Image::ExifTool::JFIF::Main'); my %dirInfo = ( DataPt => \$jfif, DirStart => 0, DirLen => length $jfif, Parent => 'JFIF', ); # must temporarily remove JFIF from DEL_GROUP so we can # delete JFIF and add it back again in a single step my $delJFIF = $$delGroup{JFIF}; delete $$delGroup{JFIF}; $$path[$pn] = 'JFIF'; my $newData = $self->WriteDirectory(\%dirInfo, $tagTablePtr); $$delGroup{JFIF} = $delJFIF if defined $delJFIF; if (defined $newData and length $newData) { my $app0hdr = "\xff\xe0" . pack('n', length($newData) + 7); Write($outfile,$app0hdr,"JFIF\0",$newData) or $err = 1; } } } # don't create anything before APP0 or APP1 EXIF (containing IFD0) last if $markerName eq 'APP0' or $dirCount{IFD0} or $dirCount{ExtendedEXIF}; # EXIF information must come immediately after APP0 if (exists $$addDirs{IFD0} and not defined $doneDir{IFD0}) { $doneDir{IFD0} = 1; $verbose and print $out "Creating APP1:\n"; # write new EXIF data $$self{TIFF_TYPE} = 'APP1'; my $tagTablePtr = GetTagTable('Image::ExifTool::Exif::Main'); my %dirInfo = ( DirName => 'IFD0', Parent => 'APP1', ); $$path[$pn] = 'APP1'; my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr, \&WriteTIFF); if (defined $buff and length $buff) { if (length($buff) + length($exifAPP1hdr) > $maxSegmentLen) { if ($self->Options('NoMultiExif')) { $self->Error('EXIF is too large for JPEG segment'); } else { $self->Warn('Creating multi-segment EXIF',1); } } # switch to buffered output if required if (($$self{PREVIEW_INFO} or $$self{LeicaTrailer} or $$self{HiddenData}) and not $oldOutfile) { $writeBuffer = ''; $oldOutfile = $outfile; $outfile = \$writeBuffer; # account for segment, EXIF and TIFF headers $$self{PREVIEW_INFO}{Fixup}{Start} += 18 if $$self{PREVIEW_INFO}; $$self{LeicaTrailer}{Fixup}{Start} += 18 if $$self{LeicaTrailer}; $$self{HiddenData}{Fixup}{Start} += 18 if $$self{HiddenData}; } # write as multi-segment my $n = WriteMultiSegment($outfile, 0xe1, $exifAPP1hdr, \$buff, 'EXIF'); if (not $n) { $err = 1; } elsif ($n > 1 and $oldOutfile) { # (punt on this because updating the pointers would be a real pain) $self->Error("Can't write multi-segment EXIF with external pointers"); } ++$$self{CHANGED}; } } # APP13 Photoshop segment next last if $dirCount{Photoshop}; if (exists $$addDirs{Photoshop} and not defined $doneDir{Photoshop}) { $doneDir{Photoshop} = 1; $verbose and print $out "Creating APP13:\n"; # write new APP13 Photoshop record to memory my $tagTablePtr = GetTagTable('Image::ExifTool::Photoshop::Main'); my %dirInfo = ( Parent => 'APP13', ); $$path[$pn] = 'APP13'; my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $buff and length $buff) { WriteMultiSegment($outfile, 0xed, $psAPP13hdr, \$buff) or $err = 1; ++$$self{CHANGED}; } } # then APP1 XMP segment last if $dirCount{XMP}; if (exists $$addDirs{XMP} and not defined $doneDir{XMP}) { $doneDir{XMP} = 1; $verbose and print $out "Creating APP1:\n"; # write new XMP data my $tagTablePtr = GetTagTable('Image::ExifTool::XMP::Main'); my %dirInfo = ( Parent => 'APP1', # specify MaxDataLen so XMP is split if required MaxDataLen => $maxXMPLen - length($xmpAPP1hdr), ); $$path[$pn] = 'APP1'; my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $buff and length $buff) { WriteMultiXMP($self, $outfile, \$buff, $dirInfo{ExtendedXMP}, $dirInfo{ExtendedGUID}) or $err = 1; } } # then APP2 ICC_Profile segment last if $dirCount{ICC_Profile}; if (exists $$addDirs{ICC_Profile} and not defined $doneDir{ICC_Profile}) { $doneDir{ICC_Profile} = 1; next if $$delGroup{ICC_Profile} and $$delGroup{ICC_Profile} != 2; $verbose and print $out "Creating APP2:\n"; # write new ICC_Profile data my $tagTablePtr = GetTagTable('Image::ExifTool::ICC_Profile::Main'); my %dirInfo = ( Parent => 'APP2', ); $$path[$pn] = 'APP2'; my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $buff and length $buff) { WriteMultiSegment($outfile, 0xe2, "ICC_PROFILE\0", \$buff, 'ICC') or $err = 1; ++$$self{CHANGED}; } } # then APP12 Ducky segment last if $dirCount{Ducky}; if (exists $$addDirs{Ducky} and not defined $doneDir{Ducky}) { $doneDir{Ducky} = 1; $verbose and print $out "Creating APP12 Ducky:\n"; # write new Ducky segment data my $tagTablePtr = GetTagTable('Image::ExifTool::APP12::Ducky'); my %dirInfo = ( Parent => 'APP12', ); $$path[$pn] = 'APP12'; my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $buff and length $buff) { my $size = length($buff) + 5; if ($size <= $maxSegmentLen) { # write the new segment with appropriate header my $app12hdr = "\xff\xec" . pack('n', $size + 2); Write($outfile, $app12hdr, 'Ducky', $buff) or $err = 1; } else { $self->Warn("APP12 Ducky segment too large! ($size bytes)"); } } } # then APP14 Adobe segment last if $dirCount{Adobe}; if (exists $$addDirs{Adobe} and not defined $doneDir{Adobe}) { $doneDir{Adobe} = 1; my $buff = $self->GetNewValue('Adobe'); if ($buff) { $verbose and print $out "Creating APP14:\n Creating Adobe segment\n"; my $size = length($buff); if ($size <= $maxSegmentLen) { # write the new segment with appropriate header my $app14hdr = "\xff\xee" . pack('n', $size + 2); Write($outfile, $app14hdr, $buff) or $err = 1; ++$$self{CHANGED}; } else { $self->Warn("APP14 Adobe segment too large! ($size bytes)"); } } } # finally, COM segment last if $dirCount{COM}; if (exists $$addDirs{COM} and not defined $doneDir{COM}) { $doneDir{COM} = 1; next if $$delGroup{File} and $$delGroup{File} != 2; my $newComment = $self->GetNewValue('Comment'); if (defined $newComment) { if ($verbose) { print $out "Creating COM:\n"; $self->VerboseValue('+ Comment', $newComment); } WriteMultiSegment($outfile, 0xfe, '', \$newComment) or $err = 1; ++$$self{CHANGED}; } } last; # didn't want to loop anyway } $$path[$pn] = $markerName; # decrement counter for this directory since we are about to process it --$dirCount{$dirName}; # # rewrite existing segments # # handle SOF markers: SOF0-SOF15, except DHT(0xc4), JPGA(0xc8) and DAC(0xcc) if (($marker & 0xf0) == 0xc0 and ($marker == 0xc0 or $marker & 0x03)) { $verbose and print $out "JPEG $markerName:\n"; Write($outfile, $hdr, $segData) or $err = 1; next; } elsif ($marker == 0xda) { # SOS pop @$path; $verbose and print $out "JPEG SOS\n"; # write SOS segment $s = pack('n', length($segData) + 2); Write($outfile, $hdr, $s, $segData) or $err = 1; my ($buff, $endPos, $trailInfo); my $delPreview = $$self{DEL_PREVIEW}; $trailInfo = IdentifyTrailer($raf) unless $$delGroup{Trailer}; my $nvTrail = $self->GetNewValueHash($Image::ExifTool::Extra{Trailer}); unless ($oldOutfile or $delPreview or $trailInfo or $$delGroup{Trailer} or $nvTrail or $$self{HiddenData}) { # blindly copy the rest of the file while ($raf->Read($buff, 65536)) { Write($outfile, $buff) or $err = 1, last; } $rtnVal = 1; # success unless we have a file write error last; # all done } # write the rest of the image (as quickly as possible) up to the EOI my $endedWithFF; for (;;) { my $n = $raf->Read($buff, 65536) or last Marker; if (($endedWithFF and $buff =~ m/^\xd9/sg) or $buff =~ m/\xff\xd9/sg) { $rtnVal = 1; # the JPEG is OK # write up to the EOI my $pos = pos($buff); Write($outfile, substr($buff, 0, $pos)) or $err = 1; $buff = substr($buff, $pos); last; } unless ($n == 65536) { $self->Error('JPEG EOI marker not found'); last Marker; } Write($outfile, $buff) or $err = 1; $endedWithFF = substr($buff, 65535, 1) eq "\xff" ? 1 : 0; } # remember position of last data copied $endPos = $$self{TrailerStart} = $raf->Tell() - length($buff); # write new trailer if specified if ($nvTrail) { # access new value directly to avoid copying a potentially very large data block if ($$nvTrail{Value} and $$nvTrail{Value}[0]) { # (note: "0" will also delete the trailer) $self->VPrint(0, ' Writing new trailer'); Write($outfile, $$nvTrail{Value}[0]) or $err = 1; ++$$self{CHANGED}; } elsif ($raf->Seek(0, 2) and $raf->Tell() != $endPos) { $self->VPrint(0, ' Deleting trailer (', $raf->Tell() - $endPos, ' bytes)'); ++$$self{CHANGED}; # changed if there was previously a trailer } last; # all done } # copy HiddenData if necessary if ($$self{HiddenData}) { my $pad; my $hd = $$self{HiddenData}; my $hdOff = $$hd{Offset} + $$hd{Base}; require Image::ExifTool::Sony; # read HiddenData, updating $hdOff with actual offset if necessary my $dataPt = Image::ExifTool::Sony::ReadHiddenData($self, $hdOff, $$hd{Size}); if ($dataPt) { # preserve padding to avoid invalidating MPF pointers (yuk!) my $padLen = $hdOff - $endPos; unless ($padLen >= 0 and $raf->Seek($endPos,0) and $raf->Read($pad,$padLen)==$padLen) { $self->Error('Error reading HiddenData padding',1); $pad = ''; } $endPos += length($pad) + length($$dataPt); # update end position } else { $$dataPt = $pad = ''; } my $fixup = $$self{HiddenData}{Fixup}; # set MakerNote pointer and size (subtract 10 for segment and EXIF headers) $fixup->SetMarkerPointers($outfile, 'HiddenData', length($$outfile) + length($pad) - 10); # clean up and write the buffered data $outfile = $oldOutfile; undef $oldOutfile; Write($outfile, $writeBuffer, $pad, $$dataPt) or $err = 1; undef $writeBuffer; } # rewrite existing trailers if ($trailInfo) { my $tbuf = ''; $raf->Seek(-length($buff), 1); # seek back to just after EOI $$trailInfo{OutFile} = \$tbuf; # rewrite the trailer $$trailInfo{ScanForAFCP} = 1; # scan if necessary $self->ProcessTrailers($trailInfo) or undef $trailInfo; } if (not $oldOutfile) { # do nothing special } elsif ($$self{LeicaTrailer}) { my $trailLen; if ($trailInfo) { $trailLen = $$trailInfo{DataPos} - $endPos; } else { $raf->Seek(0, 2) or $err = 1; $trailLen = $raf->Tell() - $endPos; } my $fixup = $$self{LeicaTrailer}{Fixup}; $$self{LeicaTrailer}{TrailPos} = $endPos; $$self{LeicaTrailer}{TrailLen} = $trailLen; # get _absolute_ position of new Leica trailer my $absPos = Tell($oldOutfile) + length($$outfile); require Image::ExifTool::Panasonic; my $dat = Image::ExifTool::Panasonic::ProcessLeicaTrailer($self, $absPos); # allow some junk before Leica trailer (just in case) my $junk = $$self{LeicaTrailerPos} - $endPos; # set MakerNote pointer and size (subtract 10 for segment and EXIF headers) $fixup->SetMarkerPointers($outfile, 'LeicaTrailer', length($$outfile) - 10 + $junk); # use this fixup to set the size too (sneaky) my $trailSize = defined($dat) ? length($dat) - $junk : $$self{LeicaTrailer}{Size}; $$fixup{Start} -= 4; $$fixup{Shift} += 4; $fixup->SetMarkerPointers($outfile, 'LeicaTrailer', $trailSize) if defined $trailSize; $$fixup{Start} += 4; $$fixup{Shift} -= 4; # clean up and write the buffered data $outfile = $oldOutfile; undef $oldOutfile; Write($outfile, $writeBuffer) or $err = 1; undef $writeBuffer; if (defined $dat) { Write($outfile, $dat) or $err = 1; # write new Leica trailer $delPreview = 1; # delete existing Leica trailer } } else { # locate preview image and fix up preview offsets my $scanLen = $$self{Make} =~ /^SONY/i ? 65536 : 1024; if (length($buff) < $scanLen) { # make sure we have enough trailer to scan my $buf2; $buff .= $buf2 if $raf->Read($buf2, $scanLen - length($buff)); } # get new preview image position, relative to EXIF base my $newPos = length($$outfile) - 10; # (subtract 10 for segment and EXIF headers) my $junkLen; # adjust position if image isn't at the start (eg. Olympus E-1/E-300) if ($buff =~ /(\xff\xd8\xff.|.\xd8\xff\xdb)(..)/sg) { my ($jpegHdr, $segLen) = ($1, $2); $junkLen = pos($buff) - 6; # Sony previewimage trailer has a 32 byte header if ($$self{Make} =~ /^SONY/i and $junkLen > 32) { # with some newer Sony models, the makernotes preview pointer # points to JPEG at end of EXIF inside MPImage preview (what a pain!) if ($jpegHdr eq "\xff\xd8\xff\xe1") { # is the first segment EXIF? $segLen = unpack('n', $segLen); # the EXIF segment length # Sony PreviewImage starts with last 2 bytes of EXIF segment # (and first byte is usually "\0", not "\xff", so don't check this) if (length($buff) > $junkLen + $segLen + 6 and substr($buff, $junkLen + $segLen + 3, 3) eq "\xd8\xff\xdb") { $junkLen += $segLen + 2; # (note: this will not copy the trailer after PreviewImage, # which is a 14kB block full of zeros for the A77) } } $junkLen -= 32; } $newPos += $junkLen; } # fix up the preview offsets to point to the start of the new image my $previewInfo = $$self{PREVIEW_INFO}; delete $$self{PREVIEW_INFO}; my $fixup = $$previewInfo{Fixup}; $newPos += ($$previewInfo{BaseShift} || 0); # adjust to absolute file offset if necessary (Samsung STMN) $newPos += Tell($oldOutfile) + 10 if $$previewInfo{Absolute}; if ($$previewInfo{Relative}) { # adjust for our base by looking at how far the pointer got shifted $newPos -= ($fixup->GetMarkerPointers($outfile, 'PreviewImage') || 0); } elsif ($$previewInfo{ChangeBase}) { # Leica S2 uses relative offsets for the preview only (leica sucks) my $makerOffset = $fixup->GetMarkerPointers($outfile, 'LeicaTrailer'); $newPos -= $makerOffset if $makerOffset; } $fixup->SetMarkerPointers($outfile, 'PreviewImage', $newPos); # clean up and write the buffered data $outfile = $oldOutfile; undef $oldOutfile; Write($outfile, $writeBuffer) or $err = 1; undef $writeBuffer; # write preview image if ($$previewInfo{Data} ne 'LOAD_PREVIEW') { # write any junk that existed before the preview image Write($outfile, substr($buff,0,$junkLen)) or $err = 1 if $junkLen; # write the saved preview image Write($outfile, $$previewInfo{Data}) or $err = 1; delete $$previewInfo{Data}; # (don't increment CHANGED because we could be rewriting existing preview) $delPreview = 1; # remove old preview } } # copy over preview image (or other data) if necessary unless ($delPreview) { my $extra; if ($trailInfo) { # copy everything up to start of first processed trailer $extra = $$trailInfo{DataPos} - $endPos; } else { # copy everything up to end of file $raf->Seek(0, 2) or $err = 1; $extra = $raf->Tell() - $endPos; } if ($extra > 0) { if ($$delGroup{Trailer}) { $verbose and print $out " Deleting unknown trailer ($extra bytes)\n"; ++$$self{CHANGED}; } else { # copy over unknown trailer $verbose and print $out " Preserving unknown trailer ($extra bytes)\n"; $raf->Seek($endPos, 0) or $err = 1; CopyBlock($raf, $outfile, $extra) or $err = 1; } } } # write trailer if necessary if ($trailInfo) { $self->WriteTrailerBuffer($trailInfo, $outfile) or $err = 1; undef $trailInfo; } last; # all done parsing file } elsif ($marker==0xd9 and $isEXV) { # write EXV EOI (any trailer will be lost) Write($outfile, "\xff\xd9") or $err = 1; $rtnVal = 1; last; } elsif ($marker==0x00 or $marker==0x01 or ($marker>=0xd0 and $marker<=0xd7)) { $verbose and $marker and print $out "JPEG $markerName:\n"; # handle stand-alone markers 0x00, 0x01 and 0xd0-0xd7 (NULL, TEM, RST0-RST7) Write($outfile, $hdr) or $err = 1; next; } # # NOTE: A 'next' statement after this point will cause $$segDataPt # not to be written if there is an output file, so in this case # the $$self{CHANGED} flags must be updated # my $segDataPt = \$segData; $length = length($segData); print $out "JPEG $markerName ($length bytes)\n" if $verbose; # group delete of APP segments if ($$delGroup{$dirName}) { $verbose and print $out " Deleting $dirName segment\n"; $self->Warn('ICC_Profile deleted. Image colors may be affected') if $dirName eq 'ICC_Profile'; ++$$self{CHANGED}; next Marker; } my ($segType, $del); # rewrite this segment only if we are changing a tag which is contained in its # directory (or deleting '*', in which case we need to identify the segment type) while (exists $$editDirs{$markerName} or $$delGroup{'*'}) { if ($marker == 0xe0) { # APP0 (JFIF, CIFF) if ($$segDataPt =~ /^JFIF\0/) { $segType = 'JFIF'; $$delGroup{JFIF} and $del = 1, last; last unless $$editDirs{JFIF}; SetByteOrder('MM'); my $tagTablePtr = GetTagTable('Image::ExifTool::JFIF::Main'); my %dirInfo = ( DataPt => $segDataPt, DataPos => $segPos, DataLen => $length, DirStart => 5, # directory starts after identifier DirLen => $length-5, Parent => $markerName, ); my $newData = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $newData and length $newData) { $$segDataPt = "JFIF\0" . $newData; } } elsif ($$segDataPt =~ /^JFXX\0\x10/) { $segType = 'JFXX'; $$delGroup{JFIF} and $del = 1; } elsif ($$segDataPt =~ /^(II|MM).{4}HEAPJPGM/s) { $segType = 'CIFF'; $$delGroup{CIFF} and $del = 1, last; last unless $$editDirs{CIFF}; my $newData = ''; my %dirInfo = ( RAF => File::RandomAccess->new($segDataPt), OutFile => \$newData, ); require Image::ExifTool::CanonRaw; if (Image::ExifTool::CanonRaw::WriteCRW($self, \%dirInfo) > 0) { if (length $newData) { $$segDataPt = $newData; } else { undef $segDataPt; $del = 1; # delete this segment } } } } elsif ($marker == 0xe1) { # APP1 (EXIF, XMP) # check for EXIF data if ($$segDataPt =~ /^(.{0,4})Exif\0./is) { my $hdrLen = length $exifAPP1hdr; if (length $1) { $hdrLen += length $1; $self->Error('Unknown garbage at start of EXIF segment',1); } elsif ($$segDataPt !~ /^Exif\0/) { $self->Error('Incorrect EXIF segment identifier',1); } $segType = 'EXIF'; last unless $$editDirs{IFD0}; # add this data to the combined data if it exists if (defined $combinedSegData) { $combinedSegData .= substr($$segDataPt,$hdrLen); $segDataPt = \$combinedSegData; $segPos = $firstSegPos; $length = length $combinedSegData; # update length } # peek ahead to see if the next segment is extended EXIF if ($dirOrder[0] eq 'ExtendedEXIF') { # initialize combined data if necessary unless (defined $combinedSegData) { $combinedSegData = $$segDataPt; $firstSegPos = $segPos; $self->Warn('File contains multi-segment EXIF',1); } next Marker; # get the next segment to combine } $doneDir{IFD0} and $self->Warn('Multiple APP1 EXIF records'); $doneDir{IFD0} = 1; # check del groups now so we can change byte order in one step if ($$delGroup{IFD0} or $$delGroup{EXIF}) { delete $doneDir{IFD0}; # delete so we will create a new one $del = 1; last; } # rewrite EXIF as if this were a TIFF file in memory my %dirInfo = ( DataPt => $segDataPt, DataPos => -$hdrLen, # (remember: relative to Base!) DirStart => $hdrLen, Base => $segPos + $hdrLen, Parent => $markerName, DirName => 'IFD0', ); # write new EXIF data to memory my $tagTablePtr = GetTagTable('Image::ExifTool::Exif::Main'); my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr, \&WriteTIFF); if (defined $buff) { undef $$segDataPt; # free the old buffer $segDataPt = \$buff; } else { last Marker unless $self->Options('IgnoreMinorErrors'); } # delete segment if IFD contains no entries length $$segDataPt or $del = 1, last; if (length($$segDataPt) + length($exifAPP1hdr) > $maxSegmentLen) { if ($self->Options('NoMultiExif')) { $self->Error('EXIF is too large for JPEG segment'); } else { $self->Warn('Writing multi-segment EXIF',1); } } # switch to buffered output if required if (($$self{PREVIEW_INFO} or $$self{LeicaTrailer} or $$self{HiddenData}) and not $oldOutfile) { $writeBuffer = ''; $oldOutfile = $outfile; $outfile = \$writeBuffer; # must account for segment, EXIF and TIFF headers $$self{PREVIEW_INFO}{Fixup}{Start} += 18 if $$self{PREVIEW_INFO}; $$self{LeicaTrailer}{Fixup}{Start} += 18 if $$self{LeicaTrailer}; $$self{HiddenData}{Fixup}{Start} += 18 if $$self{HiddenData}; } # write as multi-segment my $n = WriteMultiSegment($outfile, $marker, $exifAPP1hdr, $segDataPt, 'EXIF'); if (not $n) { $err = 1; } elsif ($n > 1 and $oldOutfile) { # (punt on this because updating the pointers would be a real pain) $self->Error("Can't write multi-segment EXIF with external pointers"); } undef $combinedSegData; undef $$segDataPt; next Marker; # check for XMP data } elsif ($$segDataPt =~ /^($xmpAPP1hdr|$xmpExtAPP1hdr)/) { $segType = 'XMP'; $$delGroup{XMP} and $del = 1, last; $doneDir{XMP} = ($doneDir{XMP} || 0) + 1; last unless $$editDirs{XMP}; if ($doneDir{XMP} + $dirCount{XMP} > 1) { # must assemble all XMP segments before writing my ($guid, $extXMP); if ($$segDataPt =~ /^$xmpExtAPP1hdr/) { # save extended XMP data if (length $$segDataPt < 75) { $extendedXMP{Error} = 'Truncated data'; } else { my ($size, $off) = unpack('x67N2', $$segDataPt); $guid = substr($$segDataPt, 35, 32); if ($guid =~ /[^A-Za-z0-9]/) { # (technically, should be uppercase) $extendedXMP{Error} = 'Invalid GUID'; } else { # remember extended data for each GUID $extXMP = $extendedXMP{$guid}; if ($extXMP) { $size == $$extXMP{Size} or $extendedXMP{Error} = 'Inconsistent size'; } else { $extXMP = $extendedXMP{$guid} = { }; } $$extXMP{Size} = $size; $$extXMP{$off} = substr($$segDataPt, 75); } } } else { # save all main XMP segments (should normally be only one) $extendedXMP{Main} = [] unless $extendedXMP{Main}; push @{$extendedXMP{Main}}, substr($$segDataPt, length $xmpAPP1hdr); } # continue processing only if we have read all the segments next Marker if $dirCount{XMP}; # reconstruct an XMP super-segment $$segDataPt = $xmpAPP1hdr; my $goodGuid = ''; foreach (@{$extendedXMP{Main}}) { # get the HasExtendedXMP GUID if it exists if (/:HasExtendedXMP\s*(=\s*['"]|>)(\w{32})/) { # warn of subsequent XMP blocks specifying a different # HasExtendedXMP (have never seen this) if ($goodGuid and $goodGuid ne $2) { $self->Warn('Multiple XMP segments specifying different extended XMP GUID'); } $goodGuid = $2; # GUID for the standard extended XMP } $$segDataPt .= $_; } # GUID of the extended XMP that we want to read my $readGuid = $$self{OPTIONS}{ExtendedXMP} || 0; $readGuid = $goodGuid if $readGuid eq '1'; foreach $guid (sort keys %extendedXMP) { next unless length $guid == 32; # ignore other (internal) keys if ($guid ne $readGuid and $readGuid ne '2') { my $non = $guid eq $goodGuid ? '' : 'non-'; $self->Warn("Ignored ${non}standard extended XMP (GUID $guid)"); next; } if ($guid ne $goodGuid) { $self->Warn("Reading non-standard extended XMP (GUID $guid)"); } $extXMP = $extendedXMP{$guid}; next unless ref $extXMP eq 'HASH'; # (just to be safe) my $size = $$extXMP{Size}; my (@offsets, $off); for ($off=0; $off<$size; ) { last unless defined $$extXMP{$off}; push @offsets, $off; $off += length $$extXMP{$off}; } if ($off == $size) { # add all XMP to super-segment $$segDataPt .= $$extXMP{$_} foreach @offsets; } else { $self->Error("Incomplete extended XMP (GUID $guid)", 1); } } $self->Error("$extendedXMP{Error} in extended XMP", 1) if $extendedXMP{Error}; } my $start = length $xmpAPP1hdr; my $tagTablePtr = GetTagTable('Image::ExifTool::XMP::Main'); my %dirInfo = ( DataPt => $segDataPt, DirStart => $start, Parent => $markerName, # limit XMP size and create extended XMP if necessary MaxDataLen => $maxXMPLen - length($xmpAPP1hdr), ); my $newData = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $newData) { undef %extendedXMP; if (length $newData) { # write multi-segment XMP (XMP plus extended XMP if necessary) WriteMultiXMP($self, $outfile, \$newData, $dirInfo{ExtendedXMP}, $dirInfo{ExtendedGUID}) or $err = 1; undef $$segDataPt; # free the old buffer next Marker; } else { $$segDataPt = ''; # delete the XMP } } else { $verbose and print $out " [XMP rewritten with no changes]\n"; if ($doneDir{XMP} > 1) { # re-write original multi-segment XMP my ($dat, $guid, $extXMP, $off); foreach $dat (@{$extendedXMP{Main}}) { # main XMP next unless length $dat; $s = pack('n', length($xmpAPP1hdr) + length($dat) + 2); Write($outfile, $hdr, $s, $xmpAPP1hdr, $dat) or $err = 1; } foreach $guid (sort keys %extendedXMP) { # extended XMP next unless length $guid == 32; $extXMP = $extendedXMP{$guid}; next unless ref $extXMP eq 'HASH'; my $size = $$extXMP{Size} or next; for ($off=0; defined $$extXMP{$off}; $off += length $$extXMP{$off}) { $s = pack('n', length($xmpExtAPP1hdr) + length($$extXMP{$off}) + 42); Write($outfile, $hdr, $s, $xmpExtAPP1hdr, $guid, pack('N2', $size, $off), $$extXMP{$off}) or $err = 1; } } undef $$segDataPt; # free the old buffer undef %extendedXMP; next Marker; } # continue on to re-write original single-segment XMP } $del = 1 unless length $$segDataPt; } elsif ($$segDataPt =~ /^http/ or $$segDataPt =~ /Warn('Ignored APP1 XMP segment with non-standard header', 1); } } elsif ($marker == 0xe2) { # APP2 (ICC Profile, FPXR, MPF) if ($$segDataPt =~ /^ICC_PROFILE\0/ and $length >= 14) { $segType = 'ICC_Profile'; $$delGroup{ICC_Profile} and $del = 1, last; # must concatenate blocks of profile my $chunkNum = Get8u($segDataPt, 12); my $chunksTot = Get8u($segDataPt, 13); if (defined $iccChunksTotal) { # abort parsing ICC_Profile if the total chunk count is inconsistent if ($chunksTot != $iccChunksTotal and defined $iccChunkCount) { # an error because the accumulated profile data will be lost $self->Error('Inconsistent ICC_Profile chunk count', 1); undef $iccChunkCount; # abort ICC_Profile parsing undef $chunkNum; # avoid 2nd warning below ++$$self{CHANGED}; # we are deleting the bad chunks before this one } } else { $iccChunkCount = 0; $iccChunksTotal = $chunksTot; $self->Warn('ICC_Profile chunk count is zero') if !$chunksTot; } if (defined $iccChunkCount) { # save this chunk if (defined $iccChunk[$chunkNum]) { $self->Warn("Duplicate ICC_Profile chunk number $chunkNum"); $iccChunk[$chunkNum] .= substr($$segDataPt, 14); } else { $iccChunk[$chunkNum] = substr($$segDataPt, 14); } # continue accumulating chunks unless we have all of them next Marker unless ++$iccChunkCount >= $iccChunksTotal; undef $iccChunkCount; # prevent reprocessing $doneDir{ICC_Profile} = 1; # combine the ICC_Profile chunks my $icc_profile = ''; defined $_ and $icc_profile .= $_ foreach @iccChunk; undef @iccChunk; # free memory $segDataPt = \$icc_profile; $length = length $icc_profile; my $tagTablePtr = GetTagTable('Image::ExifTool::ICC_Profile::Main'); my %dirInfo = ( DataPt => $segDataPt, DataPos => $segPos + 14, DataLen => $length, DirStart => 0, DirLen => $length, Parent => $markerName, ); my $newData = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $newData) { undef $$segDataPt; # free the old buffer $segDataPt = \$newData; } length $$segDataPt or $del = 1, last; # write as ICC multi-segment WriteMultiSegment($outfile, $marker, "ICC_PROFILE\0", $segDataPt, 'ICC') or $err = 1; undef $$segDataPt; next Marker; } elsif (defined $chunkNum) { $self->Warn('Invalid or extraneous ICC_Profile chunk(s)'); # fall through to preserve this extra profile... } } elsif ($$segDataPt =~ /^FPXR\0/) { $segType = 'FPXR'; $$delGroup{FlashPix} and $del = 1; } elsif ($$segDataPt =~ /^MPF\0/) { $segType = 'MPF'; $$delGroup{MPF} and $del = 1; } } elsif ($marker == 0xe3) { # APP3 (Kodak Meta) if ($$segDataPt =~ /^(Meta|META|Exif)\0\0/) { $segType = 'Kodak Meta'; $$delGroup{Meta} and $del = 1, last; $doneDir{Meta} and $self->Warn('Multiple APP3 Meta segments'); $doneDir{Meta} = 1; last unless $$editDirs{Meta}; # rewrite Meta IFD as if this were a TIFF file in memory my %dirInfo = ( DataPt => $segDataPt, DataPos => -6, # (remember: relative to Base!) DirStart => 6, Base => $segPos + 6, Parent => $markerName, DirName => 'Meta', ); # write new data to memory my $tagTablePtr = GetTagTable('Image::ExifTool::Kodak::Meta'); my $buff = $self->WriteDirectory(\%dirInfo, $tagTablePtr, \&WriteTIFF); if (defined $buff) { # update segment with new data $$segDataPt = substr($$segDataPt,0,6) . $buff; } else { last Marker unless $self->Options('IgnoreMinorErrors'); } # delete segment if IFD contains no entries $del = 1 unless length($$segDataPt) > 6; } } elsif ($marker == 0xe5) { # APP5 (Ricoh RMETA) if ($$segDataPt =~ /^RMETA\0/) { $segType = 'Ricoh RMETA'; $$delGroup{RMETA} and $del = 1; } } elsif ($marker == 0xe8 or $marker == 0xe9) { # APP8/9 (SEAL) if ($$segDataPt =~ /^SEAL\0/) { $segType = 'SEAL'; $$delGroup{SEAL} and $del = 1; } } elsif ($marker == 0xeb) { # APP10 (JUMBF) if ($$segDataPt =~ /^JP/) { $segType = 'JUMBF'; $$delGroup{JUMBF} and $del = 1; } } elsif ($marker == 0xec) { # APP12 (Ducky) if ($$segDataPt =~ /^Ducky/) { $segType = 'Ducky'; $$delGroup{Ducky} and $del = 1, last; $doneDir{Ducky} and $self->Warn('Multiple APP12 Ducky segments'); $doneDir{Ducky} = 1; last unless $$editDirs{Ducky}; my $tagTablePtr = GetTagTable('Image::ExifTool::APP12::Ducky'); my %dirInfo = ( DataPt => $segDataPt, DataPos => $segPos, DataLen => $length, DirStart => 5, # directory starts after identifier DirLen => $length-5, Parent => $markerName, ); my $newData = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $newData) { undef $$segDataPt; # free the old buffer # add header to new segment unless empty $newData = 'Ducky' . $newData if length $newData; $segDataPt = \$newData; } $del = 1 unless length $$segDataPt; } } elsif ($marker == 0xed) { # APP13 (Photoshop) if ($$segDataPt =~ /^$psAPP13hdr/) { $segType = 'Photoshop'; # add this data to the combined data if it exists if (defined $combinedSegData) { $combinedSegData .= substr($$segDataPt,length($psAPP13hdr)); $segDataPt = \$combinedSegData; $length = length $combinedSegData; # update length } # peek ahead to see if the next segment is photoshop data too if ($dirOrder[0] eq 'Photoshop') { # initialize combined data if necessary $combinedSegData = $$segDataPt unless defined $combinedSegData; next Marker; # get the next segment to combine } if ($doneDir{Photoshop}) { $self->Warn('Multiple Photoshop records'); # only rewrite the first Photoshop segment when deleting this group # (to remove multiples when deleting and adding back in one step) $$delGroup{Photoshop} and $del = 1, last; } $doneDir{Photoshop} = 1; # process APP13 Photoshop record my $tagTablePtr = GetTagTable('Image::ExifTool::Photoshop::Main'); my %dirInfo = ( DataPt => $segDataPt, DataPos => $segPos, DataLen => $length, DirStart => 14, # directory starts after identifier DirLen => $length-14, Parent => $markerName, ); my $newData = $self->WriteDirectory(\%dirInfo, $tagTablePtr); if (defined $newData) { undef $$segDataPt; # free the old buffer $segDataPt = \$newData; } length $$segDataPt or $del = 1, last; # write as multi-segment WriteMultiSegment($outfile, $marker, $psAPP13hdr, $segDataPt) or $err = 1; undef $combinedSegData; undef $$segDataPt; next Marker; } } elsif ($marker == 0xee) { # APP14 (Adobe) if ($$segDataPt =~ /^Adobe/) { $segType = 'Adobe'; # delete it and replace it later if editing if ($$delGroup{Adobe} or $$editDirs{Adobe}) { $del = 1; undef $doneDir{Adobe}; # so we can add it back again above } } } elsif ($marker == 0xfe) { # COM (JPEG comment) my $newComment; unless ($doneDir{COM}) { $doneDir{COM} = 1; unless ($$delGroup{File} and $$delGroup{File} != 2) { my $tagInfo = $Image::ExifTool::Extra{Comment}; my $nvHash = $self->GetNewValueHash($tagInfo); my $val = $segData; $val =~ s/\0+$//; # allow for stupid software that adds NULL terminator if ($self->IsOverwriting($nvHash, $val) or $$delGroup{File}) { $newComment = $self->GetNewValue($nvHash); } else { delete $$editDirs{COM}; # we aren't editing COM after all last; } } } $self->VerboseValue('- Comment', $$segDataPt); if (defined $newComment) { # write out the comments $self->VerboseValue('+ Comment', $newComment); WriteMultiSegment($outfile, 0xfe, '', \$newComment) or $err = 1; } else { $verbose and print $out " Deleting COM segment\n"; } ++$$self{CHANGED}; # increment the changed flag undef $segDataPt; # don't write existing comment } last; # didn't want to loop anyway } # delete necessary segments (including unknown segments if deleting all) if ($del or ($$delGroup{'*'} and not $segType and $marker>=0xe0 and $marker<=0xef)) { $segType = 'unknown' unless $segType; $verbose and print $out " Deleting $markerName $segType segment\n"; ++$$self{CHANGED}; next Marker; } # write out this segment if $segDataPt is still defined if (defined $segDataPt and defined $$segDataPt) { # write the data for this record (the data could have been # modified, so recalculate the length word) my $size = length($$segDataPt); if ($size > $maxSegmentLen) { $segType or $segType = 'Unknown'; $self->Error("$segType $markerName segment too large! ($size bytes)"); $err = 1; } else { $s = pack('n', length($$segDataPt) + 2); Write($outfile, $hdr, $s, $$segDataPt) or $err = 1; } undef $$segDataPt; # free the buffer undef $segDataPt; } } # make sure the ICC_Profile was complete $self->Error('Incomplete ICC_Profile record', 1) if defined $iccChunkCount; pop @$path if @$path > $pn; # if oldOutfile is still set, there was an error copying the JPEG $oldOutfile and return 0; if ($rtnVal) { # add any new trailers we are creating my $trailPt = $self->AddNewTrailers(); Write($outfile, $$trailPt) or $err = 1 if $trailPt; } # set return value to -1 if we only had a write error $rtnVal = -1 if $rtnVal and $err; if ($creatingEXV and $rtnVal > 0 and not $$self{CHANGED}) { $self->Error('Nothing written'); $rtnVal = -1; } return $rtnVal; } #------------------------------------------------------------------------------ # Validate an image for writing # Inputs: 0) ExifTool object reference, 1) raw value reference # Returns: error string or undef on success sub CheckImage($$) { my ($self, $valPtr) = @_; if (length($$valPtr) and $$valPtr!~/^\xff\xd8/ and not $self->Options('IgnoreMinorErrors')) { return '[Minor] Not a valid image'; } return undef; } #------------------------------------------------------------------------------ # check a value for validity # Inputs: 0) value reference, 1) format string, 2) optional count # Returns: error string, or undef on success # Notes: May modify value (if a count is specified for a string, it is null-padded # to the specified length, and floating point values are rounded to integer if required) sub CheckValue($$;$) { my ($valPtr, $format, $count) = @_; my (@vals, $val, $n); if ($format eq 'string' or $format eq 'undef') { return undef unless $count and $count > 0; my $len = length($$valPtr); if ($format eq 'string') { $len >= $count and return 'String too long'; } else { $len > $count and return 'Data too long'; } if ($len < $count) { $$valPtr .= "\0" x ($count - $len); } return undef; } if ($count and $count != 1) { @vals = split(' ',$$valPtr); $count < 0 and ($count = @vals or return undef); } else { $count = 1; @vals = ( $$valPtr ); } if (@vals != $count) { my $str = @vals > $count ? 'Too many' : 'Not enough'; return "$str values specified ($count required)"; } for ($n=0; $n<$count; ++$n) { $val = shift @vals; if ($format =~ /^int/) { # make sure the value is integer unless (IsInt($val)) { if (IsHex($val)) { $val = $$valPtr = hex($val); } else { # round single floating point values to the nearest integer return 'Not an integer' unless IsFloat($val) and $count == 1; $val = $$valPtr = int($val + ($val < 0 ? -0.5 : 0.5)); } } my $rng = $intRange{$format} or return "Bad int format: $format"; return "Value below $format minimum" if $val < $$rng[0]; # (allow 0xfeedfeed code as value for 16-bit pointers) return "Value above $format maximum" if $val > $$rng[1] and $val != 0xfeedfeed; } elsif ($format =~ /^rational/ or $format eq 'float' or $format eq 'double') { # make sure the value is a valid floating point number unless (IsFloat($val)) { # allow 'inf', 'undef' and fractional rational values if ($format =~ /^rational/) { next if $val eq 'inf' or $val eq 'undef'; if ($val =~ m{^([-+]?\d+)/(\d+)$}) { next unless $1 < 0 and $format =~ /u$/; return 'Must be an unsigned rational'; } } return 'Not a floating point number'; } if ($format =~ /^rational\d+u$/ and $val < 0) { return 'Must be a positive number'; } } } return undef; # success! } #------------------------------------------------------------------------------ # check new value for binary data block # Inputs: 0) ExifTool object ref, 1) tagInfo hash ref, 2) raw value ref # Returns: error string or undef (and may modify value) on success sub CheckBinaryData($$$) { my ($self, $tagInfo, $valPtr) = @_; my $format = $$tagInfo{Format}; unless ($format) { my $table = $$tagInfo{Table}; if ($table and $$table{FORMAT}) { $format = $$table{FORMAT}; } else { # use default 'int8u' unless specified $format = 'int8u'; } } my $count; if ($format =~ /(.*)\[(.*)\]/) { $format = $1; $count = $2; # can't evaluate $count now because we don't know $size yet undef $count if $count =~ /\$size/; } return CheckValue($valPtr, $format, $count); } #------------------------------------------------------------------------------ # Rename a file (with patch for Windows Unicode file names, and other problem) # Inputs: 0) ExifTool ref, 1) old name, 2) new name # Returns: true on success sub Rename($$$) { my ($self, $old, $new) = @_; my ($result, $try, $winUni); if ($self->EncodeFileName($old)) { $self->EncodeFileName($new, 1); $winUni = 1; } elsif ($self->EncodeFileName($new)) { $old = $_[1]; $self->EncodeFileName($old, 1); $winUni = 1; } for (;;) { if ($winUni) { $result = eval { Win32API::File::MoveFileExW($old, $new, Win32API::File::MOVEFILE_REPLACE_EXISTING() | Win32API::File::MOVEFILE_COPY_ALLOWED()) }; } else { $result = rename($old, $new); } last if $result or $^O ne 'MSWin32'; # keep trying for up to 0.5 seconds # (patch for Windows denial-of-service susceptibility) $try = ($try || 1) + 1; last if $try > 50; select(undef,undef,undef,0.01); # sleep for 0.01 sec } return $result; } #------------------------------------------------------------------------------ # Delete a file (with patch for Windows Unicode file names) # Inputs: 0) ExifTool ref, 1-N) names of files to delete # Returns: number of files deleted sub Unlink($@) { my $self = shift; my $result = 0; while (@_) { my $file = shift; if ($self->EncodeFileName($file)) { ++$result if eval { Win32API::File::DeleteFileW($file) }; } else { ++$result if unlink $file; } } return $result; } #------------------------------------------------------------------------------ # Set file times (Unix seconds since the epoch) # Inputs: 0) ExifTool ref, 1) file name or ref, 2) access time, 3) modification time, # 4) inode change or creation time (or undef for any time to avoid setting) # 5) flag to suppress warning # Returns: 1 on success, 0 on error my $k32SetFileTime; sub SetFileTime($$;$$$$) { my ($self, $file, $atime, $mtime, $ctime, $noWarn) = @_; my $saveFile; local *FH; # open file by name if necessary unless (ref $file) { # (file will be automatically closed when *FH goes out of scope) unless ($self->Open(\*FH, $file, '+<')) { my $success; if (defined $atime or defined $mtime) { my ($a, $m, $c) = $self->GetFileTime($file); $atime = $a unless defined $atime; $mtime = $m unless defined $mtime; $success = eval { utime($atime, $mtime, $file) } if defined $atime and defined $mtime; } $self->Warn('Error opening file for update') unless $success; return $success; } $saveFile = $file; $file = \*FH; } # on Windows, try to work around incorrect file times when daylight saving time is in effect if ($^O eq 'MSWin32') { if (not eval { require Win32::API }) { $self->Warn('Install Win32::API for proper handling of Windows file times'); } elsif (not eval { require Win32API::File }) { $self->Warn('Install Win32API::File for proper handling of Windows file times'); } else { # get Win32 handle, needed for SetFileTime my $win32Handle = eval { Win32API::File::GetOsFHandle($file) }; unless ($win32Handle) { $self->Warn('Win32API::File GetOsFHandle returned invalid handle'); return 0; } # convert Unix seconds to FILETIME structs my $time; foreach $time ($atime, $mtime, $ctime) { # set to NULL if not defined (i.e. do not change) defined $time or $time = 0, next; # convert to 100 ns intervals since 0:00 UTC Jan 1, 1601 # (89 leap years between 1601 and 1970) my $wt = ($time + (((1970-1601)*365+89)*24*3600)) * 1e7; my $hi = int($wt / 4294967296); $time = pack 'LL', int($wt - $hi * 4294967296), $hi; # pack FILETIME struct } unless ($k32SetFileTime) { return 0 if defined $k32SetFileTime; $k32SetFileTime = Win32::API->new('KERNEL32', 'SetFileTime', 'NPPP', 'I'); unless ($k32SetFileTime) { $self->Warn('Error loading Win32::API SetFileTime'); $k32SetFileTime = 0; return 0; } } unless ($k32SetFileTime->Call($win32Handle, $ctime, $atime, $mtime)) { $self->Warn('Win32::API SetFileTime returned ' . Win32::GetLastError()); return 0; } return 1; } } # other OS (or Windows fallback) if (defined $atime and defined $mtime) { my $success; local $SIG{'__WARN__'} = \&SetWarning; # (this may not be necessary) for (;;) { undef $evalWarning; # (this may fail on the first try if futimes is not implemented) $success = eval { utime($atime, $mtime, $file) }; last if $success or not defined $saveFile; close $file; $file = $saveFile; undef $saveFile; } unless ($noWarn) { if ($@ or $evalWarning) { $self->Warn(CleanWarning($@ || $evalWarning)); } elsif (not $success) { $self->Warn('Error setting file time'); } } return $success; } return 1; # (nothing to do) } #------------------------------------------------------------------------------ # Add data to hash checksum # Inputs: 0) ExifTool ref, 1) RAF ref, 2) data size (or undef to read to end of file), # 3) data name (or undef for no warnings or messages), 4) flag for no verbose message # Returns: number of bytes read and hashed sub ImageDataHash($$$;$$) { my ($self, $raf, $size, $type, $noMsg) = @_; my $hash = $$self{ImageDataHash} or return; my ($bytesRead, $n) = (0, 65536); my $buff; for (;;) { if (defined $size) { last unless $size; $n = $size > 65536 ? 65536 : $size; $size -= $n; } unless ($raf->Read($buff, $n)) { $self->Warn("Error reading $type data") if $type and defined $size; last; } $hash->add($buff); $bytesRead += length $buff; } if ($$self{OPTIONS}{Verbose} and $bytesRead and $type and not $noMsg) { $self->VPrint(0, "$$self{INDENT}(ImageDataHash: $bytesRead bytes of $type data)\n"); } return $bytesRead; } #------------------------------------------------------------------------------ # Copy data block from RAF to output file in max 64kB chunks # Inputs: 0) RAF ref, 1) outfile ref, 2) block size # Returns: 1 on success, 0 on read error, undef on write error sub CopyBlock($$$) { my ($raf, $outfile, $size) = @_; my $buff; for (;;) { last unless $size > 0; my $n = $size > 65536 ? 65536 : $size; $raf->Read($buff, $n) == $n or return 0; Write($outfile, $buff) or return undef; $size -= $n; } return 1; } #------------------------------------------------------------------------------ # Copy image data from one file to another # Inputs: 0) ExifTool object reference # 1) reference to list of image data [ position, size, pad bytes ] # 2) output file ref # Returns: true on success sub CopyImageData($$$) { my ($self, $imageDataBlocks, $outfile) = @_; my $raf = $$self{RAF}; my ($dataBlock, $err); my $num = @$imageDataBlocks; $self->VPrint(0, " Copying $num image data blocks\n") if $num; foreach $dataBlock (@$imageDataBlocks) { my ($pos, $size, $pad) = @$dataBlock; $raf->Seek($pos, 0) or $err = 'read', last; my $result = CopyBlock($raf, $outfile, $size); $result or $err = defined $result ? 'read' : 'writ'; # pad if necessary Write($outfile, "\0" x $pad) or $err = 'writ' if $pad; last if $err; } if ($err) { $self->Error("Error ${err}ing image data"); return 0; } return 1; } #------------------------------------------------------------------------------ # Write to binary data block # Inputs: 0) ExifTool object ref, 1) source dirInfo ref, 2) tag table ref # Returns: Binary data block or undefined on error sub WriteBinaryData($$$) { my ($self, $dirInfo, $tagTablePtr) = @_; $self or return 1; # allow dummy access to autoload this package # get default format ('int8u' unless specified) my $dataPt = $$dirInfo{DataPt} or return undef; my $dataLen = length $$dataPt; my $defaultFormat = $$tagTablePtr{FORMAT} || 'int8u'; my $increment = FormatSize($defaultFormat); unless ($increment) { warn "Unknown format $defaultFormat\n"; return undef; } # extract data members first if necessary my @varOffsets; if ($$tagTablePtr{DATAMEMBER}) { $$dirInfo{DataMember} = $$tagTablePtr{DATAMEMBER}; $$dirInfo{VarFormatData} = \@varOffsets; $self->ProcessBinaryData($dirInfo, $tagTablePtr); delete $$dirInfo{DataMember}; delete $$dirInfo{VarFormatData}; } my $dirStart = $$dirInfo{DirStart} || 0; my $dirLen = $$dirInfo{DirLen}; $dirLen = $dataLen - $dirStart if not defined $dirLen or $dirLen > $dataLen - $dirStart; my $newData = substr($$dataPt, $dirStart, $dirLen) or return undef; my $dirName = $$dirInfo{DirName}; my $varSize = 0; my @varInfo = @varOffsets; my $tagInfo; $dataPt = \$newData; foreach $tagInfo (sort { $$a{TagID} <=> $$b{TagID} } $self->GetNewTagInfoList($tagTablePtr)) { my $tagID = $$tagInfo{TagID}; # evaluate conditional tags now if necessary if (ref $$tagTablePtr{$tagID} eq 'ARRAY' or $$tagInfo{Condition}) { my $writeInfo = $self->GetTagInfo($tagTablePtr, $tagID); next unless $writeInfo and $writeInfo eq $tagInfo; } # add offsets for variable-sized tags if necessary while (@varInfo and $varInfo[0][0] < $tagID) { $varSize = $varInfo[0][1]; # get accumulated variable size shift @varInfo; } my $count = 1; my $format = $$tagInfo{Format}; my $entry = int($tagID) * $increment + $varSize; # relative offset of this entry if ($format) { if ($format =~ /(.*)\[(.*)\]/) { $format = $1; $count = $2; my $size = $dirLen; # used in eval # evaluate count to allow count to be based on previous values #### eval Format size ($size, $self) - NOTE: %val not supported for writing $count = eval $count; $@ and warn($@), next; } elsif ($format eq 'string') { # string with no specified count runs to end of block $count = ($dirLen > $entry) ? $dirLen - $entry : 0; } } else { $format = $defaultFormat; } # read/write using variable format if changed in Hook $format = $varInfo[0][2] if @varInfo and $varInfo[0][0] == $tagID; my $val = ReadValue($dataPt, $entry, $format, $count, $dirLen-$entry); next unless defined $val; my $nvHash = $self->GetNewValueHash($tagInfo, $$self{CUR_WRITE_GROUP}); next unless $self->IsOverwriting($nvHash, $val) > 0; my $newVal = $self->GetNewValue($nvHash); next unless defined $newVal; # can't delete from a binary table # update DataMember with new value if necessary $$self{$$tagInfo{DataMember}} = $newVal if $$tagInfo{DataMember}; # only write masked bits if specified my $mask = $$tagInfo{Mask}; $newVal = (($newVal << $$tagInfo{BitShift}) & $mask) | ($val & ~$mask) if $mask; # set the size if ($$tagInfo{DataTag} and not $$tagInfo{IsOffset}) { warn 'Internal error' unless $newVal == 0xfeedfeed; my $data = $self->GetNewValue($$tagInfo{DataTag}); $newVal = length($data) if defined $data; my $format = $$tagInfo{Format} || $$tagTablePtr{FORMAT} || 'int32u'; if ($format =~ /^int16/ and $newVal > 0xffff) { $self->Error("$$tagInfo{DataTag} is too large (64 KiB max. for this file)"); } } my $rtnVal = WriteValue($newVal, $format, $count, $dataPt, $entry); if (defined $rtnVal) { $self->VerboseValue("- $dirName:$$tagInfo{Name}", $val); $self->VerboseValue("+ $dirName:$$tagInfo{Name}", $newVal); ++$$self{CHANGED}; } } # add necessary fixups for any offsets if ($$tagTablePtr{IS_OFFSET} and $$dirInfo{Fixup}) { $varSize = 0; @varInfo = @varOffsets; my $fixup = $$dirInfo{Fixup}; my $tagID; foreach $tagID (@{$$tagTablePtr{IS_OFFSET}}) { $tagInfo = $self->GetTagInfo($tagTablePtr, $tagID) or next; while (@varInfo and $varInfo[0][0] < $tagID) { $varSize = $varInfo[0][1]; shift @varInfo; } my $entry = $tagID * $increment + $varSize; # (no offset to dirStart for new dir data) next unless $entry <= $dirLen - 4; # (Ricoh has 16-bit preview image offsets, so can't just assume int32u) my $format = $$tagInfo{Format} || $$tagTablePtr{FORMAT} || 'int32u'; my $offset = ReadValue($dataPt, $entry, $format, 1, $dirLen-$entry); # ignore if offset is zero (eg. Ricoh DNG uses this to indicate no preview) next unless $offset; $fixup->AddFixup($entry, $$tagInfo{DataTag}, $format); next unless $$tagInfo{DataTag} and defined $$tagInfo{OffsetPair}; # NOTE: here we assume there are no var-sized tags between the # OffsetPair tags. If this ever becomes possible we must recalculate # $varSize for the OffsetPair tag here! $entry = $$tagInfo{OffsetPair} * $increment + $varSize; my $size = ReadValue($dataPt, $entry, $format, 1, $dirLen-$entry); next unless defined $size; if ($$tagInfo{DataTag} eq 'HiddenData') { $$self{HiddenData} = { Offset => $offset, Size => $size, Fixup => Image::ExifTool::Fixup->new, Base => $$dirInfo{Base}, }; next; } # handle the preview image now if this is a JPEG file next unless $$tagInfo{DataTag} eq 'PreviewImage' and $$self{FILE_TYPE} eq 'JPEG'; my $previewInfo = $$self{PREVIEW_INFO}; $previewInfo or $previewInfo = $$self{PREVIEW_INFO} = { Fixup => Image::ExifTool::Fixup->new, }; # set flag indicating we are using short pointers $$previewInfo{IsShort} = 1 unless $format eq 'int32u'; $$previewInfo{Absolute} = 1 if $$tagInfo{IsOffset} and $$tagInfo{IsOffset} eq '3'; # get the value of the Composite::PreviewImage tag $$previewInfo{Data} = $self->GetNewValue(GetCompositeTagInfo('PreviewImage')); unless (defined $$previewInfo{Data}) { if ($offset >= 0 and $offset + $size <= $$dirInfo{DataLen}) { $$previewInfo{Data} = substr(${$$dirInfo{DataPt}},$offset,$size); } else { $$previewInfo{Data} = 'LOAD_PREVIEW'; # flag to load preview later } } } } # write any necessary SubDirectories if ($$tagTablePtr{IS_SUBDIR}) { $varSize = 0; @varInfo = @varOffsets; my $tagID; foreach $tagID (@{$$tagTablePtr{IS_SUBDIR}}) { my $tagInfo = $self->GetTagInfo($tagTablePtr, $tagID); next unless defined $tagInfo; while (@varInfo and $varInfo[0][0] < $tagID) { $varSize = $varInfo[0][1]; shift @varInfo; } my $entry = int($tagID) * $increment + $varSize; last if $entry >= $dirLen; # get value for Condition if necessary unless ($tagInfo) { my $more = $dirLen - $entry; $more = 128 if $more > 128; my $v = substr($newData, $entry, $more); $tagInfo = $self->GetTagInfo($tagTablePtr, $tagID, \$v); next unless $tagInfo; } my $subdir = $$tagInfo{SubDirectory} or next; my $start = $$subdir{Start}; my $len; if (not $start) { $start = $entry; $len = $dirLen - $start; } elsif ($start =~ /\$/) { my $count = 1; my $format = $$tagInfo{Format} || $defaultFormat; $format =~ /(.*)\[(.*)\]/ and ($format, $count) = ($1, $2); my $val = ReadValue($dataPt, $entry, $format, $count, $dirLen - $entry); # ignore directories with a zero offset (ie. missing Nikon ShotInfo entries) next unless $val; my $dirStart = 0; #### eval Start ($val, $dirStart) $start = eval($start); next if $start < $dirStart or $start > $dataLen; $len = $$subdir{DirLen}; $len = $dataLen - $start unless $len and $len <= $dataLen - $start; } my %subdirInfo = ( DataPt => \$newData, DirStart => $start, DirLen => $len, TagInfo => $tagInfo, ); my $dat = $self->WriteDirectory(\%subdirInfo, GetTagTable($$subdir{TagTable})); substr($newData, $start, $len) = $dat if defined $dat and length $dat; } } return $newData; } #------------------------------------------------------------------------------ # Write TIFF as a directory # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref # Returns: New directory data or undefined on error sub WriteTIFF($$$) { my ($self, $dirInfo, $tagTablePtr) = @_; $self or return 1; # allow dummy access my $buff = ''; $$dirInfo{OutFile} = \$buff; return $buff if $self->ProcessTIFF($dirInfo, $tagTablePtr) > 0; return undef; } 1; # end __END__ =head1 NAME Image::ExifTool::Writer.pl - ExifTool routines for writing meta information =head1 SYNOPSIS These routines are autoloaded by Image::ExifTool when required. =head1 DESCRIPTION This module contains ExifTool write routines and other infrequently used routines. =head1 AUTHOR Copyright 2003-2024, Phil Harvey (philharvey66 at gmail.com) This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L =cut