RSS Git Download  Clone
Raw Blame History
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 = (
        '/usr/local/bin/'.$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) => '&plusmn;', # '±' 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;