Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions classes/ChromePdfGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace Initbiz\PDFGenerator\Classes;


use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

class ChromePdfGenerator
{
protected string $executable;
protected array $options = ['--no-sandbox',
'--headless=new',
'--disable-gpu',
'--run-all-compositor-stages-before-draw',
'--virtual-time-budget=5000'];
protected string $baseTemp;

public function __construct($storagePath = "app/tmp", $executable = "google-chrome-stable"){
// Prepare base temp directory
$this->executable = $executable;
$this->prepareBaseTemp($storagePath);
$this->prepareCatalog();
}

protected function prepareBaseTemp($storagePath){
$this->baseTemp = storage_path($storagePath);
if (!is_dir($this->baseTemp)) {
mkdir($this->baseTemp, 0755, true);
}
}

protected function prepareCatalog(){
// Create isolated user-data-dir for Chrome
$userDataDir = $this->baseTemp . DIRECTORY_SEPARATOR . 'chrome_user_data';
if (!is_dir($userDataDir)) {
mkdir($userDataDir, 0700, true);
}
$this->options[] = '--user-data-dir=' . escapeshellarg($userDataDir);
}

protected function buildCliParams(array $options): array {
$params = [];

foreach ($options as $key => $value) {
if (is_bool($value)) {
if ($value) {
$params[] = "--$key";
}
} else {
$params[] = "--$key=" . escapeshellarg($value);
}
}

return $params;
}

/**
* Generate a PDF from raw HTML using headless Chrome,
* ensuring a writable user-data directory and full completion.
*
* @param string $html The raw HTML content to convert.
* @param string $filename Desired PDF filename (with or without .pdf extension).
* @param array $options Optional Chrome CLI flags (without URL or --print-to-pdf).
* @return string Full path to the generated PDF file in the temp folder.
* @throws \RuntimeException If PDF generation fails or output is missing.
*/
public function generate(string $html, string $filename, array $options = []): string
{
// Create temporary HTML file
$htmlPath = $this->baseTemp . DIRECTORY_SEPARATOR . 'chrome_html_' . uniqid() . '.html';
file_put_contents($htmlPath, $html);

// Normalize PDF filename and ensure storage dir exists
$filename = pathinfo($filename, PATHINFO_EXTENSION) === 'pdf' ? $filename : $filename . '.pdf';
$outputPath = $filename;

// Default Chrome flags

$this->options[] = "--print-to-pdf=" . escapeshellarg($outputPath);

// Build full command and merge stderr/stdout
$cmd = array_merge([$this->executable], $this->options, $this->buildCliParams($options), [escapeshellarg($htmlPath)]);
$cmdString = implode(' ', $cmd) . ' 2>&1';
$process = Process::fromShellCommandline($cmdString);
$process->setTimeout(null);

// Ensure Chrome runs under a writable HOME
$process->setEnv(['HOME' => $this->baseTemp]);

try {
$process->mustRun();
} catch (ProcessFailedException $e) {
@unlink($htmlPath);
$output = trim($process->getErrorOutput() ?: $process->getOutput());
throw new \RuntimeException('PDF generation failed: ' . $e->getMessage() . '\nChrome output: ' . $output);
}

@unlink($htmlPath);

// Wait for any remaining Chrome child processes
if (!$process->isTerminated()) {
$process->wait();
}

// Verify output file
if (!file_exists($outputPath)) {
$msg = trim($process->getOutput() . ' ' . $process->getErrorOutput());
throw new \RuntimeException("PDF not found at {$outputPath}. Output: {$msg}");
}

return $outputPath;
}
}
46 changes: 35 additions & 11 deletions classes/PdfGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class PdfGenerator
*/
public $snappyPdf;

/**
* Google Chrome PDF class
* @var ChromePdfGenerator
*/
public $googleChrome;

