File Coverage

blib/lib/EBook/Ishmael/EBook/Zip.pm
Criterion Covered Total %
statement 152 173 87.8
branch 24 44 54.5
condition 9 24 37.5
subroutine 27 28 96.4
pod 0 9 0.0
total 212 278 76.2


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::Zip;
2 17     17   294 use 5.016;
  17         63  
3             our $VERSION = '2.03';
4 17     17   100 use strict;
  17         38  
  17         435  
5 17     17   128 use warnings;
  17         31  
  17         1131  
6              
7 17     17   105 use Encode qw(decode);
  17         52  
  17         1018  
8 17     17   154 use File::Basename;
  17         89  
  17         1412  
9 17     17   112 use File::Path qw(remove_tree);
  17         33  
  17         958  
10 17     17   115 use File::Spec;
  17         37  
  17         504  
11 17     17   90 use List::Util qw(first);
  17         34  
  17         1348  
12              
13 17     17   144 use XML::LibXML;
  17         48  
  17         155  
14              
15 17     17   3648 use EBook::Ishmael::CharDet;
  17         41  
  17         1031  
16 17     17   164 use EBook::Ishmael::Dir;
  17         86  
  17         1076  
17 17     17   162 use EBook::Ishmael::ImageID qw(image_path_id is_image_path);
  17         39  
  17         1174  
18 17     17   101 use EBook::Ishmael::EBook::Metadata;
  17         49  
  17         417  
19 17     17   85 use EBook::Ishmael::TextToHtml;
  17         32  
  17         944  
20 17     17   98 use EBook::Ishmael::Unzip qw(unzip safe_tmp_unzip);
  17         30  
  17         35779  
