File Coverage

blib/lib/EBook/Ishmael/EBook/CHM.pm
Criterion Covered Total %
statement 40 201 19.9
branch 0 56 0.0
condition 0 23 0.0
subroutine 13 26 50.0
pod 0 9 0.0
total 53 315 16.8


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::CHM;
2 17     17   315 use 5.016;
  17         67  
3             our $VERSION = '2.01';
4 17     17   112 use strict;
  17         36  
  17         462  
5 17     17   85 use warnings;
  17         512  
  17         993  
6              
7 17     17   119 use File::Basename;
  17         86  
  17         1741  
8 17     17   120 use File::Temp qw(tempdir);
  17         34  
  17         1070  
9 17     17   113 use File::Spec;
  17         51  
  17         535  
10              
11 17     17   109 use File::Which;
  17         39  
  17         1029  
12 17     17   109 use XML::LibXML;
  17         35  
  17         204  
13              
14 17     17   4608 use EBook::Ishmael::Dir;
  17         39  
  17         1084  
15 17     17   127 use EBook::Ishmael::EBook::Metadata;
  17         63  
  17         564  
16 17     17   87 use EBook::Ishmael::ImageID;
  17         33  
  17         1449  
17 17     17   119 use EBook::Ishmael::ShellQuote qw(safe_qx);
  17         38  
  17         52084  
