File Coverage

blib/lib/Image/ExifTool/WriteRIFF.pl
Criterion Covered Total %
statement 123 187 65.7
branch 77 166 46.3
condition 27 65 41.5
subroutine 2 2 100.0
pod 0 1 0.0
total 229 421 54.3


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: WriteRIFF.pl
3             #
4             # Description: Write RIFF-format files
5             #
6             # Revisions: 2020-09-26 - P. Harvey Created
7             #
8             # Notes: Currently writes only WEBP files
9             #
10             # References: https://developers.google.com/speed/webp/docs/riff_container
11             #------------------------------------------------------------------------------
12              
13             package Image::ExifTool::RIFF;
14              
15 1     1   7 use strict;
  1         2  
  1         1841  
16              
17             # map of where information is stored in WebP image
18             my %webpMap = (
19             'XMP ' => 'RIFF', # (the RIFF chunk name is 'XMP ')
20             EXIF => 'RIFF',
21             ICCP => 'RIFF',
22             C2PA => 'RIFF',
23             JUMBF => 'C2PA',
24             XMP => 'XMP ',
25             IFD0 => 'EXIF',
26             IFD1 => 'IFD0',
27             ICC_Profile => 'ICCP',
28             ExifIFD => 'IFD0',
29             GPS => 'IFD0',
30             SubIFD => 'IFD0',
31             GlobParamIFD => 'IFD0',
32             PrintIM => 'IFD0',
33             InteropIFD => 'ExifIFD',
34             MakerNotes => 'ExifIFD',
35             );
36              
37             my %deletableGroup = (
38             "XMP\0" => 'XMP', # delete incorrectly written "XMP\0" tag with XMP group
39             SEAL => 'SEAL', # delete SEAL tag with SEAL group
40             );
41              
42             #------------------------------------------------------------------------------
43             # Write RIFF file (currently WebP-type only)
44             # Inputs: 0) ExifTool object ref, 1) dirInfo ref
45             # Returns: 1 on success, 0 if this wasn't a valid RIFF file, or -1 if
46             # an output file was specified and a write error occurred
47             sub WriteRIFF($$)
48             {
49 3     3 0 11 my ($et, $dirInfo) = @_;
50 3 50       12 $et or return 1; # allow dummy access to autoload this package
51 3         9 my $outfile = $$dirInfo{OutFile};
52 3         8 my $outsize = 0;
53 3         6 my $raf = $$dirInfo{RAF};
54 3         14 my $verbose = $et->Options('Verbose');
55 3         8 my ($buff, $err, $pass, %has, %dirDat, $imageWidth, $imageHeight);
56              
57             # do this in 2 passes so we can set the size of the containing RIFF chunk
58             # without having to buffer the output (also to set the WebP_Flags)
59 3         8 for ($pass=0; ; ++$pass) {
60 6         12 my %doneDir;
61             # verify this is a valid RIFF file
62 6 50       18 return 0 unless $raf->Read($buff, 12) == 12;
63 6 50       54 return 0 unless $buff =~ /^(RIFF|RF64)....(.{4})/s;
64              
65 6 50 33     48 unless ($1 eq 'RIFF' and $2 eq 'WEBP') {
66 0         0 my $type = $2;
67 0         0 $type =~ tr/-_a-zA-Z//dc;
68 0         0 $et->Error("Can't currently write $1 $type files");
69 0         0 return 1;
70             }
71 6         26 SetByteOrder('II');
72              
73             # determine which directories we must write for this file type
74 6 100       25 $et->Options(Verbose => 0) if $pass; # (avoid duplicate Verbose options here)
75 6         47 $et->InitWriteDirs(\%webpMap);
76 6         21 my $addDirs = $$et{ADD_DIRS};
77 6         14 my $editDirs = $$et{EDIT_DIRS};
78 6 100       26 $$addDirs{IFD0} = 'EXIF' if $$addDirs{EXIF}; # set flag to add IFD0 if adding EXIF (don't ask)
79 6         16 my ($createVP8X, $deleteVP8X);
80              
81             # write header
82 6 100       20 if ($pass) {
83 3         22 $et->Options(Verbose => $verbose);
84             my $needsVP8X = ($has{ANIM} or $has{'XMP '} or $has{EXIF} or
85 3   33     31 $has{ALPH} or $has{ICCP});
86 3 50 33     35 if ($has{VP8X} and not $needsVP8X and $$et{CHANGED}) {
    50 33        
      33        
87 0         0 $deleteVP8X = 1; # delete the VP8X chunk
88 0         0 $outsize -= 18; # account for missing VP8X
89             } elsif ($needsVP8X and not $has{VP8X}) {
90 0 0       0 if (defined $imageWidth) {
91 0         0 ++$$et{CHANGED};
92 0         0 $createVP8X = 1; # add VP8X chunk
93 0         0 $outsize += 18; # account for VP8X size
94             } else {
95 0         0 $et->Warn('Error getting image size for required VP8X chunk');
96             }
97             }
98             # finally we can set the overall RIFF chunk size:
99 3         18 Set32u($outsize - 8, \$buff, 4);
100 3 50       13 Write($outfile, $buff) or $err = 1;
101             # create VP8X chunk if necessary
102 3 50       11 if ($createVP8X) {
103 0         0 $et->VPrint(0," Adding required VP8X chunk (Extended WEBP)\n");
104 0         0 my $flags = 0;
105 0 0       0 $flags |= 0x02 if $has{ANIM};
106 0 0       0 $flags |= 0x04 if $has{'XMP '};
107 0 0       0 $flags |= 0x08 if $has{EXIF};
108 0 0       0 $flags |= 0x10 if $has{ALPH};
109 0 0       0 $flags |= 0x20 if $has{ICCP};
110 0         0 Write($outfile, 'VP8X', pack('V3v', 10, $flags,
111             ($imageWidth-1) | ((($imageHeight-1) & 0xff) << 24),
112             ($imageHeight-1) >> 8));
113             # write ICCP after VP8X
114 0 0 0     0 Write($outfile, $dirDat{ICCP}) or $err = 1 if $dirDat{ICCP};
115             }
116             } else {
117 3         7 $outsize += length $buff;
118             }
119 6         12 my $pos = 12;
120             #
121             # Read chunks in RIFF image
122             #
123 6         11 for (;;) {
124 34         56 my ($tag, $len);
125 34         97 my $num = $raf->Read($buff, 8);
126 34 100       70 if ($num < 8) {
127 8 50       21 $num and $et->Error('RIFF format error'), return 1;
128             # all done if we hit end of file unless we need to add EXIF or XMP
129 8 50 66     96 last unless $$addDirs{EXIF} or $$addDirs{'XMP '} or $$addDirs{ICCP};
      33        
130             # continue to add required EXIF or XMP chunks
131 2         6 $num = $len = 0;
132 2         7 $buff = $tag = '';
133             } else {
134 26         39 $pos += 8;
135 26         88 ($tag, $len) = unpack('a4V', $buff);
136 26 50       60 if ($len <= 0) {
137 0 0       0 if ($len < 0) {
    0          
138 0         0 $et->Error('Invalid chunk length');
139 0         0 return 1;
140             } elsif ($tag eq "\0\0\0\0") {
141             # avoid reading through corrupted files filled with nulls because it takes forever
142 0         0 $et->Error('Encountered empty null chunk. Processing aborted');
143 0         0 return 1;
144             } else { # (just in case a tag may have no data)
145 0 0       0 if ($pass) {
146 0 0       0 Write($outfile, $buff) or $err = 1;
147             } else {
148 0         0 $outsize += length $buff;
149             }
150 0         0 next;
151             }
152             }
153             }
154             # RIFF chunks are padded to an even number of bytes
155 28         49 my $len2 = $len + ($len & 0x01);
156             # handle incorrect "XMP\0" chunk ID written by Google software
157 28 50       62 if ($deletableGroup{$tag}) {
158 0 0       0 if ($$et{DEL_GROUP}{$deletableGroup{$tag}}) {
    0          
159             # just ignore this chunk if deleting the associated group
160 0 0       0 $raf->Seek($len2, 1) or $et->Error('Seek error'), last;
161 0 0       0 $et->VPrint(0, " Deleting $deletableGroup{$tag}\n") if $pass;
162 0         0 ++$$et{CHANGED};
163 0         0 next;
164             } elsif ($tag eq "XMP\0") {
165 0 0       0 $et->Warn('Incorrect XMP tag ID',1) if $pass;
166             }
167             }
168             # edit/add/delete necessary metadata chunks (EXIF must come before XMP)
169 28 50 100     133 if ($$editDirs{$tag} or $tag eq '' or ($tag eq 'XMP ' and $$addDirs{EXIF})) {
      33        
      66        
170 10         17 my $handledTag;
171 10 100       19 if ($len2) {
172 8 50 33     42 $et->Warn("Duplicate '${tag}' chunk") if $doneDir{$tag} and not $pass;
173 8         15 $doneDir{$tag} = 1;
174 8 50       19 $raf->Read($buff, $len2) == $len2 or $et->Error("Truncated '${tag}' chunk"), last;
175 8         13 $pos += $len2; # update current position
176             } else {
177 2         5 $buff = '';
178             }
179             #
180             # add/edit/delete EXIF/XMP/ICCP (note: EXIF must come before XMP, and ICCP is written elsewhere)
181             #
182 10         43 my %dirName = ( EXIF => 'IFD0', 'XMP ' => 'XMP', ICCP => 'ICC_Profile', C2PA => 'JUMBF' );
183 10         33 my %tblName = ( EXIF => 'Exif', 'XMP ' => 'XMP', ICCP => 'ICC_Profile', C2PA => 'Jpeg2000' );
184 10         14 my $dir;
185 10         24 foreach $dir ('EXIF', 'XMP ', 'ICCP', 'C2PA' ) {
186 40 50 66     144 next unless $tag eq $dir or ($$addDirs{$dir} and
      66        
      66        
187             ($tag eq '' or ($tag eq 'XMP ' and $dir eq 'EXIF')));
188 12         20 my $start;
189 12 100       31 unless ($pass) {
190             # write the EXIF and save the result for the next pass
191 6         16 my $dataPt = \$buff;
192 6 100       35 if ($tag eq 'EXIF') {
    100          
193             # (only need to set directory $start for EXIF)
194 2 50       30 if ($buff =~ /^Exif\0\0/) {
195 0 0       0 if ($$et{DEL_GROUP}{EXIF}) {
196             # remove incorrect header if rewriting anyway
197 0         0 $buff = substr($buff, 6);
198 0         0 $len -= 6;
199 0         0 $len2 -= 6;
200             } else {
201 0 0       0 $et->Warn('Improper EXIF header',1) unless $pass;
202 0         0 $start = 6;
203             }
204             } else {
205 2         6 $start = 0;
206             }
207             } elsif ($dir ne $tag) {
208             # create from scratch
209 2         7 my $buf2 = '';
210 2         6 $dataPt = \$buf2;
211             }
212             # write the new directory to memory
213             my %dirInfo = (
214             DataPt => $dataPt,
215             DataPos => 0, # (relative to Base)
216             DirStart => $start,
217             Base => $pos - $len2,
218             Parent => $dir,
219 6         49 DirName => $dirName{$dir},
220             );
221             # must pass the TagInfo to enable deletion of C2PA information
222 6 100       27 if (ref $Image::ExifTool::RIFF::Main{$dir} eq 'HASH') {
223 3         15 $dirInfo{TagInfo} = $Image::ExifTool::RIFF::Main{$dir};
224             }
225 6         32 my $tagTablePtr = GetTagTable("Image::ExifTool::$tblName{$dir}::Main");
226             # (override writeProc for EXIF because it has the TIFF header)
227 6 100       28 my $writeProc = $dir eq 'EXIF' ? \&Image::ExifTool::WriteTIFF : undef;
228 6         29 $dirDat{$dir} = $et->WriteDirectory(\%dirInfo, $tagTablePtr, $writeProc);
229             }
230 12         43 delete $$addDirs{$dir}; # (don't try to add again)
231 12 50       43 if (defined $dirDat{$dir}) {
232 12 100       31 if ($dir eq $tag) {
233 8         13 $handledTag = 1; # set flag indicating we edited this tag
234             # increment CHANGED count if we are deleting the directory
235 8 100       20 ++$$et{CHANGED} unless length $dirDat{$dir};
236             }
237 12 100       32 if (length $dirDat{$dir}) {
238 8 100       30 if ($pass) {
239             # write metadata chunk now (but not ICCP because it was added earlier)
240 4 50 50     14 Write($outfile, $dirDat{$dir}) or $err = 1 unless $dir eq 'ICCP';
241             } else {
242             # preserve (incorrect EXIF) header if it existed
243 4 50       11 my $hdr = $start ? substr($buff,0,$start) : '';
244             # (don't overwrite $len here because it may be XMP length)
245 4         10 my $dirLen = length($dirDat{$dir}) + length($hdr);
246             # add chunk header and padding
247 4 50       11 my $pad = $dirLen & 0x01 ? "\0" : '';
248 4         11 $dirDat{$dir} = $dir . Set32u($dirLen) . $hdr . $dirDat{$dir} . $pad;
249 4         9 $outsize += length($dirDat{$dir});
250 4         14 $has{$dir} = 1;
251             }
252             }
253             }
254             }
255             #
256             # just copy XMP, EXIF or ICC if nothing changed
257             #
258 10 50 66     31 if (not $handledTag and length $buff) {
259             # write the chunk without changes
260 0 0       0 if ($pass) {
261 0 0       0 Write($outfile, $tag, Set32u($len), $buff) or $err = 1;
262             } else {
263 0         0 $outsize += 8 + length($buff);
264 0         0 $has{$tag} = 1;
265             }
266             }
267 10         39 next;
268             }
269 18         35 $pos += $len2; # set read position at end of chunk data
270             #
271             # update necessary flags in VP8X chunk
272             #
273 18 100       63 if ($tag eq 'VP8X') {
274 6         10 my $buf2;
275 6 50 33     25 if ($len2 < 10 or $raf->Read($buf2, $len2) != $len2) {
276 0         0 $et->Error('Truncated VP8X chunk');
277 0         0 return 1;
278             }
279 6 100       16 if ($pass) {
280 3 50       8 if ($deleteVP8X) {
281 0         0 $et->VPrint(0," Deleting unnecessary VP8X chunk (Standard WEBP)\n");
282 0         0 next;
283             }
284             # ...but first set the VP8X flags
285 3         12 my $flags = Get32u(\$buf2, 0);
286 3         5 $flags &= ~0x2c; # (reset flags for everything we can write)
287 3 100       14 $flags |= 0x04 if $has{'XMP '};
288 3 100       10 $flags |= 0x08 if $has{EXIF};
289 3 50       9 $flags |= 0x20 if $has{ICCP};
290 3         10 Set32u($flags, \$buf2, 0);
291 3 50       8 Write($outfile, $buff, $buf2) or $err = 1;
292             } else {
293             # get the image size
294 3         13 $imageWidth = (Get32u(\$buf2, 4) & 0xffffff) + 1;
295 3         9 $imageHeight = (Get32u(\$buf2, 6) >> 8) + 1;
296 3         8 $outsize += 8 + $len2;
297 3         10 $has{$tag} = 1;
298             }
299             # write ICCP after VP8X
300 6 50 0     19 Write($outfile, $dirDat{ICCP}) or $err = 1 if $dirDat{ICCP};
301 6         15 next;
302             }
303             #
304             # just copy all other chunks
305             #
306 12 100       26 if ($pass) {
307             # write chunk header (still in $buff)
308 6 50       34 Write($outfile, $buff) or $err = 1;
309             } else {
310 6         10 $outsize += length $buff;
311 6         21 $has{$tag} = 1;
312             }
313 12 50 66     37 unless ($pass or defined $imageWidth) {
314             # get WebP image size from VP8 or VP8L header
315 0 0 0     0 if ($tag eq 'VP8 ' and $len2 >= 16) {
    0 0        
316 0 0       0 $raf->Read($buff, 16) == 16 or $et->Error('Truncated VP8 chunk'), return 1;
317 0         0 $outsize += 16;
318 0 0       0 if ($buff =~ /^...\x9d\x01\x2a/s) {
319 0         0 $imageWidth = Get16u(\$buff, 6) & 0x3fff;
320 0         0 $imageHeight = Get16u(\$buff, 8) & 0x3fff;
321             }
322 0         0 $len2 -= 16;
323             } elsif ($tag eq 'VP8L' and $len2 >= 6) {
324 0 0       0 $raf->Read($buff, 6) == 6 or $et->Error('Truncated VP8L chunk'), return 1;
325 0         0 $outsize += 6;
326 0 0       0 if ($buff =~ /^\x2f/s) {
327 0         0 my $word = Get32u(\$buff, 2);
328 0         0 $imageWidth = (Get16u(\$buff, 1) & 0x3fff) + 1;
329 0         0 $imageHeight = (($word >> 6) & 0x3fff) + 1;
330 0 0       0 $has{ALPH} = 1 if $word & 0x100000; # set alpha flag if necessary
331             }
332 0         0 $len2 -= 6;
333             }
334             }
335 12 100       23 if ($pass) {
336             # copy the chunk data in 64k blocks
337 6         21 while ($len2) {
338 6         12 my $num = $len2;
339 6 50       13 $num = 65536 if $num > 65536;
340 6 50       15 $raf->Read($buff, $num) == $num or $et->Error('Truncated RIFF chunk'), last;
341 6 50       14 Write($outfile, $buff) or $err = 1, last;
342 6         16 $len2 -= $num;
343             }
344             } else {
345 6 50       35 $raf->Seek($len2, 1) or $et->Error('Seek error'), last;
346 6         14 $outsize += $len2;
347             }
348             }
349 6 100       18 last if $pass;
350 3 50       15 $raf->Seek(0,0) or $et->Error('Seek error'), last;
351             }
352 3 50       31 return $err ? -1 : 1;
353             }
354              
355             1; # end
356              
357             __END__