File Coverage

blib/lib/Image/GIF/Encoder/PP.pm
Criterion Covered Total %
statement 235 277 84.8
branch 53 82 64.6
condition 6 15 40.0
subroutine 26 28 92.8
pod 4 15 26.6
total 324 417 77.7


line stmt bran cond sub pod time code
1             package Image::GIF::Encoder::PP;
2             # Copyright (c) 2021-2022 Gavin Hayes, see LICENSE in the root of the project
3 1     1   2928 use version 0.77; our $VERSION = qv(v0.1.1);
  1         2114  
  1         8  
4 1     1   95 use strict;
  1         4  
  1         20  
5 1     1   5 use warnings;
  1         1  
  1         725  
6              
7             sub write_num {
8 516     516 0 1077 my ($fh, $val) = @_;
9 516         1372 print $fh pack('v', $val);
10             }
11              
12             sub new_node {
13 12160     12160 0 19149 my ($key, $degree) = @_;
14 12160         34258 my %node = (
15             'key' => $key,
16             'children' => []
17             );
18              
19 12160         32057 return \%node;
20             }
21              
22             sub new_trie {
23 102     102 0 374 my ($degree, $nkeys) = @_;
24 102         387 my $root = new_node(0, $degree);
25             # Create nodes for single pixels.
26 102         474 for($$nkeys = 0; $$nkeys < $degree; $$nkeys += 1) {
27 432         780 $root->{'children'}[$$nkeys] = new_node($$nkeys, $degree);
28             }
29 102         315 $$nkeys += 2; #skip clear code and stop code
30 102         356 return $root;
31             }
32              
33       102 0   sub del_trie {
34             # nothing needed to free in perl
35             }
36              
37             sub put_loop {
38 2     2 0 7 my ($gif, $loop) = @_;
39 2         4 print {$gif->{'fh'}} pack('CCC', 0x21, 0xFF, 0x0B);
  2         4  
40 2         4 print {$gif->{'fh'}} "NETSCAPE2.0";
  2         5  
41 2         10 print {$gif->{'fh'}} pack('CC', 0x03, 0x01);
  2         7  
42 2         10 write_num($gif->{'fh'}, $loop);
43 2         4 print {$gif->{'fh'}} "\0";
  2         4  
44             }
45              
46             # Add packed key to buffer, updating offset and partial.
47             # $gif->{'offset'} holds position to put next *bit*
48             # $gif->{'partial'} holds bits to include in next byte
49             sub put_key {
50 11932     11932 0 19369 my ($gif, $key, $key_size) = @_;
51              
52 11932         21496 my $byte_offset = int($gif->{'offset'} / 8);
53 11932         18163 my $bit_offset = $gif->{'offset'} % 8;
54 11932         18121 $gif->{'partial'} |= ($key << $bit_offset);
55 11932         15786 my $bits_to_write = $bit_offset + $key_size;
56 11932         21884 while ($bits_to_write >= 8) {
57 10253         25240 vec($gif->{'buffer'}, $byte_offset++, 8) = $gif->{'partial'} & 0xFF;
58 10253 100       23332 if ($byte_offset == 0xFF) {
59 3         14 print {$gif->{'fh'}} "\xFF";
  3         7  
60 3 50       13 length($gif->{'buffer'}) == 0xFF or die("misport");
61 3         19 print {$gif->{'fh'}} $gif->{'buffer'};
  3         14  
62 3         4 $byte_offset = 0;
63             }
64 10253         14703 $gif->{'partial'} >>= 8;
65 10253         18874 $bits_to_write -= 8;
66             }
67            
68 11932         21370 $gif->{'offset'} = ($gif->{'offset'} + $key_size) % (0xFF * 8);
69             }
70              
71             sub end_key {
72 102     102 0 303 my ($gif) = @_;
73 102         270 my $byte_offset = int($gif->{'offset'} / 8);
74 102 100       357 if ($gif->{'offset'} % 8) {
75 101         344 vec($gif->{'buffer'}, $byte_offset++, 8) = $gif->{'partial'} & 0xFF;
76             }
77 102 50       264 if ($byte_offset) {
78 102         211 print {$gif->{'fh'}} pack('C', $byte_offset);
  102         460  
79 102         224 print {$gif->{'fh'}} substr($gif->{'buffer'}, 0, $byte_offset);
  102         382  
80             }
81 102         191 print {$gif->{'fh'}} "\0";
  102         209  
82 102         308 $gif->{'offset'} = $gif->{'partial'} = 0;
83             }
84              
85 1     1   8 use constant FRAME_CUR => 0;
  1         2  
  1         172  
