├── .docs └── README.md ├── .editorconfig ├── .github ├── .kodiak.toml └── workflows │ ├── codesniffer.yml │ ├── coverage.yml │ ├── phpstan.yml │ └── tests.yml ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── phpstan.neon ├── ruleset.xml └── src ├── Exceptions ├── InvalidArgumentException.php ├── InvalidStateException.php ├── LogicalException.php └── MissingServiceException.php ├── PdfResponse.php └── PdfResponseFactory.php /.docs/README.md: -------------------------------------------------------------------------------- 1 | # Contributte\PdfResponse 2 | 3 | ## Content 4 | 5 | - [Usage - how use it](#usage) 6 | - [How to prepare PDF from template](#how-to-prepare-pdf-from-template) 7 | - [Save file to server](#save-file-to-server) 8 | - [Attach file to an email](#attach-file-to-an-email) 9 | - [Force file to download](#force-file-to-download) 10 | - [Force file to display in a browser](#force-file-to-display-in-a-browser) 11 | - [Set a pdf background easily](#set-a-pdf-background-easily) 12 | - [Create pdf with latte only](#create-pdf-with-latte-only) 13 | - [Configuration of custom temp dir for mPDF in PdfResponse](#configuration-of-custom-temp-dir-for-mpdf-in-pdfresponse) 14 | 15 | ## Usage 16 | 17 | ### How to prepare PDF from template 18 | 19 | ```php 20 | use Contributte\PdfResponse\PdfResponse; 21 | 22 | // in a Presenter 23 | public function actionPdf() 24 | { 25 | $template = $this->createTemplate(); 26 | $template->setFile(__DIR__ . "/path/to/template.latte"); 27 | $template->someValue = 123; 28 | // Tip: In template to make a new page use 29 | 30 | $pdf = new PdfResponse($template); 31 | 32 | // optional 33 | $pdf->documentTitle = date("Y-m-d") . " My super title"; // creates filename 2012-06-30-my-super-title.pdf 34 | $pdf->pageFormat = "A4-L"; // wide format 35 | $pdf->getMPDF()->setFooter("|© www.mysite.com|"); // footer 36 | 37 | // do something with $pdf 38 | $this->sendResponse($pdf); 39 | } 40 | ``` 41 | 42 | ### Save file to server 43 | 44 | ```php 45 | use Contributte\PdfResponse\PdfResponse; 46 | 47 | public function actionPdf() 48 | { 49 | $template = $this->createTemplate(); 50 | $template->setFile(__DIR__ . "/path/to/template.latte"); 51 | 52 | $pdf = new PdfResponse($template); 53 | 54 | $pdf->save(__DIR__ . "/path/to/directory"); // as a filename $this->documentTitle will be used 55 | $pdf->save(__DIR__ . "/path/to/directory", "filename"); // OR use a custom name 56 | } 57 | ``` 58 | 59 | ### Attach file to an email 60 | 61 | ```php 62 | use Contributte\PdfResponse\PdfResponse; 63 | 64 | public function actionPdf() 65 | { 66 | $template = $this->createTemplate(); 67 | $template->setFile(__DIR__ . "/path/to/template.latte"); 68 | 69 | $pdf = new PdfResponse($template); 70 | 71 | $savedFile = $pdf->save(__DIR__ . "/path/to/directory"); 72 | $mail = new Nette\Mail\Message; 73 | $mail->addTo("john@doe.com"); 74 | $mail->addAttachment($savedFile); 75 | $mailer = new SendmailMailer(); 76 | $mailer->send($mail); 77 | } 78 | ``` 79 | 80 | ### Force file to download 81 | 82 | ```php 83 | use Contributte\PdfResponse\PdfResponse; 84 | 85 | public function actionPdf() 86 | { 87 | $template = $this->createTemplate(); 88 | $template->setFile(__DIR__ . "/path/to/template.latte"); 89 | 90 | $pdf = new PdfResponse($template); 91 | $pdf->setSaveMode(PdfResponse::DOWNLOAD); //default behavior 92 | $this->sendResponse($pdf); 93 | } 94 | ``` 95 | 96 | ### Force file to display in a browser 97 | 98 | ```php 99 | use Contributte\PdfResponse\PdfResponse; 100 | 101 | public function actionPdf() 102 | { 103 | $template = $this->createTemplate(); 104 | $template->setFile(__DIR__ . "/path/to/template.latte"); 105 | 106 | $pdf = new PdfResponse($template); 107 | $pdf->setSaveMode(PdfResponse::INLINE); 108 | $this->sendResponse($pdf); 109 | } 110 | ``` 111 | 112 | ### Set a pdf background easily 113 | 114 | ```php 115 | use Contributte\PdfResponse\PdfResponse; 116 | 117 | public function actionPdf() 118 | { 119 | $pdf = new PdfResponse(''); 120 | $pdf->setBackgroundTemplate(__DIR__ . "/path/to/an/existing/file.pdf"); 121 | 122 | // to write into an existing document use the following statements 123 | $mpdf = $pdf->getMPDF(); 124 | $mpdf->WriteFixedPosHTML('hello world', 1, 10, 10, 10); 125 | 126 | // to write to another page 127 | $mpdf->AddPage(); 128 | 129 | // to move to exact page, use 130 | $mpdf->page = 3; // = move to 3rd page 131 | 132 | $this->sendResponse($pdf); 133 | } 134 | ``` 135 | 136 | ### Create pdf with latte only 137 | 138 | ```php 139 | use Contributte\PdfResponse\PdfResponse; 140 | 141 | public function actionPdf() 142 | { 143 | $latte = new Latte\Engine; 144 | $latte->setTempDirectory('/path/to/cache'); 145 | $latte->addFilter('money', function($val) { return ...; }); // formerly registerHelper() 146 | 147 | $latte->onCompile[] = function($latte) { 148 | $latte->addMacro(...); // when you want add some own macros, see http://goo.gl/d5A1u2 149 | }; 150 | 151 | $template = $latte->renderToString(__DIR__ . "/path/to/template.latte"); 152 | 153 | $pdf = new PdfResponse($template); 154 | $this->sendResponse($pdf); 155 | } 156 | ``` 157 | 158 | ### Configuration of custom temp dir for mPDF in PdfResponse 159 | 160 | ```neon 161 | services: 162 | - Contributte\PdfResponse\PdfResponseFactory([tempDir: %tempDir%/pdf]) 163 | ``` 164 | 165 | and in your PHP code: 166 | 167 | ```php 168 | use Contributte\PdfResponse\PdfResponse; 169 | 170 | public function __construct( 171 | private readonly PdfResponseFactory $pdfResponseFactory 172 | ) 173 | { 174 | } 175 | 176 | public function actionPdf() 177 | { 178 | $template = $this->createTemplate(); 179 | $template->setFile(__DIR__ . "/path/to/template.latte"); 180 | 181 | $response = $this->pdfResponseFactory->createResponse(); 182 | $response->setTemplate($template); 183 | $response->setSaveMode(PdfResponse::INLINE); 184 | 185 | $this->sendResponse($response); 186 | } 187 | ``` 188 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = tab 11 | indent_size = tab 12 | tab_width = 4 13 | 14 | [{*.json, *.yaml, *.yml, *.md}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge] 4 | automerge_label = "automerge" 5 | blacklist_title_regex = "^WIP.*" 6 | blacklist_labels = ["WIP"] 7 | method = "rebase" 8 | delete_branch_on_merge = true 9 | notify_on_conflict = true 10 | optimistic_updates = false 11 | -------------------------------------------------------------------------------- /.github/workflows/codesniffer.yml: -------------------------------------------------------------------------------- 1 | name: "Codesniffer" 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: ["*"] 8 | 9 | schedule: 10 | - cron: "0 8 * * 1" 11 | 12 | jobs: 13 | codesniffer: 14 | name: "Codesniffer" 15 | uses: contributte/.github/.github/workflows/codesniffer.yml@v1 16 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: "Coverage" 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: ["*"] 8 | 9 | schedule: 10 | - cron: "0 8 * * 1" 11 | 12 | jobs: 13 | coverage: 14 | name: "Nette Tester" 15 | uses: contributte/.github/.github/workflows/nette-tester-coverage.yml@v1 16 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: "Phpstan" 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: ["*"] 8 | 9 | schedule: 10 | - cron: "0 8 * * 1" 11 | 12 | jobs: 13 | phpstan: 14 | name: "Phpstan" 15 | uses: contributte/.github/.github/workflows/phpstan.yml@v1 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Nette Tester" 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: ["*"] 8 | 9 | schedule: 10 | - cron: "0 8 * * 1" 11 | 12 | jobs: 13 | test82: 14 | name: "Nette Tester" 15 | uses: contributte/.github/.github/workflows/nette-tester.yml@v1 16 | with: 17 | php: "8.2" 18 | 19 | test81: 20 | name: "Nette Tester" 21 | uses: contributte/.github/.github/workflows/nette-tester.yml@v1 22 | with: 23 | php: "8.1" 24 | 25 | testlowest: 26 | name: "Nette Tester" 27 | uses: contributte/.github/.github/workflows/nette-tester.yml@v1 28 | with: 29 | php: "8.1" 30 | composer: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable --prefer-lowest" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install qa cs csf phpstan tests coverage 2 | 3 | install: 4 | composer update 5 | 6 | qa: phpstan cs 7 | 8 | cs: 9 | ifdef GITHUB_ACTION 10 | vendor/bin/phpcs --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp -q --report=checkstyle src tests | cs2pr 11 | else 12 | vendor/bin/phpcs --standard=ruleset.xml --encoding=utf-8 --extensions="php,phpt" --colors -nsp src tests 13 | endif 14 | 15 | csf: 16 | vendor/bin/phpcbf --standard=ruleset.xml --encoding=utf-8 --colors -nsp src tests 17 | 18 | phpstan: 19 | vendor/bin/phpstan analyse -c phpstan.neon 20 | 21 | tests: 22 | vendor/bin/tester -s -p php --colors 1 -C tests/Cases 23 | 24 | coverage: 25 | ifdef GITHUB_ACTION 26 | vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage coverage.xml --coverage-src src tests/Cases 27 | else 28 | vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage coverage.html --coverage-src src tests/Cases 29 | endif 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://heatbadger.now.sh/github/readme/contributte/pdf/) 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 |

