File Coverage

blib/lib/EBook/Ishmael/ImageID.pm
Criterion Covered Total %
statement 24 38 63.1
branch 11 22 50.0
condition 0 3 0.0
subroutine 8 9 88.8
pod 3 3 100.0
total 46 75 61.3


line stmt bran cond sub pod time code
1             package EBook::Ishmael::ImageID;
2 17     17   309 use 5.016;
  17         61  
3             our $VERSION = '2.01';
4 17     17   129 use strict;
  17         2134  
  17         601  
5 17     17   105 use warnings;
  17         67  
  17         1010  
6              
7 17     17   101 use Exporter 'import';
  17         27  
  17         1071  
8             our @EXPORT = qw(image_id image_size is_image_path);
9              
10 17     17   91 use List::Util qw(max);
  17         43  
  17         1075  
11              
12 17     17   10582 use XML::LibXML;
  17         1039353  
  17         347  
13              
14             my $IMGRX = sprintf "(%s)", join '|', qw(
15             png jpg jpeg tif tiff gif bmp webp svg jxl avif
16             );
17              
18             # This function may not support many image formats as it was designed for
19             # getting image sizes for CHM files to determine the cover image. CHMs
20             # primarily use GIFs.
21             # TODO: Add tif support
22             # TODO: Add webp support (probably never)
23             my %SIZE = (
24             # size stored as two BE ushorts in the SOF0 marker, at offset 5.
25             'jpg' => sub {
26              
27             my $ref = shift;
28              
29             my $len = length $$ref;
30              
31             my $p = 2;
32              
33             my $sof = join ' ', 0xff, 0xc0;
34              
35             while ($p < $len) {
36              
37             my $id = join ' ', unpack "CC", substr $$ref, $p, 2;
38             $p += 2;
39             my $mlen = unpack "n", substr $$ref, $p, 2;
40              
41             unless ($id eq $sof) {
42             $p += $mlen;
43             next;
44             }
45              
46             my ($y, $x) = unpack "nn", substr $$ref, $p + 3, 4;
47              
48             return [ $x, $y ];
49              
50             }
51              
52             return undef;
53              
54             },
55             # size stored as two BE ulongs at offset 16
56             'png' => sub {
57              
58             my $ref = shift;
59              
60             return undef unless length $$ref > 24;
61              
62             my ($x, $y) = unpack "N N", substr $$ref, 16, 8;
63              
64             return [ $x, $y ];
65              
66             },
67             # size stored as two LE ushorts at offset 6
68             'gif' => sub {
69              
70             my $ref = shift;
71              
72             return undef unless length $$ref > 10;
73              
74             my ($x, $y) = unpack "v v", substr $$ref, 6, 4;
75              
76             return [ $x, $y ];
77              
78             },
79             # size storage depends on header. For an OS header, two LE ushorts at
80             # offset 18. For Windows, two LE signed longs at offset 18.
81             'bmp' => sub {
82              
83             my $ref = shift;
84              
85             return undef unless length $$ref > 24;
86              
87             my $dbisize = unpack "V", substr $$ref, 14, 4;
88              
89             my ($x, $y);
90              
91             # OS
92             if ($dbisize == 16) {
93             ($x, $y) = unpack "v v", substr $$ref, 18, 4;
94             # Win
95             } else {
96             ($x, $y) = unpack "(ll)<", substr $$ref, 18, 8;
97             return undef if $x < 0 or $y < 0;
98             }
99              
100             return [ $x, $y ];
101              
102             },
103             # Get width and height attributes of root node.
104             'svg' => sub {
105              
106             my $ref = shift;
107              
108             my $dom = eval { XML::LibXML->load_xml(string => $ref) }
109             or return undef;
110              
111             my $svg = $dom->documentElement;
112              
113             my $x = $svg->getAttribute('width') or return undef;
114             my $y = $svg->getAttribute('height') or return undef;
115              
116             return [ $x, $y ];
117              
118             },
119             );
120              
121             sub image_id {
122              
123 132     132 1 238 my $ref = shift;
124              
125 132 100       909 if ($$ref =~ /^\xff\xd8\xff/) {
    100          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
126 60         205 return 'jpg';
127             } elsif ($$ref =~ /^\x89\x50\x4e\x47\x0d\x0a\x1a\x0a/) {
128 62         245 return 'png';
129             } elsif ($$ref =~ /^GIF8[79]a/) {
130 0         0 return 'gif';
131             } elsif ($$ref =~ /^\x52\x49\x46\x46....\x57\x45\x42\x50\x56\x50\x38/) {
132 0         0 return 'webp';
133             } elsif ($$ref =~ /^BM/) {
134 0         0 return 'bmp';
135             } elsif ($$ref =~ /^(\x49\x49\x2a\x00|\x4d\x4d\x00\x2a)/) {
136 0         0 return 'tif';
137             } elsif ($$ref =~ /\A....ftypavif/s) {
138 0         0 return 'avif';
139             } elsif ($$ref =~ /^(\xff\x0a|\x00{3}\x0c\x4a\x58\x4c\x20\x0d\x0a\x87\x0a)/) {
140 0         0 return 'jxl';
141             } elsif (substr($$ref, 0, 1024) =~ /<\s*svg[^<>]*>/) {
142 0         0 return 'svg';
143             } else {
144 10         39 return undef;
145             }
146              
147             }
148              
149             sub image_size {
150              
151 0     0 1 0 my $ref = shift;
152 0   0     0 my $fmt = shift // image_id($ref);
153              
154 0 0       0 unless (defined $fmt) {
155 0         0 die "Could not determine image data format\n";
156             }
157              
158 0 0       0 unless (exists $SIZE{ $fmt }) {
159 0         0 return undef;
160             }
161              
162 0         0 return $SIZE{ $fmt }->($ref);
163              
164             }
165              
166             sub is_image_path {
167              
168 313     313 1 621 my $path = shift;
169              
170 313         2841 return $path =~ /\.$IMGRX$/;
171              
172             }
173              
174             1;
175              
176             =head1 NAME
177              
178             EBook::Ishmael::ImageID - Identify image data format
179              
180             =head1 SYNOPSIS
181              
182             use EBook::Ishmael::ImageID;
183              
184             my $format = image_id($dataref);
185              
186             =head1 DESCRIPTION
187              
188             B is a module that provides the C
189             subroutine, which identifies the image format of a given buffer. This is
190             developer documentation, for L user documentation you should consult
191             its manual.
192              
193             Currently, the following formats are supported:
194              
195             =over 4
196              
197             =item jpg
198              
199             =item png
200              
201             =item gif
202              
203             =item webp
204              
205             =item bmp
206              
207             =item tif
208              
209             =item svg
210              
211             =item avif
212              
213             =item jxl
214              
215             =back
216              
217             =head1 SUBROUTINES
218              
219             =over 4
220              
221             =item $f = image_id($dataref)
222              
223             Returns a string of the image format of the given image buffer. C<$dataref>
224             must be a scalar ref. Returns C if the image's format could not be
225             identified.
226              
227             =item [$x, $y] = image_size($dataref, [$fmt])
228              
229             Returns an C<$x>/C<$y> pair representing the image data's size. C<$fmt> is an
230             optional argument specifying the format to use for the image data. If not
231             specified, C will identify the format itself. If the image size
232             could not be determined, returns C.
233              
234             This subroutine does not support the following formats (yet):
235              
236             =over 4
237              
238             =item webp
239              
240             =item tif
241              
242             =item avif
243              
244             =item jxl
245              
246             =back
247              
248             =item $bool = is_image_path($path)
249              
250             Returns true if C<$path> looks like an image path name.
251              
252             =back
253              
254             =head1 AUTHOR
255              
256             Written by Samuel Young, Esamyoung12788@gmail.comE.
257              
258             This project's source can be found on its
259             L. Comments and pull
260             requests are welcome!
261              
262             =head1 COPYRIGHT
263              
264             Copyright (C) 2025-2026 Samuel Young
265              
266             This program is free software: you can redistribute it and/or modify
267             it under the terms of the GNU General Public License as published by
268             the Free Software Foundation, either version 3 of the License, or
269             (at your option) any later version.
270              
271             =cut