File Coverage

blib/lib/EBook/Ishmael/EBook/Epub.pm
Criterion Covered Total %
statement 182 207 87.9
branch 51 92 55.4
condition 2 4 50.0
subroutine 24 24 100.0
pod 0 10 0.0
total 259 337 76.8


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::Epub;
2 17     17   333 use 5.016;
  17         83  
3             our $VERSION = '2.03';
4 17     17   101 use strict;
  17         34  
  17         532  
5 17     17   107 use warnings;
  17         57  
  17         998  
6              
7 17     17   92 use File::Basename;
  17         42  
  17         1388  
8 17     17   128 use File::Path qw(remove_tree);
  17         64  
  17         1012  
9 17     17   105 use File::Spec;
  17         32  
  17         498  
10              
11 17     17   150 use XML::LibXML;
  17         32  
  17         144  
12              
13 17     17   3331 use EBook::Ishmael::EBook::Metadata;
  17         74  
  17         627  
14 17     17   122 use EBook::Ishmael::ImageID qw(mimetype_id);
  17         27  
  17         1002  
15 17     17   8682 use EBook::Ishmael::Time qw(guess_time);
  17         83  
  17         2359  
16 17     17   639 use EBook::Ishmael::Unzip qw(unzip safe_tmp_unzip);
  17         633  
  17         42428  
