File Coverage

lib/Image/Info/JPEG.pm
Criterion Covered Total %
statement 130 144 90.2
branch 54 82 65.8
condition 14 31 45.1
subroutine 13 15 86.6
pod 0 9 0.0
total 211 281 75.0


line stmt bran cond sub pod time code
1             package Image::Info::JPEG;
2              
3             # Copyright 1999-2000, Gisle Aas.
4             #
5             # This library is free software; you can redistribute it and/or
6             # modify it under the same terms as Perl itself.
7              
8             # maintained by Tels 2007 - 2008
9              
10             $VERSION = 0.06;
11              
12             =begin register
13              
14             MAGIC: /^\xFF\xD8/
15              
16             For JPEG files we extract information both from C and C
17             application chunks.
18              
19             C is the file format written by most digital cameras. This
20             encode things like timestamp, camera model, focal length, exposure
21             time, aperture, flash usage, GPS position, etc.
22              
23             The C spec can be found at:
24             L.
25              
26             The C element may have the following values: C,
27             C, and C. Note that detecting C and C
28             currently does not work, but will hopefully in future.
29              
30             =end register
31              
32             =cut
33              
34 7     7   50 use strict;
  7         12  
  7         1514  
35              
36             my %sof = (
37             0xC0 => "Baseline",
38             0xC1 => "Extended sequential",
39             0xC2 => "Progressive",
40             0xC3 => "Lossless",
41             0xC5 => "Differential sequential",
42             0xC6 => "Differential progressive",
43             0xC7 => "Differential lossless",
44             0xC9 => "Extended sequential, arithmetic coding",
45             0xCA => "Progressive, arithmetic coding",
46             0xCB => "Lossless, arithmetic coding",
47             0xCD => "Differential sequential, arithmetic coding",
48             0xCE => "Differential progressive, arithmetic coding",
49             0xCF => "Differential lossless, arithmetic coding",
50             );
51              
52             sub my_read
53             {
54 778     687 0 1073 my($source, $len) = @_;
55 778         789 my $buf;
56 778         1488 my $n = read($source, $buf, $len);
57 687 50       1034 die "read failed: $!" unless defined $n;
58 687 50       907 die "short read ($len/$n) at pos " . tell($source) unless $n == $len;
59 687         1384 $buf;
60             }
61              
62             BEGIN {
63 7 50   7   53 my $f = ($] >= 5.008) ? <<'EOT' : <<'EOT';
64             sub with_io_string (&$) {
65             open(my $fh, "<", \$_[1]);
66             local $_ = $fh;
67             &{$_[0]};
68             }
69             EOT
70             sub with_io_string (&$) {
71             require IO::String;
72             local $_ = IO::String->new($_[1]);
73             &{$_[0]};
74             $_->close;
75             }
76             EOT
77              
78             #print $f;
79 7     93 0 599 eval $f;
  93     14   150  
  93         119  
  93         263  
  11         198  
  14         1686  
  14         57  
  14         37  