86 1     1   7 use constant FRAME_LAST => 1;
  1         2  
  1         677  
87              
88             sub put_image {
89 102     102 0 326 my ($gif, $frameindex, $w, $h, $x, $y) = @_;
90 102 50       504 my $frameref = ($frameindex == FRAME_CUR) ? \$gif->{'frame'} : \$gif->{'back'};
91 102         358 my $degree = 1 << $gif->{'depth'};
92              
93 102         186 print {$gif->{'fh'}} ",";
  102         228  
94 102         428 write_num($gif->{'fh'}, $x);
95 102         404 write_num($gif->{'fh'}, $y);
96 102         301 write_num($gif->{'fh'}, $w);
97 102         379 write_num($gif->{'fh'}, $h);
98 102         266 print {$gif->{'fh'}} pack('CC', 0x0, $gif->{'depth'});
  102         580  
99 102         228 my $nkeys;
100 102         391 my $node = new_trie($degree, \$nkeys);
101 102         269 my $root = $node;
102 102         315 my $key_size = $gif->{'depth'} + 1;
103            
104 102         489 put_key($gif, $degree, $key_size); # clear code
105              
106 102         411 for (my $i = $y; $i < $y+$h; $i++) {
107 4272         7960 for (my $j = $x; $j < $x+$w; $j++) {
108 232982         362060 my $pixel = vec($$frameref, $i*$gif->{'w'}+$j, 8) & ($degree - 1);
109 232982         314142 my $child = $node->{'children'}[$pixel];
110 232982 100       341937 if ($child) {
111 221356         411933 $node = $child;
112             } else {
113 11626         24195 put_key($gif, $node->{'key'}, $key_size);
114 11626 50       19526 if ($nkeys < 0x1000) {
115 11626 100       21043 if ($nkeys == (1 << $key_size)) {
116 263         438 $key_size++;
117             }
118 11626         19221 $node->{'children'}[$pixel] = new_node($nkeys++, $degree);
119             } else {
120 0         0 put_key($gif, $degree, $key_size); # clear code
121 0         0 del_trie($root, $degree);
122 0         0 $root = $node = new_trie($degree, \$nkeys);
123 0         0 $key_size = $gif->{'depth'} + 1;
124             }
125 11626         29752 $node = $root->{'children'}[$pixel];
126             }
127             }
128             }
129 102         405 put_key($gif, $node->{'key'}, $key_size);
130 102         316 put_key($gif, $degree + 1, $key_size); # stop code
131 102         467 end_key($gif);
132 102         470 del_trie($root, $degree);
133             }
134              
135             sub get_bbox {
136 100     100 0 292 my ($gif, $w, $h, $x, $y) = @_;
137 100         302 my $left = $gif->{'w'}; my $right = 0;
  100         191  
138 100         233 my $top = $gif->{'h'}; my $bottom = 0;
  100         165  
139 100         178 my $k = 0;
140 100         362 for (my $i = 0; $i < $gif->{'h'}; $i++) {
141 9964         18167 for (my $j = 0; $j < $gif->{'w'}; $j++, $k++) {
142 994096 100       2260822 if (vec($gif->{'frame'}, $k, 8) != vec($gif->{'back'}, $k, 8)) {
143 163092 100       272657 if ($j < $left) {
144 109         204 $left = $j;
145             }
146 163092 100       265555 if ($j > $right) {
147 2152         2769 $right = $j;
148             }
149 163092 100       261853 if ($i < $top) {
150 100         184 $top = $i;
151             }
152 163092 100       354495 if ($i > $bottom) {
153 4107         7955 $bottom = $i;
154             }
155             }
156             }
157             }
158 100 50 33     1020 if ($left != $gif->{'w'} && $top != $gif->{'h'}) {
159 100         249 $$x = $left; $$y = $top;
  100         244  
160 100         264 $$w = $right - $left + 1;
161 100         231 $$h = $bottom - $top + 1;
162 100         675 return 1;
163             } else {
164 0         0 return 0;
165             }
166             }
167              
168 1     1   8 use constant DM_UNSPEC => 0 << 2;
  1         2  
  1         127  
