File Coverage

blib/lib/Image/ExifTool/GIF.pm
Criterion Covered Total %
statement 202 273 73.9
branch 122 258 47.2
condition 37 120 30.8
subroutine 4 4 100.0
pod 0 1 0.0
total 365 656 55.6


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: GIF.pm
3             #
4             # Description: Read and write GIF meta information
5             #
6             # Revisions: 10/18/2005 - P. Harvey Separated from ExifTool.pm
7             # 05/23/2008 - P. Harvey Added ability to read/write XMP
8             # 10/28/2011 - P. Harvey Added ability to read/write ICC_Profile
9             #
10             # References: 1) http://www.w3.org/Graphics/GIF/spec-gif89a.txt
11             # 2) http://www.adobe.com/devnet/xmp/
12             # 3) http://graphcomp.com/info/specs/ani_gif.html
13             # 4) http://www.color.org/icc_specs2.html
14             # 5) http://www.midiox.com/mmgif.htm
15             #------------------------------------------------------------------------------
16              
17             package Image::ExifTool::GIF;
18              
19 6     6   6746 use strict;
  6         14  
  6         282  
20 6     6   34 use vars qw($VERSION);
  6         13  
  6         432  
21 6     6   86 use Image::ExifTool qw(:DataAccess :Utils);
  6         15  
  6         24823  
