File Coverage

blib/lib/EBook/Ishmael/EBook/Zip.pm
Criterion Covered Total %
statement 154 175 88.0
branch 24 44 54.5
condition 9 24 37.5
subroutine 27 28 96.4
pod 0 9 0.0
total 214 280 76.4


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::Zip;
2 17     17   239 use 5.016;
  17         43  
3             our $VERSION = '2.05';
4 17     17   88 use strict;
  17         25  
  17         263  
5 17     17   42 use warnings;
  17         20  
  17         710  
6              
7 17     17   59 use Encode qw(decode);
  17         21  
  17         806  
8 17     17   77 use File::Basename;
  17         23  
  17         887  
9 17     17   71 use File::Path qw(remove_tree);
  17         34  
  17         598  
10 17     17   69 use File::Spec;
  17         40  
  17         332  
11 17     17   61 use List::Util qw(first);
  17         32  
  17         747  
12              
13 17     17   66 use XML::LibXML;
  17         20  
  17         99  
14              
15 17     17   2049 use EBook::Ishmael::CharDet;
  17         27  
  17         527  
16 17     17   77 use EBook::Ishmael::Dir;
  17         33  
  17         748  
17 17     17   72 use EBook::Ishmael::ImageID qw(image_path_id is_image_path);
  17         23  
  17         628  
18 17     17   81 use EBook::Ishmael::EBook::Metadata;
  17         36  
  17         408  
19 17     17   59 use EBook::Ishmael::HTML qw(prepare_html text2html);
  17         32  
  17         629  
