1

I am trying to generate a PDF for an order receipt on-the-fly from HTML that is also generated on-the-fly, then email it to someone.

I really don't want to create a file, attach it to an email, then delete the file, so I'm trying to send the html to wkhtmltopdf via STDIN (from Perl) and then capture the PDF output from wkhtmltopdf in an email attachment using MIME::Lite Email::Mime.

This absolutely works using Perl to allow people to download a dynamically-generated PDF file from my website, but trying to use it with MIME::Lite Email::Mime doesn't work. (it probably would have worked, but since it's out of date, we're using Email::Mime instead)

I'm absolutely sure that this is due to my lack of fundamental understanding of working with filehandles, pipes, backticks, and other less-oft used things, and I'd love to get a better grasp of these things.

Here's what works:

#!/usr/bin/perl
#### takes string containing HTML and outputs PDF to browser to download
#### (otherwise would output to STDOUT)

print "Content-Disposition: attachment; filename='testPDF.pdf'\n";
print "Content-type: application/octet-stream\n\n";

my $htmlToPrint = "<html>a bunch of html</html>";

### open a filehandle and pipe it to wkhtmltopdf
### *the arguments "- -" tell wkhtmltopdf to get 
###  input from STDIN and send output to STDOUT*
open(my $makePDF, "|-", "wkhtmltopdf", "-", "-") || die("$!");
print $makePDF $htmlToPrint;  ## sends my HTML to wkhtmltopdf which streams immediately to STDOUT

exit 1;

You can run this as is from Apache, and it will present a download dialog to the user and download a readable, correct pdf named 'testPDF.pdf'.

EDIT: The solution is the Capture::Tiny module (and Email::Mime):

#!/usr/bin/perl
use Capture::Tiny qw( capture );
use Email::Sender::Simple;
use Email::MIME::Creator;

my $htmlToPrint = "<html>a bunch of html</html>";

### it's important to capture STDERR as well, since wkhtmltopdf outputs
### its console messages on STDERR instead of STDOUT, so it can output
### the PDF to STDOUT; otherwise it will spam your error log    
(my $pdfstream, my $consoleOutput, my @retvals) = capture {
    open(my $makePDF, "|-", "wkhtmltopdf", "-", "-") || die("$!");
    print $makePDF $htmlToPrint;
};

my @parts = (
Email::MIME->create(
    attributes => {
        content_type => "text/plain",
        disposition  => "inline",
        charset      => "US-ASCII",
        encoding     => "quoted-printable",
    },
    body_str => "Your order receipt is attached as a PDF.",
),
Email::MIME->create(
    attributes => {
        filename     => "YourOrderReceipt.pdf",
        content_type => "application/pdf",
        disposition  => "attachment",
        encoding     => "base64",  ## base64 is ESSENTIAL, binary and quoted-printable do not work!
        name         => "YourOrderReceipt.pdf",
    },
    body => $pdfstream,
),
);

my $email = Email::MIME->create(
  header_str => [
      From => 'Some Person <me@mydomain.com>',
      To   => 'customer@theirdomain.com',
      Subject => "Your order receipt is attached...",
  ],
  parts => [ @parts ],
);

Email::Sender::Simple->send($email);
exit 1;

This all works perfectly now.

Most of the problem appears to be that wkhtmltopdf does not buffer the PDF output and send it line by line; it immediately streams all of the PDF output to STDOUT as soon as it gets the HTML input from STDIN.

I think this is why I could not get open2 or open3 to work.

I also tried open (my $pdfOutput, "echo \"$htmlToPrint\"| wkhtmltopdf - -|"), but this runs in the shell, so even with $htmlToPrint enclosed in quotes, the command chokes on the symbols used in the HTML.

Hope someone finds this helpful...

waldo22
  • 35
  • 9
  • The first sentence of the `MIME::Lite` documentation recommends not using `MIME::Lite`. – jordanm Jan 30 '13 at 04:07
  • thanks @jordanm, I spent a long time reading the documentation, but missed that somehow. – waldo22 Jan 30 '13 at 04:57
  • I updated this post with working code. I believe `MIME::Lite` would have worked as well, but I think using the newer, recommended `Email::MIME` is a Good Thing. Problem was you can't do bi-directional communication with pipes; you need a module like `Capture::Tiny`. The email attachment also had to be `base64` encoded in order to work. Tried `binary` and `quoted-printable` to no effect. – waldo22 Feb 02 '13 at 09:15

1 Answers1

1

You need to use open2 or open3 to send input to a cmd then gather its output without using backtick.

local(*HIS_IN, *HIS_OUT, *HIS_ERR);
my $pid = open3(*HIS_IN, *HIS_OUT, *HIS_ERR,'wkhtmltopdf', '-', '-');
waitpid( $pid, 0 );
my $child_exit_status = $? >> 8;

You could use more fresh alternatives to send emails:

  use Email::MIME::Creator;
  use IO::All;

  # multipart message
  my @parts = (
      Email::MIME->create(
          attributes => {
              filename     => "report.pdf",
              content_type => "application/pdf",
              encoding     => "quoted-printable",
              name         => "2004-financials.pdf",
          },
          #body => io( *HIS_OUT )->all, it may work
          body => *HIS_OUT,

      ),
      Email::MIME->create(
          attributes => {
              content_type => "text/plain",
              disposition  => "attachment",
              charset      => "US-ASCII",
          },
          body_str => "Hello there!",
      ),
  );

  my $email = Email::MIME->create(
      header_str => [ From => 'casey@geeknest.com' ],
      parts      => [ @parts ],
  );
  # standard modifications
  $email->header_str_set( To            => rcpts()        );

  use Email::Sender::Simple;
  Email::Sender::Simple->send($email);
waldo22
  • 35
  • 9
user1126070
  • 5,059
  • 1
  • 16
  • 15
  • Can this be done using backticks? If so why would you chose open3 over backticks or vice-versa? – waldo22 Jan 30 '13 at 23:28
  • Not posible w. backtick. You would like to stream a content to STDIN and you would like to capture its output without using temp files. Maybe you could do something like this: open(OUT,"echo $html| wkhtmltopdf - -|") or die; But it won't work for large files. – user1126070 Jan 31 '13 at 09:22
  • I'll give that a try. Neither open2 nor open3 seem to work at the moment; I think it's because wkhtmltopdf streams the output instead of writing it out one line at a time... – waldo22 Feb 01 '13 at 01:44
  • OK, I solve this, but I think it needs a new answer. Your `Email::MIME` suggestion was a great one. `open(OUT,"echo $html| wkhtmltopdf - -|")` works for IO, but because it uses the shell, it will choke on the special characters in HTML, so it can't be used for this. Your open2/open3 suggestions led me to this question: [link](http://stackoverflow.com/questions/109124/how-do-you-capture-stderr-stdout-and-the-exit-code-all-at-once-in-perl), which led me to the [Capture::Tiny](http://search.cpan.org/dist/Capture-Tiny/) progam, which solved my problem. I've updated my code above. – waldo22 Feb 02 '13 at 03:29
  • I posted the working code on my original question. Would it be more helpful to edit your post and accept it as the answer, or for me to post a separate answer and accept that? – waldo22 Feb 02 '13 at 09:18