File Coverage

blib/lib/EBook/Ishmael/ImageID.pm
Criterion Covered Total %
statement 29 43 67.4
branch 12 24 50.0
condition 0 3 0.0
subroutine 10 11 90.9
pod 5 5 100.0
total 56 86 65.1


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