From 95eee0685a6e92c51336c6a3b761196ea0c6e75a Mon Sep 17 00:00:00 2001 From: Massimo Infunti Date: Sun, 8 Mar 2026 15:53:12 +0100 Subject: [PATCH 1/6] adding weasyprint support --- README.md | 4 +- src/Pdf/Engine/WeasyprintEngine.php | 223 ++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/Pdf/Engine/WeasyprintEngine.php diff --git a/README.md b/README.md index ccd08c7..4c66674 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Engines included in the plugin: * Mpdf (^8.0.4) * Tcpdf (^6.3) * WkHtmlToPdf **RECOMMENDED ENGINE** +* WeasyPrint (** best: fast and high fidelity** if you have the privileges to install something on your server - [https://doc.courtbouillon.org/weasyprint/stable/first_steps.html]) Community maintained engines: * [PDFreactor](https://github.com/jmischer/cake-pdfreactor) @@ -18,7 +19,7 @@ Community maintained engines: ## Requirements -* One of the following render engines: DomPdf, Mpdf, Tcpdf or wkhtmltopdf +* One of the following render engines: DomPdf, Mpdf, Tcpdf, wkhtmltopdf or WeasyPrint * pdftk (optional) See: http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/ @@ -78,6 +79,7 @@ Configuration options: * cwd: current working directory (Only for wkhtmltopdf) * options: Engine specific options. Currently used for following engine: * `WkHtmlToPdfEngine`: The options are passed as CLI arguments + * `WeasyprintEngine`: The options are passed as CLI arguments * `TexToPdfEngine`: The options are passed as CLI arguments * `DomPdfEngine`: The options are passed to constructor of `Dompdf` class * `MpdfEngine`: The options are passed to constructor of `Mpdf` class diff --git a/src/Pdf/Engine/WeasyprintEngine.php b/src/Pdf/Engine/WeasyprintEngine.php new file mode 100644 index 0000000..08d2992 --- /dev/null +++ b/src/Pdf/Engine/WeasyprintEngine.php @@ -0,0 +1,223 @@ +_windowsEnvironment = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + + if ($this->_windowsEnvironment) { + $this->_binary = 'C:/Progra~1/WeasyPrint/bin/weasyprint.exe'; + } + } + + /** + * Generates Pdf from html + * + * @throws \Cake\Core\Exception\CakeException + * @return string Raw PDF data + * @throws \Exception If no output is generated to stdout by weasyprint. + */ + public function output(): string + { + $command = $this->_getCommand(); + $content = $this->_exec($command, $this->_Pdf->html()); + + if (!empty($content['stdout'])) { + return $content['stdout']; + } + + if (!empty($content['stderr'])) { + throw new CakeException(sprintf( + 'System error "%s" when executing command "%s". ' . + 'Try using the binary/package provided on http://weasyprint.org/downloads.html', + $content['stderr'], + $command + )); + } + + throw new CakeException("weasyprint didn't return any data"); + } + + /** + * Execute the weasyprint commands for rendering pdfs + * + * @param string $cmd the command to execute + * @param string $input Html to pass to weasyprint + * @return array the result of running the command to generate the pdf + */ + protected function _exec(string $cmd, string $input): array + { + $result = ['stdout' => '', 'stderr' => '', 'return' => '']; + + $cwd = $this->getConfig('cwd'); + + $proc = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd); + if ($proc === false) { + throw new CakeException('Unable to execute weasyprint, proc_open() failed'); + } + + fwrite($pipes[0], $input); + fclose($pipes[0]); + + $result['stdout'] = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $result['stderr'] = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + $result['return'] = proc_close($proc); + + return $result; + } + + /** + * Get the command to render a pdf + * + * @return string the command for generating the pdf + * @throws \Cake\Core\Exception\CakeException + */ + protected function _getCommand(): string + { + $binary = $this->getBinaryPath(); + + $options = [ + 'dpi' => 96, + + ]; + + $margin = $this->_Pdf->margin(); + foreach ($margin as $key => $value) { + if ($value !== null) { + $options['margin-' . $key] = $value . 'mm'; + } + } + $options = array_merge($options, (array)$this->getConfig('options')); + + if ($this->_windowsEnvironment) { + $command = '"' . $binary . '"'; + } else { + $command = $binary; + } + + foreach ($options as $key => $value) { + if (!$value) { + continue; + } + $command .= $this->parseOptions($key, $value); + } + /** @var array $footer */ + $footer = $this->_Pdf->footer(); + foreach ($footer as $location => $text) { + if ($text !== null) { + $command .= " --footer-$location \"" . addslashes($text) . '"'; + } + } + + /** @var array $header */ + $header = $this->_Pdf->header(); + foreach ($header as $location => $text) { + if ($text !== null) { + $command .= " --header-$location \"" . addslashes($text) . '"'; + } + } + $command .= ' - -'; + + return $command; + } + + /** + * Parses a value of options to create a part of the command. + * Created to reuse logic to parse the cover and toc options. + * + * @param string $key the option key name + * @param array|string|float|true $value the option value + * @return string part of the command + */ + protected function parseOptions(string $key, string|bool|array|float $value): string + { + $command = ''; + if (is_array($value)) { + if ($key === 'toc') { + $command .= ' toc'; + foreach ($value as $k => $v) { + $command .= $this->parseOptions($k, $v); + } + } elseif ($key === 'cover') { + if (!isset($value['url'])) { + throw new CakeException('The url for the cover is missing. Use the "url" index.'); + } + $command .= ' cover ' . escapeshellarg((string)$value['url']); + unset($value['url']); + foreach ($value as $k => $v) { + $command .= $this->parseOptions($k, $v); + } + } else { + foreach ($value as $k => $v) { + $command .= sprintf(' --%s %s %s', $key, escapeshellarg($k), escapeshellarg((string)$v)); + } + } + } elseif ($value === true) { + if ($key === 'toc') { + $command .= ' toc'; + } else { + $command .= ' --' . $key; + } + } else { + if ($key === 'cover') { + $command .= ' cover ' . escapeshellarg((string)$value); + } else { + $command .= sprintf(' --%s %s', $key, escapeshellarg((string)$value)); + } + } + + return $command; + } + + /** + * Get path to weasyprint binary. + * + * @return string + */ + public function getBinaryPath(): string + { + $binary = $this->getConfig('binary', $this->_binary); + + /** @psalm-suppress ForbiddenCode */ + if ( + is_executable($binary) || + (!$this->_windowsEnvironment && shell_exec('which ' . escapeshellarg($binary))) + ) { + return $binary; + } + + throw new CakeException(sprintf('weasyprint binary is not found or not executable: %s', $binary)); + } +} From 8defa30bc2762c607b1a2f18d71b5ded6532c5dd Mon Sep 17 00:00:00 2001 From: Massimo Infunti Date: Sun, 8 Mar 2026 15:54:28 +0100 Subject: [PATCH 2/6] improvements of readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c66674..3215265 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Engines included in the plugin: * Mpdf (^8.0.4) * Tcpdf (^6.3) * WkHtmlToPdf **RECOMMENDED ENGINE** -* WeasyPrint (** best: fast and high fidelity** if you have the privileges to install something on your server - [https://doc.courtbouillon.org/weasyprint/stable/first_steps.html]) +* WeasyPrint (**best: fast and high fidelity** if you have the privileges to install something on your server - [https://doc.courtbouillon.org/weasyprint/stable/first_steps.html]) Community maintained engines: * [PDFreactor](https://github.com/jmischer/cake-pdfreactor) From 9b96a215b31edb6cfa33ba7f740d144258d4d033 Mon Sep 17 00:00:00 2001 From: Massimo Infunti Date: Sun, 8 Mar 2026 16:59:59 +0100 Subject: [PATCH 3/6] aggiunta test --- .../Pdf/Engine/WeasyprintEngineTest.php | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php diff --git a/tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php b/tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php new file mode 100644 index 0000000..f34833c --- /dev/null +++ b/tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php @@ -0,0 +1,204 @@ +markTestSkipped('weasyprint not found'); + } + + $class = new ReflectionClass(WeasyprintEngine::class); + $method = $class->getMethod('_getCommand'); + $method->setAccessible(true); + + // Default options only + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' - -"; + $this->assertEquals($expected, $result); + + // A falsy option (false) must be skipped; a string option must be included + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'options' => [ + 'quiet' => false, + 'encoding' => 'UTF-8', + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' --encoding 'UTF-8' - -"; + $this->assertEquals($expected, $result); + + // With all margins set to 0 + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + ], + 'margin' => [ + 'bottom' => 0, + 'left' => 0, + 'right' => 0, + 'top' => 0, + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' --margin-bottom '0mm' --margin-left '0mm' --margin-right '0mm' --margin-top '0mm' - -"; + $this->assertEquals($expected, $result); + + // Various option types: boolean, string, integer, associative array + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'options' => [ + 'boolean' => true, + 'string' => 'value', + 'integer' => 42, + 'array' => [ + 'first' => 'firstValue', + 'second' => 'secondValue', + ], + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' --boolean --string 'value' --integer '42' --array 'first' 'firstValue' --array 'second' 'secondValue' - -"; + $this->assertEquals($expected, $result); + + // With footer and header + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + ], + ]); + $Pdf->footer('Footer left'); + $Pdf->header(null, 'Page {page}'); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' --footer-left \"Footer left\" --header-center \"Page {page}\" - -"; + $this->assertEquals($expected, $result); + + // With cover (string) and toc (boolean true) + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'options' => [ + 'cover' => 'cover.html', + 'toc' => true, + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' cover 'cover.html' toc - -"; + $this->assertEquals($expected, $result); + + // With cover (array) and toc (array of options) + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'options' => [ + 'cover' => [ + 'url' => 'cover.html', + 'enable-smart-shrinking' => true, + 'zoom' => 10, + ], + 'toc' => [ + 'zoom' => 5, + 'encoding' => 'ISO-8859-1', + ], + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' cover 'cover.html' --enable-smart-shrinking --zoom '10' toc --zoom '5' --encoding 'ISO-8859-1' - -"; + $this->assertEquals($expected, $result); + + // With global zoom override alongside cover (array) and toc (array) + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'options' => [ + 'zoom' => 4, + 'cover' => [ + 'url' => 'cover.html', + 'enable-smart-shrinking' => true, + 'zoom' => 10, + ], + 'toc' => [ + 'disable-dotted-lines' => true, + 'xsl-style-sheet' => 'style.xsl', + 'zoom' => 5, + 'encoding' => 'ISO-8859-1', + ], + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --dpi '96' --zoom '4' cover 'cover.html' --enable-smart-shrinking --zoom '10' toc --disable-dotted-lines --xsl-style-sheet 'style.xsl' --zoom '5' --encoding 'ISO-8859-1' - -"; + $this->assertEquals($expected, $result); + } + + public function testCoverUrlMissing() + { + if (!shell_exec('which weasyprint')) { + $this->markTestSkipped('weasyprint not found'); + } + + $this->expectException(CakeException::class); + $this->expectExceptionMessage('The url for the cover is missing. Use the "url" index.'); + + $class = new ReflectionClass(WeasyprintEngine::class); + $method = $class->getMethod('_getCommand'); + $method->setAccessible(true); + + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'options' => [ + 'cover' => [ + 'enable-smart-shrinking' => true, + 'zoom' => 10, + ], + ], + ], + ]); + $method->invokeArgs($Pdf->engine(), []); + } + + public function testGetBinaryPath() + { + $this->expectException(CakeException::class); + $this->expectExceptionMessage('weasyprint binary is not found or not executable: /foo/bar'); + + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.Weasyprint', + 'binary' => '/foo/bar', + ], + ]); + + /** @var \CakePdf\Pdf\Engine\WeasyprintEngine $engine */ + $engine = $Pdf->engine(); + $engine->getBinaryPath(); + } +} From 0c3dfd4ca23b800c92c7e9ef78446971e883ed40 Mon Sep 17 00:00:00 2001 From: ADmad Date: Sun, 15 Mar 2026 18:19:08 +0530 Subject: [PATCH 4/6] Fixes and cleaup for WeasyPrint engine --- .github/workflows/ci.yml | 2 +- README.md | 45 ++-- ...syprintEngine.php => WeasyPrintEngine.php} | 76 ++----- .../Pdf/Engine/WeasyPrintEngineTest.php | 102 +++++++++ .../Pdf/Engine/WeasyprintEngineTest.php | 204 ------------------ 5 files changed, 134 insertions(+), 295 deletions(-) rename src/Pdf/Engine/{WeasyprintEngine.php => WeasyPrintEngine.php} (60%) create mode 100644 tests/TestCase/Pdf/Engine/WeasyPrintEngineTest.php delete mode 100644 tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c429e0..a63c58e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Install packages uses: awalsh128/cache-apt-pkgs-action@v1 with: - packages: xfonts-base xfonts-75dpi wkhtmltopdf + packages: xfonts-base xfonts-75dpi wkhtmltopdf weasyprint version: ubuntu-24.04 - name: Composer install diff --git a/README.md b/README.md index 3215265..155c5cd 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,17 @@ Engines included in the plugin: * DomPdf (^3.0) * Mpdf (^8.0.4) * Tcpdf (^6.3) -* WkHtmlToPdf **RECOMMENDED ENGINE** -* WeasyPrint (**best: fast and high fidelity** if you have the privileges to install something on your server - [https://doc.courtbouillon.org/weasyprint/stable/first_steps.html]) +* WeasyPrint (**Recommended** if you have the privileges to install something on your server) +* WkHtmlToPdf (project no longer maintained but binaries still available for various environments) Community maintained engines: * [PDFreactor](https://github.com/jmischer/cake-pdfreactor) - ## Requirements -* One of the following render engines: DomPdf, Mpdf, Tcpdf, wkhtmltopdf or WeasyPrint +* One of the following render engines: DomPdf, Mpdf, Tcpdf, WeasyPrint or wkhtmltopdf * pdftk (optional) See: http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/ - ## Installation Using [Composer](http://getcomposer.org): @@ -34,7 +32,7 @@ composer require friendsofcake/cakepdf CakePdf does not include any of the supported PDF engines, you need to install the ones you intend to use yourself. -Packages for the recommend `wkhtmltopdf` engine can be downloaded from https://wkhtmltopdf.org/downloads.html. +Check [WeasyPrint's](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation) installation guide to install it on your system. DomPdf, Mpdf and Tcpdf can be installed via composer using one of the following commands: ``` @@ -75,23 +73,23 @@ pass the config array to constructor. The value for engine should have the Configuration options: * engine: Engine to be used (required), or an array of engine config options * className: Engine class to use - * binary: Binary file to use (Only for wkhtmltopdf) - * cwd: current working directory (Only for wkhtmltopdf) + * binary: Binary file to use (Only for weasyprint/wkhtmltopdf) + * cwd: current working directory (Only for weasyprint/wkhtmltopdf) * options: Engine specific options. Currently used for following engine: * `WkHtmlToPdfEngine`: The options are passed as CLI arguments - * `WeasyprintEngine`: The options are passed as CLI arguments + * `WeasyPrintEngine`: The options are passed as CLI arguments * `TexToPdfEngine`: The options are passed as CLI arguments * `DomPdfEngine`: The options are passed to constructor of `Dompdf` class * `MpdfEngine`: The options are passed to constructor of `Mpdf` class * crypto: Crypto engine to be used, or an array of crypto config options * className: Crypto class to use * binary: Binary file to use -* pageSize: Change the default size, defaults to A4 -* orientation: Change the default orientation, defaults to portrait -* margin: Array or margins with the keys: bottom, left, right, top and their values -* title: Title of the document -* delay: A delay in milliseconds to wait before rendering the pdf -* windowStatus: The required window status before rendering the pdf +* pageSize: Change the default size, defaults to A4 (Needs to be set via CSS for WeasyPrint) +* orientation: Change the default orientation, defaults to portrait (Needs to be set via CSS for WeasyPrint) +* margin: Array or margins with the keys: bottom, left, right, top and their values (Needs to be set via CSS for WeasyPrint) +* title: Title of the document (Needs to be set via CSS for WeasyPrint) +* delay: A delay in milliseconds to wait before rendering the pdf (wkhtmltopdf only) +* windowStatus: The required window status before rendering the pdf (wkhtmltopdf only) * encoding: Change the encoding, defaults to UTF-8 * download: Set to true to force a download, only when using PdfView * filename: Filename for the document when using forced download @@ -99,14 +97,7 @@ Configuration options: Example: ```php Configure::write('CakePdf', [ - 'engine' => 'CakePdf.WkHtmlToPdf', - 'margin' => [ - 'bottom' => 15, - 'left' => 50, - 'right' => 30, - 'top' => 45, - ], - 'orientation' => 'landscape', + 'engine' => 'CakePdf.WeasyPrint', 'download' => true, ]); ``` @@ -120,13 +111,13 @@ class InvoicesController extends AppController { parent::initialize(); - // https://book.cakephp.org/5/en/controllers.html#content-type-negotiation + // https://book.cakephp.org/5.x/controllers.html#content-type-negotiation $this->addViewClasses([PdfView::class]); } // In your Invoices controller you could set additional configs, // or override the global ones: - public function view($id = null) + public function view($id = null): void { $invoice = $this->Invoice->get($id); $this->viewBuilder()->setOption( @@ -147,11 +138,9 @@ options for the relevant class. For example: ```php Configure::write('CakePdf', [ 'engine' => [ - 'className' => 'CakePdf.WkHtmlToPdf', + 'className' => 'CakePdf.WeasyPrint', // Options usable depend on the engine used. 'options' => [ - 'print-media-type' => false, - 'outline' => true, 'dpi' => 96, 'cover' => [ 'url' => 'cover.html', diff --git a/src/Pdf/Engine/WeasyprintEngine.php b/src/Pdf/Engine/WeasyPrintEngine.php similarity index 60% rename from src/Pdf/Engine/WeasyprintEngine.php rename to src/Pdf/Engine/WeasyPrintEngine.php index 08d2992..6d63753 100644 --- a/src/Pdf/Engine/WeasyprintEngine.php +++ b/src/Pdf/Engine/WeasyPrintEngine.php @@ -6,7 +6,7 @@ use Cake\Core\Exception\CakeException; use CakePdf\Pdf\CakePdf; -class WeasyprintEngine extends AbstractPdfEngine +class WeasyPrintEngine extends AbstractPdfEngine { /** * Path to the weasyprint executable binary @@ -41,9 +41,8 @@ public function __construct(CakePdf $Pdf) /** * Generates Pdf from html * - * @throws \Cake\Core\Exception\CakeException * @return string Raw PDF data - * @throws \Exception If no output is generated to stdout by weasyprint. + * @throws \Cake\Core\Exception\CakeException If no output is generated to stdout by weasyprint. */ public function output(): string { @@ -56,10 +55,9 @@ public function output(): string if (!empty($content['stderr'])) { throw new CakeException(sprintf( - 'System error "%s" when executing command "%s". ' . - 'Try using the binary/package provided on http://weasyprint.org/downloads.html', + 'System error "%s" when executing command "%s".', $content['stderr'], - $command + $command, )); } @@ -71,11 +69,11 @@ public function output(): string * * @param string $cmd the command to execute * @param string $input Html to pass to weasyprint - * @return array the result of running the command to generate the pdf + * @return array{stdout: string, stderr: string, return: int} the result of running the command to generate the pdf */ protected function _exec(string $cmd, string $input): array { - $result = ['stdout' => '', 'stderr' => '', 'return' => '']; + $result = ['stdout' => '', 'stderr' => '', 'return' => 0]; $cwd = $this->getConfig('cwd'); @@ -109,16 +107,9 @@ protected function _getCommand(): string $binary = $this->getBinaryPath(); $options = [ - 'dpi' => 96, - + 'encoding' => $this->_Pdf->encoding(), ]; - $margin = $this->_Pdf->margin(); - foreach ($margin as $key => $value) { - if ($value !== null) { - $options['margin-' . $key] = $value . 'mm'; - } - } $options = array_merge($options, (array)$this->getConfig('options')); if ($this->_windowsEnvironment) { @@ -128,26 +119,12 @@ protected function _getCommand(): string } foreach ($options as $key => $value) { - if (!$value) { + if (!$value && $value !== 0 && $value !== '0') { continue; } $command .= $this->parseOptions($key, $value); } - /** @var array $footer */ - $footer = $this->_Pdf->footer(); - foreach ($footer as $location => $text) { - if ($text !== null) { - $command .= " --footer-$location \"" . addslashes($text) . '"'; - } - } - /** @var array $header */ - $header = $this->_Pdf->header(); - foreach ($header as $location => $text) { - if ($text !== null) { - $command .= " --header-$location \"" . addslashes($text) . '"'; - } - } $command .= ' - -'; return $command; @@ -155,47 +132,22 @@ protected function _getCommand(): string /** * Parses a value of options to create a part of the command. - * Created to reuse logic to parse the cover and toc options. * * @param string $key the option key name - * @param array|string|float|true $value the option value + * @param array|string|float|int|true $value the option value * @return string part of the command */ - protected function parseOptions(string $key, string|bool|array|float $value): string + protected function parseOptions(string $key, string|bool|array|float|int $value): string { $command = ''; if (is_array($value)) { - if ($key === 'toc') { - $command .= ' toc'; - foreach ($value as $k => $v) { - $command .= $this->parseOptions($k, $v); - } - } elseif ($key === 'cover') { - if (!isset($value['url'])) { - throw new CakeException('The url for the cover is missing. Use the "url" index.'); - } - $command .= ' cover ' . escapeshellarg((string)$value['url']); - unset($value['url']); - foreach ($value as $k => $v) { - $command .= $this->parseOptions($k, $v); - } - } else { - foreach ($value as $k => $v) { - $command .= sprintf(' --%s %s %s', $key, escapeshellarg($k), escapeshellarg((string)$v)); - } + foreach ($value as $v) { + $command .= sprintf(' --%s %s', $key, escapeshellarg((string)$v)); } } elseif ($value === true) { - if ($key === 'toc') { - $command .= ' toc'; - } else { - $command .= ' --' . $key; - } + $command .= ' --' . $key; } else { - if ($key === 'cover') { - $command .= ' cover ' . escapeshellarg((string)$value); - } else { - $command .= sprintf(' --%s %s', $key, escapeshellarg((string)$value)); - } + $command .= sprintf(' --%s %s', $key, escapeshellarg((string)$value)); } return $command; diff --git a/tests/TestCase/Pdf/Engine/WeasyPrintEngineTest.php b/tests/TestCase/Pdf/Engine/WeasyPrintEngineTest.php new file mode 100644 index 0000000..93f07f8 --- /dev/null +++ b/tests/TestCase/Pdf/Engine/WeasyPrintEngineTest.php @@ -0,0 +1,102 @@ +markTestSkipped('weasyprint not found'); + } + + $class = new ReflectionClass(WeasyPrintEngine::class); + $method = $class->getMethod('_getCommand'); + + // Default options only + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.WeasyPrint', + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --encoding 'UTF-8' - -"; + $this->assertEquals($expected, $result); + + // A falsy option (false) must be skipped; a string option must be included + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.WeasyPrint', + 'options' => [ + 'quiet' => false, + 'encoding' => 'UTF-8', + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --encoding 'UTF-8' - -"; + $this->assertEquals($expected, $result); + + // Various option types: boolean, string, integer, array + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.WeasyPrint', + 'options' => [ + 'boolean' => true, + 'string' => 'value', + 'integer' => 42, + 'array' => [ + 'firstValue', + 'secondValue', + ], + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --encoding 'UTF-8' --boolean --string 'value' --integer '42' --array 'firstValue' --array 'secondValue' - -"; + $this->assertEquals($expected, $result); + + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.WeasyPrint', + 'options' => [ + 'presentational-hints' => true, + 'pdf-variant' => 'pdf/ua-1', + ], + ], + ]); + $result = $method->invokeArgs($Pdf->engine(), []); + $expected = "weasyprint --encoding 'UTF-8' --presentational-hints --pdf-variant 'pdf/ua-1' - -"; + $this->assertEquals($expected, $result); + } + + public function testGetBinaryPath() + { + $this->expectException(CakeException::class); + $this->expectExceptionMessage('weasyprint binary is not found or not executable: /foo/bar'); + + $Pdf = new CakePdf([ + 'engine' => [ + 'className' => 'CakePdf.WeasyPrint', + 'binary' => '/foo/bar', + ], + ]); + + /** @var \CakePdf\Pdf\Engine\WeasyPrintEngine $engine */ + $engine = $Pdf->engine(); + $engine->getBinaryPath(); + } +} diff --git a/tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php b/tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php deleted file mode 100644 index f34833c..0000000 --- a/tests/TestCase/Pdf/Engine/WeasyprintEngineTest.php +++ /dev/null @@ -1,204 +0,0 @@ -markTestSkipped('weasyprint not found'); - } - - $class = new ReflectionClass(WeasyprintEngine::class); - $method = $class->getMethod('_getCommand'); - $method->setAccessible(true); - - // Default options only - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' - -"; - $this->assertEquals($expected, $result); - - // A falsy option (false) must be skipped; a string option must be included - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'options' => [ - 'quiet' => false, - 'encoding' => 'UTF-8', - ], - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' --encoding 'UTF-8' - -"; - $this->assertEquals($expected, $result); - - // With all margins set to 0 - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - ], - 'margin' => [ - 'bottom' => 0, - 'left' => 0, - 'right' => 0, - 'top' => 0, - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' --margin-bottom '0mm' --margin-left '0mm' --margin-right '0mm' --margin-top '0mm' - -"; - $this->assertEquals($expected, $result); - - // Various option types: boolean, string, integer, associative array - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'options' => [ - 'boolean' => true, - 'string' => 'value', - 'integer' => 42, - 'array' => [ - 'first' => 'firstValue', - 'second' => 'secondValue', - ], - ], - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' --boolean --string 'value' --integer '42' --array 'first' 'firstValue' --array 'second' 'secondValue' - -"; - $this->assertEquals($expected, $result); - - // With footer and header - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - ], - ]); - $Pdf->footer('Footer left'); - $Pdf->header(null, 'Page {page}'); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' --footer-left \"Footer left\" --header-center \"Page {page}\" - -"; - $this->assertEquals($expected, $result); - - // With cover (string) and toc (boolean true) - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'options' => [ - 'cover' => 'cover.html', - 'toc' => true, - ], - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' cover 'cover.html' toc - -"; - $this->assertEquals($expected, $result); - - // With cover (array) and toc (array of options) - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'options' => [ - 'cover' => [ - 'url' => 'cover.html', - 'enable-smart-shrinking' => true, - 'zoom' => 10, - ], - 'toc' => [ - 'zoom' => 5, - 'encoding' => 'ISO-8859-1', - ], - ], - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' cover 'cover.html' --enable-smart-shrinking --zoom '10' toc --zoom '5' --encoding 'ISO-8859-1' - -"; - $this->assertEquals($expected, $result); - - // With global zoom override alongside cover (array) and toc (array) - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'options' => [ - 'zoom' => 4, - 'cover' => [ - 'url' => 'cover.html', - 'enable-smart-shrinking' => true, - 'zoom' => 10, - ], - 'toc' => [ - 'disable-dotted-lines' => true, - 'xsl-style-sheet' => 'style.xsl', - 'zoom' => 5, - 'encoding' => 'ISO-8859-1', - ], - ], - ], - ]); - $result = $method->invokeArgs($Pdf->engine(), []); - $expected = "weasyprint --dpi '96' --zoom '4' cover 'cover.html' --enable-smart-shrinking --zoom '10' toc --disable-dotted-lines --xsl-style-sheet 'style.xsl' --zoom '5' --encoding 'ISO-8859-1' - -"; - $this->assertEquals($expected, $result); - } - - public function testCoverUrlMissing() - { - if (!shell_exec('which weasyprint')) { - $this->markTestSkipped('weasyprint not found'); - } - - $this->expectException(CakeException::class); - $this->expectExceptionMessage('The url for the cover is missing. Use the "url" index.'); - - $class = new ReflectionClass(WeasyprintEngine::class); - $method = $class->getMethod('_getCommand'); - $method->setAccessible(true); - - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'options' => [ - 'cover' => [ - 'enable-smart-shrinking' => true, - 'zoom' => 10, - ], - ], - ], - ]); - $method->invokeArgs($Pdf->engine(), []); - } - - public function testGetBinaryPath() - { - $this->expectException(CakeException::class); - $this->expectExceptionMessage('weasyprint binary is not found or not executable: /foo/bar'); - - $Pdf = new CakePdf([ - 'engine' => [ - 'className' => 'CakePdf.Weasyprint', - 'binary' => '/foo/bar', - ], - ]); - - /** @var \CakePdf\Pdf\Engine\WeasyprintEngine $engine */ - $engine = $Pdf->engine(); - $engine->getBinaryPath(); - } -} From 68c20b83c901f379beaf05a4878473878598a222 Mon Sep 17 00:00:00 2001 From: ADmad Date: Sun, 15 Mar 2026 19:04:53 +0530 Subject: [PATCH 5/6] CI --- .github/workflows/ci.yml | 4 +- psalm-baseline.xml | 58 +----------------- psalm.xml | 20 +------ src/Pdf/CakePdf.php | 60 +++++++------------ .../Pdf/Engine/WkHtmlToPdfEngineTest.php | 2 - 5 files changed, 25 insertions(+), 119 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a63c58e..da0fb39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,9 +51,9 @@ jobs: - name: Run PHPUnit run: | if [[ ${{ matrix.php-version }} == '8.5' ]]; then - vendor/bin/phpunit --display-warnings --display-deprecations --display-phpunit-deprecations --display-incomplete --display-skipped --coverage-clover=coverage.xml + vendor/bin/phpunit --display-all-issues --fail-on-all-issues --do-not-fail-on-skipped --do-not-fail-on-incomplete --coverage-clover=coverage.xml else - vendor/bin/phpunit --display-warnings --display-deprecations + vendor/bin/phpunit --display-phpunit-deprecations --display-warnings --display-deprecations fi - name: Code Coverage Report diff --git a/psalm-baseline.xml b/psalm-baseline.xml index dd6f180..2bd6c76 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,12 +1,10 @@ - + - - @@ -29,59 +27,5 @@ - - - - - - - - - - - - - - - - - - - - html()]]> - _Pdf->orientation()]]> - _Pdf->pageSize()]]> - - - - - - _Pdf->html()]]> - - - - - - - - _Pdf->html()]]> - _Pdf->orientation()]]> - - - - - - - _Pdf->encoding()]]> - - - - - - - - _Pdf->html()]]> - - diff --git a/psalm.xml b/psalm.xml index c90aaac..8cdd551 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,5 +1,6 @@ - - - - - - - - - - - - - - - - - - + diff --git a/src/Pdf/CakePdf.php b/src/Pdf/CakePdf.php index ddd1e9c..1f4e30e 100644 --- a/src/Pdf/CakePdf.php +++ b/src/Pdf/CakePdf.php @@ -314,8 +314,7 @@ public function output(?string $html = null): string * Get/Set Html. * * @param string|null $html Html to set - * @return $this|string - * @psalm-return ($html is null ? string : $this) + * @return ($html is null ? string : $this) */ public function html(?string $html = null): static|string { @@ -438,8 +437,7 @@ public function crypto(string|array|null $name = null): AbstractPdfCrypto * Get/Set Page size. * * @param string|null $pageSize Page size to set - * @return $this|string - * @psalm-return ($pageSize is null ? string : $this) + * @return ($pageSize is null ? string : $this) */ public function pageSize(?string $pageSize = null): static|string { @@ -455,8 +453,7 @@ public function pageSize(?string $pageSize = null): static|string * Get/Set Orientation. * * @param string|null $orientation orientation to set - * @return $this|string - * @psalm-return ($orientation is null ? string : $this) + * @return ($orientation is null ? string : $this) */ public function orientation(?string $orientation = null): static|string { @@ -605,8 +602,7 @@ public function margin( * Get/Set bottom margin. * * @param string|int|null $margin margin to set - * @return $this|string|int|null - * @psalm-return ($margin is null ? string|int|null : $this) + * @return ($margin is null ? string|int|null : $this) */ public function marginBottom(string|int|null $margin = null): static|string|int|null { @@ -622,8 +618,7 @@ public function marginBottom(string|int|null $margin = null): static|string|int| * Get/Set left margin. * * @param string|int|null $margin margin to set - * @return $this|string|int|null - * @psalm-return ($margin is null ? string|int|null : $this) + * @return ($margin is null ? string|int|null : $this) */ public function marginLeft(string|int|null $margin = null): static|string|int|null { @@ -639,8 +634,7 @@ public function marginLeft(string|int|null $margin = null): static|string|int|nu * Get/Set right margin. * * @param string|int|null $margin margin to set - * @return $this|string|int|null - * @psalm-return ($margin is null ? string|int|null : $this) + * @return ($margin is null ? string|int|null : $this) */ public function marginRight(string|int|null $margin = null): static|string|int|null { @@ -656,8 +650,7 @@ public function marginRight(string|int|null $margin = null): static|string|int|n * Get/Set top margin. * * @param string|int|null $margin margin to set - * @return $this|string|int|null - * @psalm-return ($margin is null ? string|int|null : $this) + * @return ($margin is null ? string|int|null : $this) */ public function marginTop(string|int|null $margin = null): static|string|int|null { @@ -673,8 +666,7 @@ public function marginTop(string|int|null $margin = null): static|string|int|nul * Get/Set document title. * * @param string|null $title title to set - * @return $this|string|null - * @psalm-return ($title is null ? string|null : $this) + * @return ($title is null ? string|null : $this) */ public function title(?string $title = null): static|string|null { @@ -690,8 +682,7 @@ public function title(?string $title = null): static|string|null * Get/Set javascript delay. * * @param int|null $delay delay to set in milliseconds - * @return $this|int|null - * @psalm-return ($delay is null ? int|null : $this) + * @return ($delay is null ? int|null : $this) */ public function delay(?int $delay = null): static|int|null { @@ -708,8 +699,7 @@ public function delay(?int $delay = null): static|int|null * Waits until the status is equal to the string before rendering the pdf * * @param string|null $status status to set as string - * @return $this|string|null - * @psalm-return ($status is null ? string|null : $this) + * @return ($status is null ? string|null : $this) */ public function windowStatus(?string $status = null): static|string|null { @@ -725,8 +715,7 @@ public function windowStatus(?string $status = null): static|string|null * Get/Set protection. * * @param bool|null $protect True or false - * @return $this|bool - * @psalm-return ($protect is null ? bool : $this) + * @return ($protect is null ? bool : $this) */ public function protect(?bool $protect = null): static|bool { @@ -744,8 +733,7 @@ public function protect(?bool $protect = null): static|bool * The user password is used to control who can open the PDF document. * * @param string|null $password password to set - * @return $this|string|null - * @psalm-return ($password is null ? string|null : $this) + * @return ($password is null ? string|null : $this) */ public function userPassword(?string $password = null): static|string|null { @@ -763,8 +751,7 @@ public function userPassword(?string $password = null): static|string|null * The owner password is used to control who can modify, print, manage the PDF document. * * @param string|null $password password to set - * @return $this|string|null - * @psalm-return ($password is null ? string|null : $this) + * @return ($password is null ? string|null : $this) */ public function ownerPassword(?string $password = null): static|string|null { @@ -785,8 +772,7 @@ public function ownerPassword(?string $password = null): static|string|null * * @param array|string|bool|null $permissions Permissions to set * @throws \Cake\Core\Exception\CakeException - * @return $this|array|string|bool|null - * @psalm-return ($permissions is null ? array|string|bool|null : $this) + * @return ($permissions is null ? array|string|bool|null : $this) */ public function permissions(bool|array|string|null $permissions = null): static|array|string|bool|null { @@ -827,8 +813,7 @@ public function permissions(bool|array|string|null $permissions = null): static| * * @param string|bool|null $cache Cache config name to use, If true is passed, 'cake_pdf' will be used. * @throws \Cake\Core\Exception\CakeException - * @return $this|string|false - * @psalm-return ($cache is null ? string|false : $this) + * @return ($cache is null ? string|false : $this) */ public function cache(bool|string|null $cache = null): static|string|false { @@ -882,8 +867,7 @@ public function template(string|false|null $template = false, ?string $layout = * Template path * * @param ?string $templatePath The path of the template to use - * @return $this|string - * @psalm-return ($templatePath is null ? string : $this) + * @return ($templatePath is null ? string : $this) */ public function templatePath(?string $templatePath = null): static|string { @@ -900,8 +884,7 @@ public function templatePath(?string $templatePath = null): static|string * Layout path * * @param ?string $layoutPath The path of the layout file to use - * @return $this|string - * @psalm-return ($layoutPath is null ? string : $this) + * @return ($layoutPath is null ? string : $this) */ public function layoutPath(?string $layoutPath = null): static|string { @@ -918,8 +901,7 @@ public function layoutPath(?string $layoutPath = null): static|string * View class for render * * @param string|null $viewClass name of the view class to use - * @return $this|string - * @psalm-return ($viewClass is null ? string : $this) + * @return ($viewClass is null ? string : $this) */ public function viewRender(?string $viewClass = null): static|string { @@ -935,8 +917,7 @@ public function viewRender(?string $viewClass = null): static|string * Variables to be set on render * * @param array $viewVars view variables to set - * @return $this|array - * @psalm-return ($viewVars is null ? array : $this) + * @return ($viewVars is null ? array : $this) */ public function viewVars(?array $viewVars = null): static|array { @@ -952,8 +933,7 @@ public function viewVars(?array $viewVars = null): static|array * Theme to use when rendering * * @param string $theme theme to use - * @return $this|string|null - * @psalm-return ($theme is null ? string|null : $this) + * @return ($theme is null ? string|null : $this) */ public function theme(?string $theme = null): static|string|null { diff --git a/tests/TestCase/Pdf/Engine/WkHtmlToPdfEngineTest.php b/tests/TestCase/Pdf/Engine/WkHtmlToPdfEngineTest.php index ca4bc23..95a8033 100644 --- a/tests/TestCase/Pdf/Engine/WkHtmlToPdfEngineTest.php +++ b/tests/TestCase/Pdf/Engine/WkHtmlToPdfEngineTest.php @@ -25,7 +25,6 @@ public function testGetCommand() $class = new ReflectionClass(WkHtmlToPdfEngine::class); $method = $class->getMethod('_getCommand'); - $method->setAccessible(true); $Pdf = new CakePdf([ 'engine' => [ @@ -160,7 +159,6 @@ public function testCoverUrlMissing() $class = new ReflectionClass(WkHtmlToPdfEngine::class); $method = $class->getMethod('_getCommand'); - $method->setAccessible(true); $Pdf = new CakePdf([ 'engine' => [ 'className' => 'CakePdf.WkHtmlToPdf', From f14a660dd04aa6c669cbb272612e706f09c88e29 Mon Sep 17 00:00:00 2001 From: ADmad Date: Sun, 15 Mar 2026 21:20:26 +0530 Subject: [PATCH 6/6] update readme --- README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 155c5cd..0281bf7 100644 --- a/README.md +++ b/README.md @@ -142,26 +142,21 @@ Configure::write('CakePdf', [ // Options usable depend on the engine used. 'options' => [ 'dpi' => 96, - 'cover' => [ - 'url' => 'cover.html', - 'enable-smart-shrinking' => true, - ], - 'toc' => true, ], /** - * For Mac OS X / Linux by default the `wkhtmltopdf` binary should + * For Mac OS X / Linux by default the `weasyprint` binary should * be available through environment path or you can specify location as: */ - // 'binary' => '/usr/local/bin/wkhtmltopdf', + // 'binary' => '/usr/local/bin/weasyprint', /** * On Windows the engine uses the path shown below as default. * You NEED to use the path like old fashioned MS-DOS Paths, * otherwise you will get error like: - * "WKHTMLTOPDF didn't return any data" + * "weasyprint didn't return any data" */ - // 'binary' => 'C:\\Progra~1\\wkhtmltopdf\\bin\\wkhtmltopdf.exe', + // 'binary' => 'C:/Progra~1/WeasyPrint/bin/weasyprint.exe', ], ]); ```