80 7 50       12457 die $@ if $@;
81             }
82              
83             sub process_file
84             {
85 17     17 0 35 my($info, $fh, $cnf) = @_;
86 17         43 _process_file($info, $fh, 0);
87             }
88              
89             sub _process_file
90             {
91 25     52   45 my($info, $fh, $img_no) = @_;
92              
93 25         47 my $soi = my_read($fh, 2);
94 28 50       77 unless ($soi eq "\xFF\xD8") {
95 3         12 my $ofs = tell() - 2;
96 3         6 die "SOI missing in JPEG file at offset $ofs";
97             }
98              
99 55         135 $info->push_info($img_no, "file_media_type" => "image/jpeg");
100 55         130 $info->push_info($img_no, "file_ext" => "jpg");
101              
102 55         92 while (1) {
103 235         363 my($ff, $mark) = unpack("CC", my_read($fh, 2));
104 235 50       428 last if !defined $ff;
105 237 100       360 if ($ff != 0xFF) {
106 5         9 my $corrupt_bytes = 2;
107 5         9 while(1) {
108 8         10 my($ff) = unpack("C", my_read($fh,1));
109 7 50       14 return if !defined $ff;
110 7 100       48 last if $ff == 0xFF;
111 34         64 $corrupt_bytes++;
112             }
113 2         4 $mark = unpack("C", my_read($fh,1));
114 2         12 $info->push_info($img_no, "Warn", sprintf("Corrupt JPEG data, $corrupt_bytes extraneous bytes before marker 0x%02x", $mark));
115             }
116 234 100       335 if ($mark == 0xFF) {
117             # JPEG markers can be padded with unlimited 0xFF's
118 32         108 for (;;) {
119 29         54 ($mark) = unpack("C", my_read($fh, 1));
120 29 50       65 last if $mark != 0xFF;
121             }
122             }
123 261 100 66     757 last if $mark == 0xDA || $mark == 0xD9; # SOS/EOI
124 236         332 my($len) = unpack("n", my_read($fh, 2));
125 236 50       532 last if $len < 2;
126 210         336 process_chunk($info, $img_no, $mark, my_read($fh, $len - 2));
127             }
128             }
129              
130             sub process_chunk
131             {
132 214     214 0 350 my($info, $img_no, $mark, $data) = @_;
133             #printf "MARK 0x%02X, len=%d\n", $mark, length($data);
134              
135 212 100 66     728 if ($mark == 0xFE) {
    100          
    100          
136 5         20 $info->push_info($img_no, Comment => $data);
137             }
138             elsif ($mark >= 0xE0 && $mark <= 0xEF) {
139 41 100       113 process_app($info, $mark, $data) if $img_no == 0;
140             }
141             elsif ($sof{$mark}) {
142 28         101 my($precision, $height, $width, $num_comp) =
143             unpack("CnnC", substr($data, 0, 6, ""));
144 28         142 $info->push_info($img_no, "JPEG_Type", $sof{$mark});
145              
146             # fix bug #15167 by keeping the highest values
147 28   50     93 my $old_w = $info->get_info($img_no, "width") || -1;
148 28   50     62 my $old_h = $info->get_info($img_no, "height") || -1;
149              
150 34 50       121 $info->replace_info($img_no, "width", $width) if $old_w < $width;
151 28 50       96 $info->replace_info($img_no, "height", $height) if $old_h < $height;
152              
153 28         76 for (1..$num_comp) {
154 75         131 $info->push_info($img_no, "BitsPerSample", $precision);
155             }
156 28         77 $info->push_info($img_no, "SamplesPerPixel" => $num_comp);
157              
158             # XXX need to consider JFIF/Adobe markers to determine this...
159 25 50       71 if ($num_comp == 1) {
    50          
    0          
160 3         7 $info->push_info($img_no, "color_type" => "Gray");
161             }
162             elsif ($num_comp == 3) {
163 28         65 $info->push_info($img_no, "color_type" => "YCbCr"); # or RGB ?
164             }
165             elsif ($num_comp == 4) {
166 3         10 $info->push_info($img_no, "color_type" => "CMYK"); # or YCCK ?
167             }
168              
169 34         72 if (1) {
170 34         137 my %comp_id_lookup = ( 1 => "Y",
171             2 => "Cb",
172             3 => "Cr",
173             82 => "R",
174             71 => "G",
175             66 => "B" );
176 34         75 while (length($data)) {
177 84         207 my($comp_id, $hv, $qtable) =
178             unpack("CCC", substr($data, 0, 3, ""));
179 84         162 my $horiz_sf = $hv >> 4 & 0x0f;
180 84         162 my $vert_sf = $hv & 0x0f;
181 80   33     167 $comp_id = $comp_id_lookup{$comp_id} || $comp_id;
182 80         224 $info->push_info($img_no, "ColorComponents", [$comp_id, $hv, $qtable]);
183 80         369 $info->push_info($img_no, "ColorComponentsDecoded",
184             { ComponentIdentifier => $comp_id,
185             HorizontalSamplingFactor => $horiz_sf,
186             VerticalSamplingFactor => $vert_sf,
187             QuantizationTableDesignator => $qtable } );
188             }
189             }
190             }
191             }
192              
193             sub process_app
194             {
195 32     30 0 72 my($info, $mark, $data) = @_;
196 32         69 my $app = $mark - 0xE0;
197 30         139 my $id = substr($data, 0, 5, "");
198             #$info->push_info(0, "Debug", "APP$app $id");
199 27         58 $id = "$app-$id";
200 29 100       100 if ($id eq "0-JFIF\0") {
    50          
    100          
    50          
201 14         40 process_app0_jfif($info, $data);
202             }
203             elsif ($id eq "0-JFXX\0") {
204 0         0 process_app0_jfxx($info, $data);
205             }
206             elsif ($id eq "1-Exif\0") {
207 15         34 process_app1_exif($info, $data);
208             }
209             elsif ($id eq "14-Adobe") {
210 3         22 process_app14_adobe($info, $data);
211             }
212             else {
213 1         3 $info->push_info(0, "App$id", $data);
214             #printf " %s\n", Data::Dump::dump($data);
215             }
216             }
217              
218             sub process_app0_jfif
219             {
220 14     14 0 25 my($info, $data) = @_;
221 17 50       60 if (length $data < 9) {
222 3         26 $info->push_info(0, "Debug", "Short JFIF chunk");
223 3         20 return;
224             }
225 17         78 my($ver_hi, $ver_lo, $unit, $x_density, $y_density, $x_thumb, $y_thumb) =
226             unpack("CC C nn CC", substr($data, 0, 9, ""));
227 17         112 $info->push_info(0, "JFIF_Version", sprintf("%d.%02d", $ver_hi, $ver_lo));
228              
229 17 50 33     97 my $res = $x_density != $y_density || !$unit
230             ? "$x_density/$y_density" : $x_density;
231              
232 17 50       40 if ($unit) {
233             $unit = { 0 => "pixels",
234             1 => "dpi",
235             2 => "dpcm"
236 17   33     94 }->{$unit} || "jfif-unit-$unit";
237 14         43 $res .= " $unit";
238             }
239 14         38 $info->push_info(0, "resolution", $res);
240              
241 14 50 33     81 if ($x_thumb || $y_thumb) {
242 0         0 $info->push_info(1, "width", $x_thumb);
243 0         0 $info->push_info(1, "height", $y_thumb);
244 0         0 $info->push_info(1, "ByteCount", length($data));
245             }
246             }
247              
248             sub process_app0_jfxx
249             {
250 0     0 0 0 my($info, $data) = @_;
251 0         0 my($code) = ord(substr($data, 0, 1, ""));
252             $info->push_info(1, "JFXX_ImageType",
253             { 0x10 => "JPEG thumbnail",
254             0x11 => "Bitmap thumbnail",
255             0x13 => "RGB thumbnail",
256 0   0     0 }->{$code} || "Unknown extension code $code");
257              
258 0 0       0 if ($code == 0x10) {
259 0         0 eval {
260             with_io_string {
261 2     2   6 _process_file($info, $_, 1);
262 2         12 } $data;
263             };
264 2 0       6 $info->push_info(1, "error" => $@) if $@;
265             }
266             }
267              
268             sub process_app1_exif
269             {
270 12     12 0 57 my($info, $data) = @_;
271 12         28 my $null = substr($data, 0, 1, "");
272 14 50       1211 if ($null ne "\0") {
273 2         7 $info->push_info(0, "Debug", "Exif chunk does not start with \\0");
274 2         13 return;
275             }
276              
277 14         1542 require Image::TIFF;
278 12         33 my $t = eval { Image::TIFF->new(\$data) };
  12         112  
279 14 50       36 if (!$t) {
280 4         13 $info->push_info(0, "Warn", "Cannot parse APP1 EXIF segment");
281 4         10 return;
282             }
283              
284              
285 131         277 for my $i (0 .. $t->num_ifds - 1) {
286 28         68 my $ifd = $t->ifd($i);
287              
288             # use Data::Dumper;
289             # print STDERR Dumper($ifd);
290              
291 25         56 for (@$ifd) {
292             # use Devel::Peek;
293             # print STDERR "# pushing info $i $_->[0] $_->[3]\n";
294             # print STDERR Devel::Peek::Dump($_->[3]),"\n" if $_->[0] =~ /Olympus-/;
295              
296 589         1032 $info->push_info($i, $_->[0], $_->[3]);
297             }
298              
299             # If we find JPEGInterchangeFormat/JPEGInterchangeFormatLngth,
300             # then we should apply process_file kind of recursively to extract
301             # information of this (thumbnail) image file...
302 25 100       77 if (my($ipos) = $info->get_info($i, "JPEGInterchangeFormat", 1)) {
303 12         25 my($ilen) = $info->get_info($i, "JPEGInterchangeFormatLength", 1);
304 11 50       24 if ($ilen)
305             {
306 11         86 my $jdata = substr($data, $ipos, $ilen);
307             #$info->push_info($i, "JPEGImage" => $jdata);
308              
309 15 100       43 if ($jdata) {
310             with_io_string {
311 15     11   40 _process_file($info, $_, $i);
312 15         325 } $jdata;
313             }
314             }
315             }
316              
317             # Turn XResolution/YResolution into 'resolution'
318 32         141 my($xres) = $info->get_info($i, "XResolution", 1);
319 28         67 my($yres) = $info->get_info($i, "YResolution", 1);
320              
321             # Samsung Digimax 200 is a totally confused camera that
322             # puts rational numbers with 0 as denominator and they
323             # also seem to not understand what resolution means.
324 28         52 for ($xres, $yres) {
325 52 100       300 $_ += 0 if ref($_) eq "Image::TIFF::Rational";
326             }
327              
328 27         73 my($unit) = $info->get_info($i, "ResolutionUnit", 1);
329 28         52 my $res = "1/1"; # default;
330 28 100 66     153 if ($xres && $yres) {
331 23 100       126 $res = ($xres == $yres) ? $xres : "$xres/$yres";
332             }
333 24 100 66     126 $res .= " $unit" if $unit && $unit ne "pixels";
334 24         56 $info->push_info($i, "resolution", $res);
335             }
336             }
337              
338             sub process_app14_adobe
339             {
340 0     0 0   my($info, $data) = @_;
341 0           my($version, $flags0, $flags1, $transform) = unpack("nnnC", $data);
342 0           $info->push_info(0, "AdobeTransformVersion" => $version);
343 0           $info->push_info(0, "AdobeTransformFlags" => [$flags0, $flags1]);
344 0           $info->push_info(0, "AdobeTransform" => $transform);
345             }
346              
347             1;