File Coverage

blib/lib/EBook/Ishmael/EBook/CHM.pm
Criterion Covered Total %
statement 43 195 22.0
branch 0 60 0.0
condition 0 26 0.0
subroutine 14 27 51.8
pod 0 9 0.0
total 57 317 17.9


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::CHM;
2 17     17   249 use 5.016;
  17         42  
3             our $VERSION = '2.05';
4 17     17   69 use strict;
  17         30  
  17         333  
5 17     17   66 use warnings;
  17         43  
  17         701  
6              
7 17     17   109 use File::Basename;
  17         23  
  17         1104  
8 17     17   79 use File::Temp qw(tempdir);
  17         35  
  17         647  
9 17     17   58 use File::Spec;
  17         19  
  17         252  
10              
11 17     17   43 use File::Which;
  17         30  
  17         602  
12 17     17   66 use XML::LibXML;
  17         20  
  17         123  
13              
14 17     17   2575 use EBook::Ishmael::Dir;
  17         23  
  17         639  
15 17     17   65 use EBook::Ishmael::EBook::Metadata;
  17         29  
  17         378  
16 17     17   6119 use EBook::Ishmael::HTML qw(prepare_html);
  17         43  
  17         984  
17 17     17   87 use EBook::Ishmael::ImageID qw(image_path_id image_size);
  17         22  
  17         613  
18 17     17   59 use EBook::Ishmael::ShellQuote qw(safe_qx);
  17         25  
  17         30430  