22              
23             $VERSION = '1.21';
24              
25             # road map of directory locations in GIF images
26             my %gifMap = (
27             XMP => 'GIF',
28             ICC_Profile => 'GIF',
29             );
30              
31             # application extensions that we can write, and the order they are written
32             my @appExtensions = ( 'XMP Data/XMP', 'ICCRGBG1/012' );
33              
34             %Image::ExifTool::GIF::Main = (
35             GROUPS => { 2 => 'Image' },
36             VARS => { ID_FMT => 'none' },
37             NOTES => q{
38             This table lists information extracted from GIF images. See
39             L for the official GIF89a
40             specification.
41             },
42             GIFVersion => { },
43             FrameCount => { Notes => 'number of animated images' },
44             Text => { Notes => 'text displayed in image' },
45             Comment => {
46             # for documentation only -- flag as writable for the docs, but
47             # it won't appear in the TagLookup because there is no WRITE_PROC
48             Writable => 2,
49             },
50             Duration => {
51             Notes => 'duration of a single animation iteration',
52             PrintConv => 'sprintf("%.2f s",$val)',
53             },
54             ScreenDescriptor => {
55             SubDirectory => { TagTable => 'Image::ExifTool::GIF::Screen' },
56             },
57             Extensions => { # (for documentation only)
58             SubDirectory => { TagTable => 'Image::ExifTool::GIF::Extensions' },
59             },
60             TransparentColor => { },
61             );
62              
63             # GIF89a application extensions:
64             %Image::ExifTool::GIF::Extensions = (
65             GROUPS => { 2 => 'Image' },
66             NOTES => 'Tags extracted from GIF89a application extensions.',
67             WRITE_PROC => sub { return 1 }, # (dummy proc to facilitate writable directories)
68             'NETSCAPE/2.0' => { #3
69             Name => 'Animation',
70             SubDirectory => { TagTable => 'Image::ExifTool::GIF::Animation' },
71             },
72             'XMP Data/XMP' => { #2
73             Name => 'XMP',
74             # IncludeLengthBytes indicates the length bytes are part of the data value...
75             # undef = data may contain nulls and is split into 255-byte blocks
76             # 1 = data may not contain nulls and is not split; NULL padding is added as necessary
77             # 2 = data is not split and may be edited in place; 257-byte landing zone is added
78             # (Terminator may be specified for a value of 1 above, but must be specified for 2)
79             IncludeLengthBytes => 2,
80             Terminator => q(<\\?xpacket end=['"][wr]['"]\\?>), # (regex to match end of valid data)
81             Writable => 2, # (writable directory!)
82             SubDirectory => { TagTable => 'Image::ExifTool::XMP::Main' },
83             },
84             'ICCRGBG1/012' => { #4
85             Name => 'ICC_Profile',
86             Writable => 2, # (writable directory!)
87             SubDirectory => { TagTable => 'Image::ExifTool::ICC_Profile::Main' },
88             },
89             'MIDICTRL/Jon' => { #5
90             Name => 'MIDIControl',
91             SubDirectory => { TagTable => 'Image::ExifTool::GIF::MIDIControl' },
92             },
93             'MIDISONG/Dm7' => { #5
94             Name => 'MIDISong',
95             Groups => { 2 => 'Audio' },
96             Binary => 1,
97             },
98             'C2PA_GIF/' => { #https://c2pa.org/specifications/ (NC) (authentication code is 0x010000 binary, so removed from tag ID)
99             Name => 'JUMBF',
100             SubDirectory => { TagTable => 'Image::ExifTool::Jpeg2000::Main' },
101             },
102             );
103              
104             # GIF locical screen descriptor
105             %Image::ExifTool::GIF::Screen = (
106             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
107             GROUPS => { 2 => 'Image' },
108             NOTES => 'Information extracted from the GIF logical screen descriptor.',
109             0 => {
110             Name => 'ImageWidth',
111             Format => 'int16u',
112             },
113             2 => {
114             Name => 'ImageHeight',
115             Format => 'int16u',
116             },
117             4.1 => {
118             Name => 'HasColorMap',
119             Mask => 0x80,
120             PrintConv => { 0 => 'No', 1 => 'Yes' },
121             },
122             4.2 => {
123             Name => 'ColorResolutionDepth',
124             Mask => 0x70,
125             ValueConv => '$val + 1',
126             },
127             4.3 => {
128             Name => 'BitsPerPixel',
129             Mask => 0x07,
130             ValueConv => '$val + 1',
131             },
132             5 => 'BackgroundColor',
133             6 => {
134             Name => 'PixelAspectRatio',
135             RawConv => '$val ? $val : undef',
136             ValueConv => '($val + 15) / 64',
137             },
138             );
139              
140             # GIF Netscape 2.0 animation extension (ref 3)
141             %Image::ExifTool::GIF::Animation = (
142             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
143             GROUPS => { 2 => 'Image' },
144             NOTES => 'Information extracted from the "NETSCAPE2.0" animation extension.',
145             1 => {
146             Name => 'AnimationIterations',
147             Format => 'int16u',
148             PrintConv => '$val ? $val : "Infinite"',
149             },
150             );
151              
152             # GIF MIDICTRL extension (ref 5)
153             %Image::ExifTool::GIF::MIDIControl = (
154             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
155             GROUPS => { 2 => 'Audio' },
156             NOTES => 'Information extracted from the MIDI control block extension.',
157             0 => 'MIDIControlVersion',
158             1 => 'SequenceNumber',
159             2 => 'MelodicPolyphony',
160             3 => 'PercussivePolyphony',
161             4 => {
162             Name => 'ChannelUsage',
163             Format => 'int16u',
164             PrintConv => 'sprintf("0x%.4x", $val)',
165             },
166             6 => {
167             Name => 'DelayTime',
168             Format => 'int16u',
169             ValueConv => '$val / 100',
170             PrintConv => '$val . " s"',
171             },
172             );
173              
174             #------------------------------------------------------------------------------
175             # Read/write meta information in GIF image
176             # Inputs: 0) ExifTool object reference, 1) Directory information ref
177             # Returns: 1 on success, 0 if this wasn't a valid GIF file, or -1 if
178             # an output file was specified and a write error occurred
179             sub ProcessGIF($$)
180             {
181 7     7 0 22 my ($et, $dirInfo) = @_;
182 7         23 my $outfile = $$dirInfo{OutFile};
183 7         17 my $raf = $$dirInfo{RAF};
184 7         30 my $verbose = $et->Options('Verbose');
185 7         22 my $out = $et->Options('TextOut');
186 7         39 my ($a, $s, $ch, $length, $buff);
187 7         0 my ($err, $newComment, $setComment, $nvComment, $newExt);
188 7         0 my ($addDirs, %doneDir);
189 7         20 my ($frameCount, $delayTime) = (0, 0);
190              
191             # verify this is a valid GIF file
192 7 50 33     29 return 0 unless $raf->Read($buff, 6) == 6
      33        
193             and $buff =~ /^GIF(8[79]a)$/
194             and $raf->Read($s, 7) == 7;
195              
196 7         26 my $ver = $1;
197 7         17 my $rtnVal = 0;
198 7         29 my $tagTablePtr = GetTagTable('Image::ExifTool::GIF::Main');
199 7         24 my $extTable = GetTagTable('Image::ExifTool::GIF::Extensions');
200 7         33 SetByteOrder('II');
201              
202 7 100       20 if ($outfile) {
203             # add any user-defined writable app extensions to the list
204 3         6 my $ext;
205 3         37 foreach $ext (sort keys %$extTable) {
206 36 100       91 next unless ref $$extTable{$ext} eq 'HASH';
207 21         40 my $extInfo = $$extTable{$ext};
208 21 50 100     161 next unless $$extInfo{SubDirectory} and $$extInfo{Writable} and not $gifMap{$$extInfo{Name}};
      66        
209 0         0 $gifMap{$$extInfo{Name}} = 'GIF';
210 0         0 push @appExtensions, $ext;
211             }
212 3         28 $et->InitWriteDirs(\%gifMap, 'XMP'); # make XMP the preferred group for GIF
213 3         11 $addDirs = $$et{ADD_DIRS};
214             # determine if we are editing the File:Comment tag
215 3         9 my $delGroup = $$et{DEL_GROUP};
216 3         18 $newComment = $et->GetNewValue('Comment', \$nvComment);
217 3 50 33     14 $setComment = 1 if $nvComment or $$delGroup{File};
218             # change to GIF 89a if adding comment, XMP or ICC_Profile
219 3 50 33     19 $buff = 'GIF89a' if %$addDirs or defined $newComment;
220 3 50       17 Write($outfile, $buff, $s) or $err = 1;
221 3         15 $newExt = $et->GetNewTagInfoHash($extTable);
222             } else {
223 4         26 $et->SetFileType(); # set file type
224 4         24 $et->HandleTag($tagTablePtr, 'GIFVersion', $ver);
225 4         12 $et->HandleTag($tagTablePtr, 'ScreenDescriptor', $s);
226             }
227 7         35 my $flags = Get8u(\$s, 4);
228 7 50       27 if ($flags & 0x80) { # does this image contain a color table?
229             # calculate color table size
230 7         20 $length = 3 * (2 << ($flags & 0x07));
231 7 50       30 $raf->Read($buff, $length) == $length or return 0; # skip color table
232 7 100 33     30 Write($outfile, $buff) or $err = 1 if $outfile;
233             }
234             #
235             # loop through GIF blocks
236             #
237             Block:
238 7         13 for (;;) {
239 33 50       116 last unless $raf->Read($ch, 1);
240             # write out any new metadata now if this isn't an extension block
241 33 100 100     111 if ($outfile and ord($ch) != 0x21) {
242             # write the comment first if necessary
243 6 0 33     14 if (defined $newComment and $$nvComment{IsCreating}) {
244             # write comment marker
245 0 0       0 Write($outfile, "\x21\xfe") or $err = 1;
246 0 0       0 $verbose and print $out " + Comment = $newComment\n";
247 0         0 my $len = length($newComment);
248             # write out the comment in 255-byte chunks, each
249             # chunk beginning with a length byte
250 0         0 my $n;
251 0         0 for ($n=0; $n<$len; $n+=255) {
252 0         0 my $size = $len - $n;
253 0 0       0 $size > 255 and $size = 255;
254 0         0 my $str = substr($newComment,$n,$size);
255 0 0       0 Write($outfile, pack('C',$size), $str) or $err = 1;
256             }
257 0 0       0 Write($outfile, "\0") or $err = 1; # empty chunk as terminator
258 0         0 undef $newComment;
259 0         0 undef $nvComment; # delete any other extraneous comments
260 0         0 ++$$et{CHANGED}; # increment file changed flag
261             }
262             # add application extensions if necessary
263 6         10 my $ext;
264 6         20 my @new = sort keys %$newExt;
265 6         13 foreach $ext (@appExtensions, @new) {
266 13         24 my $extInfo = $$extTable{$ext};
267 13         22 my $name = $$extInfo{Name};
268 13 100 66     46 if ($$newExt{$ext}) {
    50          
269 1         3 delete $$newExt{$ext};
270 1         2 $doneDir{$name} = 1; # (we wrote this as a block instead)
271 1         3 $buff = $et->GetNewValue($extInfo);
272 1         3 $et->VerboseValue("+ GIF:$name", $buff);
273             } elsif (exists $$addDirs{$name} and not defined $doneDir{$name}) {
274 0         0 $doneDir{$name} = 1;
275 0         0 my $tbl = GetTagTable($$extInfo{SubDirectory}{TagTable});
276 0         0 my %dirInfo = ( Parent => 'GIF' );
277 0 0       0 $verbose and print $out "Creating $name application extension block:\n";
278 0         0 $buff = $et->WriteDirectory(\%dirInfo, $tbl);
279             } else {
280 12         22 next;
281             }
282 1 50 33     6 if (defined $buff and length $buff) {
283 1         2 ++$$et{CHANGED};
284 1 50       6 Write($outfile, "\x21\xff\x0b", substr($ext,0,8), substr($ext,9,3)) or $err = 1;
285 1         3 my $pos = 0;
286 1 50       3 if (not $$extTable{$ext}{IncludeLengthBytes}) {
    0          
287 1         2 my $len = length $buff;
288 1         4 while ($pos < length $buff) {
289 2         4 my $n = length($buff) - $pos;
290 2 100       4 $n = 255 if $n > 255;
291 2 50       7 Write($outfile, chr($n), substr($buff, $pos, $n)) or $err = 1;
292 2         3 $pos += $n;
293             }
294 1 50       3 Write($outfile, "\0") or $err = 1; # write null terminator
295             } elsif ($$extTable{$ext}{IncludeLengthBytes} < 2) {
296 0         0 $pos += ord(substr($buff,$pos,1)) + 1 while $pos < length $buff;
297             # write data, null padding and terminator
298 0 0       0 Write($outfile, $buff, "\0" x ($pos - length($buff) + 1)) or $err = 1;
299             } else {
300             # write data, landing zone and null terminator
301 0 0       0 Write($outfile, $buff, pack('C*',1,reverse(0..255),0)) or $err = 1;
302             }
303 1         2 ++$doneDir{$name}; # set to 2 to indicate we added it
304             } else {
305 0 0       0 $verbose and print $out " -> no $name to add\n";
306             }
307             }
308             }
309 33 100       100 if (ord($ch) == 0x2c) {
310 7         16 ++$frameCount;
311 7 100 33     28 Write($outfile, $ch) or $err = 1 if $outfile;
312             # image descriptor
313 7 50 33     20 last unless $raf->Read($buff, 8) == 8 and $raf->Read($ch, 1);
314 7 100 33     24 Write($outfile, $buff, $ch) or $err = 1 if $outfile;
315 7 50 33     24 if ($verbose and not $outfile) {
316 0         0 my ($left, $top, $w, $h) = unpack('v*', $buff);
317 0         0 print $out "Image: left=$left top=$top width=$w height=$h\n";
318             }
319 7 50       22 if (ord($ch) & 0x80) { # does color table exist?
320 0         0 $length = 3 * (2 << (ord($ch) & 0x07));
321             # skip the color table
322 0 0       0 last unless $raf->Read($buff, $length) == $length;
323 0 0 0     0 Write($outfile, $buff) or $err = 1 if $outfile;
324             }
325             # skip "LZW Minimum Code Size" byte
326 7 50       21 last unless $raf->Read($buff, 1);
327 7 100 33     22 Write($outfile,$buff) or $err = 1 if $outfile;
328             # skip image blocks
329 7         13 for (;;) {
330 14 50       33 last unless $raf->Read($ch, 1);
331 14 100 33     38 Write($outfile, $ch) or $err = 1 if $outfile;
332 14 100       35 last unless ord($ch);
333 7 50       19 last unless $raf->Read($buff, ord($ch));
334 7 100 33     19 Write($outfile,$buff) or $err = 1 if $outfile;
335             }
336 7         17 next; # continue with next field
337             }
338             # last if ord($ch) == 0x3b; # normal end of GIF marker
339 26 100       80 unless (ord($ch) == 0x21) {
340 7 100       18 if ($outfile) {
341 3 50       9 Write($outfile, $ch) or $err = 1;
342             # copy the rest of the file
343 3         9 while ($raf->Read($buff, 65536)) {
344 0 0       0 Write($outfile, $buff) or $err = 1;
345             }
346             }
347 7         12 $rtnVal = 1;
348 7         12 last;
349             }
350             # get extension block type/size
351 19 50       45 last unless $raf->Read($s, 2) == 2;
352             # get marker and block size
353 19         65 ($a,$length) = unpack("C"x2, $s);
354              
355 19 100 33     87 if ($a == 0xfe) { # comment extension
    50 0        
    0 0        
    0          
356              
357 7         18 my $comment = '';
358 7         23 while ($length) {
359 10 50       30 last unless $raf->Read($buff, $length) == $length;
360 10 100       45 $et->VerboseDump(\$buff) unless $outfile;
361             # add buffer to comment string
362 10         24 $comment .= $buff;
363 10 50       27 last unless $raf->Read($ch, 1); # read next block header
364 10         25 $length = ord($ch); # get next block size
365             }
366 7 50       25 last if $length; # was a read error if length isn't zero
367 7 100       19 if ($outfile) {
368 3         7 my $isOverwriting;
369 3 50       9 if ($setComment) {
370 3 50       11 if ($nvComment) {
371 3         19 $isOverwriting = $et->IsOverwriting($nvComment,$comment);
372             # get new comment again (may have been shifted)
373 3 50       20 $newComment = $et->GetNewValue($nvComment) if defined $newComment;
374             } else {
375             # group delete, or deleting additional comments after writing one
376 0         0 $isOverwriting = 1;
377             }
378             }
379 3 50       13 if ($isOverwriting) {
380 3         10 ++$$et{CHANGED}; # increment file changed flag
381 3         18 $et->VerboseValue('- GIF:Comment', $comment);
382 3         7 $comment = $newComment;
383 3 50       15 $et->VerboseValue('+ GIF:Comment', $comment) if defined $comment;
384 3         6 undef $nvComment; # just delete remaining comments
385             } else {
386 0         0 undef $setComment; # leave remaining comments alone
387             }
388 3 50       10 if (defined $comment) {
389             # write comment marker
390 3 50       13 Write($outfile, "\x21\xfe") or $err = 1;
391 3         6 my $len = length($comment);
392             # write out the comment in 255-byte chunks, each
393             # chunk beginning with a length byte
394 3         7 my $n;
395 3         11 for ($n=0; $n<$len; $n+=255) {
396 4         8 my $size = $len - $n;
397 4 100       10 $size > 255 and $size = 255;
398 4         13 my $str = substr($comment,$n,$size);
399 4 50       23 Write($outfile, pack('C',$size), $str) or $err = 1;
400             }
401 3 50       8 Write($outfile, "\0") or $err = 1; # empty chunk as terminator
402             }
403 3         7 undef $newComment; # don't write the new comment again
404             } else {
405 4         7 $rtnVal = 1;
406 4 50       22 $et->FoundTag('Comment', $comment) if $comment;
407 4         10 undef $comment;
408             # assume no more than one comment in FastScan mode
409 4 50       12 last if $et->Options('FastScan');
410             }
411 7         17 next;
412              
413             } elsif ($a == 0xff and $length == 0x0b) { # application extension
414              
415 12 50       34 last unless $raf->Read($buff, $length) == $length;
416 12         96 my $hdr = "$ch$s$buff";
417             # add "/" for readability
418 12         41 my $tag = substr($buff, 0, 8) . '/' . substr($buff, 8);
419 12         44 $tag =~ tr/\0-\x1f//d; # remove nulls and control characters
420 12 50       39 $verbose and print $out "Application Extension: $tag\n";
421              
422 12         37 my $extInfo = $$extTable{$tag};
423 12         29 my ($subdir, $inclLen, $justCopy, $name);
424 12 50       30 if ($extInfo) {
425 12 100 100     48 if ($outfile and $$newExt{$$extInfo{TagID}}) {
426 1         4 delete $$newExt{$$extInfo{TagID}}; # don't create again
427             # (write as a block -- don't define $subdir)
428             } else {
429 11         27 $subdir = $$extInfo{SubDirectory};
430             }
431 12         29 $inclLen = $$extInfo{IncludeLengthBytes};
432 12         30 $name = $$extInfo{Name};
433             # rewrite as-is unless this is a writable
434 12 50 66     41 $justCopy = 1 if $outfile and not $$extInfo{Writable};
435             } else {
436 0 0       0 $justCopy = 1 if $outfile;
437             }
438 12 50 0     30 Write($outfile, $hdr) or $err = 1 if $justCopy;
439              
440             # read the extension data
441 12         23 my $dat = '';
442 12         20 for (;;) {
443 378 50       740 $raf->Read($ch, 1) or last Block; # read next block header
444 378 100       727 $length = ord($ch) or last; # get next block size
445 366 50       700 $raf->Read($buff, $length) == $length or last Block;
446 366 50 0     634 Write($outfile, $ch, $buff) or $err = 1 if $justCopy;
447 366 100       704 $dat .= $inclLen ? $ch . $buff : $buff;
448             }
449 12 50       35 if ($justCopy) {
    100          
450 0 0       0 Write($outfile, "\0") or $err = 1;
451 0         0 next;
452             } elsif ($inclLen) {
453             # remove landing zone or padding
454 7 50 33     149 if ($$extInfo{Terminator} and $dat =~ /$$extInfo{Terminator}/g) {
    0          
455 7         42 $dat = substr($dat, 0, pos($dat));
456             } elsif ($dat =~ /\0/g) {
457 0         0 $dat = substr($dat, 0, pos($dat) - 1);
458             }
459             }
460 12 100 33     31 if ($subdir) {
    50          
    0          
461 11         142 my %dirInfo = (
462             DataPt => \$dat,
463             DataLen => length $dat,
464             DirLen => length $dat,
465             DirName => $name,
466             Parent => 'GIF',
467             );
468 11         52 my $subTable = GetTagTable($$subdir{TagTable});
469 11 100       39 unless ($outfile) {
470 7         38 $et->ProcessDirectory(\%dirInfo, $subTable);
471 7         40 next;
472             }
473 4 50       13 next if $justCopy;
474 4 50 33     14 if ($doneDir{$name} and $doneDir{$name} > 1) {
475 0         0 $et->Warn("Duplicate $name block created");
476             }
477 4         26 $buff = $et->WriteDirectory(\%dirInfo, $subTable);
478 4 100       11 if (defined $buff) {
479 3 100       11 next unless length $buff; # delete this extension if length is zero
480 2         6 $dat = $buff;
481             }
482 3         37 $doneDir{$name} = 1;
483             } elsif ($outfile and not $justCopy) {
484 1         4 my $nvHash = $et->GetNewValueHash($extInfo);
485 1 50 33     6 if ($nvHash and $et->IsOverwriting($nvHash, $dat)) {
486 1         2 ++$$et{CHANGED};
487 1         4 my $val = $et->GetNewValue($extInfo);
488 1         5 $et->VerboseValue("- GIF:$name", $dat);
489 1 50 33     5 next unless defined $val and length $val;
490 1         3 $dat = $val;
491 1         3 $et->VerboseValue("+ GIF:$name", $dat);
492 1         2 $doneDir{$name} = 1; # (possibly wrote dir as a block)
493             }
494             } elsif (not $outfile) {
495 0         0 $et->HandleTag($extTable, $tag, $dat);
496 0         0 next;
497             }
498 4 50       26 Write($outfile, $hdr) or $err = 1; # write extension header
499 4 100       12 if ($inclLen) {
500             # check for null just to be safe
501 3 50 33     21 $et->Error("$name contained NULL character") if $inclLen and $dat =~ /\0/;
502 3 50       8 if ($inclLen > 1) {
503             # add landing zone (without terminator, which will be added later)
504 3 50       70 $dat .= pack('C*',1,reverse(0..255)) if $inclLen;
505             } else {
506             # pad with nulls as required
507 0         0 my $pos = 0;
508 0         0 $pos += ord(substr($dat,$pos,1)) + 1 while $pos < length $dat;
509 0         0 $dat .= "\0" x ($pos - length($dat));
510             }
511             # write data and landing zone
512 3 50       9 Write($outfile, $dat) or $err = 1;
513             } else {
514             # write as sub-blocks
515 1         3 my $pos = 0;
516 1         3 my $len = length $dat;
517 1         4 while ($pos < $len) {
518 2         5 my $n = $len - $pos;
519 2 100       6 $n = 255 if $n > 255;
520 2 50       13 Write($outfile, chr($n), substr($dat, $pos, $n)) or $err = 1;
521 2         9 $pos += $n;
522             }
523             }
524 4 50       10 Write($outfile, "\0") or $err = 1; # write null terminator
525 4         13 next;
526              
527             } elsif ($a == 0xf9 and $length == 4) { # graphic control extension
528              
529 0 0       0 last unless $raf->Read($buff, $length) == $length;
530             # sum the individual delay times
531 0         0 my $delay = Get16u(\$buff, 1);
532 0         0 $delayTime += $delay;
533 0 0       0 $verbose and printf $out "Graphic Control: delay=%.2f\n", $delay / 100;
534             # get transparent colour
535 0         0 my $bits = Get8u(\$buff, 0);
536 0 0       0 $et->HandleTag($tagTablePtr, 'TransparentColor', Get8u(\$buff,3)) if $bits & 0x01;
537 0 0       0 $raf->Seek(-$length, 1) or last;
538              
539             } elsif ($a == 0x01 and $length == 12) { # plain text extension
540              
541 0 0       0 last unless $raf->Read($buff, $length) == $length;
542 0 0 0     0 Write($outfile, $ch, $s, $buff) or $err = 1 if $outfile;
543 0 0 0     0 if ($verbose and not $outfile) {
544 0         0 my ($left, $top, $w, $h) = unpack('v4', $buff);
545 0         0 print $out "Text: left=$left top=$top width=$w height=$h\n";
546             }
547 0         0 my $text = '';
548 0         0 for (;;) {
549 0 0       0 last unless $raf->Read($ch, 1);
550 0 0       0 $length = ord($ch) or last;
551 0 0       0 last unless $raf->Read($buff, $length) == $length;
552 0 0 0     0 Write($outfile, $ch, $buff) or $err = 1 if $outfile; # write block
553 0         0 $text .= $buff;
554             }
555 0 0 0     0 Write($outfile, "\0") or $err = 1 if $outfile; # write terminator block
556 0         0 $et->HandleTag($tagTablePtr, 'Text', $text);
557 0         0 next;
558             }
559 0 0 0     0 Write($outfile, $ch, $s) or $err = 1 if $outfile;
560             # skip the block
561 0         0 while ($length) {
562 0 0       0 last unless $raf->Read($buff, $length) == $length;
563 0 0 0     0 Write($outfile, $buff) or $err = 1 if $outfile;
564 0 0       0 last unless $raf->Read($ch, 1); # read next block header
565 0 0 0     0 Write($outfile, $ch) or $err = 1 if $outfile;
566 0         0 $length = ord($ch); # get next block size
567             }
568             }
569 7 100       20 unless ($outfile) {
570 4 50       11 $et->HandleTag($tagTablePtr, 'FrameCount', $frameCount) if $frameCount > 1;
571 4 50       14 $et->HandleTag($tagTablePtr, 'Duration', $delayTime/100) if $delayTime;
572             }
573              
574             # set return value to -1 if we only had a write error
575 7 50 33     33 $rtnVal = -1 if $rtnVal and $err;
576 7         40 return $rtnVal;
577             }
578              
579              
580             1; #end
581              
582             __END__