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   4256 use strict;
  5         12  
  5         202  
14 5     5   35 use vars qw($VERSION);
  5         13  
  5         236  
15 5     5   42 use Image::ExifTool qw(:DataAccess :Utils);
  5         11  
  5         7037  
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 113 my ($et, $dirInfo) = @_;
75 29         88 my $raf = $$dirInfo{RAF};
76 29         113 my $curPos = $raf->Tell();
77 29   100     198 my $offset = $$dirInfo{Offset} || 0; # offset from end of file
78 29         76 my $rtnVal = 0;
79              
80 29         61 NoAFCP: for (;;) {
81 29         71 my ($buff, $fix, $dirBuff, $valBuff, $fixup, $vers);
82             # look for AXS trailer
83 29 50 33     113 last unless $raf->Seek(-12-$offset, 2) and
      33        
84             $raf->Read($buff, 12) == 12 and
85             $buff =~ /^(AXS(!|\*))/;
86 29         134 my $endPos = $raf->Tell();
87 29         132 my $hdr = $1;
88 29 50       604 SetByteOrder($2 eq '!' ? 'MM' : 'II');
89 29         250 my $startPos = Get32u(\$buff, 4);
90 29 50 33     182 if ($raf->Seek($startPos, 0) and $raf->Read($buff, 12) == 12 and $buff =~ /^$hdr/) {
      33        
91 29         90 $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         118 $$dirInfo{DataPos} = $startPos + $fix; # actual start position
121 29         109 $$dirInfo{DirLen} = $endPos - ($startPos + $fix);
122              
123 29         71 $rtnVal = 1;
124 29         126 my $verbose = $et->Options('Verbose');
125 29         158 my $out = $et->Options('TextOut');
126 29         96 my $outfile = $$dirInfo{OutFile};
127 29 100       137 if ($outfile) {
128             # allow all AFCP information to be deleted
129 8 50       45 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         29 $dirBuff = $valBuff = '';
135 8         53 require Image::ExifTool::Fixup;
136 8         27 $fixup = $$dirInfo{Fixup};
137 8 50       62 $fixup or $fixup = $$dirInfo{Fixup} = new Image::ExifTool::Fixup;
138 8         37 $vers = substr($buff, 4, 2); # get version number
139             } else {
140 21 50 33     173 $et->DumpTrailer($dirInfo) if $verbose or $$et{HTML_DUMP};
141             }
142             # read AFCP directory data
143 29         142 my $numEntries = Get16u(\$buff, 6);
144 29         104 my $dir;
145 29 50       161 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     247 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       97 $fix and $et->Warn("Adjusted AFCP offsets by $fix", 1);
155             #
156             # process AFCP directory
157             #
158 29         108 my $tagTablePtr = GetTagTable('Image::ExifTool::AFCP::Main');
159 29         91 my ($index, $entry);
160 29         157 for ($index=0; $index<$numEntries; ++$index) {
161 58         132 my $entry = 12 * $index;
162 58         220 my $tag = substr($dir, $entry, 4);
163 58         243 my $size = Get32u(\$dir, $entry + 4);
164 58         235 my $offset = Get32u(\$dir, $entry + 8);
165 58 50 33     433 if ($size < 0x80000000 and
      33        
166             $raf->Seek($offset+$fix, 0) and
167             $raf->Read($buff, $size) == $size)
168             {
169 58 100       248 if ($outfile) {
170             # rewrite this information
171 16         68 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
172 16 50 66     115 if ($tagInfo and $$tagInfo{SubDirectory}) {
173 8         65 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         63 my $newDir = $et->WriteDirectory(\%subdirInfo, $subTable);
182 8 100       86 if (defined $newDir) {
183 1         2 $size = length $newDir;
184 1         4 $buff = $newDir;
185             }
186             }
187 16         98 $fixup->AddFixup(length($dirBuff) + 8);
188 16         90 $dirBuff .= $tag . Set32u($size) . Set32u(length $valBuff);
189 16         101 $valBuff .= $buff;
190             } else {
191             # extract information
192 42         218 $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     243 if ($outfile and length($dirBuff)) {
206 8         37 my $outPos = Tell($outfile); # get current outfile position
207             # apply fixup to directory pointers
208 8         46 my $valPos = $outPos + 12; # start of value data
209 8         33 $fixup->{Shift} += $valPos + length($dirBuff);
210 8         58 $fixup->ApplyFixup(\$dirBuff);
211             # write the AFCP header, directory, value data and EOF record (with zero checksums)
212 8 50       74 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         74 $fixup->AddFixup(length($dirBuff) + length($valBuff) + 4);
216 8         34 $fixup->{Start} += $valPos;
217 8         24 $fixup->{Shift} -= $valPos;
218             }
219 29         117 last;
220             }
221 29         115 return $rtnVal;
222             }
223              
224             1; # end
225              
226             __END__