20 17     17   80 use EBook::Ishmael::Unzip qw(unzip safe_tmp_unzip);
  17         44  
  17         24589  
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             # TODO: Sort files more accurately
27             # For example, a zip file with these files would be sorted incorrectly:
28             # z/file-1.html
29             # z/file-2.html
30             # ...
31             # z/file-10.html These will be sorted before file-2.html
32             # z/file-11.html
33              
34             my $MAGIC = pack 'C4', ( 0x50, 0x4b, 0x03, 0x04 );
35              
36             sub heuristic {
37              
38 36     36 0 70 my $class = shift;
39 36         53 my $file = shift;
40 36         47 my $fh = shift;
41              
42 36 100       139 return 0 unless $file =~ /\.zip$/;
43              
44 12         56 read $fh, my $mag, length $MAGIC;
45              
46 12         39 return $mag eq $MAGIC;
47              
48             }
49              
50             sub _images {
51              
52 0     0   0 my $dir = shift;
53              
54 0         0 my @img;
55              
56 0         0 for my $f (dir($dir)) {
57 0 0 0     0 if (-d $f) {
    0          
58 0         0 push @img, _images($f);
59             } elsif (-f $f and is_image_path($f)) {
60 0         0 push @img, $f;
61             }
62             }
63              
64 0         0 return @img;
65              
66             }
67              
68             sub _files {
69              
70 22     22   31 my $self = shift;
71 22         25 my $dir = shift;
72              
73 22         67 for my $f (dir($dir)) {
74 44 100 66     636 if (-d $f) {
    100 33        
    50          
75 11         52 $self->_files($f);
76             } elsif (-f $f and is_image_path($f)) {
77 11         15 push @{ $self->{_images} }, [ $f, image_path_id($f) ];
  11         46  
78             } elsif (-f $f and $f =~ /\.(x?html?|txt)$/) {
79 22         28 push @{ $self->{_content} }, $f;
  22         57  
80             }
81             }
82              
83             }
84              
85             sub new {
86              
87 11     11 0 19 my $class = shift;
88 11         20 my $file = shift;
89 11         19 my $enc = shift;
90 11   50     43 my $net = shift // 1;
91              
92 11         79 my $self = {
93             Source => undef,
94             Metadata => EBook::Ishmael::EBook::Metadata->new,
95             Encode => $enc,
96             Network => $net,
97             _tmpdir => undef,
98             _content => [],
99             _images => [],
100             _cover => undef,
101             };
102              
103 11         25 bless $self, $class;
104              
105 11         412 my $title = (fileparse($file, qr/\.[^.]*/))[0];
106              
107 11         317 $self->{Source} = File::Spec->rel2abs($file);
108              
109 11         72 $self->{_tmpdir} = safe_tmp_unzip;
110 11         4550 unzip($self->{Source}, $self->{_tmpdir});
111              
112 11         51 $self->_files($self->{_tmpdir});
113              
114 11 50       16 unless (@{ $self->{_content} }) {
  11         35  
115 0         0 die "$self->{Source}: Found no content files in Zip ebook archive\n";
116             }
117              
118 11     11   45 $self->{_cover} = first { basename($_) =~ m/cover/i } @{ $self->{_images} };
  11         377  
  11         54  
119 11   33     83 $self->{_cover} //= $self->{_images}[0];
120              
121 11         49 $self->{Metadata}->set_title($title);
122 11         195 $self->{Metadata}->set_modified((stat $self->{Source})[9]);
123 11         41 $self->{Metadata}->set_format('Zip');
124              
125 11         38 return $self;
126              
127             }
128              
129             sub html {
130              
131 3     3 0 7 my $self = shift;
132 3         4 my $out = shift;
133              
134 3         7 my $html = '';
135              
136 3         92 for my $f (@{ $self->{_content} }) {
  3         12  
137 6 100       1828 if ($f =~ /\.txt$/) {
138 3 50       125 open my $fh, '<', $f
139             or die "Failed to open $f for reading: $!\n";
140 3         9 binmode $fh;
141 3         5 my $text = do { local $/; <$fh> };
  3         15  
  3         231  
142 3         28 close $fh;
143 3 50       14 if (not defined $self->{Encode}) {
144 3   50     17 $self->{Encode} = chardet($text) // 'ASCII';
145             }
146 3         50 $html .= text2html(decode($self->{Encode}, $text));
147             } else {
148             my $dom = XML::LibXML->load_html(
149             location => $f,
150             recover => 2,
151             no_network => !$self->{Network},
152 3         34 );
153 3         2119 my ($body) = $dom->findnodes('/html/body');
154 3   33     150 $body //= $dom->documentElement;
155 3         20 prepare_html($body);
156 3         14 $html .= join '', map { $_->toString } $body->childNodes;
  657         2272  
157             }
158             }
159              
160 3 50       13 if (defined $out) {
161 0 0       0 open my $fh, '>', $out
162             or die "Failed to open $out for writing: $!\n";
163 0         0 binmode $fh, ':utf8';
164 0         0 print { $fh } $html;
  0         0  
165 0         0 close $fh;
166 0         0 return $out;
167             } else {
168 3         27 return $html;
169             }
170              
171             }
172              
173             sub raw {
174              
175 3     3 0 33 my $self = shift;
176 3         8 my $out = shift;
177              
178 3         4 my $raw;
179 3         6 for my $c (@{ $self->{_content} }) {
  3         8  
180 6 100       195 if ($c =~ /\.txt$/) {
181 3 50       116 open my $fh, '<', $c
182             or die "Failed to open $c for reading: $!\n";
183 3         9 binmode $fh;
184 3         5 my $r = do { local $/; <$fh> };
  3         16  
  3         190  
185 3         26 close $fh;
186 3 100       10 if (not defined $self->{Encode}) {
187 2   50     12 $self->{Encode} = chardet($r) // 'ASCII';
188             }
189 3         54 $raw .= decode($self->{Encode}, $r) . "\n\n";
190             } else {
191             my $dom = XML::LibXML->load_html(
192             location => $c,
193             recover => 2,
194             no_network => !$self->{Network},
195 3         33 );
196 3         2152 my ($body) = $dom->findnodes('/html/body');
197 3   33     104 $body //= $dom->documentElement;
198 3         19 prepare_html($body);
199 3         183 $raw .= $body->textContent . "\n\n";
200             }
201             }
202 3         1127 $raw =~ s/\n\n//;
203              
204 3 50       13 if (defined $out) {
205 0 0       0 open my $fh, '>', $out
206             or die "Failed to open $out for writing: $!\n";
207 0         0 binmode $fh, ':utf8';
208 0         0 print { $fh } $raw;
  0         0  
209 0         0 close $fh;
210 0         0 return $out;
211             } else {
212 3         17 return $raw;
213             }
214              
215             }
216              
217             sub metadata {
218              
219 4     4 0 1100 my $self = shift;
220              
221 4         16 return $self->{Metadata};
222              
223             }
224              
225             sub has_cover {
226              
227 4     4 0 1052 my $self = shift;
228              
229 4         16 return defined $self->{_cover};
230              
231             }
232              
233             sub cover {
234              
235 2     2 0 3 my $self = shift;
236              
237 2 50       6 return (undef, undef) unless $self->has_cover;
238              
239 2 50       120 open my $fh, '<', $self->{_cover}[0]
240             or die "Failed to open $self->{_cover}[0] for reading: $!\n";
241 2         9 binmode $fh;
242 2         6 my $bin = do { local $/ = undef; readline $fh };
  2         13  
  2         97  
243 2         23 close $fh;
244              
245 2         15 return ($bin, $self->{_cover}[1]);
246              
247             }
248              
249             sub image_num {
250              
251 4     4 0 805 my $self = shift;
252              
253 4         6 return scalar @{ $self->{_images} };
  4         12  
254              
255             }
256              
257             sub image {
258              
259 2     2 0 4 my $self = shift;
260 2         7 my $n = shift;
261              
262 2 50       5 if ($n >= $self->image_num) {
263 0         0 return (undef, undef);
264             }
265              
266 2 50       74 open my $fh, '<', $self->{_images}[$n][0]
267             or die "Failed to open $self->{_images}[$n][0] for reading: $!\n";
268 2         7 binmode $fh;
269 2         5 my $img = do { local $/ = undef; readline $fh };
  2         9  
  2         62  
270 2         33 close $fh;
271              
272 2         14 return ($img, $self->{_images}[$n][1]);
273              
274             }
275              
276             DESTROY {
277              
278 11     11   618 my $self = shift;
279              
280 11 50       8208 remove_tree($self->{_tmpdir}, { safe => 1 }) if -d $self->{_tmpdir};
281              
282             }
283              
284             1;