Created
August 14, 2022 02:20
-
-
Save kicken/381afdd9aaccd436c3d93c543d54815b to your computer and use it in GitHub Desktop.
HTML to PDF Conversion using NodeJS and Puppeteer.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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