File Coverage

blib/lib/EBook/Ishmael/EBook/PDF.pm
Criterion Covered Total %
statement 40 181 22.1
branch 0 54 0.0
condition 0 8 0.0
subroutine 13 31 41.9
pod 0 9 0.0
total 53 283 18.7


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::PDF;
2 17     17   248 use 5.016;
  17         51  
3             our $VERSION = '2.05';
4 17     17   70 use strict;
  17         26  
  17         278  
5 17     17   50 use warnings;
  17         27  
  17         742  
6              
7 17     17   69 use File::Temp qw(tempfile tempdir);
  17         20  
  17         790  
8 17     17   68 use Time::Piece;
  17         20  
  17         138  
9              
10 17     17   1112 use File::Which;
  17         28  
  17         675  
11 17     17   63 use XML::LibXML;
  17         48  
  17         138  
12              
13 17     17   2496 use EBook::Ishmael::Dir;
  17         22  
  17         686  
14 17     17   63 use EBook::Ishmael::EBook::Metadata;
  17         32  
  17         413  
15 17     17   53 use EBook::Ishmael::HTML qw(prepare_html);
  17         29  
  17         611  
16 17     17   61 use EBook::Ishmael::ImageID qw(image_path_id);
  17         26  
  17         683  
17 17     17   75 use EBook::Ishmael::ShellQuote qw(safe_qx);
  17         36  
  17         30192  
