File Coverage

blib/lib/EBook/Ishmael/EBook/Zip.pm
Criterion Covered Total %
statement 144 171 84.2
branch 22 44 50.0
condition 9 22 40.9
subroutine 26 27 96.3
pod 0 9 0.0
total 201 273 73.6


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