From d65031ab4aeb7d5efafa572db4379bc286e4088f Mon Sep 17 00:00:00 2001 From: PiRifle Date: Tue, 10 Jun 2025 23:46:07 +0200 Subject: [PATCH 1/2] added google chrome renderer --- classes/ChromePdfGenerator.php | 98 ++++++++++++++++++++++++++++++++++ classes/PdfGenerator.php | 46 ++++++++++++---- lang/en/lang.php | 3 ++ models/Settings.php | 12 +++++ models/settings/fields.yaml | 13 ++++- updates/version.yaml | 2 + 6 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 classes/ChromePdfGenerator.php diff --git a/classes/ChromePdfGenerator.php b/classes/ChromePdfGenerator.php new file mode 100644 index 0000000..b407fba --- /dev/null +++ b/classes/ChromePdfGenerator.php @@ -0,0 +1,98 @@ +executable = $executable; + $this->prepareBaseTemp($storagePath); + $this->prepareCatalog(); + } + + public function prepareBaseTemp($storagePath){ + $this->baseTemp = storage_path($storagePath); + if (!is_dir($this->baseTemp)) { + mkdir($this->baseTemp, 0755, true); + } + } + + public 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); + } + + /** + * 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, $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; + } +} diff --git a/classes/PdfGenerator.php b/classes/PdfGenerator.php index 65c7ffd..5ccf266 100644 --- a/classes/PdfGenerator.php +++ b/classes/PdfGenerator.php @@ -20,6 +20,12 @@ class PdfGenerator */ public $snappyPdf; + /** + * Google Chrome PDF class + * @var ChromePdfGenerator + */ + public $googleChrome; + /** * Twig layout of PDF path * @var string @@ -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; @@ -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]); } diff --git a/lang/en/lang.php b/lang/en/lang.php index ade94e2..3153681 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -20,6 +20,9 @@ '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', ], 'permissions' => [ 'pdfgenerator_tab' => 'PDF Generator', diff --git a/models/Settings.php b/models/Settings.php index 90377d4..71fcbf6 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -36,4 +36,16 @@ public function getOptionsKeyValue(): array return $parsed; } + + + public function filterFields($fields, $context = null) + { + $engine = post('pdf_engine', $this->pdf_engine); + + if ($engine === 'snappy') { + $fields->pdf_binary->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_binary_comment'; + } else if ($engine === 'chrome') { + $fields->pdf_binary->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_binary_comment_chrome'; + } + } } diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index e90e4a8..7bd9d3d 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -20,7 +20,7 @@ tabs: 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 @@ -56,3 +56,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) diff --git a/updates/version.yaml b/updates/version.yaml index 139bc6d..d94435f 100644 --- a/updates/version.yaml +++ b/updates/version.yaml @@ -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' From d33629421525fff21ad8c52deacce6a2570257a2 Mon Sep 17 00:00:00 2001 From: PiRifle Date: Wed, 11 Jun 2025 00:13:02 +0200 Subject: [PATCH 2/2] fixed overlooked custom parameters section, better class safety --- classes/ChromePdfGenerator.php | 28 ++++++++++++++++++++++------ lang/en/lang.php | 1 + models/Settings.php | 15 ++++++++++----- models/settings/fields.yaml | 1 + 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/classes/ChromePdfGenerator.php b/classes/ChromePdfGenerator.php index b407fba..4e85a47 100644 --- a/classes/ChromePdfGenerator.php +++ b/classes/ChromePdfGenerator.php @@ -8,13 +8,13 @@ class ChromePdfGenerator { - public string $executable; - public array $options = ['--no-sandbox', + protected string $executable; + protected array $options = ['--no-sandbox', '--headless=new', '--disable-gpu', '--run-all-compositor-stages-before-draw', '--virtual-time-budget=5000']; - public string $baseTemp; + protected string $baseTemp; public function __construct($storagePath = "app/tmp", $executable = "google-chrome-stable"){ // Prepare base temp directory @@ -23,14 +23,14 @@ public function __construct($storagePath = "app/tmp", $executable = "google-chro $this->prepareCatalog(); } - public function prepareBaseTemp($storagePath){ + protected function prepareBaseTemp($storagePath){ $this->baseTemp = storage_path($storagePath); if (!is_dir($this->baseTemp)) { mkdir($this->baseTemp, 0755, true); } } - public function prepareCatalog(){ + protected function prepareCatalog(){ // Create isolated user-data-dir for Chrome $userDataDir = $this->baseTemp . DIRECTORY_SEPARATOR . 'chrome_user_data'; if (!is_dir($userDataDir)) { @@ -39,6 +39,22 @@ public function prepareCatalog(){ $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. @@ -64,7 +80,7 @@ public function generate(string $html, string $filename, array $options = []): s $this->options[] = "--print-to-pdf=" . escapeshellarg($outputPath); // Build full command and merge stderr/stdout - $cmd = array_merge([$this->executable], $this->options, $options, [escapeshellarg($htmlPath)]); + $cmd = array_merge([$this->executable], $this->options, $this->buildCliParams($options), [escapeshellarg($htmlPath)]); $cmdString = implode(' ', $cmd) . ' 2>&1'; $process = Process::fromShellCommandline($cmdString); $process->setTimeout(null); diff --git a/lang/en/lang.php b/lang/en/lang.php index 3153681..c0b0828 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -23,6 +23,7 @@ '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', diff --git a/models/Settings.php b/models/Settings.php index 71fcbf6..2c531b2 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -42,10 +42,15 @@ public function filterFields($fields, $context = null) { $engine = post('pdf_engine', $this->pdf_engine); - if ($engine === 'snappy') { - $fields->pdf_binary->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_binary_comment'; - } else if ($engine === 'chrome') { - $fields->pdf_binary->commentAbove = 'initbiz.pdfgenerator::lang.settings.pdf_binary_comment_chrome'; - } + // 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){} } } diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index 7bd9d3d..1b8d715 100644 --- a/models/settings/fields.yaml +++ b/models/settings/fields.yaml @@ -13,6 +13,7 @@ tabs: value: type: text span: auto + dependsOn: [pdf_engine] pdf_tokenize: label: initbiz.pdfgenerator::lang.settings.pdf_tokenize_label