File Coverage

blib/lib/EBook/Ishmael/EBook/PDF.pm
Criterion Covered Total %
statement 37 176 21.0
branch 0 54 0.0
condition 0 8 0.0
subroutine 12 30 40.0
pod 0 9 0.0
total 49 277 17.6


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