File Coverage

blib/lib/EBook/Ishmael/EBook/CHM.pm
Criterion Covered Total %
statement 40 190 21.0
branch 0 60 0.0
condition 0 26 0.0
subroutine 13 26 50.0
pod 0 9 0.0
total 53 311 17.0


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