package LIMS::Controller::Roles::PDF; use Moose::Role; use Data::Dumper; use IO::All; use IPC::Run; use PDF::API2; use PDF::WebKit; use PDF::WebKit::Configuration; BEGIN { # necessary to make PDF::WebKit::Configuration::_find_wkhtmltopdf work properly: local $SIG{CHLD} = 'DEFAULT'; die "Couldn't find wkhtmltopdf" unless PDF::WebKit::Configuration->configuration->wkhtmltopdf; # alternative is to set path manually: # PDF::WebKit->configure( sub { $_->wkhtmltopdf('/path/to/wkhtmltopdf') } ); } has webkit_stylesheets => ( is => 'ro', isa => 'ArrayRef[Str]', lazy => 1, default => sub { [] }, traits => ['Array'], handles => { add_webkit_stylesheet => 'push', all_webkit_stylesheets => 'elements', }, ); # set metric defaults: PDF::WebKit->configure( sub { $_->default_options->{page_size} = 'A4'; $_->default_options->{margin_top} = '10mm'; $_->default_options->{margin_left} = '10mm'; $_->default_options->{margin_right} = '10mm'; $_->default_options->{margin_bottom} = '10mm'; }); # doesn't require temp file, takes html as scalarref: sub inline_html_to_pdf { # ok for single pdf - too slow for print-run my ($self, $html, %args) = @_; # warn Dumper \%args; # scalarref, optional @args my $content = _escape($html); # substitute any chars wkhtmltopdf can't handle my $webkit = PDF::WebKit->new(\$content, %args); # $content must be scalarref push @{ $webkit->stylesheets }, $_ for $self->all_webkit_stylesheets; my $pdf = $webkit->to_pdf; # warn Dumper $webkit->command; # outputs args return $pdf; } sub combine_pdfs { my ($self, $report, $args) = @_; # $report = aref of reports hashrefs my $tmpl = 'record/default.tt'; my $multi = PDF::API2->new; { # set tab header: my $reports = scalar @$report; my $start = $args->{offset} + 1; # calculate end as offset + limit, or offset + no. of reports - will work # for all instances except final block with < 50 reports but with some cc's # makings total no. of reports >= 50 my $end = ( $reports < $args->{limit} ) # if no. of reports less than limit: ? $args->{offset} + $reports # eg 201 - 233 : $args->{offset} + $args->{limit}; # eg 201 - 250 my $title = sprintf 'Print run [%s - %s]', $start, $end; $multi->info( Title => $title ); } my $footer = $self->cfg('settings')->{report_footer}; my %args = ( header_font_size => 8, footer_font_size => 8, header_spacing => 2, margin_top => 7, margin_bottom => 26, # space for footer footer_html => 'http://localhost/'.$footer, header_right => 'Printed on: [date]', # [date] replaced by system date (in local format) # footer_center => 'Page [page] of [toPage]', # moved to footer.html ); for my $r (@$report) { my $request = $r->{data}; # warn ref $request; # LIMS::DB::Request unless copy_to: $request = $request->as_tree unless ref $request eq 'HASH'; # ie copy_to from _generate_copy() # next if $request->request_initial_screen->screen->description eq 'Outreach'; my $patient = $request->{patient_case}->{patient}; my $report_id = sprintf '%s, %s :: %s/%s', uc $patient->{last_name}, ucfirst $patient->{first_name}, $request->{request_number}, $request->{year} - 2000; $args{header_left} = $report_id; # warn Dumper \%args; my $html = $self->render_view($tmpl, $r); # warn $html; # scalarref # my $pdf = $self->inline_html_to_pdf($html, %args); # too slow - 59sec vs 48sec / 50 reports: my $pdf = $self->inline_html_to_pdf_no_webkit($html, \%args); # faster my $api = PDF::API2->open_scalar($pdf); $multi->importpage($api, $_) foreach 1 .. $api->pages; } # $multi->saveas('/tmp/print_run.pdf'); # save file to disk my $combined = $multi->stringify(); return $combined; } sub inline_html_to_pdf_no_webkit { # uses same method as PDF::WebKit but faster - for print-run my ($self, $html_ref, $args) = @_; # warn Dumper $args; warn $html_ref; # scalarref # wkhtmltopdf 11+ renders slower than 10 but looks better: my $wkhtmltopdf = 'wkhtmltopdf_0.12.3'; # wkhtmltopdf_0.10.0 my @args = ( '/home/raj/perl-lib/wkhtmltopdf/'.$wkhtmltopdf, '--margin-top' => $args->{margin_top}, # with header-spacing to provide optimum layout '--margin-bottom' => $args->{margin_bottom}, # provide space for footer '--header-spacing' => $args->{header_spacing}, # provide gap between header & banner '--header-left' => $args->{header_left}, # double-quote to allow for eg o'connor '--header-font-size' => $args->{header_font_size}, '--header-right' => $args->{header_right}, '--footer-html' => $args->{footer_html}, # '--footer-spacing x', # might need this if long content reaches footer '--disable-javascript', # if any '--disable-external-links', # no urls '--quiet', '-', # stdin '-', # stdout ); # p @args; my $html = _escape($html_ref); # substitute any chars wkhtmltopdf can't handle my $output; IPC::Run::run( \@args, "<", \$html, ">", \$output ); return $output; } sub render_pdf { my ($self, $args) = @_; my $session_id = $args->{session_id}; # needs session_id for chart function my $content = $args->{content}; # warn $content; # scalarref my $data = $self->stash->{request_data}; # warn Dumper $data->as_tree; my $patient = $data->patient_case->patient; my $report_id = sprintf '%s, %s :: %s/%s', uc $patient->last_name, ucfirst $patient->first_name, $data->request_number, $data->year - 2000; # perl 5.13+ supports: my $foo = $bar =~ s/foo/bar/r; # r = non-destructive # ( my $base_addr = $self->query->url(-base => 1) ) =~ s/\:8080//; # if dev server my $settings = $self->cfg('settings'); my ($base_addr, $footer) = map $settings->{$_}, qw (base_href report_footer); my @args = ( '--margin-top 7', # combined with header-spacing to provide optimum layout '--header-spacing 2', # provide gap between header & banner qq!--header-left "$report_id"!, # double-quote to allow for eg o'connor qq!--header-right 'Printed on: [date]'!, # [date] replaced by system date (in local format) qq!--footer-html $base_addr/$footer!, '--header-font-size 8', '--margin-bottom 26', # provide space for footer '--cookie CGISESSID ' . $session_id, '--disable-javascript', # if any '--disable-external-links', # no urls # '--footer-line', # draw line above - doesn't print # '--footer-spacing x', # might need this if long content reaches footer ); # create temp file for input to wkhtmltopdf: my $tmp_file = sprintf '%s/%s.html', $self->cfg->{tmpdir}, $data->id; # warn $tmp_file; { # save file to disk: my $html = _escape($content); # substitute any chars wkhtmltopdf can't handle io($tmp_file)->print($html); # $html is str } my $pdf = `wkhtmltopdf -q @args $tmp_file -`; if ( -e $tmp_file ) { # occasionally doesn't exist - maybe simultaneous access ?? io($tmp_file)->unlink or warn "could not unlink $tmp_file: $!"; } return $pdf; } # replace some chars wkhtmltopdf can't understand: sub _escape { my $ref = shift; # scalarref my $str = ${$ref}; # doesn't need to be ref anymore - here or downstream my %h = ( # '±' (0xB1 & \x{B1}) see: https://en.wikipedia.org/wiki/Plus-minus_sign#Encodings chr(0xB1) => '±', # '±' prints as '▒' on console & '?' in pdf ); # perform substitutions on deref'd var: $str =~ s/$_/$h{$_}/g for keys %h; # warn $str; return $str; # expecting string return } 1;