19              
20             # TODO: Make more of an effort to find metadata
21              
22             my $HAS_CHMLIB = defined which('extract_chmLib');
23             my $HAS_HH = defined which('hh.exe');
24             our $CAN_TEST = $HAS_CHMLIB || $HAS_HH;
25              
26             my $MAGIC = 'ITSF';
27              
28             sub heuristic {
29              
30 115     115 0 148 my $class = shift;
31 115         151 my $file = shift;
32 115         123 my $fh = shift;
33              
34 115         3585 read $fh, my $mag, length $MAGIC;
35              
36 115         480 return $mag eq $MAGIC;
37              
38             }
39              
40             sub _extract {
41              
42 0     0     my $self = shift;
43              
44 0 0         if ($HAS_CHMLIB) {
    0          
45 0           safe_qx('extract_chmLib', $self->{Source}, $self->{_extract});
46 0 0         unless ($? >> 8 == 0) {
47 0           die "Failed to run 'extract_chmLib' on $self->{Source}\n";
48             }
49             } elsif ($HAS_HH) {
50 0           safe_qx('hh.exe', '-decompile', $self->{_extract}, $self->{Source});
51 0 0         unless ($? >> 8 == 0) {
52 0           die "Failed to run 'hh.exe' on $self->{Source}\n";
53             }
54             } else {
55 0           die "Cannot extract CHM $self->{Source}; extract_chmLib nor hh.exe installed\n";
56             }
57              
58              
59 0           return 1;
60              
61             }
62              
63             sub _urlstr {
64              
65 0     0     my $self = shift;
66              
67 0           my $strfile = File::Spec->catfile($self->{_extract}, '#URLSTR');
68              
69 0 0         unless (-f $strfile) {
70 0           die "Cannot read CHM $self->{Source}; #URLSTR missing\n";
71             }
72              
73 0 0         open my $fh, '<', $strfile
74             or die "Failed to open $strfile for reading: $!\n";
75 0           binmode $fh;
76              
77 0           my @urls = split /\0+/, do { local $/ = undef; readline $fh };
  0            
  0            
78              
79 0           for my $url (@urls) {
80 0 0         next unless $url =~ /\.html$/;
81 0           $url = File::Spec->catfile($self->{_extract}, $url);
82 0 0         next unless -f $url;
83 0           push @{ $self->{_content} }, $url;
  0            
84             }
85              
86 0           close $fh;
87              
88 0           return 1;
89              
90             }
91              
92             sub _hhc {
93              
94 0     0     my $self = shift;
95              
96 0           my ($hhc) = grep { /\.hhc$/ } dir($self->{_extract});
  0            
97              
98 0 0         unless (defined $hhc) {
99 0           die "CHM $self->{Source} is missing HHC\n";
100             }
101              
102             my $dom = XML::LibXML->load_html(
103             location => $hhc,
104             recover => 2,
105             no_network => !$self->{Network},
106 0           );
107              
108 0           my @locals = $dom->findnodes('//li/object/param[@name="Local"]');
109              
110 0           @{ $self->{_content} } =
111 0           grep { /\.html?$/ }
112 0           grep { -f }
113 0           map { File::Spec->catfile($self->{_extract}, $_->getAttribute('value')) }
114 0           grep { defined $_->getAttribute('value') }
  0            
115             @locals;
116              
117 0           my ($gen) = $dom->findnodes('/html/head/meta[@name="GENERATOR"]/@content');
118              
119 0 0         if (defined $gen) {
120 0           $self->{Metadata}->contributor([ $gen->value ]);
121             }
122              
123 0           return 1;
124              
125             }
126              
127             sub _images {
128              
129 0     0     my $self = shift;
130 0   0       my $dir = shift // $self->{_extract};
131              
132 0           for my $f (dir($dir)) {
133 0 0         if (-d $f) {
    0          
134 0           $self->_images($f);
135             } elsif (-f $f) {
136 0           my $format = image_path_id($f);
137 0 0         next if not defined $format;
138 0           push @{ $self->{_images} }, [ $f, $format ];
  0            
139             }
140             }
141              
142 0           return 1;
143              
144             }
145              
146             sub _clean_html {
147              
148 0     0     my $node = shift;
149              
150 0           my @children = grep { $_->isa('XML::LibXML::Element') } $node->childNodes;
  0            
151              
152             # Remove the ugly nav bars from the top and bottom of the page. We
153             # determine if a node is a navbar node if it contains an image that is
154             # alt-tagged with either next or prev.
155 0 0 0       if (@children and my ($alt) = $children[0]->findnodes('.//img/@alt')) {
156 0 0         if ($alt->value =~ m/next|prev/i) {
157 0           $node->removeChild(shift @children);
158             }
159             }
160 0 0 0       if (@children and my ($alt) = $children[-1]->findnodes('.//img/@alt')) {
161 0 0         if ($alt->value =~ m/next|prev/i) {
162 0           $node->removeChild(pop @children);
163             }
164             }
165              
166             # Now get rid of the horizontal space placed before/after each nav bar.
167 0 0 0       if (@children and $children[0]->nodeName eq 'hr') {
168 0           $node->removeChild(shift @children);
169             }
170 0 0 0       if (@children and $children[-1]->nodeName eq 'hr') {
171 0           $node->removeChild(pop @children);
172             }
173              
174 0           return 1;
175              
176             }
177              
178             sub new {
179              
180 0     0 0   my $class = shift;
181 0           my $file = shift;
182 0           my $enc = shift;
183 0   0       my $net = shift // 1;
184              
185 0           my $self = {
186             Source => undef,
187             Metadata => EBook::Ishmael::EBook::Metadata->new,
188             Network => $net,
189             _extract => undef,
190             _images => [],
191             _content => [],
192             };
193              
194 0           bless $self, $class;
195              
196 0           $self->{Source} = File::Spec->rel2abs($file);
197              
198 0           $self->{_extract} = tempdir(CLEANUP => 1);
199 0           $self->_extract;
200             #$self->_urlstr;
201 0           $self->_hhc;
202 0           $self->_images;
203              
204 0           $self->{Metadata}->set_title((fileparse($self->{Source}, qr/\.[^.]*/))[0]);
205 0           $self->{Metadata}->set_modified((stat $self->{Source})[9]);
206 0           $self->{Metadata}->set_format('CHM');
207              
208 0           return $self;
209              
210             }
211              
212             sub html {
213              
214 0     0 0   my $self = shift;
215 0           my $out = shift;
216              
217             my $html = join '', map {
218              
219             my $dom = XML::LibXML->load_html(
220             location => $_,
221             recover => 2,
222             no_network => !$self->{Network},
223 0           );
224              
225 0           my ($body) = $dom->findnodes('/html/body');
226 0   0       $body //= $dom->documentElement;
227 0           _clean_html($body);
228 0           prepare_html($body);
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 0           prepare_html($body);
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;