10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 |

18 | Website 🚀 contributte.org | Contact 👨🏻‍💻 f3l1x.io | Twitter 🐦 @contributte 19 |

20 | 21 | ## Usage 22 | 23 | To install latest version of `contributte/pdf` use [Composer](https://getcomposer.org/). 24 | 25 | ```bash 26 | composer require contributte/pdf 27 | ``` 28 | 29 | ## Documentation 30 | 31 | For details on how to use this package, check out our [documentation](.docs). 32 | 33 | 34 | ## Versions 35 | 36 | | State | Version | Branch | PHP | 37 | |-------------|---------|----------|--------| 38 | | dev | `^7.1` | `master` | `8.1+` | 39 | | stable | `^7.0` | `master` | `8.1+` | 40 | 41 | ## Development 42 | 43 | See [how to contribute](https://contributte.org/contributing.html) to this package. This package is currently maintained by these authors. 44 | 45 | 46 | 47 | 48 | 49 | ----- 50 | 51 | Consider to [support](https://contributte.org/partners.html) **contributte** development team. 52 | Also thank you for using this package. 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contributte/pdf", 3 | "description": "Pdf response extension for Nette Framework", 4 | "keywords": [ 5 | "nette", 6 | "contributte", 7 | "php", 8 | "mpdf", 9 | "pdf" 10 | ], 11 | "type": "library", 12 | "license": "LGPL-3.0", 13 | "homepage": "https://github.com/contributte/pdf", 14 | "authors": [ 15 | { 16 | "name": "Miroslav Paulík", 17 | "email": "miras.paulik@seznam.cz" 18 | }, 19 | { 20 | "name": "Jan Kuchař" 21 | }, 22 | { 23 | "name": "Tomáš Votruba", 24 | "email": "tomas.vot@gmail.com" 25 | }, 26 | { 27 | "name": "Petr Parolek", 28 | "homepage": "https://www.webnazakazku.cz/" 29 | }, 30 | { 31 | "name": "Milan Felix Šulc", 32 | "homepage": "https://f3l1x.io" 33 | } 34 | ], 35 | "require": { 36 | "php": ">=8.1", 37 | "mpdf/mpdf": "^8.1.0", 38 | "nette/application": "^3.1.0", 39 | "nette/http": "^3.2.0" 40 | }, 41 | "require-dev": { 42 | "nette/di": "^3.1.2", 43 | "contributte/qa": "^0.4", 44 | "contributte/tester": "^0.3", 45 | "contributte/phpstan": "^0.1", 46 | "symfony/css-selector": "^6.3.0", 47 | "symfony/dom-crawler": "^6.3.0" 48 | }, 49 | "conflict": { 50 | "masterminds/html5": "<2.8.0" 51 | }, 52 | "suggest": { 53 | "symfony/dom-crawler": "Allows filtering html tags.", 54 | "nette/nette": "PHP framework to which this extension belongs to." 55 | }, 56 | "autoload": { 57 | "psr-4": { 58 | "Contributte\\PdfResponse\\": "src/" 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true, 63 | "config": { 64 | "sort-packages": true, 65 | "allow-plugins": { 66 | "dealerdirect/phpcodesniffer-composer-installer": true 67 | } 68 | }, 69 | "extra": { 70 | "branch-alias": { 71 | "dev-master": "7.1.x-dev" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/contributte/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 9 6 | phpVersion: 80100 7 | 8 | scanDirectories: 9 | - src 10 | 11 | fileExtensions: 12 | - php 13 | 14 | paths: 15 | - src 16 | - .docs 17 | 18 | excludePaths: 19 | - src/DI/SentryExtension24.php 20 | 21 | ignoreErrors: 22 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | /tests/tmp 20 | 21 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | = 5.2) 83 | 84 | public const LAYOUT_TWORIGHT = 'tworight'; // Display the pages in two columns, with the first page displayed on the right side (mPDF >= 5.2) 85 | 86 | public const LAYOUT_DEFAULT = 'default'; // User’s default setting in Adobe Reader 87 | 88 | /** @var array */ 89 | public array $mpdfConfig = []; 90 | 91 | /** @var array onBeforeComplete event */ 92 | public array $onBeforeComplete = []; 93 | 94 | /** @var string Additional stylesheet as a html string */ 95 | public string $styles = ''; 96 | 97 | private string $documentAuthor = 'Nette Framework - Pdf response'; 98 | 99 | private string $documentTitle = 'New document'; 100 | 101 | private string|int $displayZoom = self::ZOOM_DEFAULT; 102 | 103 | private string $displayLayout = self::LAYOUT_DEFAULT; 104 | 105 | private bool $multiLanguage = false; 106 | 107 | /** @var bool, REQUIRES symfony/dom-crawler package */ 108 | private bool $ignoreStylesInHTMLDocument = false; 109 | 110 | private string|Template $source; 111 | 112 | /** @var string save mode */ 113 | private string $saveMode = self::DOWNLOAD; 114 | 115 | /** @var string path to (PDF) file */ 116 | private string $backgroundTemplate; 117 | 118 | /** @var string ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE */ 119 | private string $pageOrientation = self::ORIENTATION_PORTRAIT; 120 | 121 | /** @var string|array see second parameter ($format) at https://mpdf.github.io/reference/mpdf-functions/mpdf.html */ 122 | private string|array $pageFormat = 'A4'; 123 | 124 | /** @var string margins: top, right, bottom, left, header, footer */ 125 | private string $pageMargins = '16,15,16,15,9,9'; 126 | 127 | private ?Mpdf $mPDF = null; 128 | 129 | private ?Mpdf $generatedFile = null; 130 | 131 | /** 132 | * @param Template|string $source 133 | * @throws InvalidArgumentException 134 | */ 135 | public function __construct(Template|string|null $source = null) 136 | { 137 | if ($source === null) { 138 | return; 139 | } 140 | 141 | $this->setTemplate($source); 142 | } 143 | 144 | public function setTemplate(Template|string $source): self 145 | { 146 | $this->source = $source; 147 | 148 | return $this; 149 | } 150 | 151 | public function getDocumentAuthor(): string 152 | { 153 | return $this->documentAuthor; 154 | } 155 | 156 | public function setDocumentAuthor(string $documentAuthor): void 157 | { 158 | $this->documentAuthor = $documentAuthor; 159 | } 160 | 161 | public function getDocumentTitle(): string 162 | { 163 | return $this->documentTitle; 164 | } 165 | 166 | public function setDocumentTitle(string $documentTitle): void 167 | { 168 | $this->documentTitle = $documentTitle; 169 | } 170 | 171 | public function getDisplayZoom(): string|int 172 | { 173 | return $this->displayZoom; 174 | } 175 | 176 | public function setDisplayZoom(string|int $displayZoom): void 177 | { 178 | if ((!is_int($displayZoom) || $displayZoom <= 0) && !in_array($displayZoom, [ 179 | self::ZOOM_DEFAULT, 180 | self::ZOOM_FULLPAGE, 181 | self::ZOOM_FULLWIDTH, 182 | self::ZOOM_REAL, 183 | ], true)) { 184 | throw new InvalidArgumentException("Invalid zoom '" . $displayZoom . "', use PdfResponse::ZOOM_* constants or o positive integer."); 185 | } 186 | 187 | $this->displayZoom = $displayZoom; 188 | } 189 | 190 | public function getDisplayLayout(): string 191 | { 192 | return $this->displayLayout; 193 | } 194 | 195 | /** 196 | * @throws InvalidArgumentException 197 | */ 198 | public function setDisplayLayout(string $displayLayout): void 199 | { 200 | if (!in_array($displayLayout, [self::LAYOUT_DEFAULT, self::LAYOUT_CONTINUOUS, self::LAYOUT_SINGLE, self::LAYOUT_TWO, self::LAYOUT_TWOLEFT, self::LAYOUT_TWORIGHT], true)) { 201 | throw new InvalidArgumentException("Invalid layout '" . $displayLayout . "', use PdfResponse::LAYOUT* constants."); 202 | } 203 | 204 | $this->displayLayout = $displayLayout; 205 | } 206 | 207 | public function isMultiLanguage(): bool 208 | { 209 | return $this->multiLanguage; 210 | } 211 | 212 | public function setMultiLanguage(bool $multiLanguage): void 213 | { 214 | $this->multiLanguage = $multiLanguage; 215 | } 216 | 217 | public function isIgnoreStylesInHTMLDocument(): bool 218 | { 219 | return $this->ignoreStylesInHTMLDocument; 220 | } 221 | 222 | public function setIgnoreStylesInHTMLDocument(bool $ignoreStylesInHTMLDocument): void 223 | { 224 | $this->ignoreStylesInHTMLDocument = $ignoreStylesInHTMLDocument; 225 | } 226 | 227 | public function getSaveMode(): string 228 | { 229 | return $this->saveMode; 230 | } 231 | 232 | /** 233 | * To force download, use PdfResponse::DOWNLOAD 234 | * To show pdf in browser, use PdfResponse::INLINE 235 | * 236 | * @throws InvalidArgumentException 237 | */ 238 | public function setSaveMode(string $saveMode): void 239 | { 240 | if (!in_array($saveMode, [self::DOWNLOAD, self::INLINE], true)) { 241 | throw new InvalidArgumentException("Invalid mode '" . $saveMode . "', use PdfResponse::INLINE or PdfResponse::DOWNLOAD instead."); 242 | } 243 | 244 | $this->saveMode = $saveMode; 245 | } 246 | 247 | public function getPageOrientation(): string 248 | { 249 | return $this->pageOrientation; 250 | } 251 | 252 | /** 253 | * @throws InvalidStateException 254 | * @throws InvalidArgumentException 255 | */ 256 | public function setPageOrientation(string $pageOrientation): void 257 | { 258 | if ($this->mPDF !== null) { 259 | throw new InvalidStateException('mPDF instance already created. Set page orientation before calling getMPDF'); 260 | } 261 | 262 | if (!in_array($pageOrientation, [self::ORIENTATION_PORTRAIT, self::ORIENTATION_LANDSCAPE], true)) { 263 | throw new InvalidArgumentException('Unknown page orientation'); 264 | } 265 | 266 | $this->pageOrientation = $pageOrientation; 267 | } 268 | 269 | /** 270 | * @return string|array 271 | */ 272 | public function getPageFormat(): string|array 273 | { 274 | return $this->pageFormat; 275 | } 276 | 277 | /** 278 | * @param string|array $pageFormat 279 | * @throws InvalidStateException 280 | */ 281 | public function setPageFormat(string|array $pageFormat): void 282 | { 283 | if ($this->mPDF !== null) { 284 | throw new InvalidStateException('mPDF instance already created. Set page format before calling getMPDF'); 285 | } 286 | 287 | $this->pageFormat = $pageFormat; 288 | } 289 | 290 | public function getPageMargins(): string 291 | { 292 | return $this->pageMargins; 293 | } 294 | 295 | /** 296 | * Gets margins as array 297 | * 298 | * @return array 299 | */ 300 | public function getMargins(): array 301 | { 302 | $margins = explode(',', $this->pageMargins); 303 | 304 | $dictionary = ['top', 'right', 'bottom', 'left', 'header', 'footer']; 305 | 306 | $marginsOut = []; 307 | foreach ($margins as $key => $val) { 308 | $marginsOut[$dictionary[$key]] = (int) $val; 309 | } 310 | 311 | return $marginsOut; 312 | } 313 | 314 | /** 315 | * @throws InvalidStateException 316 | * @throws InvalidArgumentException 317 | */ 318 | public function setPageMargins(string $pageMargins): void 319 | { 320 | if ($this->mPDF !== null) { 321 | throw new InvalidStateException('mPDF instance already created. Set page margins before calling getMPDF'); 322 | } 323 | 324 | $margins = explode(',', $pageMargins); 325 | if (count($margins) !== 6) { 326 | throw new InvalidArgumentException('You must specify all margins! For example: 16,15,16,15,9,9'); 327 | } 328 | 329 | foreach ($margins as $val) { 330 | $val = (int) $val; 331 | if ($val < 0) { 332 | throw new InvalidArgumentException('Margin must not be negative number!'); 333 | } 334 | } 335 | 336 | $this->pageMargins = $pageMargins; 337 | } 338 | 339 | /** 340 | * WARNING: internally creates mPDF instance, setting some properties after calling this method 341 | * may cause an Exception 342 | * 343 | * @throws FileNotFoundException 344 | * @throws PdfParserExceptionAlias 345 | */ 346 | public function setBackgroundTemplate(string $pathToBackgroundTemplate): void 347 | { 348 | if (!file_exists($pathToBackgroundTemplate)) { 349 | throw new FileNotFoundException("File '" . $pathToBackgroundTemplate . "' not found."); 350 | } 351 | 352 | $this->backgroundTemplate = $pathToBackgroundTemplate; 353 | 354 | // if background exists, then add it as a background 355 | $mpdf = $this->getMPDF(); 356 | $pagecount = $mpdf->setSourceFile($this->backgroundTemplate); 357 | for ($i = 1; $i <= $pagecount; $i++) { 358 | $tplId = $mpdf->importPage($i); 359 | $mpdf->useTemplate($tplId); 360 | 361 | if ($i >= $pagecount) { 362 | continue; 363 | } 364 | 365 | $mpdf->AddPage(); 366 | } 367 | 368 | $mpdf->page = 1; 369 | } 370 | 371 | /** 372 | * @throws InvalidStateException 373 | */ 374 | public function getMPDF(): Mpdf 375 | { 376 | if (!$this->mPDF instanceof Mpdf) { 377 | try { 378 | $mpdf = new Mpdf($this->getMPDFConfig()); 379 | } catch (MpdfException $e) { 380 | throw new InvalidStateException('Unable to create Mpdf object', 0, $e); 381 | } 382 | 383 | $mpdf->showImageErrors = true; 384 | 385 | $this->mPDF = $mpdf; 386 | } 387 | 388 | return $this->mPDF; 389 | } 390 | 391 | public function setMPDF(Mpdf $mPDF): self 392 | { 393 | $this->mPDF = $mPDF; 394 | 395 | return $this; 396 | } 397 | 398 | /** 399 | * Sends response to output 400 | * 401 | * @throws MpdfException 402 | */ 403 | public function send(IRequest $httpRequest, IResponse $httpResponse): void 404 | { 405 | $mpdf = $this->build(); 406 | $mpdf->Output(Strings::webalize($this->documentTitle) . '.pdf', $this->saveMode); 407 | } 408 | 409 | /** 410 | * Save file to target location 411 | * Note: $name overrides property $documentTitle 412 | * 413 | * @param string $dir path to directory 414 | * @throws MpdfException 415 | */ 416 | public function save(string $dir, ?string $filename = null): string 417 | { 418 | $content = $this->toString(); 419 | $filename = Strings::lower($filename ?? $this->documentTitle); 420 | 421 | if (str_ends_with($filename, '.pdf')) { 422 | $filename = substr($filename, 0, -4); 423 | } 424 | 425 | $filename = Strings::webalize($filename, '_') . '.pdf'; 426 | 427 | $dir = rtrim($dir, '/') . '/'; 428 | file_put_contents($dir . $filename, $content); 429 | 430 | return $dir . $filename; 431 | } 432 | 433 | /** 434 | * @throws MpdfException 435 | */ 436 | public function toString(): string 437 | { 438 | $pdf = $this->build(); 439 | 440 | return (string) $pdf->Output('', Destination::STRING_RETURN); 441 | } 442 | 443 | /** 444 | * @return mixed[] 445 | */ 446 | protected function getMPDFConfig(): array 447 | { 448 | $margins = $this->getMargins(); 449 | 450 | $mpdfConfig = [ 451 | 'mode' => 'utf-8', 452 | 'format' => $this->pageFormat, 453 | 'margin_left' => $margins['left'], 454 | 'margin_right' => $margins['right'], 455 | 'margin_top' => $margins['top'], 456 | 'margin_bottom' => $margins['bottom'], 457 | 'margin_header' => $margins['header'], 458 | 'margin_footer' => $margins['footer'], 459 | 'orientation' => $this->pageOrientation, 460 | ]; 461 | 462 | return count($this->mpdfConfig) > 0 ? $this->mpdfConfig + $mpdfConfig : $mpdfConfig; 463 | }/******************************************************************************** 464 | * core * 465 | ********************************************************************************/ 466 | 467 | 468 | /******************************************************************************** 469 | * build * 470 | ********************************************************************************/ 471 | 472 | /** 473 | * Builds final pdf 474 | * 475 | * @throws InvalidStateException 476 | * @throws MissingServiceException 477 | * @throws MpdfException 478 | */ 479 | private function build(): Mpdf 480 | { 481 | if ($this->documentTitle === '') { 482 | throw new InvalidStateException("Var 'documentTitle' cannot be empty."); 483 | } 484 | 485 | if ($this->ignoreStylesInHTMLDocument) { 486 | if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { 487 | throw new MissingServiceException( 488 | "Class 'Symfony\\Component\\DomCrawler\\Crawler' not found. Try composer-require 'symfony/dom-crawler'." 489 | ); 490 | } 491 | 492 | if (!class_exists(CssSelectorConverter::class)) { 493 | throw new MissingServiceException( 494 | "Class 'Symfony\\Component\\CssSelector\\CssSelectorConverter' not found. Try composer-require 'symfony/css-selector'." 495 | ); 496 | } 497 | } 498 | 499 | if ($this->generatedFile instanceof Mpdf) { // singleton 500 | return $this->generatedFile; 501 | } 502 | 503 | if ($this->source instanceof Template) { 504 | try { 505 | /** @noinspection PhpMethodParametersCountMismatchInspection */ 506 | $html = $this->source->__toString(true); 507 | } catch (Throwable $e) { 508 | throw new InvalidStateException('Template rendering failed', 0, $e); 509 | } 510 | } else { 511 | $html = $this->source; 512 | } 513 | 514 | // Fix: $html can't be empty (mPDF generates Fatal error) 515 | if ($html === '') { 516 | $html = ''; 517 | } 518 | 519 | $mpdf = $this->getMPDF(); 520 | $mpdf->biDirectional = $this->multiLanguage; 521 | $author = $this->mpdfConfig['author'] ?? $this->documentAuthor; 522 | 523 | $mpdf->author = $author; 524 | 525 | if (count($this->mpdfConfig) > 0) { 526 | foreach ($this->mpdfConfig as $key => $value) { 527 | // @phpstan-ignore-next-line 528 | $mpdf->$key = $value; 529 | } 530 | } 531 | 532 | $mpdf->SetTitle($this->documentTitle); 533 | $mpdf->SetDisplayMode($this->displayZoom, $this->displayLayout); 534 | 535 | // Add styles 536 | if ($this->styles !== '') { 537 | $mpdf->WriteHTML($this->styles, HTMLParserMode::HEADER_CSS); 538 | } 539 | 540 | // copied from mPDF -> removes comments 541 | $html = preg_replace('//i', '', $html); 545 | } 546 | 547 | if ($html !== null) { 548 | $html = preg_replace('/<\!\-\-.*?\-\->/s', '', $html); 549 | } 550 | 551 | // @see: https://mpdf.github.io/reference/mpdf-functions/writehtml.html 552 | if ($this->ignoreStylesInHTMLDocument) { 553 | // deletes all