17              
18             my $MAGIC = pack 'C4', ( 0x50, 0x4b, 0x03, 0x04 );
19             my $CONTAINER = File::Spec->catfile(qw/META-INF container.xml/);
20              
21             my $DCNS = "http://purl.org/dc/elements/1.1/";
22              
23             # This module only supports EPUBs with a single rootfile. The standard states
24             # there can be multiple rootfiles, but I have yet to encounter one that does.
25              
26             # TODO: Make heuristic more precise.
27             # If file uses zip magic bytes, assume it to be an EPUB.
28             sub heuristic {
29              
30 115     115 0 278 my $class = shift;
31 115         220 my $file = shift;
32 115         246 my $fh = shift;
33              
34 115 100       476 return 0 if $file =~ /\.zip$/;
35              
36 103         821 read $fh, my $mag, 4;
37              
38 103         468 return $mag eq $MAGIC;
39             }
40              
41             # Set _rootfile based on container.xml's rootfile@full-path attribute.
42             sub _get_rootfile {
43              
44 10     10   28 my $self = shift;
45              
46             my $dom = XML::LibXML->load_xml(
47             location => $self->{_container},
48             no_network => !$self->{Network},
49 10         178 );
50 10         5691 my $ns = $dom->documentElement->namespaceURI;
51              
52 10         69 my $xpc = XML::LibXML::XPathContext->new($dom);
53 10         651 $xpc->registerNs('container', $ns);
54              
55 10         107 my ($rf) = $xpc->findnodes(
56             '/container:container' .
57             '/container:rootfiles' .
58             '/container:rootfile[1]' .
59             '/@full-path'
60             );
61              
62 10 50       901 unless (defined $rf) {
63 0         0 die "Could not find root file in EPUB $self->{Source}\n";
64             }
65              
66 10         218 $self->{_rootfile} = File::Spec->catfile($self->{_unzip}, $rf->value());
67              
68 10         162 return $self->{_rootfile};
69              
70             }
71              
72             # Reads metadata and _spine from rootfile.
73             sub _read_rootfile {
74              
75 10     10   22 my $self = shift;
76              
77 10         413 $self->{_contdir} = dirname($self->{_rootfile});
78              
79             my $dom = XML::LibXML-> load_xml(
80             location => $self->{_rootfile},
81             no_network => !$self->{Network},
82 10         72 );
83 10         5046 my $ns = $dom->documentElement->namespaceURI;
84              
85 10         52 my $xpc = XML::LibXML::XPathContext->new($dom);
86 10         250 $xpc->registerNs('package', $ns);
87 10         48 $xpc->registerNs('dc', $DCNS);
88              
89 10         39 my ($vernode) = $xpc->findnodes(
90             '/package:package/@version'
91             );
92 10 50       484 if (defined $vernode) {
93 10         67 $self->{_version} = $vernode->getValue;
94             }
95              
96 10         33 my ($meta) = $xpc->findnodes(
97             '/package:package/package:metadata'
98             );
99              
100 10         371 my ($manif) = $xpc->findnodes(
101             '/package:package/package:manifest'
102             );
103              
104 10         305 my ($spine) = $xpc->findnodes(
105             '/package:package/package:spine'
106             );
107              
108 10 50       318 unless (defined $meta) {
109 0         0 die "EPUB $self->{Source} is missing metadata in rootfile\n";
110             }
111              
112 10 50       31 unless (defined $manif) {
113 0         0 die "EPUB $self->{Source} is missing manifest in rootfile\n";
114             }
115              
116 10 50       31 unless (defined $spine) {
117 0         0 die "EPUB $self->{Source} is missing spine in rootfile\n";
118             }
119              
120 10         36 for my $dc ($xpc->findnodes('./dc:*', $meta)) {
121              
122 70         970 my $name = $dc->nodeName =~ s/^dc://r;
123 70         252 my $text = $dc->textContent();
124              
125 70         211 $text =~ s/\s+/ /g;
126              
127 70 100       360 if ($name eq 'contributor') {
    100          
    100          
    50          
    100          
    100          
    50          
    50          
    50          
128 10         47 $self->{Metadata}->add_contributor($text);
129             } elsif ($name eq 'creator') {
130 10         47 $self->{Metadata}->add_author($text);
131             } elsif ($name eq 'date') {
132 10         20 my $t = eval { guess_time($text) };
  10         60  
133 10 50       80 if (defined $t) {
134 0         0 $self->{Metadata}->set_created($t);
135             }
136             } elsif ($name eq 'description') {
137 0         0 $self->{Metadata}->set_description($text);
138             } elsif ($name eq 'identifier') {
139 20         78 $self->{Metadata}->set_id($text);
140             } elsif ($name eq 'language') {
141 10         48 $self->{Metadata}->set_language($text);
142             } elsif ($name eq 'publisher') {
143 0         0 $self->{Metadata}->add_contributor($text);
144             } elsif ($name eq 'subject') {
145 0         0 $self->{Metadata}->add_genre($text);
146             } elsif ($name eq 'title') {
147 10         70 $self->{Metadata}->set_title($text);
148             }
149              
150             }
151              
152 10         69 for my $itemref ($xpc->findnodes('./package:itemref', $spine)) {
153              
154 40 50       1652 my $id = $itemref->getAttribute('idref') or next;
155              
156 40 50       625 my ($item) = $xpc->findnodes(
157             "./package:item[\@id=\"$id\"]", $manif
158             ) or next;
159              
160 40 50 50     2272 unless (($item->getAttribute('media-type') // '') eq 'application/xhtml+xml') {
161 0         0 next;
162             }
163              
164 40 50       488 my $href = $item->getAttribute('href') or next;
165              
166 40         741 $href = File::Spec->catfile($self->{_contdir}, $href);
167              
168 40 50       804 next unless -f $href;
169              
170 40         89 push @{ $self->{_spine} }, $href;
  40         233  
171              
172             }
173              
174             # Get list of images
175 10         168 for my $item ($xpc->findnodes('./package:item', $manif)) {
176 80         903 my $mime = $item->getAttribute('media-type');
177 80 50       830 next if not defined $mime;
178              
179 80         186 my $format = mimetype_id($mime);
180 80 100       188 next if not defined $format;
181              
182 10 50       56 my $href = $item->getAttribute('href') or next;
183 10         210 $href = File::Spec->catfile($self->{_contdir}, $href);
184 10 50       235 next if not -f $href;
185              
186 10         28 push @{ $self->{_images} }, [ $href, $format ];
  10         62  
187              
188             }
189              
190 10         37 my ($covmeta) = $xpc->findnodes('./package:meta[@name="cover"]', $meta);
191             # Put if code in own block so that we can last out of it.
192 10 50       1223 if (defined $covmeta) {{
193 10 50       25 my $covcont = $covmeta->getAttribute('content') or last;
  10         33  
194 10 50       191 my ($covitem) = $xpc->findnodes("./package:item[\@id=\"$covcont\"]", $manif)
195             or last;
196 10 50       554 my $covhref = $covitem->getAttribute('href') or last;
197 10 50       142 my $covmime = $covitem->getAttribute('media-type') or last;
198 10 50       117 my $format = mimetype_id($covmime) or last;
199 10         128 my $covpath = File::Spec->catfile($self->{_contdir}, $covhref);
200 10 50       229 last unless -f $covpath;
201 10         74 $self->{_cover} = [ $covpath, $format ];
202             }}
203              
204 10         155 return 1;
205              
206             }
207              
208             sub new {
209              
210 10     10 0 26 my $class = shift;
211 10         21 my $file = shift;
212 10         27 my $enc = shift;
213 10   50     34 my $net = shift // 1;
214              
215 10         132 my $self = {
216             Source => undef,
217             Metadata => EBook::Ishmael::EBook::Metadata->new,
218             Network => $net,
219             _unzip => undef,
220             _container => undef,
221             _rootfile => undef,
222             # Directory where _rootfile is, as that is where all of the "content"
223             # files are.
224             _contdir => undef,
225             # List of content files in order specified by spine.
226             _spine => [],
227             _cover => undef,
228             _images => [],
229             _version => undef,
230             };
231              
232 10         34 bless $self, $class;
233              
234 10         47 $self->read($file);
235              
236 10 50       65 if (not defined $self->{Metadata}->title) {
237 0         0 $self->{Metadata}->set_title((fileparse($file, qr/\.[^.]*/))[0]);
238             }
239              
240 10 50       41 if (defined $self->{_version}) {
241 10         56 $self->{Metadata}->set_format('EPUB ' . $self->{_version});
242             } else {
243 0         0 $self->{Metadata}->set_format('EPUB');
244             }
245              
246 10         65 return $self;
247              
248             }
249              
250             sub read {
251              
252 10     10 0 20 my $self = shift;
253 10         23 my $src = shift;
254              
255 10         62 my $tmpdir = safe_tmp_unzip;
256              
257 10         6893 unzip($src, $tmpdir);
258              
259 10         387 $self->{Source} = File::Spec->rel2abs($src);
260 10         45 $self->{_unzip} = $tmpdir;
261              
262 10         115 $self->{_container} = File::Spec->catfile($self->{_unzip}, $CONTAINER);
263              
264 10 50       427 unless (-f $self->{_container}) {
265 0         0 die "$src is an invalid EPUB file: does not have a META-INF/container.xml\n";
266             }
267              
268 10         71 $self->_get_rootfile();
269 10         80 $self->_read_rootfile();
270              
271 10         572 return 1;
272              
273             }
274              
275             sub html {
276              
277 3     3 0 873 my $self = shift;
278 3         8 my $out = shift;
279              
280             my $html = join '', map {
281              
282             my $dom = XML::LibXML->load_xml(
283             location => $_,
284             no_network => !$self->{Network},
285 12         7925 );
286 12         9994 my $ns = $dom->documentElement->namespaceURI;
287              
288 12         81 my $xpc = XML::LibXML::XPathContext->new($dom);
289 12         343 $xpc->registerNs('html', $ns);
290              
291 12 50       84 my ($body) = $xpc->findnodes('/html:html/html:body')
292             or next;
293              
294 12         718 map { $_->toString } $body->childNodes;
  1830         11208  
295              
296 3         8 } @{ $self->{_spine} };
  3         12  
297              
298 3 50       1626 if (defined $out) {
299 0 0       0 open my $fh, '>', $out
300             or die "Failed to open $out for writing: $!\n";
301 0         0 binmode $fh, ':utf8';
302 0         0 print { $fh } $html;
  0         0  
303 0         0 close $fh;
304 0         0 return $out;
305             } else {
306 3         26 return $html;
307             }
308              
309             }
310              
311             sub raw {
312              
313 2     2 0 4 my $self = shift;
314 2         4 my $out = shift;
315              
316             my $raw = join "\n\n", map {
317              
318             my $dom = XML::LibXML->load_xml(
319             location => $_,
320             no_network => !$self->{Network},
321 8         308 );
322 8         3901 my $ns = $dom->documentElement->namespaceURI;
323              
324 8         28 my $xpc = XML::LibXML::XPathContext->new($dom);
325 8         143 $xpc->registerNs('html', $ns);
326              
327 8 50       19 my ($body) = $xpc->findnodes('/html:html/html:body')
328             or next;
329              
330 8         447 $body->textContent;
331              
332 2         4 } @{ $self->{_spine} };
  2         6  
333              
334 2 50       113 if (defined $out) {
335 0 0       0 open my $fh, '>', $out
336             or die "Failed to open $out for writing: $!\n";
337 0         0 binmode $fh, ':utf8';
338 0         0 print { $fh } $raw;
  0         0  
339 0         0 close $fh;
340 0         0 return $out;
341             } else {
342 2         9 return $raw;
343             }
344              
345             }
346              
347             sub metadata {
348              
349 9     9 0 13923 my $self = shift;
350              
351 9         66 return $self->{Metadata};
352              
353             }
354              
355             sub has_cover {
356              
357 4     4 0 10 my $self = shift;
358              
359 4         22 return defined $self->{_cover};
360              
361             }
362              
363             sub cover {
364              
365 2     2 0 1476 my $self = shift;
366              
367 2 50       8 return (undef, undef) if not $self->has_cover;
368              
369 2 50       124 open my $fh, '<', $self->{_cover}[0]
370             or die "Failed to open $self->{_cover}[0] for reading: $!\n";
371 2         9 binmode $fh;
372 2         6 my $img = do { local $/; readline $fh };
  2         11  
  2         136  
373 2         27 close $fh;
374              
375 2         24 return ($img, $self->{_cover}[1]);
376              
377             }
378              
379             sub image_num {
380              
381 4     4 0 1426 my $self = shift;
382              
383 4         7 return scalar @{ $self->{_images} };
  4         18  
384              
385             }
386              
387             sub image {
388              
389 2     2 0 5 my $self = shift;
390 2         5 my $n = shift;
391              
392 2 50       7 if ($n >= $self->image_num) {
393 0         0 return (undef, undef);
394             }
395              
396 2 50       93 open my $fh, '<', $self->{_images}[$n][0]
397             or die "Failed to open $self->{_images}[$n][0] for reading: $!\n";
398 2         7 binmode $fh;
399 2         4 my $img = do { local $/ = undef; readline $fh };
  2         11  
  2         83  
400 2         23 close $fh;
401              
402 2         18 return ($img, $self->{_images}[$n][1]);
403              
404             }
405              
406             DESTROY {
407              
408 10     10   1024 my $self = shift;
409              
410 10 50       22244 remove_tree($self->{_unzip}, { safe => 1 }) if -d $self->{_unzip};
411              
412             }
413              
414             1;