File Coverage

lib/PDF/WebKit.pm
Criterion Covered Total %
statement 80 136 58.8
branch 12 38 31.5
condition 4 17 23.5
subroutine 20 24 83.3
pod 3 6 50.0
total 119 221 53.8


line stmt bran cond sub pod time code
1             package PDF::WebKit;
2 3     3   418232 use 5.008008;
  3         27  
3 3     3   12 use strict;
  3         5  
  3         51  
4 3     3   9 use warnings;
  3         5  
  3         65  
5 3     3   13 use Carp ();
  3         4  
  3         37  
6 3     3   1344 use IO::File ();
  3         2804  
  3         68  
7 3     3   1256 use IPC::Run3 'run3';
  3         25808  
  3         150  
8              
9 3     3   999 use PDF::WebKit::Configuration;
  3         7  
  3         73  
10 3     3   882 use PDF::WebKit::Source;
  3         7  
  3         83  
11              
12             our $VERSION = '1.2';
13              
14 3     3   14 use Moo;
  3         5  
  3         8  
15 3     3   783 use namespace::clean;
  3         6  
  3         23  
16              
17             has source => ( is => 'rw' );
18             has stylesheets => ( is => 'rw' );
19             has options => ( is => 'ro', writer => '_set_options' );
20              
21             around 'BUILDARGS' => sub {
22             my $orig = shift;
23             my $class = shift;
24              
25             if (@_ % 2 == 0) {
26             Carp::croak "Usage: ${class}->new(\$url_file_or_html,%options)";
27             }
28              
29             my $url_file_or_html = shift;
30             my $options = { @_ };
31             return $class->$orig({ source => $url_file_or_html, options => $options });
32             };
33              
34             sub BUILD {
35 17     17 0 523 my ($self,$args) = @_;
36              
37 17         230 $self->source( PDF::WebKit::Source->new($args->{source}) );
38 17         468 $self->stylesheets( [] );
39             $self->_set_options({
40 17         27 $self->_normalize_options(%{ $self->configuration->default_options }),
41 17         17 $self->_normalize_options(%{ $args->{options} }),
  17         44  
42             $self->_normalize_options($self->_find_options_in_meta),
43             });
44              
45 17 50       43 if (not -x $self->configuration->wkhtmltopdf) {
46 0         0 my $msg = "No wkhtmltopdf executable found\n";
47 0         0 $msg .= ">> Please install wkhtmltopdf - https://github.com/jdpace/PDFKit/wiki/Installing-WKHTMLTOPDF";
48 0         0 die $msg;
49             }
50             }
51              
52             sub configuration {
53 45     45 0 98 PDF::WebKit::Configuration->configuration
54             }
55              
56             sub configure {
57 2     2 0 8364 my $class = shift;
58 2         6 $class->configuration->configure(@_);
59             }
60              
61             sub command {
62 9     9 1 249 my $self = shift;
63 9         10 my $path = shift;
64 9         14 my @args = ( $self->_executable, $self->_prepare_options, '--quiet' );
65            
66 9 100       24 if ($self->source->is_html) {
67 7         9 push @args, '-'; # Get HTML from stdin
68             }
69             else {
70 2         5 push @args, $self->source->content;
71             }
72              
73 9   100     27 push @args, $path || '-'; # write to file or stdout
74              
75 9         15 return grep { defined($_) } @args;
  156         170  
76             }
77              
78             sub _executable {
79 9     9   9 my $self = shift;
80 9         15 my $default = $self->configuration->wkhtmltopdf;
81 9 50       61 return $default if $default !~ /^\//; # it's not a path, so nothing we can do
82 0 0       0 if (-e $default) {
83 0         0 return $default;
84             }
85             else {
86 0         0 return (split(/\//, $default))[-1];
87             }
88             }
89              
90             sub to_pdf {
91 0     0 1 0 my $self = shift;
92 0         0 my $path = shift;
93              
94 0         0 $self->_append_stylesheets;
95 0         0 my @args = $self->command($path);
96              
97 0 0       0 my $input = $self->source->is_html ? $self->source->content : undef;
98 0         0 my $output;
99              
100 0         0 my %opt = map +( "binmode_std$_" => ":raw" ), "in", "out", "err";
101 0         0 run3 \@args, \$input, \$output, \my $err, \%opt;
102              
103 0 0       0 if ($path) {
104 0         0 $output = do { local (@ARGV,$/) = ($path); <> };
  0         0  
  0         0  
105             }
106              
107 0 0 0     0 if (not (defined($output) && length($output))) {
108 0         0 Carp::croak "command failed: $args[0]";
109             }
110 0         0 return $output;
111             }
112              
113             sub to_file {
114 0     0 1 0 my $self = shift;
115 0         0 my $path = shift;
116 0         0 $self->to_pdf($path);
117 0   0     0 my $FH = IO::File->new($path,"<")
118             || Carp::croak "can't open '$path': $!";
119 0         0 $FH->binmode();
120 0         0 return $FH;
121             }
122              
123             sub _find_options_in_meta {
124 17     17   19 my ($self) = @_;
125 17 100       37 return () if $self->source->is_url;
126             # if we can't parse for whatever reason, keep calm and carry on.
127 15         20 my @result = eval { $self->_pdf_webkit_meta_tags };
  15         20  
128 15 50       60 return $@ ? () : @result;
129             }
130              
131             sub _pdf_webkit_meta_tags {
132 15     15   15 my ($self) = @_;
133 15 50       14 return () unless eval { require XML::LibXML };
  15         1924  
134 0         0 my $source = $self->source;
135              
136 0         0 my $prefix = $self->configuration->meta_tag_prefix;
137              
138             # these options do not work at the constructor level in XML::LibXML 1.70, so pass
139             # them through to the parser.
140 0         0 my %options = (
141             recover => 2,
142             suppress_errors => 1,
143             suppress_warnings => 1,
144             no_network => 1,
145             );
146              
147 0         0 my $parser = XML::LibXML->new();
148 0 0       0 my $doc = $source->is_html ? $parser->parse_html_string($source->content,\%options)
    0          
149             : $source->is_file ? $parser->parse_html_file($source->string,\%options)
150             : return ();
151              
152 0         0 my %meta;
153 0         0 for my $node ($doc->findnodes('html/head/meta')) {
154 0         0 my $name = $node->getAttribute('name');
155 0 0 0     0 next unless ($name && ($name =~ s{^\Q$prefix}{}s));
156 0         0 $meta{$name} = $node->getAttribute('content');
157             }
158              
159 0         0 return %meta;
160             }
161              
162             sub _style_tag_for {
163 0     0   0 my ($self,$stylesheet) = @_;
164 0         0 my $styles = do { local (@ARGV,$/) = ($stylesheet); <> };
  0         0  
  0         0  
165 0         0 return "";
166             }
167              
168             sub _append_stylesheets {
169 0     0   0 my $self = shift;
170 0 0 0     0 if (@{ $self->stylesheets } && !$self->source->is_html) {
  0         0  
171 0         0 Carp::croak "stylesheets may only be added to an HTML source";
172             }
173 0 0       0 return unless $self->source->is_html;
174              
175 0         0 my $styles = join "", map { $self->_style_tag_for($_) } @{$self->stylesheets};
  0         0  
  0         0  
176 0 0       0 return unless length($styles) > 0;
177              
178             # can't modify in-place, because the source might be a reference to a
179             # read-only constant string literal
180 0         0 my $html = $self->source->content;
181 0 0       0 if (not ($html =~ s{(?=)}{$styles})) {
182 0         0 $html = $styles . $html;
183             }
184 0         0 $self->source->string(\$html);
185             }
186              
187             sub _prepare_options {
188 9     9   12 my ($self) = @_;
189 9         15 my $options = $self->options;
190 9         11 my @args;
191 9         24 while (my ($name,$val) = each %$options) {
192 66 100 66     141 next unless defined($val) && length($val);
193 64 100       69 if (lc($val) eq 'yes') {
194 8         17 push @args, $name;
195             }
196             else {
197 56         103 push @args, $name, $val;
198             }
199             }
200 9         28 return @args;
201             }
202              
203             sub _normalize_options {
204 51     51   50 my $self = shift;
205 51         86 my %orig_options = @_;
206 51         56 my %normalized_options;
207 51         129 while (my ($key,$val) = each %orig_options) {
208 126         150 my $normalized_key = $self->_normalize_arg($key);
209 126         306 $normalized_options{$normalized_key} = $val;
210             }
211 51         166 return %normalized_options;
212             }
213              
214             sub _normalize_arg {
215 126     126   121 my ($self,$arg) = @_;
216 126         715 $arg =~ lc($arg);
217 126         342 $arg =~ s{[^a-z0-9]}{-}g;
218 126         273 $arg =~ s{^-*}{--};
219 126         175 return $arg;
220             }
221              
222             1;
223              
224             =head1 NAME
225              
226             PDF::WebKit - Use WebKit to Generate PDFs from HTML (via wkhtmltopdf)
227              
228             =head1 SYNOPSIS
229              
230             use PDF::WebKit;
231              
232             # PDF::WebKit->new takes the HTML and any options for wkhtmltopdf
233             # run `wkhtmltopdf --extended-help` for a full list of options
234             my $kit = PDF::WebKit->new(\$html, page_size => 'Letter');
235             push @{ $kit->stylesheets }, "/path/to/css/file";
236              
237             # Get an inline PDF
238             my $pdf = $kit->to_pdf;
239              
240             # save the PDF to a file
241             my $file = $kit->to_file('/path/to/save/pdf');
242              
243             # PDF::WebKit can optionally accept a URL or a File
244             # Stylesheets cannot be added when source is provided as a URL or File.
245             my $kit = PDF::WebKit->new('http://google.com');
246             my $kit = PDF::WebKit->new('/path/to/html');
247              
248             # Add any kind of option through meta tags
249             my $kit = PDF::WebKit->new(\'
250              
251             =head1 DESCRIPTION
252              
253             PDF::WebKit uses L to
254             convert HTML documents into PDFs. It is a port of the elegant
255             L Ruby library.
256              
257             wkhtmltopdf generates beautiful PDFs by leveraging the rendering power
258             of Qt's WebKit browser engine (used by both Apple Safari and Google
259             Chrome browsers).
260              
261             =head2 Configuration
262              
263             Configuration of PDF::WebKit is configured globally by calling the
264             C<< PDF::WebKit->configure >> class method:
265              
266             PDF::WebKit->configure(sub {
267             # default `which wkhtmltopdf`
268             $_->wkhtmltopdf('/path/to/wkhtmltopdf');
269              
270             # default 'pdf-webkit-'
271             $_->meta_tag_prefix('my-prefix-');
272              
273             $_->default_options->{'--orientation'} = 'Portrait';
274             });
275              
276             See the L method for the standard default options.
277              
278             =head2 Constructor
279              
280             =over 4
281              
282             =item new($SOURCE_URL,%OPTIONS)
283              
284             =item new($SOURCE_FILENAME,%OPTIONS)
285              
286             =item new(\$SOURCE_HTML,%OPTIONS)
287              
288             Creates and returns a new instance. If the first parameter looks like a
289             URL, it is treated as a URL and handed off to wkhtmltopdf verbatim. If
290             it is is a reference to a scalar, it is an HTML document body.
291             Otherwise, the parameter is interpreted as a filename.
292              
293             The %OPTIONS hash is a list of name/value pairs for command-line
294             options to wkhtmltopdf. These options can augment or override the
295             default options. For options with no associated value, pass "YES" (case
296             insensitive) as the value, e.g. C "YES">.
297              
298             The default options are:
299              
300             --page-size Letter
301             --margin-top 0.75in
302             --margin_right 0.75in
303             --margin_bottom 0.75in
304             --margin_left 0.75in
305             --encoding UTF-8
306              
307             =back
308              
309             =head2 Methods
310              
311             =over 4
312              
313             =item command
314              
315             Returns the list of command-line arguments that would be used to execute
316             wkhtmltopdf.
317              
318             =item to_pdf
319              
320             Processes the source material and returns a PDF as a string.
321              
322             =item to_file($PATH)
323              
324             Processes the source material and creates a PDF at C<$PATH>. Returns a
325             filehandle opened on C<$PATH>.
326              
327             =back
328              
329             =head1 SEE ALSO
330              
331             L,
332             L,
333             L
334             (a lower-level wrapper for wkhtmltopdf).
335              
336             =head1 AUTHOR
337              
338             Philip Garrett
339              
340             =head1 CONTRIBUTORS
341              
342             Christian Walde
343              
344             =head1 CONTRIBUTING
345              
346             If you'd like to contribute, just fork my repository on Github, commit
347             your changes and send me a pull request.
348              
349             http://github.com/kingpong/perl-PDF-WebKit
350              
351             =head1 ACKNOWLEDGMENTS
352              
353             This code is nearly a line-by-line port of Jared Pace's PDFKit.
354             https://github.com/jdpace/PDFKit
355              
356             =head1 COPYRIGHT & LICENSE
357              
358             Copyright (c) 2011 by Informatics Corporation of America.
359              
360             This library is free software; you can redistribute it and/or modify
361             it under the same terms as Perl itself, either Perl version 5.8.8 or,
362             at your option, any later version of Perl 5 you may have available.
363              
364             =cut