18              
19             # This module basically delegates the task of processing PDFs to some of
20             # Poppler's utilities. The processing is quite inefficient, and the output is
21             # ugly; PDF isn't really a format suited for plain text rendering.
22              
23             my $HAS_PDFTOHTML = defined which('pdftohtml');
24             my $HAS_PDFINFO = defined which('pdfinfo');
25             my $HAS_PDFTOPNG = defined which('pdftopng');
26             my $HAS_CONVERT = defined which('convert');
27             my $HAS_PDFIMAGES = defined which('pdfimages');
28              
29             our $CAN_TEST = (
30             $HAS_PDFTOHTML and
31             $HAS_PDFINFO and
32             ($HAS_PDFTOPNG or $HAS_CONVERT) and
33             $HAS_PDFIMAGES
34             );
35              
36             my $MAGIC = '%PDF';
37              
38             sub heuristic {
39              
40 47     47 0 67 my $class = shift;
41 47         63 my $file = shift;
42 47         65 my $fh = shift;
43              
44 47         196 read $fh, my ($mag), 4;
45              
46 47         124 return $mag eq $MAGIC;
47              
48             }
49              
50             sub _parse_pdf_date {
51              
52 0     0     my ($date) = @_;
53 0           $date =~ s/^D://;
54 0           $date =~ s/'//g;
55              
56 0 0         my ($year, $month, $day, $hour, $min, $sec, $tz) =
57             $date =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(Z|[+-]\d{4})$/
58             or die "Invalid PDF date\n";
59              
60 0 0         if ($tz eq 'Z') {
61 0           $tz = '+0000';
62             }
63              
64 0 0         my $tp = eval {
65 0           Time::Piece->strptime(
66             "$year $month $day $hour $min $sec $tz",
67             '%Y %m %d %H %M %S %z',
68             )
69             } or die "Failed to parse '$date'\n";
70 0           return $tp->epoch;
71              
72             }
73              
74             sub _get_metadata {
75              
76 0     0     my $self = shift;
77              
78             my %meta = (
79 0     0     'Author' => sub { $self->{Metadata}->add_author(shift) },
80 0     0     'CreationDate' => sub { $self->{Metadata}->set_created(eval { _parse_pdf_date(shift) }) },
  0            
81 0     0     'Creator' => sub { $self->{Metadata}->add_contributor(shift) },
82 0     0     'ModDate' => sub { $self->{Metadata}->set_modified(eval { _parse_pdf_date(shift) }) },
  0            
83 0     0     'PDF version' => sub { $self->{Metadata}->set_format('PDF ' . shift) },
84 0     0     'Producer' => sub { $self->{Metadata}->add_contributor(shift) },
85 0     0     'Title' => sub { $self->{Metadata}->set_title(shift) },
86 0           );
87              
88 0 0         if (!$HAS_PDFINFO) {
89 0           die "Cannot read PDF $self->{Source}: pdfinfo not installed\n";
90             }
91              
92 0           my $info = safe_qx('pdfinfo', '-rawdates', $self->{Source});
93 0 0         unless ($? >> 8 == 0) {
94 0           die "Failed to run 'pdfinfo' on $self->{Source}\n";
95             }
96              
97 0           for my $l (split /\n/, $info) {
98 0           my ($field, $content) = split /:\s*/, $l, 2;
99 0 0 0       unless (exists $meta{ $field } and $content) {
100 0           next;
101             }
102 0           $meta{$field}->($content);
103             }
104              
105 0           return 1;
106              
107             }
108              
109             sub _images {
110              
111 0     0     my $self = shift;
112              
113 0           $self->{_imgdir} = tempdir(CLEANUP => 1);
114              
115 0           my $root = File::Spec->catfile($self->{_imgdir}, 'ishmael');
116              
117             # TODO: Couldn't we just use system?
118 0           safe_qx('pdfimages', '-png', $self->{Source}, $root);
119 0 0         unless ($? >> 8 == 0) {
120 0           die "Failed to run 'pdfimages' on $self->{Source}\n";
121             }
122              
123 0           for my $f (dir($self->{_imgdir})) {
124 0           my $format = image_path_id($f);
125 0 0         next if not defined $format;
126 0           push @{ $self->{_images} }, [ $f, $format ];
  0            
127             }
128              
129 0           return 1;
130              
131             }
132              
133             sub new {
134              
135 0     0 0   my $class = shift;
136 0           my $file = shift;
137 0           my $enc = shift;
138 0   0       my $net = shift // 1;
139              
140 0           my $self = {
141             Source => undef,
142             Metadata => EBook::Ishmael::EBook::Metadata->new,
143             Network => $net,
144             _imgdir => undef,
145             _images => [],
146             };
147              
148 0           bless $self, $class;
149              
150 0           $self->{Source} = File::Spec->rel2abs($file);
151              
152 0           $self->_get_metadata();
153              
154 0 0         if (not defined $self->{Metadata}->format) {
155 0           $self->{Metadata}->set_format('PDF');
156             }
157              
158 0           return $self;
159              
160             }
161              
162             sub html {
163              
164 0     0 0   my $self = shift;
165 0           my $out = shift;
166              
167 0 0         if (!$HAS_PDFTOHTML) {
168 0           die "Cannot read PDF $self->{Source}: pdftohtml not installed\n";
169             }
170              
171 0           my $raw = safe_qx('pdftohtml', '-i', '-s', '-stdout', $self->{Source});
172 0 0         unless ($? >> 8 == 0) {
173 0           die "Failed to run 'pdftohtml' on $self->{Source}\n";
174             }
175              
176             my $dom = XML::LibXML->load_html(
177             string => $raw,
178             no_network => !$self->{Network},
179 0           );
180              
181 0           my ($body) = $dom->findnodes('/html/body');
182 0           prepare_html($body);
183              
184 0           my $html = join '', map { $_->toString } $body->childNodes;
  0            
185              
186 0 0         if (defined $out) {
187 0 0         open my $fh, '>', $out
188             or die "Failed to open $out for writing: $!\n";
189 0           binmode $fh, ':utf8';
190 0           print { $fh } $html;
  0            
191 0           close $fh;
192 0           return $out;
193             } else {
194 0           return $html;
195             }
196              
197             }
198              
199             sub raw {
200              
201 0     0 0   my $self = shift;
202 0           my $out = shift;
203              
204 0 0         if (!$HAS_PDFTOHTML) {
205 0           die "Cannot read PDF $self->{Source}: pdftohtml not installed\n";
206             }
207              
208 0           my $rawml = safe_qx('pdftohtml', '-i', '-s', '-stdout', $self->{Source});
209 0 0         unless ($? >> 8 == 0) {
210 0           die "Failed to run 'pdftohtml' on $self->{Source}\n";
211             }
212              
213             my $dom = XML::LibXML->load_html(
214             string => $rawml,
215             no_network => !$self->{Network},
216 0           );
217              
218 0           my ($body) = $dom->findnodes('/html/body');
219 0           prepare_html($body);
220              
221 0           my $raw = join '', $body->textContent;
222              
223 0 0         if (defined $out) {
224 0 0         open my $fh, '>', $out
225             or die "Failed to open $out for writing: $!\n";
226 0           binmode $fh, ':utf8';
227 0           print { $fh } $raw;
  0            
228 0           close $fh;
229 0           return $out;
230             } else {
231 0           return $raw;
232             }
233              
234             }
235              
236             sub metadata {
237              
238 0     0 0   my $self = shift;
239              
240 0           return $self->{Metadata};
241              
242             }
243              
244 0     0 0   sub has_cover { 1 }
245              
246             # Convert the first page of the PDF to a png as the cover, using either
247             # xpdf's pdftopng or ImageMagick's convert.
248             sub cover {
249              
250 0     0 0   my $self = shift;
251              
252 0 0 0       unless ($HAS_PDFTOPNG or $HAS_CONVERT) {
253 0           die "Cannot dump PDF $self->{Source} cover: pdftopng or convert not installed\n";
254             }
255              
256 0           my $png;
257              
258 0 0         if ($HAS_PDFTOPNG) {
    0          
259 0           my $tmpdir = tempdir(CLEANUP => 1);
260 0           my $tmproot = File::Spec->catfile($tmpdir, 'tmp');
261              
262 0           safe_qx('pdftopng', '-f', 1, '-l', 1, $self->{Source}, $tmproot);
263 0 0         unless ($? >> 8 == 0) {
264 0           die "Failed to run 'pdftopng' on $self->{Source}\n";
265             }
266              
267 0           $png = (dir($tmpdir))[0];
268 0 0         unless (defined $png) {
269 0           die "'pdftopng' could not produce a cover image from $self->{Source}\n";
270             }
271              
272             } elsif ($HAS_CONVERT) {
273 0           my $tmppath = do {
274 0           my ($h, $p) = tempfile(UNLINK => 1);
275 0           close $h;
276 0           $p;
277             };
278              
279             # The '[0]' means the first page only
280 0           safe_qx('convert', "$self->{Source}\[0\]", '-alpha', 'deactivate', "png:$tmppath");
281 0 0         if (not -f $tmppath) {
282 0           die "'convert' could not produce a cover image from $self->{Source}\n";
283             }
284 0           $png = $tmppath;
285              
286             } else {
287 0           die;
288             }
289              
290 0 0         open my $fh, '<', $png
291             or die "Failed to open $png for reading: $!\n";
292 0           binmode $fh;
293 0           my $bin = do { local $/ = undef; readline $fh };
  0            
  0            
294 0           close $fh;
295              
296 0           return ($bin, 'png');
297              
298              
299             }
300              
301             sub image_num {
302              
303 0     0 0   my $self = shift;
304              
305 0 0         unless (defined $self->{_imgdir}) {
306 0           $self->_images;
307             }
308              
309 0           return scalar @{ $self->{_images} };
  0            
310              
311             }
312              
313             sub image {
314              
315 0     0 0   my $self = shift;
316 0           my $n = shift;
317              
318 0 0         if ($n >= $self->image_num) {
319 0           return (undef, undef);
320             }
321              
322 0 0         open my $fh, '<', $self->{_images}[$n][0]
323             or die "Failed to open $self->{_images}[$n][0]: $!\n";
324 0           binmode $fh;
325 0           my $img = do { local $/ = undef; readline $fh };
  0            
  0            
326 0           close $fh;
327              
328 0           return ($img, $self->{_images}[$n][1]);
329              
330             }
331              
332             1;