169 1     1   7 use constant DM_DND => 1 << 2; # Do Not Dispose
  1         2  
  1         84  
170 1     1   7 use constant DM_RTB => 2 << 2; # Restore To Background (clear pixel)
  1         2  
  1         49  
171 1     1   6 use constant DM_RTP => 3 << 2; # Restore To Previous (not currently used)
  1         2  
  1         1707  
172              
173             sub add_graphics_control_extension {
174 102     102 0 366 my ($gif, $d, $dm) = @_;
175 102         862 my $out = "!\xF9\x04".pack('C', $dm);
176 102 50       371 if($gif->{'transparent_index'} != -1) {
177 102         831 vec($out, 3, 8) |= 0x1; # transparent color flag
178             }
179 102         433 print {$gif->{'fh'}} $out;
  102         576  
180 102         577 write_num($gif->{'fh'}, $d);
181 102         349 vec($out, 0, 8) = 0x0;
182 102         360 vec($out, 1, 8) = 0x0;
183 102 50       409 if($gif->{'transparent_index'} != -1) {
184 102         336 vec($out, 0, 8) = $gif->{'transparent_index'};
185             }
186 102         262 print {$gif->{'fh'}} substr($out, 0, 2);
  102         360  
187             }
188              
189              
190             # external interface
191             sub new {
192 2     2 1 1058 my ($class, $filename, $width, $height, $palette, $depth, $loop, $transparent_index) = @_;
193 2         23 my $gif = {
194             'w' => $width,
195             'h' => $height,
196             'depth' => 0,
197             'transparent_index' => $transparent_index,
198             'has_unencoded_frame' => 0,
199             'fd' => undef,
200             'offset' => 0,
201             'nframes' => 0,
202             'frame' => '',
203             'back' => '',
204             'partial' => 0,
205             'buffer' => ''
206             };
207 2         20 vec($gif->{'frame'}, $width*$height-1, 8) = 0;
208 2         39 vec($gif->{'back'}, $width*$height-1, 8) = 0;
209 2         12 vec($gif->{'buffer'}, 0xFF-1, 8) = 0;
210 2 50       9 if($filename) {
211 2 50       194 open($gif->{'fh'}, '>', $filename) or return undef;
212             }
213             else {
214 0         0 $gif->{'fh'} = *STDOUT;
215             }
216              
217 2         14 bless $gif, $class;
218              
219 2         4 print {$gif->{'fh'}} "GIF89a";
  2         50  
220 2         12 write_num($gif->{'fh'}, $width);
221 2         18 write_num($gif->{'fh'}, $height);
222 2         5 my $store_gct; my $custom_gct;
223 2 50       6 if ($palette) {
224 2 50       8 if ($depth < 0) {
225 0         0 $store_gct = 1;
226             }
227             else {
228 2         25 $custom_gct = 1;
229             }
230             }
231 2 50       7 if ($depth < 0) {
232 0         0 $depth = -$depth;
233             }
234 2 100       8 $gif->{'depth'} = $depth > 1 ? $depth : 2;
235 2         3 print {$gif->{'fh'}} pack('CCC', (0xF0 | ($depth-1)), 0x00, 0x00);
  2         9  
236 2 50       6 if ($custom_gct) {
237 2         5 print {$gif->{'fh'}} substr($palette, 0, 3 << $depth);
  2         8  
238             }
239             else {
240 0         0 warn("unimplemented mode");
241 0         0 return undef;
242             }
243              
244 2 50 33     15 if ($loop >= 0 && $loop <= 0xFFFF) {
245 2         5 put_loop($gif, $loop);
246             }
247              
248 2         8 return $gif;
249             }
250              
251             sub add_frame_with_transparency {
252 102     102 0 322 my ($gif, $has_new_frame) = @_;
253 102         352 $gif->{'has_unencoded_frame'} = 0;
254 102         266 my $dm = DM_DND;
255 102         254 my $w = $gif->{'unencoded_w'};
256 102         220 my $h = $gif->{'unencoded_h'};
257 102         286 my $x = $gif->{'unencoded_x'};
258 102         295 my $y = $gif->{'unencoded_y'};
259 102 100       368 if($has_new_frame)
260             {
261             # if the new frame has any new transparent pixels (not already transparent) RTB is required
262 100         450 for(my $i = 0; $i < $gif->{'h'}; $i++)
263             {
264 9964         19450 for(my $j = 0; $j < $gif->{w}; $j++)
265             {
266 994096 100 100     3170251 if((vec($gif->{frame}, ($i*$gif->{w}) + $j, 8) == $gif->{'transparent_index'}) &&
267             (vec($gif->{back}, ($i*$gif->{w}) + $j, 8) != $gif->{'transparent_index'})) {
268 650         989 $dm = DM_RTB;
269             # adjust the BB so the pixel will be cleared on RTB
270 650 50       1194 if($i < $y)
271             {
272 0         0 my $delta = $y-$i;
273 0         0 $y = $i;
274 0         0 $h += $delta;
275             }
276              
277 650 50       1166 if($j < $x)
278             {
279 0         0 my $delta = $x-$j;
280 0         0 $x = $j;
281 0         0 $w += $delta;
282             }
283              
284 650 50       1261 if($i >= ($y+$gif->{h}))
285             {
286 0         0 $h += ($i-($y+$gif->{h})+1);
287             }
288              
289 650 50       1509 if($j >= ($x+$gif->{w}))
290             {
291 0         0 $w += ($j-($x+$gif->{w})+1);
292             }
293             }
294             }
295             }
296              
297             }
298 102         895 add_graphics_control_extension($gif, $gif->{'unencoded_delay'}, $dm);
299 102         509 put_image($gif, FRAME_LAST, $w, $h, $x, $y);
300              
301 102 100       388 if($dm == DM_RTB)
302             {
303             # RTB our internal model, used by get_bbox
304 51         238 for(my $i = $y; $i < ($y+$h); $i++)
305             {
306 4080         7475 for(my $j = $x; $j < ($x+$w); $j++)
307             {
308 225560         577422 vec($gif->{back}, $i*$gif->{w} + $j, 8) = $gif->{'transparent_index'};
309             }
310             }
311             }
312             }
313              
314             sub add_frame {
315 102     102 1 9953680 my ($gif, $delay) = @_;
316              
317             # encode an old frame if needed
318 102 100       577 if($gif->{'has_unencoded_frame'}) {
319 100         450 add_frame_with_transparency($gif, 1);
320             }
321              
322             # determine the changed area since the last frame
323 102         333 my ($w, $h, $x, $y);
324 102 100       592 if (($gif->{nframes} == 0)) {
    50          
325 2         6 $w = $gif->{'w'};
326 2         5 $h = $gif->{'h'};
327 2         4 $x = $y = 0;
328             } elsif (!get_bbox($gif, \$w, \$h, \$x, \$y)) {
329             # image's not changed; save one pixel just to add delay
330 0         0 $w = $h = 1;
331 0         0 $x = $y = 0;
332             }
333              
334             # encode the frame now if transparency isn't used at all
335 102 50       404 if($gif->{'transparent_index'} == -1) {
336 0 0       0 if($delay) {
337 0         0 add_graphics_control_extension($gif, $delay, DM_DND);
338             }
339 0         0 put_image($gif, FRAME_CUR, $w, $h, $x, $y);
340             }
341             else {
342 102         227 $gif->{'has_unencoded_frame'} = 1;
343 102         204 $gif->{'unencoded_w'} = $w;
344 102         284 $gif->{'unencoded_h'} = $h;
345 102         202 $gif->{'unencoded_x'} = $x;
346 102         223 $gif->{'unencoded_y'} = $y;
347 102         179 $gif->{'unencoded_delay'} = $delay;
348             }
349              
350             # move on to the next frame, swap the buffers
351 102         231 $gif->{'nframes'}++;
352 102         270 my $tmp = $gif->{'back'};
353 102         276 $gif->{'back'} = $gif->{'frame'};
354 102         777 $gif->{'frame'} = $tmp;
355             }
356              
357             sub _finish {
358 2     2   10 my ($gif) = @_;
359             # encode an old frame if needed
360 2 50       8 if($gif->{'has_unencoded_frame'}) {
361 2         8 add_frame_with_transparency($gif, 0);
362             }
363 2         10 print {$gif->{'fh'}} ';';
  2         339  
364             }
365              
366             sub DESTROY {
367 2     2   36 $_[0]->_finish();
368             }
369              
370             # helper functions
371             sub expand_frame {
372 0     0 1 0 my ($data, $srcbitsperpixel, $desiredbitsperpixel) = @_;
373 0 0       0 (length($data) % $srcbitsperpixel) == 0 or return undef;
374 0         0 my $count = (length($data) * 8) / $srcbitsperpixel;
375 0         0 my $dest;
376 0         0 vec($dest, $count-1, $desiredbitsperpixel) = 0;
377 0         0 for(my $i = 0; $i < $count; $i++) {
378 0         0 vec($dest, $i, $desiredbitsperpixel) = vec($data, $i, $srcbitsperpixel);
379             }
380 0         0 return $dest;
381             }
382              
383             sub _scaleUp {
384 2     2   5 my ($dest, $data, $w, $h, $times) = @_;
385 2         3 my $desti = 0;
386 2         6 for(my $y = 0; $y < $h; $y++) {
387 32         49 my $ystop = $desti + ($w * $times * $times);
388 32         52 while($desti < $ystop) {
389 128         232 for(my $x = 0; $x < $w; $x++) {
390 2048         2752 my $stop = $desti + $times;
391 2048         3358 while($desti < $stop) {
392 8192         20037 vec($$dest, $desti++, 8) = vec($data, ($y * $w) + $x, 8);
393             }
394             }
395             }
396             }
397              
398 2         10 return 1;
399             }
400              
401             sub _scaleDown {
402 0     0   0 my ($dest, $data, $w, $h, $every) = @_;
403 0         0 my $desti = 0;
404 0         0 for(my $y = 0; $y < $h; $y += $every) {
405 0         0 for(my $x = 0; $x < $w; $x += $every) {
406 0         0 vec($$dest, $desti++, 8) = vec($data, ($y * $w) + $x, 8);
407             }
408             }
409              
410 0         0 return 1;
411             }
412              
413             sub scale {
414 2     2 1 44 my ($data, $w, $h, $times, $dest) = @_;
415 2 50 33     12 ($times == int($times)) && ($times != 0) or return undef;
416 2         4 my ($neww, $newh);
417 2 50       5 if($times > 0) {
418 2         3 $neww = $w * $times;
419 2         5 $newh = $h * $times;
420 2         5 return _scaleUp($dest, $data, $w, $h, $times);
421             }
422             else {
423 0           my $div = -$times;
424 0           $neww = $w / $div;
425 0           $newh = $h / $div;
426 0 0 0       ($neww == int($neww)) && ($newh == int($newh)) or return undef;
427 0           return _scaleDown($dest, $data, $w, $h, -$times);
428             }
429             }
430              
431             1;
432              
433             __END__