/**
* Twig layout of PDF path
* @var string
Expand Down Expand Up @@ -63,21 +69,33 @@ class PdfGenerator
*/
public function __construct($filename = 'generated', $layout = null)
{
$this->snappyPdf = new Pdf();
// let snappy be default
$engine = Settings::get("pdf_engine", "snappy");

if ($engine=="snappy"){
$this->snappyPdf = new Pdf();

//By default the most resonable one is set
$binaryPath = Settings::get('pdf_binary', plugins_path('initbiz/pdfgenerator/vendor/bin/wkhtmltopdf-amd64'));
$binaryPath = ($binaryPath === "") ? plugins_path('initbiz/pdfgenerator/vendor/bin/wkhtmltopdf-amd64') : $binaryPath;

//By default the most resonable one is set
$binaryPath = Settings::get('pdf_binary', plugins_path('initbiz/pdfgenerator/vendor/bin/wkhtmltopdf-amd64'));
$binaryPath = ($binaryPath === "") ? plugins_path('initbiz/pdfgenerator/vendor/bin/wkhtmltopdf-amd64') : $binaryPath;
$pathAlias = substr($binaryPath, 0, 1);

$pathAlias = substr($binaryPath, 0, 1);
if ($pathAlias === '$') {
$binaryPath = plugins_path(substr($binaryPath, 1));
} elseif ($pathAlias === '~') {
$binaryPath = base_path(substr($binaryPath, 1));
}

if ($pathAlias === '$') {
$binaryPath = plugins_path(substr($binaryPath, 1));
} elseif ($pathAlias === '~') {
$binaryPath = base_path(substr($binaryPath, 1));
$this->snappyPdf->setBinary($binaryPath);
}

if($engine=="chrome"){
// reuse the binary settings entry, google-chrome-stable is the default binary on $PATH on debian.
$binary = Settings::get('pdf_binary', "google-chrome-stable");
$this->googleChrome = new ChromePdfGenerator(executable: $binary);
}

$this->snappyPdf->setBinary($binaryPath);

$this->filename = $filename;

Expand Down Expand Up @@ -127,7 +145,13 @@ public function generateFromTwig($layout, $localFileName, array $data = [])
{
$html = Twig::parse(File::get($layout), $data);
$settings = Settings::instance();
$this->snappyPdf->generateFromHtml($html, $localFileName, $settings->getOptionsKeyValue());

// I am so sorry for this method of handling things :C
if($this->snappyPdf)
$this->snappyPdf->generateFromHtml($html, $localFileName, $settings->getOptionsKeyValue());

if($this->googleChrome)
$this->googleChrome->generate($html, $localFileName, $settings->getOptionsKeyValue());

Event::fire('initbiz.pdfgenerator.afterGeneratePdf', [$this]);
}
Expand Down
4 changes: 4 additions & 0 deletions lang/en/lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
'pdf_rm_older_than_comment' => 'In seconds time to store PDFs. By default 172800 - two days',
'pdf_binary_label' => 'Binary path',
'pdf_binary_comment' => 'Path to wkhtmltopdf, you can start the path with \~ (root path) or \$ (plugins path)',
'pdf_engine_label' => 'Engine Used',
'pdf_engine_comment' => 'Select the engine you want to use, Snappypdf is the reliable and secure pick. Google chrome is experimental, allows rendering modern js and css!',
'pdf_binary_comment_chrome' => 'Path of your google-chrome binary',
'pdf_generator_options_comment_chrome' => 'key-value pair for google chrome cli options',
],
'permissions' => [
'pdfgenerator_tab' => 'PDF Generator',
Expand Down
17 changes: 17 additions & 0 deletions models/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,21 @@ public function getOptionsKeyValue(): array

return $parsed;
}


public function filterFields($fields, $context = null)
{
$engine = post('pdf_engine', $this->pdf_engine);

// let it fail
try {
if ($engine === 'snappy') {
$fields->pdf_generator_options->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_generator_options_comment';
$fields->pdf_binary->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_binary_comment';
} else if ($engine === 'chrome') {
$fields->pdf_generator_options->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_generator_options_comment_chrome';
$fields->pdf_binary->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_binary_comment_chrome';
}
}catch (\Exception $ex){}
}
}
14 changes: 13 additions & 1 deletion models/settings/fields.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ tabs:
value:
type: text
span: auto
dependsOn: [pdf_engine]

pdf_tokenize:
label: initbiz.pdfgenerator::lang.settings.pdf_tokenize_label
tab: initbiz.pdfgenerator::lang.settings.download_tab
commentAbove: initbiz.pdfgenerator::lang.settings.pdf_tokenize_comment
type: switch
default: true

pdf_dir:
label: initbiz.pdfgenerator::lang.settings.pdf_dir_label
tab: initbiz.pdfgenerator::lang.settings.download_tab
Expand Down Expand Up @@ -56,3 +57,14 @@ tabs:
label: initbiz.pdfgenerator::lang.settings.pdf_binary_label
tab: initbiz.pdfgenerator::lang.settings.download_tab
commentAbove: initbiz.pdfgenerator::lang.settings.pdf_binary_comment
dependsOn: [pdf_engine]

pdf_engine:
label: initbiz.pdfgenerator::lang.settings.pdf_engine_label
tab: initbiz.pdfgenerator::lang.settings.generator_tab
commentAbove: initbiz.pdfgenerator::lang.settings.pdf_engine_comment
type: dropdown
default: snappy
options:
snappy: SnappyPdf
chrome: Google Chrome (EXPERIMENTAL)
2 changes: 2 additions & 0 deletions updates/version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
- 'downloadPdfDirectly to get PDFs using data-request-download without redirect and generator options in settings'
1.2.0:
- 'Allow generator options without value'
1.3.0:
- 'Add support for google chrome rendering engine'