File Coverage

lib/Image/Info/WEBP.pm
Criterion Covered Total %
statement 57 59 96.6
branch 21 38 55.2
condition n/a
subroutine 4 4 100.0
pod 0 2 0.0
total 82 103 79.6


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Copyright (C) 2019 Preisvergleich Internet Services AG. All rights reserved.
5             # This package is free software; you can redistribute it and/or
6             # modify it under the same terms as Perl itself.
7             #
8              
9             # File magic is
10             # R I F F
11             # length (4 bytes)
12             # WEPB
13              
14             =begin register
15              
16             MAGIC: /^RIFF.{4}WEBP/s
17              
18             VP8 (lossy), VP8L (lossless) and VP8X (extended) files are supported.
19             Sets the key C<Animation> to true if the file is an animation. Otherwise
20             sets the key C<Compression> to either C<VP8> or C<Lossless>.
21              
22             =end register
23              
24             =cut
25              
26             package Image::Info::WEBP;
27              
28 2     2   11 use strict;
  2         3  
  2         75  
29 2     2   7 use warnings;
  2         2  
  2         1414  
30              
31             our $VERSION = '0.02';
32              
33             sub my_read
34             {
35 20     20 0 26 my($source, $len) = @_;
36 20         18 my $buf;
37 20         87 my $n = read($source, $buf, $len);
38 20 50       37 die "read failed: $!" unless defined $n;
39 20 50       28 die "short read ($len/$n) at pos " . tell($source) unless $n == $len;
40 20         82 $buf;
41             }
42              
43             my @upscale = (1, 5/4, 5/3, 2);
44              
45             sub process_file
46             {
47 10     10 0 18 my($info, $fh) = @_;
48              
49 10         17 my $signature = my_read($fh, 16);
50 10 50       46 die "Bad WEBP signature"
51             unless $signature =~ /\ARIFF....WEBPVP8([ LX])/s;
52              
53 10         23 my $type = $1;
54              
55 10         32 $info->push_info(0, "file_media_type" => "image/webp");
56 10         21 $info->push_info(0, "file_ext" => "webp");
57              
58             # This code is (arguably) 4 bytes out of sync with the description in the
59             # spec, because the spec describes ChunkHeader('ABCD') as an 8-byte quantity
60             # and we've processed the first 4 bytes above, but need to handle the second
61             # 4 (the length) here:
62 10 100       27 if ($type eq 'X') {
    100          
63             # 32 bits of length
64             # 8 bits of flags
65             # 24 bits reserved
66             # 24 bits canvas width
67             # 24 bits canvas height
68             # and then chunks...
69 4         7 my ($length, $flags, $raw_width, $raw_height)
70             = unpack 'VVVv', my_read($fh, 14);
71             # Of the 14 bytes now read, 10 were included in length:
72 4         7 $length -= 10;
73 4 50       9 die sprintf "Bad WEBP VP8X reserved bits 0x%02X", $flags & 0xC1
74             if $flags & 0xC1;
75 4 50       7 die sprintf "Bad WEBP VP8X reserved bits 0x%06X", $flags >> 8
76             if $flags >> 8;
77              
78             # Shuffle the 24 bit values into shape:
79 4         6 $raw_height = ($raw_height << 8) | ($raw_width >> 24);
80 4         4 $raw_width &= 0xFFFFFF;
81             # Strictly this is the canvas width/height, not that of the first frame.
82             # But 1 image, that might be animated. Hence it doesn't quite map to the
83             # "$n images in a file" model that Image::Info::GIF provides.
84              
85 4         10 $info->push_info(0, "width", 1 + $raw_width);
86 4         11 $info->push_info(0, "height", 1 + $raw_height);
87              
88 4 100       7 if ($flags & 0x02) {
89 1         3 $info->push_info(0, "Animation", 1);
90             } else {
91             # Possibly could also handle EXIF chunks here, although it's unclear
92             # how much code that should share with
93             # Image::Info::JPEG::process_app1_exif(), as that seems to have both
94             # JPEG-specific logic, and more generic EXIF logic.
95              
96 3         5 while (1) {
97             # Spec says that length is actual length, without accounting for
98             # padding. Odd sizes are padded to the next even size:
99 3 50       3 ++$length
100             if $length & 1;
101 3 50       20 die "seek failed: $!"
102             unless seek $fh, $length, 1;
103 3         2 my $buf;
104 3         26 my $n = read $fh, $buf, 8;
105 3 50       6 die "read failed: $!" unless defined $n;
106 3 50       6 die "No VP8 or VP8L chunk found in WEPB Extended File Format"
107             if $n == 0;
108 3 50       5 die "short read (8/$n) at pos " . tell $fh
109             unless $n == 8;
110 3         8 (my $chunk, $length) = unpack "a4V", $buf;
111 3 50       6 if ($chunk eq 'VP8 ') {
    0          
112 3         7 $info->push_info(0, "Compression", "VP8");
113 3         31 last;
114             } elsif ($chunk eq 'VP8L') {
115 0         0 $info->push_info(0, "Compression", "Lossless");
116 0         0 last;
117             }
118             }
119             }
120             } elsif ($type eq 'L') {
121             # There doesn't seem to be a better name for this:
122 3         11 $info->push_info(0, "Compression", "Lossless");
123             # Discard the 4 bytes of length; grab the next 5.
124 3         7 my ($sig, $size_and_flags) = unpack "x4CV", my_read($fh, 9);
125 3 50       7 die sprintf "Bad WEBP Lossless signature 0x%02X", $sig
126             unless $sig == 0x2f;
127 3         5 my $version = $size_and_flags >> 30;
128 3 50       8 die "Bad WEBP Lossless version $sig"
129             unless $version == 0;
130 3         8 $info->push_info(0, "width", 1 + $size_and_flags & 0x3FFF);
131 3         7 $info->push_info(0, "height", 1 + ($size_and_flags >> 14) & 0x3FFF);
132             } else {
133 3         9 $info->push_info(0, "Compression", "VP8");
134             # The fun format for a key frame is
135             # 32 bits of length
136             # 24 bits of frame tag
137             # 3 signature bytes
138             # 2+14 bits of width
139             # 2+14 bits of height
140             # We don't have a pack format for 3 bytes, but the bits we need can be
141             # got by approximating it as 2, 4, 2, 2:
142 3         8 my ($type, $start, $raw_horiz, $raw_vert)
143             = unpack "x4vVvv", my_read($fh, 14);
144 3 50       15 die "Bad WEBP VP8 type 1 (ie interframe)"
145             if $type & 1;
146 3         4 $start >>= 8;
147 3 50       9 die sprintf "Bad WEBP VP8 key frame start signature 0x%06X", $start
148             unless $start == 0x2a019d;
149              
150             # The top two bits of the raw width and height values are used as to
151             # flag a ratio to upscale.
152             # However, testing against dwebp and webpmux and then re-checking the
153             # documentation, it seems that these are really intended as information
154             # for the video hardware to render the image, because they don't change
155             # the size of bitmap returned from the decoder library. So return them
156             # as extra information, but don't recalculate the width and height.
157 3         11 $info->push_info(0, "width", ($raw_horiz & 0x3FFF));
158 3         10 $info->push_info(0, "height", ($raw_vert & 0x3FFF));
159 3         11 $info->push_info(0, "Width_Upscale", $upscale[$raw_horiz >> 14]);
160 3         41 $info->push_info(0, "Height_Upscale", $upscale[$raw_vert >> 14]);
161              
162             }
163             }