Skip to content

Instantly share code, notes, and snippets.

@kicken
Created August 14, 2022 02:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kicken/381afdd9aaccd436c3d93c543d54815b to your computer and use it in GitHub Desktop.
Save kicken/381afdd9aaccd436c3d93c543d54815b to your computer and use it in GitHub Desktop.
HTML to PDF Conversion using NodeJS and Puppeteer.
const fs = require('fs/promises');
const puppeteer = require('puppeteer');
const args = process.argv.slice(2);
if (args.length < 2){
console.log('Usage: html2pdf source options_json');
process.exit(20);
}
const source = args[0];
const options = args.length === 2?JSON.parse(args[1]):{};
fs.readFile(source, {encoding: 'UTF-8'}).then(async function fileReadSuccess(sourceHTML){
const browser = await puppeteer.launch({
defaultViewport: {
width: 1920,
height: 1080
}
});
const page = await browser.newPage();
try {
await page.setContent(sourceHTML, {
timeout: 10000,
waitUntil: 'networkidle0'
}
);
await page.pdf(options);
console.log('PDF Successfully written to ' + options.path);
process.exitCode = 0;
} catch (err){
console.log('PDF Generation failed with error');
console.log(err);
process.exitCode = 30;
} finally {
await browser.close();
}
});
<?php
class HTML2PDF {
private string $node;
private string $script;
private string $baseHref;
private bool $enableBackgrounds = false;
private string $orientation = self::PORTRAIT;
private string $format = self::LETTER;
private string $header = '';
private string $footer = '';
private array $pageMargins = [
'top' => '0.125in'
, 'bottom' => '0.125in'
, 'left' => '0.25in'
, 'right' => '0.25in'
];
const PORTRAIT = 'portrait';
const LANDSCAPE = 'landscape';
const LETTER = 'Letter';
const A4 = 'A4';
public function __construct(string $node = 'node', string $script = 'main.js', string $baseHref = null){
$this->node = $node;
$this->script = $script;
if ($baseHref === null){
$baseHref = sprintf('%s://%s/'
, ($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https' : 'http'
, $_SERVER['HTTP_HOST'] ?? 'localhost'
);
}
$this->baseHref = $baseHref;
}
public function setHeaderHTML($html) : self{
$this->header = $html;
return $this;
}
public function setFooterHTML($html) : self{
$this->footer = $html;
return $this;
}
/**
* Set page margins using CSS style dimensions, including units. Ex: 0.25in
*
* @param ?string $top
* @param ?string $right
* @param ?string $bottom
* @param ?string $left
*
* @return void
*/
public function setPageMargins(?string $top = null, ?string $right = null, ?string $bottom = null, ?string $left = null) : self{
$this->pageMargins = [
'top' => $top ?? $this->pageMargins['top']
, 'bottom' => $bottom ?? $this->pageMargins['bottom']
, 'left' => $left ?? $this->pageMargins['left']
, 'right' => $right ?? $this->pageMargins['right']
];
return $this;
}
public function setOrientation(string $orientation) : self{
$this->orientation = $orientation;
return $this;
}
public function setFormat(string $format) : self{
$this->format = $format;
return $this;
}
public function setEnableBackgrounds(bool $value) : self{
$this->enableBackgrounds = $value;
return $this;
}
/**
* Generate the PDF and return it as a binary string.
*
* @param string $html
*
* @return string
* @throws \RuntimeException
*/
public function generatePdf(string $html) : string{
$source = $destination = $destinationPdf = $pdf = null;
$html = $this->fixupHtml($html);
try {
[$source, $destination, $destinationPdf] = $this->createTemporaryFiles();
file_put_contents($source, $html);
$cmd = $this->createCommandLine($source, $destinationPdf);
exec($cmd, $output, $ret);
if ($ret !== 0){
throw new \RuntimeException('Failed to generate PDF with command [' . $cmd . '] Output: ' . implode(PHP_EOL, $output));
}
$pdf = file_get_contents($destinationPdf);
} finally {
$this->cleanupTemporaryFiles($source, $destination, $destinationPdf);
}
return $pdf;
}
/**
* Generate the PDF and output it to the browser.
*
* @param string $html HTML to convert
* @param string $name Filename to use for the PDF
* @param string $mode Disposition mode (attachment or inline)
*
* @throws Exception
*/
public function outputPdf(string $html, string $name = 'page.pdf', string $mode = 'inline') : void{
$type = 'application/pdf';
$content = $this->generatePdf($html);
header('Content-type: ' . $type);
header('Content-length: ' . strlen($content));
header('Cache-control: private max-age=30 must-revalidate');
header(sprintf('Content-disposition: %s; filename="%s"', $mode, $name));
echo $content;
}
public function saveTo(string $html, string $file) : bool{
return file_put_contents($file, $this->generatePdf($html)) !== false;
}
private function createTemporaryFiles() : array{
$source = tempnam(sys_get_temp_dir(), 'htmlpdf');
$destination = tempnam(sys_get_temp_dir(), 'htmlpdf');
if ($source === false || $destination === false){
$this->cleanupTemporaryFiles($source, $destination);
throw new \RuntimeException('Could not generate temporary files.');
}
$destinationPdf = $destination . '.pdf';
$fp = fopen($destinationPdf, 'x+');
if (!$fp){
$this->cleanupTemporaryFiles($source, $destination, $destinationPdf);
throw new \RuntimeException('Could not generate destination pdf file.');
}
return [$source, $destination, $destinationPdf];
}
private function createCommandLine(string $source, string $destination) : string{
$options = json_encode([
'landscape' => $this->orientation === 'landscape'
, 'path' => $destination
, 'printBackground' => $this->enableBackgrounds
, 'format' => $this->format
, 'headerTemplate' => $this->header
, 'footerTemplate' => $this->footer
, 'displayHeaderFooter' => ($this->header || $this->footer)
, 'margin' => $this->pageMargins
]);
$cmd = sprintf("\"%s\" %s %s %s 2>&1"
, $this->node
, $this->escape($this->script)
, $this->escape($source)
, $this->escape($options)
);
return $cmd;
}
private function cleanupTemporaryFiles(string $source, string $destination, ?string $destinationPdf = null) : void{
$source && unlink($source);
$destination && unlink($destination);
$destinationPdf && unlink($destinationPdf);
}
private function escape($value) : string{
if (PHP_OS === 'WINNT'){
$value = str_replace(['"'], ['""'], $value);
$value = '"' . $value . '"';
} else {
$value = escapeshellarg($value);
}
return $value;
}
private function fixupHtml(string $html) : string{
libxml_use_internal_errors(true);
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->loadHTML($html);
libxml_use_internal_errors(false);
$this->setupBaseHref($dom);
$this->setupClasses($dom);
return $dom->saveHTML();
}
private function setupBaseHref(DOMDocument $dom) : void{
$baseList = $dom->getElementsByTagName('base');
$hasBaseHref = false;
for ($idx = 0; $idx < $baseList->length; $idx++){
/** @var DOMElement $base */
$base = $baseList->item($idx);
if ($base->hasAttribute('href')){
$hasBaseHref = true;
}
}
if (!$hasBaseHref){
$base = $dom->createElement('base');
$base->setAttribute('href', $this->baseHref);
$head = $dom->getElementsByTagName('head')->item(0);
$head->insertBefore($base, $head->firstChild);
}
}
private function setupClasses(DOMDocument $dom) : void{
$html = $dom->documentElement;
$classList = preg_split('/\s+/', $html->getAttribute('class') ?? '');
$classList[] = 'o-' . strtolower($this->orientation);
$html->setAttribute('class', implode(' ', $classList));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment