File Coverage

blib/lib/Image/ExifTool/InDesign.pm
Criterion Covered Total %
statement 86 142 60.5
branch 49 128 38.2
condition 8 20 40.0
subroutine 4 4 100.0
pod 0 1 0.0
total 147 295 49.8


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: InDesign.pm
3             #
4             # Description: Read/write meta information in Adobe InDesign files
5             #
6             # Revisions: 2009-06-17 - P. Harvey Created
7             #
8             # References: 1) http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
9             #------------------------------------------------------------------------------
10              
11             package Image::ExifTool::InDesign;
12              
13 1     1   5619 use strict;
  1         2  
  1         42  
14 1     1   4 use vars qw($VERSION);
  1         3  
  1         49  
15 1     1   4 use Image::ExifTool qw(:DataAccess :Utils);
  1         2  
  1         1655  
16              
17             $VERSION = '1.09';
18              
19             # map for writing metadata to InDesign files (currently only write XMP)
20             my %indMap = (
21             XMP => 'IND',
22             );
23              
24             # GUID's used in InDesign files
25             my $masterPageGUID = "\x06\x06\xed\xf5\xd8\x1d\x46\xe5\xbd\x31\xef\xe7\xfe\x74\xb7\x1d";
26             my $objectHeaderGUID = "\xde\x39\x39\x79\x51\x88\x4b\x6c\x8E\x63\xee\xf8\xae\xe0\xdd\x38";
27             my $objectTrailerGUID = "\xfd\xce\xdb\x70\xf7\x86\x4b\x4f\xa4\xd3\xc7\x28\xb3\x41\x71\x06";
28              
29             #------------------------------------------------------------------------------
30             # Read or write meta information in an InDesign file
31             # Inputs: 0) ExifTool object reference, 1) dirInfo reference
32             # Returns: 1 on success, 0 if this wasn't a valid InDesign file, or -1 on write error
33             sub ProcessIND($$)
34             {
35 5     5 0 17 my ($et, $dirInfo) = @_;
36 5         13 my $raf = $$dirInfo{RAF};
37 5         15 my $outfile = $$dirInfo{OutFile};
38 5         15 my ($hdr, $buff, $buf2, $err, $writeLen, $foundXMP);
39              
40             # validate the InDesign file
41 5 50       21 return 0 unless $raf->Read($hdr, 16) == 16;
42 5 50       22 return 0 unless $hdr eq $masterPageGUID;
43 5 50       20 return 0 unless $raf->Read($buff, 8) == 8;
44 5 50       40 $et->SetFileType($buff eq 'DOCUMENT' ? 'INDD' : 'IND'); # set the FileType tag
45              
46             # read the master pages
47 5 50       25 $raf->Seek(0, 0) or $err = 'Seek error', goto DONE;
48 5 50 33     23 unless ($raf->Read($buff, 4096) == 4096 and
49             $raf->Read($buf2, 4096) == 4096)
50             {
51 0         0 $err = 'Unexpected end of file';
52 0         0 goto DONE; # (goto's can be our friend)
53             }
54 5         31 SetByteOrder('II');
55 5 50       93 unless ($buf2 =~ /^\Q$masterPageGUID/) {
56 0         0 $err = 'Second master page is invalid';
57 0         0 goto DONE;
58             }
59 5         39 my $seq1 = Get64u(\$buff, 264);
60 5         25 my $seq2 = Get64u(\$buf2, 264);
61             # take the most current master page
62 5 50       24 my $curPage = $seq2 > $seq1 ? \$buf2 : \$buff;
63             # byte order of stream data may be different than headers
64 5         24 my $streamInt32u = Get8u($curPage, 24);
65 5 50       28 if ($streamInt32u == 1) {
    50          
66 0         0 $streamInt32u = 'V'; # little-endian int32u
67             } elsif ($streamInt32u == 2) {
68 5         13 $streamInt32u = 'N'; # big-endian int32u
69             } else {
70 0         0 $err = 'Invalid stream byte order';
71 0         0 goto DONE;
72             }
73 5         16 my $pages = Get32u($curPage, 280);
74 5 50       20 $pages < 2 and $err = 'Invalid page count', goto DONE;
75 5         13 my $pos = $pages * 4096;
76 5 50       26 if ($pos > 0x7fffffff) {
77 0 0       0 if (not $et->Options('LargeFileSupport')) {
    0          
78 0         0 $err = 'InDesign files larger than 2 GB not supported (LargeFileSupport not set)';
79 0         0 goto DONE;
80             } elsif ($et->Options('LargeFileSupport') eq '2') {
81 0         0 $et->Warn('Processing large file (LargeFileSupport is 2)');
82             }
83             }
84 5 100       14 if ($outfile) {
85             # make XMP the preferred group for writing
86 2         13 $et->InitWriteDirs(\%indMap, 'XMP');
87              
88 2 50       16 Write($outfile, $buff, $buf2) or $err = 1, goto DONE;
89 2         18 my $result = Image::ExifTool::CopyBlock($raf, $outfile, $pos - 8192);
90 2 50       8 unless ($result) {
91 0 0       0 $err = defined $result ? 'Error reading InDesign database' : 1;
92 0         0 goto DONE;
93             }
94 2         7 $writeLen = 0;
95             } else {
96 3 50       17 $raf->Seek($pos, 0) or $err = 'Seek error', goto DONE;
97             }
98             # scan through the contiguous objects for XMP
99 5         31 my $verbose = $et->Options('Verbose');
100 5         19 my $out = $et->Options('TextOut');
101 5         33 for (;;) {
102 10 50       61 $raf->Read($hdr, 32) or last;
103 10 100 66     146 unless (length($hdr) == 32 and $hdr =~ /^\Q$objectHeaderGUID/) {
104             # this must be null padding or we have a possible error
105 5 50       41 last if $hdr =~ /^\0+$/;
106             # (could be up to 4095 bytes of non-null garbage plus 4095 null bytes from ExifTool)
107 0 0       0 $raf->Read($buff, 8192) and $hdr .= $buff;
108 0         0 my $n = length $hdr;
109 0         0 $hdr =~ s/\0+$//; # remove trailing nulls
110 0 0 0     0 if ($n > 8190 or length($hdr) > 4095) {
111 0         0 $err = 'Corrupt file or unsupported InDesign version';
112 0         0 last;
113             }
114 0         0 my $non = 'Non-null padding at end of file';
115 0 0       0 if (not $outfile) {
    0          
116 0         0 $et->Warn($non, 1);
117             } elsif (not $et->Error($non, 1)) {
118 0 0       0 Write($outfile, $hdr) or $err = 1;
119 0         0 $writeLen += length $hdr;
120             }
121 0         0 last;
122             }
123 5         28 my $len = Get32u(\$hdr, 24);
124 5 50       20 if ($verbose) {
125 0         0 printf $out "Contiguous object at offset 0x%x (%d bytes):\n", $raf->Tell(), $len;
126 0 0       0 if ($verbose > 2) {
127 0 0       0 my $len2 = $len < 1024000 ? $len : 1024000;
128 0 0       0 $raf->Seek(-$raf->Read($buff, $len2), 1) or $err = 1;
129 0         0 $et->VerboseDump(\$buff, Addr => $raf->Tell());
130             }
131             }
132             # check for XMP if stream data is long enough
133             # (56 bytes is just enough for XMP header)
134 5 50       18 if ($len > 56) {
135 5 50       26 $raf->Read($buff, 56) == 56 or $err = 'Unexpected end of file', last;
136 5 50       69 if ($buff =~ /^(....)<\?xpacket begin=(['"])\xef\xbb\xbf\2 id=(['"])W5M0MpCehiHzreSzNTczkc9d\3/s) {
137 5         21 my $lenWord = $1; # save length word for writing later
138 5         12 $len -= 4; # get length of XMP only
139 5         38 $foundXMP = 1;
140             # I have a sample where the XMP is 107 MB, and ActivePerl may run into
141             # memory troubles (with its apparent 1 GB limit) if the XMP is larger
142             # than about 400 MB, so guard against this
143 5 50       23 if ($len > 300 * 1024 * 1024) {
144 0         0 my $msg = sprintf('Insanely large XMP (%.0f MiB)', $len / (1024 * 1024));
145 0 0       0 if ($outfile) {
    0          
146 0 0       0 $et->Error($msg, 2) and $err = 1, last;
147             } elsif ($et->Options('IgnoreMinorErrors')) {
148 0         0 $et->Warn($msg);
149             } else {
150 0         0 $et->Warn("$msg. Ignored.", 1);
151 0         0 $err = 1;
152 0         0 last;
153             }
154             }
155             # load and parse the XMP data
156 5 50 33     21 unless ($raf->Seek(-52, 1) and $raf->Read($buff, $len) == $len) {
157 0         0 $err = 'Error reading XMP stream';
158 0         0 last;
159             }
160 5         37 my %dirInfo = (
161             DataPt => \$buff,
162             Parent => 'IND',
163             NoDelete => 1, # do not allow this to be deleted when writing
164             );
165             # validate xmp data length (should be same as length in header - 4)
166 5         18 my $xmpLen = unpack($streamInt32u, $lenWord);
167 5 50       19 unless ($xmpLen == $len) {
168 0 0       0 if ($xmpLen < $len) {
169 0         0 $dirInfo{DirLen} = $xmpLen;
170             } else {
171 0         0 $err = 'Truncated XMP stream (missing ' . ($xmpLen - $len) . ' bytes)';
172             }
173             }
174 5         23 my $tagTablePtr = GetTagTable('Image::ExifTool::XMP::Main');
175 5 100       22 if ($outfile) {
176 2 50       9 last if $err;
177             # make sure that XMP is writable
178 2         11 my $classID = Get32u(\$hdr, 20);
179 2 50       10 $classID & 0x40000000 or $err = 'XMP stream is not writable', last;
180 2         15 my $xmp = $et->WriteDirectory(\%dirInfo, $tagTablePtr);
181 2 50 33     15 if ($xmp and length $xmp) {
182             # write new xmp with leading length word
183 2         17 $buff = pack($streamInt32u, length $xmp) . $xmp;
184             # update header with new length and invalid checksum
185 2         13 Set32u(length($buff), \$hdr, 24);
186 2         7 Set32u(0xffffffff, \$hdr, 28);
187             } else {
188 0         0 $$et{CHANGED} = 0; # didn't change anything
189 0 0       0 $et->Warn("Can't delete XMP as a block from InDesign file") if defined $xmp;
190             # put length word back at start of stream
191 0         0 $buff = $lenWord . $buff;
192             }
193             } else {
194 3         20 $et->ProcessDirectory(\%dirInfo, $tagTablePtr);
195             }
196 5         27 $len = 0; # we got the full stream (nothing left to read)
197             } else {
198 0         0 $len -= 56; # we got 56 bytes of the stream
199             }
200             } else {
201 0         0 $buff = ''; # must reset this for writing later
202             }
203 5 100       23 if ($outfile) {
    50          
204             # write object header and data
205 2 50       11 Write($outfile, $hdr, $buff) or $err = 1, last;
206 2         14 my $result = Image::ExifTool::CopyBlock($raf, $outfile, $len);
207 2 50       8 unless ($result) {
208 0 0       0 $err = defined $result ? 'Truncated stream data' : 1;
209 0         0 last;
210             }
211 2         8 $writeLen += 32 + length($buff) + $len;
212             } elsif ($len) {
213             # skip over remaining stream data
214 0 0       0 $raf->Seek($len, 1) or $err = 'Seek error', last;
215             }
216 5 50       47 $raf->Read($buff, 32) == 32 or $err = 'Unexpected end of file', last;
217 5 50       62 unless ($buff =~ /^\Q$objectTrailerGUID/) {
218 0         0 $err = 'Invalid object trailer';
219 0         0 last;
220             }
221 5 100       18 if ($outfile) {
222             # make sure object UID and ClassID are the same in the trailer
223 2 50       14 substr($hdr,16,8) eq substr($buff,16,8) or $err = 'Non-matching object trailer', last;
224             # write object trailer
225 2 50       12 Write($outfile, $objectTrailerGUID, substr($hdr,16)) or $err = 1, last;
226 2         8 $writeLen += 32;
227             }
228             }
229 5 100       15 if ($outfile) {
230             # write null padding if necessary
231             # (InDesign files must be an even number of 4096-byte blocks)
232 2         7 my $part = $writeLen % 4096;
233 2 50 50     21 Write($outfile, "\0" x (4096 - $part)) or $err = 1 if $part;
234             }
235             DONE:
236 5 50       16 if (not $err) {
    0          
    0          
237 5 50 66     41 $et->Warn('No XMP stream to edit') if $outfile and not $foundXMP;
238 5         34 return 1; # success!
239             } elsif (not $outfile) {
240             # issue warning on read error
241 0 0         $et->Warn($err) unless $err eq '1';
242             } elsif ($err ne '1') {
243             # set error and return success code
244 0           $et->Error($err);
245             } else {
246 0           return -1; # write error
247             }
248 0           return 1;
249             }
250              
251             1; # end
252              
253             __END__