File Coverage

lib/Image/Info/AVIF.pm
Criterion Covered Total %
statement 126 141 89.3
branch 41 94 43.6
condition 4 12 33.3
subroutine 10 10 100.0
pod 0 5 0.0
total 181 262 69.0


line stmt bran cond sub pod time code
1             package Image::Info::AVIF;
2              
3 2     2   42 { use 5.006; }
  2         7  
4 2     2   13 use warnings;
  2         13  
  2         67  
5 2     2   10 use strict;
  2         3  
  2         414  
6              
7             our $VERSION = "0.01";
8              
9 1     1 0 7 sub die_for_info($) { die bless({ err=>$_[0] }, __PACKAGE__."::__ERROR__") }
10              
11             BEGIN {
12 2 50   2   17 if("$]" >= 5.008) {
13 2     8   3203 *io_string = sub ($) { open(my $fh, "<", \$_[0]); $fh };
  12         115  
  12         836  
14             } else {
15 0         0 require IO::String;
16 0         0 *io_string = sub ($) { IO::String->new($_[0]) };
  0         0  
17             }
18             }
19              
20             sub read_block($$) {
21 71     74 0 91 my($fh, $len) = @_;
22 71         79 my $d = "";
23 70         73 while(1) {
24 175         186 my $dlen = length($d);
25 175 100       220 last if $dlen == $len;
26 105         214 my $n = read($fh, $d, $len - $dlen, $dlen);
27 140 50       224 if(!defined($n)) {
    50          
28 70         108 die_for_info "read error: $!";
29             } elsif($n == 0) {
30 35         102 die_for_info "truncated file";
31             }
32             }
33 105         234 return $d;
34             }
35              
36             sub read_nulterm($) {
37 2     37 0 3 my($fh) = @_;
38 2         3 my $d = do { local $/ = "\x00"; <$fh> };
  37         102  
  3         11  
39 3 50 33     15 defined($d) && $d =~ /\x00\z/ or die_for_info "truncated file";
40 3         9 chop $d;
41 3         16 return $d;
42             }
43              
44             sub read_heif($$) {
45 9     9 0 21 my($fh, $box_types_to_keep) = @_;
46 9         11 my %boxes;
47 9         32 while(!eof($fh)) {
48 32         47 my($len, $type) = unpack("Na4", read_block($fh, 8));
49 32         43 my $pos = 8;
50 32         54 my $bufp;
51 42 100 66     166 if($type =~ $box_types_to_keep && !exists($boxes{$type})) {
52 30         54 $boxes{$type} = "";
53 30         37 $bufp = \$boxes{$type};
54             }
55 42 50       169 if($len == 1) {
56 8         21 my($lenhi, $lenlo) = unpack("NN", read_block($fh, 8));
57 8         12 $pos += 8;
58 14         26 $len = ($lenhi << 32) | $lenlo;
59 0 0       0 $len >> 32 == $lenhi or die_for_info "box size overflow";
60             }
61 28 50       34 $len >= $pos or die_for_info "bad box length";
62 28         28 $len -= $pos;
63 28         39 while($len) {
64 42 50       65 my $toread = $len < (1<<16) ? $len : (1<<16);
65 42         73 my $d = read_block($fh, $toread);
66 42 100       80 defined($bufp) and $$bufp .= $d;
67 42         100 $len -= $toread;
68             }
69             }
70 22         49 return \%boxes;
71             }
72              
73             my @primaries_type;
74             $primaries_type[$_] = "RGB" foreach 1, 4, 5, 6, 7, 9, 11, 22;
75             $primaries_type[10] = "CIEXYZ";
76              
77             sub process_file {
78 16     6 0 40 my($info, $source) = @_;
79 16 50       49 if(!eval { local $SIG{__DIE__};
  6         21  
80 3         13 my $boxes = read_heif($source, qr/\A(?:ftyp|meta)\z/);
81 3         10 my $ftyp = $boxes->{ftyp};
82 3 50       8 defined $ftyp or die_for_info "no ftyp box";
83 3 50 33     15 length($ftyp) >= 8 && !(length($ftyp) & 3)
84             or die_for_info "malformed ftyp box";
85 3 50       13 substr($ftyp, 0, 4) eq "avif"
86             or die_for_info "major brand is not \"avif\"";
87 3         11 $info->replace_info(0, file_media_type => "image/avif");
88 3         12 $info->replace_info(0, file_ext => "avif");
89 3         6 my $mboxes;
90             {
91 3         7 my $meta = $boxes->{meta};
92 3 50       5 defined $meta or die_for_info "no meta box";
93 3         13 my $metafh = io_string($meta);
94 3 50       5 read_block($metafh, 1) eq "\x00"
95             or die_for_info "malformed meta box";
96 3         11 read_block($metafh, 3);
97 3         11 $mboxes = read_heif($metafh, qr/\A(?:hdlr|iprp)\z/);
98             }
99             {
100 3         8 my $hdlr = $mboxes->{hdlr};
  3         7  
  3         9  
101 3 50       20 defined $hdlr or die_for_info "no hdlr box";
102 3         7 my $hdlrfh = io_string($hdlr);
103 3 50       8 read_block($hdlrfh, 1) eq "\x00"
104             or die_for_info "malformed hdlr box";
105 3         8 read_block($hdlrfh, 3);
106 3 50       4 unpack("N", read_block($hdlrfh, 4)) == 0
107             or die_for_info "non-zero pre-defined value";
108 3 50       17 read_block($hdlrfh, 4) eq "pict"
109             or die_for_info "handler type is not \"pict\"";
110 3         7 read_block($hdlrfh, 12);
111 3         9 read_nulterm($hdlrfh);
112             }
113 3         4 my $pboxes;
114             {
115 3         6 my $iprp = $mboxes->{iprp};
  3         5  
116 3 50       6 defined $iprp or die_for_info "no iprp box";
117 3         5 my $iprpfh = io_string($iprp);
118 3         9 $pboxes = read_heif($iprpfh, qr/\Aipco\z/);
119             }
120 3         6 my $cboxes;
121             {
122 3         6 my $ipco = $pboxes->{ipco};
  3         7  
123 3 50       16 defined $ipco or die_for_info "no ipco box";
124 3         5 my $ipcofh = io_string($ipco);
125 3         13 $cboxes = read_heif($ipcofh,
126             qr/\A(?:irot|clap|ispe|pixi|colr|pasp)\z/);
127             }
128 3         8 my $rot = 0;
129 3 50       8 if(defined(my $irot = $cboxes->{irot})) {
130 1 0       4 length($irot) >= 1 or die_for_info "malformed irot box";
131 1         2 my($angle) = unpack("C", $irot);
132 1 0       4 !($angle & -4) or die_for_info "malformed irot box";
133 0 0       0 $rot = 1 if $angle & 1;
134             }
135 2 50       10 if(defined(my $clap = $cboxes->{clap})) {
    50          
136 0 0       0 length($clap) >= 32 or die_for_info "malformed clap box";
137 0         0 my($width_num, $width_den, $height_num, $height_den) =
138             unpack("NNNN", $clap);
139 1 0 0     4 $width_den != 0 && $height_den != 0
140             or die_for_info "malformed clap box";
141 0         0 my $width = int($width_num/$width_den);
142 0         0 my $height = int($height_num/$height_den);
143 0 0       0 ($width, $height) = ($height, $width) if $rot;
144 0         0 $info->replace_info(0, width => $width);
145 0         0 $info->replace_info(0, height => $height);
146             } elsif(defined(my $ispe = $cboxes->{ispe})) {
147 2 50       4 length($ispe) >= 12 or die_for_info "malformed ispe box";
148 2         7 my($ver, undef, $width, $height) = unpack("Ca3NN", $ispe);
149 2 50       4 $ver == 0 or die_for_info "malformed ispe box";
150 3 50       27 ($width, $height) = ($height, $width) if $rot;
151 3         11 $info->replace_info(0, width => $width);
152 3         7 $info->replace_info(0, height => $height);
153             }
154 3 50       8 if(defined(my $pixi = $cboxes->{pixi})) {
155 3 50       10 length($pixi) >= 5 or die_for_info "malformed pixi box";
156 3         7 my($ver, undef, $planes) = unpack("Ca3C", $pixi);
157 3 50       8 $ver == 0 or die_for_info "malformed pixi box";
158 3 50       9 length($pixi) >= 5+$planes or die_for_info "malformed pixi box";
159 3         19 $info->replace_info(0, SamplesPerPixel => $planes);
160             $info->replace_info(0, BitsPerSample =>
161 3         10 [ map { unpack(q(C), substr($pixi, 5+$_, 1)) } 0..$planes-1 ]);
  7         19  
162             }
163 3 50       13 if(defined(my $colr = $cboxes->{colr})) {
164 3 50       7 length($colr) >= 4 or die_for_info "malformed colr box";
165 5         13 my $type = substr($colr, 0, 4);
166 3 50       18 if($type eq "nclx") {
167 3 50       13 length($colr) >= 11 or die_for_info "malformed colr box";
168 3         9 my($prim) = unpack("n", substr($colr, 4, 2));
169 3 50       7 if(defined(my $ctype = $primaries_type[$prim])) {
170 3         9 $info->replace_info(0, color_type => $ctype);
171             }
172             }
173             }
174 3 50       16 if(defined(my $pasp = $cboxes->{pasp})) {
175 1 0       3 length($pasp) >= 8 or die_for_info "malformed pasp box";
176 1         3 my($hspc, $vspc) = unpack("NN", $pasp);
177 1         4 $info->replace_info(0, resolution => "$vspc/$hspc");
178             }
179 2         19 1;
180             }) {
181 0         0 my $err = $@;
182 0 0       0 if(ref($err) eq __PACKAGE__."::__ERROR__") {
183 1         10 $info->replace_info(0, error => $err->{err});
184             } else {
185 0           die $err;
186             }
187             }
188             }
189              
190             1;
191              
192             =begin register
193              
194             MAGIC: /\A....ftypavif/s
195              
196             Supports the basic standard info key names.
197              
198             =end register
199              
200             =head1 NAME
201              
202             Image::Info::AVIF - AV1 Image File Format support for Image::Info
203              
204             =head1 SYNOPSIS
205              
206             use Image::Info qw(image_info);
207              
208             $info = image_info("image.avif");
209             if($error = $info->{error}) {
210             die "Can't parse image info: $error\n";
211             }
212             $color = $info->{color_type};
213              
214             =head1 DESCRIPTION
215              
216             This module supplies information about AVIF files within the
217             L system. It supports the basic standard info key names.
218              
219             =head1 SEE ALSO
220              
221             L
222              
223             =head1 AUTHOR
224              
225             Andrew Main (Zefram)
226              
227             =head1 COPYRIGHT
228              
229             Copyright (C) 2023 Andrew Main (Zefram)
230              
231             =head1 LICENSE
232              
233             This module is free software; you can redistribute it and/or modify it
234             under the same terms as Perl itself.
235              
236             =head1 ACKNOWLEDGEMENT
237              
238             The development of this module was funded by
239             Preisvergleich Internet Services AG.
240              
241             =cut