18              
19             # TODO: Make more of an effort to find metadata
20             # TODO: Add support for Windows via hh.exe
21             # I have tried to add hh support before, and it didn't work out well because
22             # hh doesn't know how to handle quoted arguments for some reason.
23              
24             my $HAS_CHMLIB = defined which('extract_chmLib');
25             our $CAN_TEST = $HAS_CHMLIB;
26              
27             my $MAGIC = 'ITSF';
28              
29             sub heuristic {
30              
31 115     115 0 235 my $class = shift;
32 115         203 my $file = shift;
33 115         194 my $fh = shift;
34              
35 115         4927 read $fh, my $mag, length $MAGIC;
36              
37 115         747 return $mag eq $MAGIC;
38              
39             }
40              
41             sub _extract {
42              
43 0     0     my $self = shift;
44              
45 0 0         if (!$HAS_CHMLIB) {
46 0           die "Cannot extract CHM $self->{Source}; extract_chmLib not installed\n";
47             }
48              
49 0           safe_qx('extract_chmLib', $self->{Source}, $self->{_extract});
50 0 0         unless ($? >> 8 == 0) {
51 0           die "Failed to run 'extract_chmLib' on $self->{Source}\n";
52             }
53              
54 0           return 1;
55              
56             }
57              
58             sub _urlstr {
59              
60 0     0     my $self = shift;
61              
62 0           my $strfile = File::Spec->catfile($self->{_extract}, '#URLSTR');
63              
64 0 0         unless (-f $strfile) {
65 0           die "Cannot read CHM $self->{Source}; #URLSTR missing\n";
66             }
67              
68 0 0         open my $fh, '<', $strfile
69             or die "Failed to open $strfile for reading: $!\n";
70 0           binmode $fh;
71              
72 0           my @urls = split /\0+/, do { local $/ = undef; readline $fh };
  0            
  0            
73              
74 0           for my $url (@urls) {
75 0 0         next unless $url =~ /\.html$/;
76 0           $url = File::Spec->catfile($self->{_extract}, $url);
77 0 0         next unless -f $url;
78 0           push @{ $self->{_content} }, $url;
  0            
79             }
80              
81 0           close $fh;
82              
83 0           return 1;
84              
85             }
86              
87             sub _hhc {
88              
89 0     0     my $self = shift;
90              
91 0           my ($hhc) = grep { /\.hhc$/ } dir($self->{_extract});
  0            
92              
93 0 0         unless (defined $hhc) {
94 0           die "CHM $self->{Source} is missing HHC\n";
95             }
96              
97             my $dom = XML::LibXML->load_html(
98             location => $hhc,
99             recover => 2,
100             no_network => !$self->{Network},
101 0           );
102              
103 0           my @locals = $dom->findnodes('//li/object/param[@name="Local"]');
104              
105 0           @{ $self->{_content} } =
106 0           grep { /\.html?$/ }
107 0           grep { -f }
108 0           map { File::Spec->catfile($self->{_extract}, $_->getAttribute('value')) }
109 0           grep { defined $_->getAttribute('value') }
  0            
110             @locals;
111              
112 0           my ($gen) = $dom->findnodes('/html/head/meta[@name="GENERATOR"]/@content');
113              
114 0 0         if (defined $gen) {
115 0           $self->{Metadata}->contributor([ $gen->value ]);
116             }
117              
118 0           return 1;
119              
120             }
121              
122             sub _images {
123              
124 0     0     my $self = shift;
125 0   0       my $dir = shift // $self->{_extract};
126              
127 0           for my $f (dir($dir)) {
128 0 0         if (-d $f) {
    0          
129 0           $self->_images($f);
130             } elsif (is_image_path($f)) {
131 0           push @{ $self->{_images} }, $f;
  0            
132             }
133             }
134              
135             # Get image and data pairs.
136             my @imgdat = map {
137              
138 0 0         open my $fh, '<', $_
139             or die "Failed to open $_ for reading: $!\n";
140 0           binmode $fh;
141              
142 0           [ $_, do { local $/ = undef; readline $fh } ];
  0            
  0            
143              
144 0           } @{ $self->{_images} };
  0            
145              
146             my @covers =
147 0           map { $_->[0] }
148             # Sort by size, we want the largest image
149 0           sort { -s $a->[0] <=> -s $b->[0] }
150             # Cover images probably have at least a 1.3 height-width ratio.
151 0           grep { $_->[1][1] / $_->[1][0] >= 1.3 }
152 0           grep { defined $_->[1] }
153 0           map { [ $_->[0], image_size(\$_->[1]) ] }
  0            
154             @imgdat;
155              
156 0 0         $self->{_cover} = $covers[-1] if @covers;
157              
158 0           return 1;
159              
160             }
161              
162             sub _clean_html {
163              
164 0     0     my $node = shift;
165              
166 0           my @children = grep { $_->isa('XML::LibXML::Element') } $node->childNodes;
  0            
167              
168             # Remove the ugly nav bars from the top and bottom of the page. We
169             # determine if a node is a navbar node if it contains an image that is
170             # alt-tagged with either next or prev.
171 0 0 0       if (@children and my ($alt) = $children[0]->findnodes('.//img/@alt')) {
172 0 0         if ($alt->value =~ m/next|prev/i) {
173 0           $node->removeChild(shift @children);
174             }
175             }
176 0 0 0       if (@children and my ($alt) = $children[-1]->findnodes('.//img/@alt')) {
177 0 0         if ($alt->value =~ m/next|prev/i) {
178 0           $node->removeChild(pop @children);
179             }
180             }
181              
182             # Now get rid of the horizontal space placed before/after each nav bar.
183 0 0 0       if (@children and $children[0]->nodeName eq 'hr') {
184 0           $node->removeChild(shift @children);
185             }
186 0 0 0       if (@children and $children[-1]->nodeName eq 'hr') {
187 0           $node->removeChild(pop @children);
188             }
189              
190 0           return 1;
191              
192             }
193              
194             sub new {
195              
196 0     0 0   my $class = shift;
197 0           my $file = shift;
198 0           my $enc = shift;
199 0   0       my $net = shift // 1;
200              
201 0           my $self = {
202             Source => undef,
203             Metadata => EBook::Ishmael::EBook::Metadata->new,
204             Network => $net,
205             _extract => undef,
206             _images => [],
207             _content => [],
208             _cover => undef,
209             };
210              
211 0           bless $self, $class;
212              
213 0           $self->{Source} = File::Spec->rel2abs($file);
214              
215 0           $self->{_extract} = tempdir(CLEANUP => 1);
216 0           $self->_extract;
217             #$self->_urlstr;
218 0           $self->_hhc;
219 0           $self->_images;
220              
221 0           $self->{Metadata}->set_title((fileparse($self->{Source}, qr/\.[^.]*/))[0]);
222 0           $self->{Metadata}->set_modified((stat $self->{Source})[9]);
223 0           $self->{Metadata}->set_format('CHM');
224              
225 0           return $self;
226              
227             }
228              
229             sub html {
230              
231 0     0 0   my $self = shift;
232 0           my $out = shift;
233              
234             my $html = join '', map {
235              
236             my $dom = XML::LibXML->load_html(
237             location => $_,
238             recover => 2,
239             no_network => !$self->{Network},
240 0           );
241              
242 0           my ($body) = $dom->findnodes('/html/body');
243 0   0       $body //= $dom->documentElement;
244              
245 0           _clean_html($body);
246              
247 0           map { $_->toString } $body->childNodes;
  0            
248              
249 0           } @{ $self->{_content} };
  0            
250              
251 0 0         if (defined $out) {
252 0 0         open my $fh, '>', $out
253             or die "Failed to open $out for writing: $!\n";
254 0           binmode $fh, ':utf8';
255 0           print { $fh } $html;
  0            
256 0           close $fh;
257 0           return $out;
258             } else {
259 0           return $html;
260             }
261              
262             }
263              
264             sub raw {
265              
266 0     0 0   my $self = shift;
267 0           my $out = shift;
268              
269             my $raw = join '', map {
270              
271             my $dom = XML::LibXML->load_html(
272             location => $_,
273             recover => 2,
274             no_network => !$self->{Network},
275 0           );
276              
277 0           my ($body) = $dom->findnodes('/html/body');
278 0   0       $body //= $dom->documentElement;
279              
280 0           $body->textContent;
281              
282 0           } @{ $self->{_content} };
  0            
283              
284 0 0         if (defined $out) {
285 0 0         open my $fh, '>', $out
286             or die "Failed to open $out for writing: $!\n";
287 0           binmode $fh, ':utf8';
288 0           print { $fh } $raw;
  0            
289 0           close $fh;
290 0           return $out;
291             } else {
292 0           return $raw;
293             }
294              
295             }
296              
297             sub metadata {
298              
299 0     0 0   my $self = shift;
300              
301 0           return $self->{Metadata};
302              
303             }
304              
305             sub has_cover {
306              
307 0     0 0   my $self = shift;
308              
309 0           return defined $self->{_cover};
310              
311             }
312              
313             sub cover {
314              
315 0     0 0   my $self = shift;
316 0           my $out = shift;
317              
318 0 0         return undef unless $self->has_cover;
319              
320             open my $rh, '<', $self->{_cover}
321 0 0         or die "Failed to open $self->{_cover} for reading: $!\n";
322 0           binmode $rh;
323 0           my $bin = do { local $/ = undef; readline $rh };
  0            
  0            
324 0           close $rh;
325              
326 0 0         if (defined $out) {
327 0 0         open my $wh, '>', $out
328             or die "Failed to open $out for writing: $!\n";
329 0           binmode $wh;
330 0           print { $wh } $bin;
  0            
331 0           close $wh;
332 0           return $out;
333             } else {
334 0           return $bin;
335             }
336              
337             }
338              
339             sub image_num {
340              
341 0     0 0   my $self = shift;
342              
343 0           return scalar @{ $self->{_images} };
  0            
344              
345             }
346              
347             sub image {
348              
349 0     0 0   my $self = shift;
350 0           my $n = shift;
351              
352 0 0         if ($n >= $self->image_num) {
353 0           return undef;
354             }
355              
356 0 0         open my $fh, '<', $self->{_images}[$n]
357             or die "Failed to open $self->{_images}[$n] for reading: $!\n";
358 0           binmode $fh;
359 0           my $img = do { local $/ = undef; readline $fh };
  0            
  0            
360 0           close $fh;
361              
362 0           return \$img;
363              
364             }
365              
366             1;