21              
22             # This isn't any official format, but generic zip archives are a common way of
23             # distributing some ebooks. This module basically looks for any text or HTML
24             # files and extracts content from those.
25              
26             my $MAGIC = pack 'C4', ( 0x50, 0x4b, 0x03, 0x04 );
27              
28             sub heuristic {
29              
30 36     36 0 79 my $class = shift;
31 36         117 my $file = shift;
32 36         85 my $fh = shift;
33              
34 36 100       221 return 0 unless $file =~ /\.zip$/;
35              
36 12         81 read $fh, my $mag, length $MAGIC;
37              
38 12         72 return $mag eq $MAGIC;
39              
40             }
41              
42             sub _images {
43              
44 0     0   0 my $dir = shift;
45              
46 0         0 my @img;
47              
48 0         0 for my $f (dir($dir)) {
49 0 0 0     0 if (-d $f) {
    0          
50 0         0 push @img, _images($f);
51             } elsif (-f $f and is_image_path($f)) {
52 0         0 push @img, $f;
53             }
54             }
55              
56 0         0 return @img;
57              
58             }
59              
60             sub _files {
61              
62 22     22   46 my $self = shift;
63 22         46 my $dir = shift;
64              
65 22         90 for my $f (dir($dir)) {
66 44 100 66     945 if (-d $f) {
    100 33        
    50          
67 11         54 $self->_files($f);
68             } elsif (-f $f and is_image_path($f)) {
69 11         30 push @{ $self->{_images} }, [ $f, image_path_id($f) ];
  11         59  
70             } elsif (-f $f and $f =~ /\.(x?html?|txt)$/) {
71 22         48 push @{ $self->{_content} }, $f;
  22         91  
72             }
73             }
74              
75             }
76              
77             sub new {
78              
79 11     11 0 23 my $class = shift;
80 11         23 my $file = shift;
81 11         23 my $enc = shift;
82 11   50     38 my $net = shift // 1;
83              
84 11         120 my $self = {
85             Source => undef,
86             Metadata => EBook::Ishmael::EBook::Metadata->new,
87             Encode => $enc,
88             Network => $net,
89             _tmpdir => undef,
90             _content => [],
91             _images => [],
92             _cover => undef,
93             };
94              
95 11         33 bless $self, $class;
96              
97 11         505 my $title = (fileparse($file, qr/\.[^.]*/))[0];
98              
99 11         419 $self->{Source} = File::Spec->rel2abs($file);
100              
101 11         69 $self->{_tmpdir} = safe_tmp_unzip;
102 11         6352 unzip($self->{Source}, $self->{_tmpdir});
103              
104 11         71 $self->_files($self->{_tmpdir});
105              
106 11 50       21 unless (@{ $self->{_content} }) {
  11         47  
107 0         0 die "$self->{Source}: Found no content files in Zip ebook archive\n";
108             }
109              
110 11     11   65 $self->{_cover} = first { basename($_) =~ m/cover/i } @{ $self->{_images} };
  11         569  
  11         70  
111 11   33     114 $self->{_cover} //= $self->{_images}[0];
112              
113 11         78 $self->{Metadata}->set_title($title);
114 11         269 $self->{Metadata}->set_modified((stat $self->{Source})[9]);
115 11         53 $self->{Metadata}->set_format('Zip');
116              
117 11         51 return $self;
118              
119             }
120              
121             sub html {
122              
123 3     3 0 9 my $self = shift;
124 3         9 my $out = shift;
125              
126 3         8 my $html = '';
127              
128 3         7 for my $f (@{ $self->{_content} }) {
  3         11  
129 6 100       2909 if ($f =~ /\.txt$/) {
130 3 50       209 open my $fh, '<', $f
131             or die "Failed to open $f for reading: $!\n";
132 3         13 binmode $fh;
133 3         9 my $text = do { local $/; <$fh> };
  3         18  
  3         291  
134 3         34 close $fh;
135 3 50       20 if (not defined $self->{Encode}) {
136 3   50     24 $self->{Encode} = chardet($text) // 'ASCII';
137             }
138 3         49 $html .= text2html(decode($self->{Encode}, $text));
139             } else {
140             my $dom = XML::LibXML->load_html(
141             location => $f,
142             recover => 2,
143             no_network => !$self->{Network},
144 3         44 );
145 3         3245 my ($body) = $dom->findnodes('/html/body');
146 3   33     164 $body //= $dom->documentElement;
147 3         24 $html .= join '', map { $_->toString } $body->childNodes;
  657         3893  
148             }
149             }
150              
151 3 50       45 if (defined $out) {
152 0 0       0 open my $fh, '>', $out
153             or die "Failed to open $out for writing: $!\n";
154 0         0 binmode $fh, ':utf8';
155 0         0 print { $fh } $html;
  0         0  
156 0         0 close $fh;
157 0         0 return $out;
158             } else {
159 3         27 return $html;
160             }
161              
162             }
163              
164             sub raw {
165              
166 3     3 0 8 my $self = shift;
167 3         7 my $out = shift;
168              
169 3         6 my $raw;
170 3         8 for my $c (@{ $self->{_content} }) {
  3         13  
171 6 100       371 if ($c =~ /\.txt$/) {
172 3 50       204 open my $fh, '<', $c
173             or die "Failed to open $c for reading: $!\n";
174 3         16 binmode $fh;
175 3         6 my $r = do { local $/; <$fh> };
  3         22  
  3         272  
176 3         37 close $fh;
177 3 100       26 if (not defined $self->{Encode}) {
178 2   50     14 $self->{Encode} = chardet($r) // 'ASCII';
179             }
180 3         47 $raw .= decode($self->{Encode}, $r) . "\n\n";
181             } else {
182             my $dom = XML::LibXML->load_html(
183             location => $c,
184             recover => 2,
185             no_network => !$self->{Network},
186 3         41 );
187 3         3445 my ($body) = $dom->findnodes('/html/body');
188 3   33     166 $body //= $dom->documentElement;
189 3         240 $raw .= $body->textContent . "\n\n";
190             }
191             }
192 3         1492 $raw =~ s/\n\n//;
193              
194 3 50       14 if (defined $out) {
195 0 0       0 open my $fh, '>', $out
196             or die "Failed to open $out for writing: $!\n";
197 0         0 binmode $fh, ':utf8';
198 0         0 print { $fh } $raw;
  0         0  
199 0         0 close $fh;
200 0         0 return $out;
201             } else {
202 3         21 return $raw;
203             }
204              
205             }
206              
207             sub metadata {
208              
209 4     4 0 1713 my $self = shift;
210              
211 4         17 return $self->{Metadata};
212              
213             }
214              
215             sub has_cover {
216              
217 4     4 0 1748 my $self = shift;
218              
219 4         24 return defined $self->{_cover};
220              
221             }
222              
223             sub cover {
224              
225 2     2 0 6 my $self = shift;
226              
227 2 50       10 return (undef, undef) unless $self->has_cover;
228              
229 2 50       130 open my $fh, '<', $self->{_cover}[0]
230             or die "Failed to open $self->{_cover}[0] for reading: $!\n";
231 2         12 binmode $fh;
232 2         5 my $bin = do { local $/ = undef; readline $fh };
  2         14  
  2         112  
233 2         28 close $fh;
234              
235 2         22 return ($bin, $self->{_cover}[1]);
236              
237             }
238              
239             sub image_num {
240              
241 4     4 0 1455 my $self = shift;
242              
243 4         40 return scalar @{ $self->{_images} };
  4         26  
244              
245             }
246              
247             sub image {
248              
249 2     2 0 9 my $self = shift;
250 2         6 my $n = shift;
251              
252 2 50       10 if ($n >= $self->image_num) {
253 0         0 return (undef, undef);
254             }
255              
256 2 50       119 open my $fh, '<', $self->{_images}[$n][0]
257             or die "Failed to open $self->{_images}[$n][0] for reading: $!\n";
258 2         8 binmode $fh;
259 2         6 my $img = do { local $/ = undef; readline $fh };
  2         16  
  2         105  
260 2         28 close $fh;
261              
262 2         23 return ($img, $self->{_images}[$n][1]);
263              
264             }
265              
266             DESTROY {
267              
268 11     11   1036 my $self = shift;
269              
270 11 50       10902 remove_tree($self->{_tmpdir}, { safe => 1 }) if -d $self->{_tmpdir};
271              
272             }
273              
274             1;