File Coverage

blib/lib/Image/ExifTool/AFCP.pm
Criterion Covered Total %
statement 74 103 71.8
branch 20 48 41.6
condition 14 38 36.8
subroutine 4 4 100.0
pod 0 1 0.0
total 112 194 57.7


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   16065 use strict;
  5         12  
  5         221  
14 5     5   26 use vars qw($VERSION);
  5         8  
  5         278  
15 5     5   28 use Image::ExifTool qw(:DataAccess :Utils);
  5         8  
  5         6891  
16              
17             $VERSION = '1.10';
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 'ScanForTrailer' 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 ScanForTrailer 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 90 my ($et, $dirInfo) = @_;
75 29         88 my $raf = $$dirInfo{RAF};
76 29         116 my $curPos = $raf->Tell();
77 29   100     131 my $offset = $$dirInfo{Offset} || 0; # offset from end of file
78 29         73 my $rtnVal = 0;
79              
80 29         55 NoAFCP: for (;;) {
81 29         83 my ($buff, $fix, $dirBuff, $valBuff, $fixup, $vers);
82             # look for AXS trailer
83 29 50 33     107 last unless $raf->Seek(-12-$offset, 2) and
      33        
84             $raf->Read($buff, 12) == 12 and
85             $buff =~ /^(AXS(!|\*))/;
86 29         118 my $endPos = $raf->Tell();
87 29         103 my $hdr = $1;
88 29 50       202 SetByteOrder($2 eq '!' ? 'MM' : 'II');
89 29         110 my $startPos = Get32u(\$buff, 4);
90 29 50 33     114 if ($raf->Seek($startPos, 0) and $raf->Read($buff, 12) == 12 and $buff =~ /^$hdr/) {
      33        
91 29         67 $fix = 0;
92             } else {
93 0         0 $rtnVal = -1;
94             # look for start of AXS trailer if 'ScanForTrailer'
95 0 0 0     0 last unless $$dirInfo{ScanForTrailer} 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         99 $$dirInfo{DataPos} = $startPos + $fix; # actual start position
121 29         125 $$dirInfo{DirLen} = $endPos - ($startPos + $fix);
122              
123 29         54 $rtnVal = 1;
124 29         135 my $verbose = $et->Options('Verbose');
125 29         105 my $out = $et->Options('TextOut');
126 29         82 my $outfile = $$dirInfo{OutFile};
127 29 100       118 if ($outfile) {
128             # allow all AFCP information to be deleted
129 8 50       36 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         28 $dirBuff = $valBuff = '';
135 8         61 require Image::ExifTool::Fixup;
136 8         24 $fixup = $$dirInfo{Fixup};
137 8 50       89 $fixup or $fixup = $$dirInfo{Fixup} = Image::ExifTool::Fixup->new;
138 8         27 $vers = substr($buff, 4, 2); # get version number
139             } else {
140 21 50 33     153 $et->DumpTrailer($dirInfo) if $verbose or $$et{HTML_DUMP};
141             }
142             # read AFCP directory data
143 29         124 my $numEntries = Get16u(\$buff, 6);
144 29         70 my $dir;
145 29 50       118 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     149 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       98 $fix and $et->Warn("Adjusted AFCP offsets by $fix", 1);
155             #
156             # process AFCP directory
157             #
158 29         119 my $tagTablePtr = GetTagTable('Image::ExifTool::AFCP::Main');
159 29         65 my ($index, $entry);
160 29         147 for ($index=0; $index<$numEntries; ++$index) {
161 58         143 my $entry = 12 * $index;
162 58         210 my $tag = substr($dir, $entry, 4);
163 58         254 my $size = Get32u(\$dir, $entry + 4);
164 58         175 my $offset = Get32u(\$dir, $entry + 8);
165 58 50 33     354 if ($size < 0x80000000 and
      33        
166             $raf->Seek($offset+$fix, 0) and
167             $raf->Read($buff, $size) == $size)
168             {
169 58 100       167 if ($outfile) {
170             # rewrite this information
171 16         81 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
172 16 50 66     67 if ($tagInfo and $$tagInfo{SubDirectory}) {
173 8         64 my %subdirInfo = (
174             DataPt => \$buff,
175             DirStart => 0,
176             DirLen => $size,
177             DataPos => $offset + $fix,
178             Parent => 'AFCP',
179             );
180 8         35 my $subTable = GetTagTable($tagInfo->{SubDirectory}->{TagTable});
181 8         47 my $newDir = $et->WriteDirectory(\%subdirInfo, $subTable);
182 8 100       50 if (defined $newDir) {
183 1         2 $size = length $newDir;
184 1         7 $buff = $newDir;
185             }
186             }
187 16         103 $fixup->AddFixup(length($dirBuff) + 8);
188 16         64 $dirBuff .= $tag . Set32u($size) . Set32u(length $valBuff);
189 16         83 $valBuff .= $buff;
190             } else {
191             # extract information
192 42         262 $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     153 if ($outfile and length($dirBuff)) {
206 8         41 my $outPos = Tell($outfile); # get current outfile position
207             # apply fixup to directory pointers
208 8         26 my $valPos = $outPos + 12; # start of value data
209 8         28 $fixup->{Shift} += $valPos + length($dirBuff);
210 8         70 $fixup->ApplyFixup(\$dirBuff);
211             # write the AFCP header, directory, value data and EOF record (with zero checksums)
212 8 50       54 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         49 $fixup->AddFixup(length($dirBuff) + length($valBuff) + 4);
216 8         24 $fixup->{Start} += $valPos;
217 8         38 $fixup->{Shift} -= $valPos;
218             }
219 29         102 last;
220             }
221 29         156 return $rtnVal;
222             }
223              
224             1; # end
225              
226             __END__