| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 2 |  |  |  |  |  |  | # File:         AFCP.pm | 
| 3 |  |  |  |  |  |  | # | 
| 4 |  |  |  |  |  |  | # Description:  Read/write AFCP trailer | 
| 5 |  |  |  |  |  |  | # | 
| 6 |  |  |  |  |  |  | # Revisions:    12/26/2005 - P. Harvey Created | 
| 7 |  |  |  |  |  |  | # | 
| 8 |  |  |  |  |  |  | # References:   1) http://web.archive.org/web/20080828211305/http://www.tocarte.com/media/axs_afcp_spec.pdf | 
| 9 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 10 |  |  |  |  |  |  |  | 
| 11 |  |  |  |  |  |  | package Image::ExifTool::AFCP; | 
| 12 |  |  |  |  |  |  |  | 
| 13 | 5 |  |  | 5 |  | 4448 | use strict; | 
|  | 5 |  |  |  |  | 12 |  | 
|  | 5 |  |  |  |  | 201 |  | 
| 14 | 5 |  |  | 5 |  | 27 | use vars qw($VERSION); | 
|  | 5 |  |  |  |  | 15 |  | 
|  | 5 |  |  |  |  | 269 |  | 
| 15 | 5 |  |  | 5 |  | 41 | use Image::ExifTool qw(:DataAccess :Utils); | 
|  | 5 |  |  |  |  | 11 |  | 
|  | 5 |  |  |  |  | 7009 |  | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | $VERSION = '1.08'; | 
| 18 |  |  |  |  |  |  |  | 
| 19 |  |  |  |  |  |  | sub ProcessAFCP($$); | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | %Image::ExifTool::AFCP::Main = ( | 
| 22 |  |  |  |  |  |  | PROCESS_PROC => \&ProcessAFCP, | 
| 23 |  |  |  |  |  |  | NOTES => q{ | 
| 24 |  |  |  |  |  |  | AFCP stands for AXS File Concatenation Protocol, and is a poorly designed | 
| 25 |  |  |  |  |  |  | protocol for appending information to the end of files.  This can be used as | 
| 26 |  |  |  |  |  |  | an auxiliary technique to store IPTC information in images, but is | 
| 27 |  |  |  |  |  |  | incompatible with some file formats. | 
| 28 |  |  |  |  |  |  |  | 
| 29 |  |  |  |  |  |  | ExifTool will read and write (but not create) AFCP IPTC information in JPEG | 
| 30 |  |  |  |  |  |  | and TIFF images. | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | See | 
| 33 |  |  |  |  |  |  | L | 
| 34 |  |  |  |  |  |  | for the AFCP specification. | 
| 35 |  |  |  |  |  |  | }, | 
| 36 |  |  |  |  |  |  | IPTC => { SubDirectory => { TagTable => 'Image::ExifTool::IPTC::Main' } }, | 
| 37 |  |  |  |  |  |  | TEXT => 'Text', | 
| 38 |  |  |  |  |  |  | Nail => { | 
| 39 |  |  |  |  |  |  | Name => 'ThumbnailImage', | 
| 40 |  |  |  |  |  |  | Groups => { 2 => 'Preview' }, | 
| 41 |  |  |  |  |  |  | # (the specification allows for a variable amount of padding before | 
| 42 |  |  |  |  |  |  | #  the image after a 10-byte header, so look for the JPEG SOI marker, | 
| 43 |  |  |  |  |  |  | #  otherwise assume a fixed 8 bytes of padding) | 
| 44 |  |  |  |  |  |  | RawConv => q{ | 
| 45 |  |  |  |  |  |  | pos($val) = 10; | 
| 46 |  |  |  |  |  |  | my $start = ($val =~ /\xff\xd8\xff/g) ? pos($val) - 3 : 18; | 
| 47 |  |  |  |  |  |  | my $img = substr($val, $start); | 
| 48 |  |  |  |  |  |  | return $self->ValidateImage(\$img, $tag); | 
| 49 |  |  |  |  |  |  | }, | 
| 50 |  |  |  |  |  |  | }, | 
| 51 |  |  |  |  |  |  | PrVw => { | 
| 52 |  |  |  |  |  |  | Name => 'PreviewImage', | 
| 53 |  |  |  |  |  |  | Groups => { 2 => 'Preview' }, | 
| 54 |  |  |  |  |  |  | RawConv => q{ | 
| 55 |  |  |  |  |  |  | pos($val) = 10; | 
| 56 |  |  |  |  |  |  | my $start = ($val =~ /\xff\xd8\xff/g) ? pos($val) - 3 : 18; | 
| 57 |  |  |  |  |  |  | my $img = substr($val, $start); | 
| 58 |  |  |  |  |  |  | return $self->ValidateImage(\$img, $tag); | 
| 59 |  |  |  |  |  |  | }, | 
| 60 |  |  |  |  |  |  | }, | 
| 61 |  |  |  |  |  |  | ); | 
| 62 |  |  |  |  |  |  |  | 
| 63 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 64 |  |  |  |  |  |  | # Read/write AFCP information in a file | 
| 65 |  |  |  |  |  |  | # Inputs: 0) ExifTool object reference, 1) dirInfo reference | 
| 66 |  |  |  |  |  |  | # (Set 'ScanForAFCP' member in dirInfo to scan from current position for AFCP) | 
| 67 |  |  |  |  |  |  | # Returns: 1 on success, 0 if this file didn't contain AFCP information | 
| 68 |  |  |  |  |  |  | #          -1 on write error or if the offsets were incorrect on reading | 
| 69 |  |  |  |  |  |  | # - updates DataPos to point to actual AFCP start if ScanForAFCP is set | 
| 70 |  |  |  |  |  |  | # - updates DirLen to trailer length | 
| 71 |  |  |  |  |  |  | # - returns Fixup reference in dirInfo hash when writing | 
| 72 |  |  |  |  |  |  | sub ProcessAFCP($$) | 
| 73 |  |  |  |  |  |  | { | 
| 74 | 29 |  |  | 29 | 0 | 108 | my ($et, $dirInfo) = @_; | 
| 75 | 29 |  |  |  |  | 100 | my $raf = $$dirInfo{RAF}; | 
| 76 | 29 |  |  |  |  | 116 | my $curPos = $raf->Tell(); | 
| 77 | 29 |  | 100 |  |  | 176 | my $offset = $$dirInfo{Offset} || 0;    # offset from end of file | 
| 78 | 29 |  |  |  |  | 82 | my $rtnVal = 0; | 
| 79 |  |  |  |  |  |  |  | 
| 80 | 29 |  |  |  |  | 77 | NoAFCP: for (;;) { | 
| 81 | 29 |  |  |  |  | 79 | my ($buff, $fix, $dirBuff, $valBuff, $fixup, $vers); | 
| 82 |  |  |  |  |  |  | # look for AXS trailer | 
| 83 | 29 | 50 | 33 |  |  | 125 | last unless $raf->Seek(-12-$offset, 2) and | 
|  |  |  | 33 |  |  |  |  | 
| 84 |  |  |  |  |  |  | $raf->Read($buff, 12) == 12 and | 
| 85 |  |  |  |  |  |  | $buff =~ /^(AXS(!|\*))/; | 
| 86 | 29 |  |  |  |  | 148 | my $endPos = $raf->Tell(); | 
| 87 | 29 |  |  |  |  | 148 | my $hdr = $1; | 
| 88 | 29 | 50 |  |  |  | 257 | SetByteOrder($2 eq '!' ? 'MM' : 'II'); | 
| 89 | 29 |  |  |  |  | 214 | my $startPos = Get32u(\$buff, 4); | 
| 90 | 29 | 50 | 33 |  |  | 174 | if ($raf->Seek($startPos, 0) and $raf->Read($buff, 12) == 12 and $buff =~ /^$hdr/) { | 
|  |  |  | 33 |  |  |  |  | 
| 91 | 29 |  |  |  |  | 89 | $fix = 0; | 
| 92 |  |  |  |  |  |  | } else { | 
| 93 | 0 |  |  |  |  | 0 | $rtnVal = -1; | 
| 94 |  |  |  |  |  |  | # look for start of AXS trailer if 'ScanForAFCP' | 
| 95 | 0 | 0 | 0 |  |  | 0 | last unless $$dirInfo{ScanForAFCP} and $raf->Seek($curPos, 0); | 
| 96 | 0 |  |  |  |  | 0 | my $actualPos = $curPos; | 
| 97 |  |  |  |  |  |  | # first look for header right at current position | 
| 98 | 0 |  |  |  |  | 0 | for (;;) { | 
| 99 | 0 | 0 | 0 |  |  | 0 | last if $raf->Read($buff, 12) == 12 and $buff =~ /^$hdr/; | 
| 100 | 0 | 0 |  |  |  | 0 | last NoAFCP if $actualPos != $curPos; | 
| 101 |  |  |  |  |  |  | # scan for AXS header (could be after preview image) | 
| 102 | 0 |  |  |  |  | 0 | for (;;) { | 
| 103 | 0 |  |  |  |  | 0 | my $buf2; | 
| 104 | 0 | 0 |  |  |  | 0 | $raf->Read($buf2, 65536) or last NoAFCP; | 
| 105 | 0 |  |  |  |  | 0 | $buff .= $buf2; | 
| 106 | 0 | 0 |  |  |  | 0 | if ($buff =~ /$hdr/g) { | 
| 107 | 0 |  |  |  |  | 0 | $actualPos += pos($buff) - length($hdr); | 
| 108 | 0 |  |  |  |  | 0 | last;   # ok, now go back and re-read header | 
| 109 |  |  |  |  |  |  | } | 
| 110 | 0 |  |  |  |  | 0 | $buf2 = substr($buf2, -3);  # only need last 3 bytes for next test | 
| 111 | 0 |  |  |  |  | 0 | $actualPos += length($buff) - length($buf2); | 
| 112 | 0 |  |  |  |  | 0 | $buff = $buf2; | 
| 113 |  |  |  |  |  |  | } | 
| 114 | 0 | 0 |  |  |  | 0 | last unless $raf->Seek($actualPos, 0);  # seek to start of AFCP | 
| 115 |  |  |  |  |  |  | } | 
| 116 |  |  |  |  |  |  | # calculate shift for fixing AFCP offsets | 
| 117 | 0 |  |  |  |  | 0 | $fix = $actualPos - $startPos; | 
| 118 |  |  |  |  |  |  | } | 
| 119 |  |  |  |  |  |  | # set variables returned in dirInfo hash | 
| 120 | 29 |  |  |  |  | 126 | $$dirInfo{DataPos} = $startPos + $fix;  # actual start position | 
| 121 | 29 |  |  |  |  | 144 | $$dirInfo{DirLen} = $endPos - ($startPos + $fix); | 
| 122 |  |  |  |  |  |  |  | 
| 123 | 29 |  |  |  |  | 72 | $rtnVal = 1; | 
| 124 | 29 |  |  |  |  | 189 | my $verbose = $et->Options('Verbose'); | 
| 125 | 29 |  |  |  |  | 133 | my $out = $et->Options('TextOut'); | 
| 126 | 29 |  |  |  |  | 109 | my $outfile = $$dirInfo{OutFile}; | 
| 127 | 29 | 100 |  |  |  | 118 | if ($outfile) { | 
| 128 |  |  |  |  |  |  | # allow all AFCP information to be deleted | 
| 129 | 8 | 50 |  |  |  | 37 | if ($$et{DEL_GROUP}{AFCP}) { | 
| 130 | 0 | 0 |  |  |  | 0 | $verbose and print $out "  Deleting AFCP\n"; | 
| 131 | 0 |  |  |  |  | 0 | ++$$et{CHANGED}; | 
| 132 | 0 |  |  |  |  | 0 | last; | 
| 133 |  |  |  |  |  |  | } | 
| 134 | 8 |  |  |  |  | 20 | $dirBuff = $valBuff = ''; | 
| 135 | 8 |  |  |  |  | 47 | require Image::ExifTool::Fixup; | 
| 136 | 8 |  |  |  |  | 19 | $fixup = $$dirInfo{Fixup}; | 
| 137 | 8 | 50 |  |  |  | 64 | $fixup or $fixup = $$dirInfo{Fixup} = new Image::ExifTool::Fixup; | 
| 138 | 8 |  |  |  |  | 32 | $vers = substr($buff, 4, 2); # get version number | 
| 139 |  |  |  |  |  |  | } else { | 
| 140 | 21 | 50 | 33 |  |  | 141 | $et->DumpTrailer($dirInfo) if $verbose or $$et{HTML_DUMP}; | 
| 141 |  |  |  |  |  |  | } | 
| 142 |  |  |  |  |  |  | # read AFCP directory data | 
| 143 | 29 |  |  |  |  | 137 | my $numEntries = Get16u(\$buff, 6); | 
| 144 | 29 |  |  |  |  | 75 | my $dir; | 
| 145 | 29 | 50 |  |  |  | 157 | unless ($raf->Read($dir, 12 * $numEntries) == 12 * $numEntries) { | 
| 146 | 0 |  |  |  |  | 0 | $et->Error('Error reading AFCP directory', 1); | 
| 147 | 0 |  |  |  |  | 0 | last; | 
| 148 |  |  |  |  |  |  | } | 
| 149 | 29 | 50 | 33 |  |  | 177 | if ($verbose > 2 and not $outfile) { | 
| 150 | 0 |  |  |  |  | 0 | my $dat = $buff . $dir; | 
| 151 | 0 |  |  |  |  | 0 | print $out "  AFCP Directory:\n"; | 
| 152 | 0 |  |  |  |  | 0 | $et->VerboseDump(\$dat, Addr => $$dirInfo{DataPos}, Width  => 12); | 
| 153 |  |  |  |  |  |  | } | 
| 154 | 29 | 50 |  |  |  | 89 | $fix and $et->Warn("Adjusted AFCP offsets by $fix", 1); | 
| 155 |  |  |  |  |  |  | # | 
| 156 |  |  |  |  |  |  | # process AFCP directory | 
| 157 |  |  |  |  |  |  | # | 
| 158 | 29 |  |  |  |  | 101 | my $tagTablePtr = GetTagTable('Image::ExifTool::AFCP::Main'); | 
| 159 | 29 |  |  |  |  | 101 | my ($index, $entry); | 
| 160 | 29 |  |  |  |  | 154 | for ($index=0; $index<$numEntries; ++$index) { | 
| 161 | 58 |  |  |  |  | 145 | my $entry = 12 * $index; | 
| 162 | 58 |  |  |  |  | 166 | my $tag = substr($dir, $entry, 4); | 
| 163 | 58 |  |  |  |  | 239 | my $size = Get32u(\$dir, $entry + 4); | 
| 164 | 58 |  |  |  |  | 182 | my $offset = Get32u(\$dir, $entry + 8); | 
| 165 | 58 | 50 | 33 |  |  | 330 | if ($size < 0x80000000 and | 
|  |  |  | 33 |  |  |  |  | 
| 166 |  |  |  |  |  |  | $raf->Seek($offset+$fix, 0) and | 
| 167 |  |  |  |  |  |  | $raf->Read($buff, $size) == $size) | 
| 168 |  |  |  |  |  |  | { | 
| 169 | 58 | 100 |  |  |  | 193 | if ($outfile) { | 
| 170 |  |  |  |  |  |  | # rewrite this information | 
| 171 | 16 |  |  |  |  | 73 | my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag); | 
| 172 | 16 | 50 | 66 |  |  | 99 | if ($tagInfo and $$tagInfo{SubDirectory}) { | 
| 173 | 8 |  |  |  |  | 59 | my %subdirInfo = ( | 
| 174 |  |  |  |  |  |  | DataPt => \$buff, | 
| 175 |  |  |  |  |  |  | DirStart => 0, | 
| 176 |  |  |  |  |  |  | DirLen => $size, | 
| 177 |  |  |  |  |  |  | DataPos => $offset + $fix, | 
| 178 |  |  |  |  |  |  | Parent => 'AFCP', | 
| 179 |  |  |  |  |  |  | ); | 
| 180 | 8 |  |  |  |  | 34 | my $subTable = GetTagTable($tagInfo->{SubDirectory}->{TagTable}); | 
| 181 | 8 |  |  |  |  | 96 | my $newDir = $et->WriteDirectory(\%subdirInfo, $subTable); | 
| 182 | 8 | 100 |  |  |  | 48 | if (defined $newDir) { | 
| 183 | 1 |  |  |  |  | 2 | $size = length $newDir; | 
| 184 | 1 |  |  |  |  | 4 | $buff = $newDir; | 
| 185 |  |  |  |  |  |  | } | 
| 186 |  |  |  |  |  |  | } | 
| 187 | 16 |  |  |  |  | 94 | $fixup->AddFixup(length($dirBuff) + 8); | 
| 188 | 16 |  |  |  |  | 83 | $dirBuff .= $tag . Set32u($size) . Set32u(length $valBuff); | 
| 189 | 16 |  |  |  |  | 80 | $valBuff .= $buff; | 
| 190 |  |  |  |  |  |  | } else { | 
| 191 |  |  |  |  |  |  | # extract information | 
| 192 | 42 |  |  |  |  | 211 | $et->HandleTag($tagTablePtr, $tag, $buff, | 
| 193 |  |  |  |  |  |  | DataPt => \$buff, | 
| 194 |  |  |  |  |  |  | Size => $size, | 
| 195 |  |  |  |  |  |  | Index => $index, | 
| 196 |  |  |  |  |  |  | DataPos => $offset + $fix, | 
| 197 |  |  |  |  |  |  | ); | 
| 198 |  |  |  |  |  |  | } | 
| 199 |  |  |  |  |  |  | } else { | 
| 200 | 0 |  |  |  |  | 0 | $et->Warn("Bad AFCP directory"); | 
| 201 | 0 | 0 |  |  |  | 0 | $rtnVal = -1 if $outfile; | 
| 202 | 0 |  |  |  |  | 0 | last; | 
| 203 |  |  |  |  |  |  | } | 
| 204 |  |  |  |  |  |  | } | 
| 205 | 29 | 100 | 66 |  |  | 185 | if ($outfile and length($dirBuff)) { | 
| 206 | 8 |  |  |  |  | 37 | my $outPos = Tell($outfile);    # get current outfile position | 
| 207 |  |  |  |  |  |  | # apply fixup to directory pointers | 
| 208 | 8 |  |  |  |  | 25 | my $valPos = $outPos + 12;      # start of value data | 
| 209 | 8 |  |  |  |  | 31 | $fixup->{Shift} += $valPos + length($dirBuff); | 
| 210 | 8 |  |  |  |  | 47 | $fixup->ApplyFixup(\$dirBuff); | 
| 211 |  |  |  |  |  |  | # write the AFCP header, directory, value data and EOF record (with zero checksums) | 
| 212 | 8 | 50 |  |  |  | 48 | Write($outfile, $hdr, $vers, Set16u(length($dirBuff)/12), Set32u(0), | 
| 213 |  |  |  |  |  |  | $dirBuff, $valBuff, $hdr, Set32u($outPos), Set32u(0)) or $rtnVal = -1; | 
| 214 |  |  |  |  |  |  | # complete fixup so the calling routine can apply further shifts | 
| 215 | 8 |  |  |  |  | 65 | $fixup->AddFixup(length($dirBuff) + length($valBuff) + 4); | 
| 216 | 8 |  |  |  |  | 22 | $fixup->{Start} += $valPos; | 
| 217 | 8 |  |  |  |  | 25 | $fixup->{Shift} -= $valPos; | 
| 218 |  |  |  |  |  |  | } | 
| 219 | 29 |  |  |  |  | 98 | last; | 
| 220 |  |  |  |  |  |  | } | 
| 221 | 29 |  |  |  |  | 94 | return $rtnVal; | 
| 222 |  |  |  |  |  |  | } | 
| 223 |  |  |  |  |  |  |  | 
| 224 |  |  |  |  |  |  | 1;  # end | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  | __END__ |