Skip to content

Instantly share code, notes, and snippets.

@Kr3m
Created May 14, 2019 19:16
Show Gist options
  • Save Kr3m/fdb60a65b826a3bb2025369f18294d65 to your computer and use it in GitHub Desktop.
Save Kr3m/fdb60a65b826a3bb2025369f18294d65 to your computer and use it in GitHub Desktop.
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment