Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
HTML to PDF conversion in PHP using wkhtmltopdf (and optionally Smarty)

Recently I was asked to generate PDF invoices for an online shop. I looked at various PHP PDF generators, but wasn't particularly impressed with any of them.

Then I found (via Stack Overflow) a command-line HTML-to-PDF convertor called wkhtmltopdf, which uses WebKit (the same layout engine as Safari and Google Chrome) and therefore is very accurate.

There is a class for PHP integration on the Wiki, but I found it overly complicated and it uses temp files which aren't necessary. This is the code I wrote instead.

I used Smarty for generating the HTML for the PDF, but you can use any template engine, or pure PHP if you prefer.

Note: I originally tried to install wkhtmltopdf from source, but it's much easier to use the static binary instead.

<?php
// Convert a file to an inline data URL within the Smarty template
// Usage:
// <img src="{data_url type="image/jpg" file="www/images/logo-print.jpg"}" width="270" />
// Note: Specify a fixed width, because the image is actually 1125px wide,
// squashed into a 270px wide space. Generally you want to use images that are
// at least 4 times larger (height and width) than the space you're putting them
// in - this makes it print at 300 DPI (good quality) rather than 72 DPI (fine
// for the screen but poor quality for printing).
function smarty_function_data_url($params, &$smarty)
{
if (!$params['type']) {
trigger_error('data_url: Missing "type" parameter');
return;
}
if (!$params['file']) {
trigger_error('data_url: Missing "file" parameter');
return;
}
$type = $params['type'];
$file = BASEDIR . '/' . $params['file'];
if (!is_file($file)) {
trigger_error('data_url: Cannot open file "' . $file . '"');
return;
}
return 'data:' . $params['type'] . ';base64,' . base64_encode(file_get_contents($file));
}
<?php
function generate_pdf($template, $vars = array(), $filename = 'download.pdf')
{
// Get the HTML to convert to a PDF
// (using Smarty - replace this if you want)
global $smarty;
$smarty->assign($vars);
$html = $smarty->fetch($template);
// Run wkhtmltopdf
$descriptorspec = array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'w'), // stderr
);
$process = proc_open('wkhtmltopdf -q - -', $descriptorspec, $pipes);
// Send the HTML on stdin
fwrite($pipes[0], $html);
fclose($pipes[0]);
// Read the outputs
$pdf = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
// Close the process
fclose($pipes[1]);
$return_value = proc_close($process);
// Output the results
if ($errors) {
throw new Exception('PDF generation failed: ' . $errors);
} else {
header('Content-Type: application/pdf');
header('Cache-Control: public, must-revalidate, max-age=0'); // HTTP/1.1
header('Pragma: public');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); // Date in the past
header('Last-Modified: ' . gmdate('D, d M Y H:i:s').' GMT');
header('Content-Length: ' . strlen($pdf));
header('Content-Disposition: inline; filename="' . $filename . '";');
echo $pdf;
}
}
/*
This CSS is used to force new pages at the appropriate points
<div class="page">Page 1 content here</div>
<div class="page">Page 2 content here</div>
*/
.page {
page-break-after: always;
}
/*
This avoids page breaks inside a box:
<div class="instructions">Text here won't be split up</div>
*/
.instructions {
page-break-inside: avoid;
}
/*
This forces the instructions box to sit right at the bottom of the page:
<div class="page">
<div class="push-instructions">
Content...
</div>
<div class="instructions">
Instructions...
</div>
</div>
Note that this only works if the height of the instructions box is known in
advance. I obtained the exact height by trial-and-error. I couldn't find a way
to stick it to the bottom of the page when the height is variable.
When the page content is too long the instructions box is automatically moved
to the top of the next page.
*/
.push-instructions {
min-height: 180mm;
}
@UziTech

This comment has been minimized.

Copy link

UziTech commented Aug 18, 2015

Thank you. This has saved me so much time

@sidoudou

This comment has been minimized.

Copy link

sidoudou commented Oct 10, 2017

Thanks a lot. You're a life saver!
Strange how wkhtmltopdf never bothered proposing a class like that or at least explain how to do!
I have slightly changed the first couple of lines in order to call a simple php script generating my html using file_get_contents instead of Smarty and it works like a charm.
Thanks again

@magna25

This comment has been minimized.

Copy link

magna25 commented Jul 10, 2018

Wow! I have literally tried every library out there and they are overly complex for such a small task..Thanks

@briedis

This comment has been minimized.

Copy link

briedis commented Aug 27, 2018

If you need to download PDF from URL, you can use this snippet:

function generate_pdf(string $url)
    {
        $descriptors = [
            1 => ['pipe', 'w'], // stdout
            2 => ['pipe', 'w'], // stderr
        ];

        // Set to true, if you encounter "QXcbConnection: Could not connect to display"
        $useXvfb = true;

        $process = proc_open(($useXvfb ? ' xvfb-run ' : '') . 'wkhtmltopdf -q ' . escapeshellarg($url) . ' -', $descriptors, $pipes);

        $pdf = stream_get_contents($pipes[1]);
        $errors = stream_get_contents($pipes[2]);

        // Close the process
        fclose($pipes[1]);
        proc_close($process);

        if ($errors) {
            throw new \Exception('PDF generation error: ' . $errors);
        }

        return $pdf;
    }
@Macdom

This comment has been minimized.

Copy link

Macdom commented Jul 31, 2019

okay, but how to use it? Simple generate_pdf("input.html") returns:
Call to a member function assign() on a non-object in C:\xampp\htdocs\pdf\5\new\generate_pdf.php on line 7
I have no idea what arguments is this function supposed to accept.

Trying to pass it like this: $data = smarty_function_data_url("input.html"); outputs even more errors. I clearly don't know how to use your tool. I'd be thankful if you provided an example.

@kelvinthiongo

This comment has been minimized.

Copy link

kelvinthiongo commented Nov 6, 2019

Thanks very much, you saved my ass!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.