File Coverage

blib/lib/EBook/Ishmael/EBook/PDF.pm
Criterion Covered Total %
statement 34 169 20.1
branch 0 50 0.0
condition 0 8 0.0
subroutine 11 28 39.2
pod 0 9 0.0
total 45 264 17.0


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