File Coverage

lib/Image/Animated/JPEG.pm
Criterion Covered Total %
statement 179 200 89.5
branch 134 200 67.0
condition 28 96 29.1
subroutine 9 11 81.8
pod 4 8 50.0
total 354 515 68.7


line stmt bran cond sub pod time code
1             package Image::Animated::JPEG;
2              
3 3     3   226213 use strict;
  3         24  
  3         88  
4 3     3   14 use warnings;
  3         4  
  3         72  
5              
6 3     3   1624 use Encode;
  3         31044  
  3         8072  
7              
8             our $VERSION = '0.03';
9              
10             sub index {
11 4     4 1 3536 my $io_file = shift;
12 4         6 my $args = shift;
13              
14 4         7 my @frames;
15              
16 4 0 33     10 print "Parsing file...\n" if $args && $args->{debug};
17              
18             # check
19 4         10 my $soi = my_read($io_file, 2);
20 4 100       11 unless ($soi eq "\xFF\xD8") {
21 1         12 die "Does not look like a JPEG file: SOI missing at start of file";
22             }
23              
24             # first frame
25 3         5 my $cnt = 1;
26 3 0 33     7 print " frame $cnt begins at 0\n" if $args && $args->{debug};
27 3         12 push(@frames, {
28             offset => 0,
29             io_file => $io_file, # hand over, per frame (might be multiple files)
30             });
31              
32             # subsequent frames
33 3         13 local $/ = "\xFF\xD9\xFF\xD8"; # we look for EOI+SOI marker to avoid false-positives with embedded thumbs
34 3         27 while(my $chunk = <$io_file>){
35 5         13 my $pos = tell($io_file) - 2; # -2: compensate for inclusion of begin-marker
36 5 0 33     9 print " frame $cnt ends at $pos\n" if $args && $args->{debug};
37 5         13 push(@frames, {
38             offset => $pos,
39             io_file => $io_file, # a quirk for playajpeg: hand over the fh, per frame, as playajpeg may play a sequence of jpegs as animation
40             });
41 5         12 $frames[$cnt - 1]->{length} = $pos - $frames[$cnt - 1]->{offset};
42 5         21 $cnt++;
43             }
44 3         21 seek($io_file, 0,0); # rewind fh
45 3         7 pop(@frames); # last boundary is not beginning of a new frame
46 3         24 $frames[-1]->{length} += 2; # last frame won't end with EOI+SOI
47              
48 3         18 return \@frames;
49             }
50              
51             sub process { # mostly from Image::Info
52 5     5 1 18255 my $args = $_[1];
53              
54 5         9 my $fh;
55 5 100       15 if(ref($_[0]) eq 'SCALAR'){
56 2         524 require IO::String;
57 2 50       4035 $fh = IO::String->new($_[0]) or die "Error using scalar-ref for reading: $!";
58             }else{
59 3 100       145 open($fh, "<", $_[0]) or die "Error opening file for reading: $!";
60 2         11 binmode($fh);
61             }
62              
63 4 50 33     123 my $soi = my_read($fh, 2, ($args && $args->{debug} ? 1 : undef));
64 4 100       14 unless ($soi eq "\xFF\xD8") {
65 1         3 my $offset = tell($fh) - 2;
66 1         19 die "Does not look like a JPEG file: SOI missing at offset $offset";
67             }
68              
69 3         7 my %markers;
70             my @warnings;
71 3         6 while (1) {
72 9 50 33     28 my($ff, $mark) = unpack("CC", my_read($fh, 2, ($args && $args->{debug} ? 1 : undef)));
73 9 50       20 last if !defined $ff;
74              
75 9 50       18 if ($ff != 0xFF) { # enter when processing a chunk
76 0         0 my $corrupt_bytes = 2;
77 0         0 while(1) {
78 0 0 0     0 my($ff) = unpack("C", my_read($fh,1, ($args && $args->{debug} ? 1 : undef)));
79 0 0       0 return if !defined $ff;
80 0 0       0 last if $ff == 0xFF;
81 0         0 $corrupt_bytes++;
82             }
83 0 0 0     0 $mark = unpack("C", my_read($fh,1, ($args && $args->{debug} ? 1 : undef)));
84 0         0 push(@warnings, sprintf("Corrupt JPEG data, $corrupt_bytes extraneous bytes before marker 0x%02x", $mark));
85             }
86 9 50       16 if ($mark == 0xFF) { # munge FFs (JPEG markers can be padded with unlimited 0xFF's)
87 0         0 for (;;) {
88 0 0 0     0 ($mark) = unpack("C", my_read($fh, 1, ($args && $args->{debug} ? 1 : undef)));
89 0 0       0 last if $mark != 0xFF;
90             }
91             }
92              
93 9 50 33     31 last if $mark == 0xDA || $mark == 0xD9; # exit once we reach a SOS marker, or EOI (end of image)
94              
95 9 0 33     14 print "marker: FF ".sprintf("%#x",$mark)." \n" if $args && $args->{debug};
96 9         15 my $marker_pos = tell($fh) - 2;
97 9 50 33     48 my($len) = unpack("n", my_read($fh, 2, ($args && $args->{debug} ? 1 : undef))); # we found a marker, read its size
98 9 50       18 last if $len < 2; # data-less marker
99              
100 9 100       16 last if $mark < 0xE0; # data-less marker
101              
102             # process_chunk($info, $img_no, $mark, my_read($fh, $len - 2));
103 6 100       10 if($mark == 0xE0){
104 5 50 33     16 my $data = my_read($fh, $len - 2, ($args && $args->{debug} ? 1 : undef));
105 5 0 33     10 print "APP0 at ". $marker_pos ." len:$len \n" if $args && $args->{debug};
106              
107             # get_name($fh);
108 5         6 my $name;
109 5         5 my $rel_offset = 0;
110 5         12 for(0..10){ # app-identifiers may be arbitrarily long, but let's stop after 10 bytes
111 27         47 my ($value) = unpack("C", read_bytes(\$data, \$rel_offset, 1));
112 27 100       55 last if $value == 0x00;
113 22         30 $name .= chr($value);
114             }
115              
116 5         28 $markers{$name} = {
117             type => 'APP0',
118             offset => $marker_pos,
119             length => $len,
120             data_offset => ($marker_pos + 4) + (length($name) + 1),
121             data_length => ($len - 2) - (length($name) + 1)
122             };
123             }else{
124 1         14 seek($fh,$len - 2,1);
125             }
126              
127             }
128              
129             # print Dumper(\%markers,\@warnings);
130 3         44 return \%markers;
131             }
132              
133             sub my_read { # from Image::Info
134 31     31 0 48 my($fh, $len) = @_;
135 31 50       49 print " my_read: len:$len \n" if $_[2];
136 31         32 my $buf;
137 31         204 my $n = read($fh, $buf, $len);
138 31 50       220 die "read failed: $!" unless defined $n;
139 31 50       46 die "short read ($len/$n) at pos " . tell($fh) unless $n == $len;
140 31         86 $buf;
141             }
142              
143              
144             sub encode_ajpeg_marker {
145 0     0 0 0 return "AJPEG\000" . encode_ajpeg_data(@_);
146             }
147              
148             sub decode_ajpeg_marker {
149 0     0 0 0 return decode_ajpeg_data( substr($_[0],6) );
150             }
151              
152             # +------------- segment --------------+
153             # | APP0-marker |
154             # | +--"marker" / data ----+
155             # | | AJPEG-marker |
156             # | | |
157             # \x00
158             # FF E0 00 00 AJPEG 00 ...
159              
160             ## expects a hashref
161             ## encodes hash keys according to the AJPEG schema
162             sub encode_ajpeg_data {
163 4     4 1 14349 my $ref = shift;
164 4         6 my $args = shift; # debug, future: version
165              
166 4 50 33     23 die "encode_ajpeg_data expects a hash-ref" unless $ref && ref($ref) eq 'HASH';
167              
168 4         5 my $binary;
169              
170 4         7 $binary = pack("C1",0); # version:0
171              
172 4         16 for my $key (keys %$ref){
173 37 50       58 unless(defined($ref->{$key})){
174 0 0 0     0 warn "encode_ajpeg_data: $key value is undef and will be ignored!" if $args && $args->{debug};
175 0         0 next;
176             }
177 37 100       55 if($key eq 'version'){
178 4 50 33     18 warn "encode_ajpeg_data: Only format version 0 is currenty implemented!" if $args && $args->{debug} && $ref->{$key} != 0;
      33        
179 4         7 next;
180             }
181 33 100       101 if($key eq 'delay'){
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
182 4 100       39 if($ref->{$key} <= 255){
    100          
183 2 50 33     11 print "encode_ajpeg_data: delay $ref->{$key} (byte)\n" if $args && $args->{debug};
184 2         8 $binary .= "\x01" . pack("C",$ref->{$key});
185             }elsif($ref->{$key} <= 65535){
186 1 50 33     6 print "encode_ajpeg_data: delay $ref->{$key} (short)\n" if $args && $args->{debug};
187 1         4 $binary .= "\x02" . pack("n",$ref->{$key});
188             }else{
189 1 50 33     5 print "encode_ajpeg_data: delay $ref->{$key} (long)\n" if $args && $args->{debug};
190 1         5 $binary .= "\x04" . pack("N",$ref->{$key});
191             }
192             }elsif($key eq 'repeat'){
193 4 100       9 if($ref->{$key} <= 255){
    50          
194 2 50 33     9 print "encode_ajpeg_data: repeat $ref->{$key} (byte)\n" if $args && $args->{debug};
195 2         9 $binary .= "\x11" . pack("C",$ref->{$key});
196             }elsif($ref->{$key} <= 65535){
197 2 50 33     8 print "encode_ajpeg_data: repeat $ref->{$key} (short)\n" if $args && $args->{debug};
198 2         6 $binary .= "\x12" . pack("n",$ref->{$key});
199             }else{
200 0         0 die "repeat values must be <= 65535";
201             }
202             }elsif($key eq 'parse_next'){
203 4 100       12 if($ref->{$key} <= 255){
    50          
204 2         7 $binary .= "\x21" . pack("C",$ref->{$key});
205             }elsif($ref->{$key} <= 65535){
206 2         5 $binary .= "\x22" . pack("n",$ref->{$key});
207             }else{
208 0         0 die "parse_next values must be <= 65535";
209             }
210             }elsif($key eq 'length'){
211 4 100       11 if($ref->{$key} <= 255){
    100          
212 1         3 $binary .= "\x31" . pack("C",$ref->{$key});
213             }elsif($ref->{$key} <= 65535){
214 2         6 $binary .= "\x32" . pack("n",$ref->{$key});
215             }else{
216 1         4 $binary .= "\x34" . pack("N",$ref->{$key});
217             }
218             }elsif($key eq 'previous'){
219 4 100       10 if($ref->{$key} <= 255){
    100          
220 1         3 $binary .= "\x41" . pack("C",$ref->{$key});
221             }elsif($ref->{$key} <= 65535){
222 2         5 $binary .= "\x42" . pack("n",$ref->{$key});
223             }else{
224 1         8 $binary .= "\x44" . pack("N",$ref->{$key});
225             }
226             }elsif($key eq 'x_offset'){
227 4 100       12 if($ref->{$key} <= 255){
    50          
228 2         5 $binary .= "\x51" . pack("C",$ref->{$key});
229             }elsif($ref->{$key} <= 65535){
230 2         5 $binary .= "\x52" . pack("n",$ref->{$key});
231             }else{
232 0         0 die "x_offset values must be <= 65535";
233             }
234             }elsif($key eq 'y_offset'){
235 4 100       11 if($ref->{$key} <= 255){
    50          
236 2         6 $binary .= "\x61" . pack("C",$ref->{$key});
237             }elsif($ref->{$key} <= 65535){
238 2         7 $binary .= "\x62" . pack("n",$ref->{$key});
239             }else{
240 0         0 die "x_offset values must be <= 65535";
241             }
242             }elsif($key eq 'dispose_op'){
243 4 50       7 if($ref->{$key} <= 2){
244 4         11 $binary .= "\x71" . pack("C",$ref->{$key});
245             }else{
246 0         0 die "dispose_op values must be <= 2";
247             }
248             }elsif($key eq 'metadata'){
249             # skip, for later
250             }else{
251 0         0 warn "encode_ajpeg_data: '$key' is not recognized";
252             }
253             }
254              
255             # specs do not require it, but it may make sense to sort
256             # "potentially longer" byte segments, like metadata, to the end
257             # of the AJPEG segmemt
258 4 100       14 if($ref->{'metadata'}){ # A0
259 1 50 33     7 print "encode_ajpeg_data: metadata \n" if $args && $args->{debug};
260 1         3 for my $mkey (keys %{ $ref->{'metadata'} }){
  1         4  
261 4 50 33     17 print " metadata: ". $mkey .":". $ref->{'metadata'}->{$mkey} ."\n" if $args && $args->{debug};
262 4         9 my $mkey_utf8 = encode('utf-8',$mkey);
263 4         359 my $mvalue_utf8 = encode('utf-8',$ref->{'metadata'}->{$mkey});
264              
265 4         157 $binary .= pack("C",160) . pack("C",length($mkey_utf8)) . $mkey_utf8 . pack("C",length($mvalue_utf8)) . $mvalue_utf8;
266             }
267             }
268              
269 4         11 return $binary;
270             }
271              
272              
273             sub read_bytes {
274 118     118 0 125 my $binary_ref = shift;
275 118         117 my $offset_ref = shift; # quirk: offset as ref, so a read_bytes(), like core::read, advances a pointer/offset
276 118         115 my $length = shift;
277              
278             # print " read $length bytes at ${$offset_ref} \n";
279 118         102 my $data = substr(${$binary_ref}, ${$offset_ref}, $length);
  118         128  
  118         147  
280 118         157 ${$offset_ref} += $length;
  118         127  
281              
282 118         272 return $data;
283             }
284              
285             ## expects a scalar holding binary data
286             # decodes AJPEG schema binary encoded keys and values
287             sub decode_ajpeg_data {
288 5     5 1 3593 my $binary = shift;
289 5         5 my $args = shift; # debug
290              
291 5 50       13 die "decode_ajpeg_data expects a scalar with binary data" unless defined($binary);
292              
293 5         9 my $length = length($binary);
294 5         6 my $offset = 0;
295              
296 5         6 my %ref;
297              
298 5 50 33     23 print "decode_ajpeg_data: length:$length \n" if $args && $args->{debug};
299 5         15 $ref{version} = unpack("C", read_bytes(\$binary,\$offset,1));
300 5 50 33     24 print "decode_ajpeg_data: version:$ref{version} \n" if $args && $args->{debug};
301              
302 5         6 my $cnt;
303 5         6 for(;;){
304 37         39 $cnt++;
305              
306 37         73 my $byte = read_bytes(\$binary,\$offset, 1);
307 37 50       61 if($byte){ # an "empty AJPEG marker" will only hold version, read_bytes beyond that will return undef (thus we skip $key_num/properties decoding)
308 37         49 my $key_num = unpack("C", $byte);
309 37 100       200 if($key_num == 1){
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
310 3         7 $ref{'delay'} = unpack("C", read_bytes(\$binary,\$offset,1));
311 3 50 33     16 print "decode_ajpeg_data: delay: $ref{'delay'} (byte) \n" if $args && $args->{debug};
312             }elsif($key_num == 2){
313 1         4 $ref{'delay'} = unpack("n", read_bytes(\$binary,\$offset,2));
314 1 50 33     6 print "decode_ajpeg_data: delay: $ref{'delay'} (short) \n" if $args && $args->{debug};
315             }elsif($key_num == 4){
316 1         4 $ref{'delay'} = unpack("N1", read_bytes(\$binary,\$offset,4));
317 1 50 33     5 print "decode_ajpeg_data: delay: $ref{'delay'} (long) \n" if $args && $args->{debug};
318             }elsif($key_num == 17){
319 2         5 $ref{'repeat'} = unpack("C", read_bytes(\$binary,\$offset,1));
320 2 50 33     14 print "decode_ajpeg_data: repeat: $ref{'repeat'} (byte) \n" if $args && $args->{debug};
321             }elsif($key_num == 18){
322 2         11 $ref{'repeat'} = unpack("n", read_bytes(\$binary,\$offset,2));
323 2 50 33     8 print "decode_ajpeg_data: repeat: $ref{'repeat'} (short) \n" if $args && $args->{debug};
324             }elsif($key_num == 33){
325 2         11 $ref{'parse_next'} = unpack("C", read_bytes(\$binary,\$offset,1));
326             }elsif($key_num == 34){
327 2         4 $ref{'parse_next'} = unpack("n", read_bytes(\$binary,\$offset,2));
328             }elsif($key_num == 49){
329 1         4 $ref{'length'} = unpack("C", read_bytes(\$binary,\$offset,1));
330             }elsif($key_num == 50){
331 2         5 $ref{'length'} = unpack("n", read_bytes(\$binary,\$offset,2));
332             }elsif($key_num == 52){
333 1         5 $ref{'length'} = unpack("N", read_bytes(\$binary,\$offset,4));
334             }elsif($key_num == 65){
335 1         3 $ref{'previous'} = unpack("C", read_bytes(\$binary,\$offset,1));
336             }elsif($key_num == 66){
337 2         4 $ref{'previous'} = unpack("n", read_bytes(\$binary,\$offset,2));
338             }elsif($key_num == 68){
339 1         3 $ref{'previous'} = unpack("N", read_bytes(\$binary,\$offset,4));
340             }elsif($key_num == 81){
341 2         4 $ref{'x_offset'} = unpack("C", read_bytes(\$binary,\$offset,1));
342             }elsif($key_num == 82){
343 2         3 $ref{'x_offset'} = unpack("n", read_bytes(\$binary,\$offset,2));
344             }elsif($key_num == 97){
345 2         7 $ref{'y_offset'} = unpack("C", read_bytes(\$binary,\$offset,1));
346             }elsif($key_num == 98){
347 2         5 $ref{'y_offset'} = unpack("n", read_bytes(\$binary,\$offset,2));
348             }elsif($key_num == 113){
349 4         9 $ref{'dispose_op'} = unpack("C", read_bytes(\$binary,\$offset,1));
350             }elsif($key_num == 160){ # 0xA0
351 4         7 my $mkey_utf8 = read_bytes(\$binary,\$offset, unpack('C',read_bytes(\$binary,\$offset,1)) );
352 4         12 my $mvalue_utf8 = read_bytes(\$binary,\$offset, unpack('C',read_bytes(\$binary,\$offset,1)) );
353              
354 4 50       15 my $mkey = decode('utf-8', $mkey_utf8) if defined($mkey_utf8);
355 4 50       194 my $mvalue = decode('utf-8', $mvalue_utf8) if defined($mvalue_utf8);
356 4 50 33     166 print "decode_ajpeg_data: metadata: ". $mkey .":". $mvalue ." \n" if $args && $args->{debug};
357 4         12 $ref{'metadata'}->{$mkey} = $mvalue;
358             }
359             }
360 37 100       70 last if $offset >= $length;
361             }
362              
363 5         16 return \%ref;
364             }
365              
366             1;
367              
368             __END__