diff --git a/classes/ChromePdfGenerator.php b/classes/ChromePdfGenerator.php new file mode 100644 index 0000000..4e85a47 --- /dev/null +++ b/classes/ChromePdfGenerator.php @@ -0,0 +1,114 @@ +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; + } +} 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..c0b0828 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -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', diff --git a/models/Settings.php b/models/Settings.php index 90377d4..2c531b2 100644 --- a/models/Settings.php +++ b/models/Settings.php @@ -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){} + } } diff --git a/models/settings/fields.yaml b/models/settings/fields.yaml index e90e4a8..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 @@ -20,7 +21,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 +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) 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'