├── Adapter
├── WkPdfEngine.php
├── WkPdfException.php
└── WkPdfFile.php
├── Api
├── Medium.php
├── Options.php
├── PdfEngine.php
├── PdfFile.php
├── SourceDocument.php
└── TableOfContents.php
├── Block
└── PdfTemplate.php
├── Jenkinsfile
├── Model
├── Cache.php
├── CachedPdfFile.php
├── Config.php
├── PdfFactory.php
├── PdfResponse.php
└── View
│ ├── PageResultWithoutHttp.php
│ └── PdfResult.php
├── README.md
├── Service
├── FakePdfEngine.php
├── FakePdfFile.php
├── FakeSourceDocument.php
├── Pdf.php
├── PdfAppendContent.php
├── PdfCover.php
├── PdfOptions.php
└── PdfTableOfContents.php
├── Test
├── Integration
│ ├── ConfigTest.php
│ ├── PdfResultTest.php
│ ├── PdfTemplateTest.php
│ └── WkPdfEngineTest.php
└── Unit
│ ├── Model
│ ├── PdfFactoryTest.php
│ └── PdfResultTest.php
│ └── Service
│ ├── FakePdfFileTest.php
│ ├── FakeSourceDocumentTest.php
│ ├── PdfAppendContentTest.php
│ ├── PdfCoverTest.php
│ ├── PdfOptionsTest.php
│ ├── PdfTableOfContentsTest.php
│ └── PdfTest.php
├── composer.json
├── docs
└── img
│ └── config.png
├── etc
├── acl.xml
├── adminhtml
│ └── system.xml
├── config.xml
├── di.xml
└── module.xml
├── grumphp.yml
├── phpunit.xml.dist
└── registration.php
/Adapter/WkPdfEngine.php:
--------------------------------------------------------------------------------
1 | wkPdf = $wkPdf;
21 | }
22 |
23 | public function __clone()
24 | {
25 | $this->wkPdf = clone $this->wkPdf;
26 | }
27 |
28 | public function addPage($html, Options $options)
29 | {
30 | $this->wkPdf->addPage($this->html($html), $options->asArray());
31 | }
32 |
33 | public function addCover($html, Options $options)
34 | {
35 | $this->wkPdf->addCover($this->html($html), $options->asArray());
36 | }
37 |
38 | public function addTableOfContents(Options $options)
39 | {
40 | $this->wkPdf->addToc($options->asArray());
41 | }
42 |
43 | /**
44 | * @param Options $globalOptions
45 | * @return PdfFile
46 | */
47 | public function generatePdf(Options $globalOptions)
48 | {
49 | /*
50 | * No factory here, WkPdfEngine and WkPdfFile are tightly coupled and should only be exchanged together
51 | */
52 | $wkPdf = clone $this->wkPdf;
53 | $wkPdf->setOptions($globalOptions->asArray());
54 | return new WkPdfFile($wkPdf);
55 | }
56 |
57 | /**
58 | * Make sure, string is recognized as HTML, not file or URL
59 | *
60 | * @param $html
61 | * @return string
62 | */
63 | private function html($html)
64 | {
65 | if (!preg_match(Pdf::REGEX_HTML, $html) && !preg_match(Pdf::REGEX_XML, $html)) {
66 | return sprintf('%s', $html);
67 | }
68 | return $html;
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/Adapter/WkPdfException.php:
--------------------------------------------------------------------------------
1 | wkPdf = $wkPdf;
18 | }
19 |
20 | public function saveAs($path)
21 | {
22 | if (false === $this->wkPdf->saveAs($path)) {
23 | throw new WkPdfException($this->wkPdf->getError());
24 | }
25 | }
26 |
27 | public function send()
28 | {
29 | if (false === $this->wkPdf->send()) {
30 | throw new WkPdfException($this->wkPdf->getError());
31 | }
32 | }
33 |
34 | public function toString()
35 | {
36 | $result = $this->wkPdf->toString();
37 | if ($result === false) {
38 | throw new WkPdfException($this->wkPdf->getError());
39 | }
40 | return $result;
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/Api/Medium.php:
--------------------------------------------------------------------------------
1 | pdfOptions = $optionsFactory->create();
33 | parent::__construct($context, $data);
34 | }
35 |
36 | public function printTo(Medium $medium)
37 | {
38 | $medium->printHtml($this->toHtml(), $this->pdfOptions);
39 | }
40 | }
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | @Library('staempfli/jenkins-shared-library') _
2 | magento2ModulePipeline {}
3 |
--------------------------------------------------------------------------------
/Model/Cache.php:
--------------------------------------------------------------------------------
1 | pdfEngine = $pdfEngine;
26 | $this->scopeConfig = $scopeConfig;
27 | }
28 |
29 | public function create()
30 | {
31 | $pdf = new Pdf($this->pdfEngine);
32 | $pdf->setOptions($this->optionsFromConfig());
33 | return $pdf;
34 | }
35 |
36 | private function optionsFromConfig()
37 | {
38 | $config = function ($xpath) {
39 | return $this->scopeConfig->getValue($xpath);
40 | };
41 | $withoutNull = function (array $array) {
42 | return array_filter($array, function ($value) {
43 | return ! is_null($value); //@codingStandardsIgnoreLine;
44 | });
45 | };
46 | return new PdfOptions(
47 | $withoutNull(
48 | [
49 | PdfOptions::KEY_GLOBAL_BINARY => $config(Config::XML_PATH_BINARY),
50 | PdfOptions::KEY_GLOBAL_VERSION9 => $config(Config::XML_PATH_VERSION9),
51 | PdfOptions::KEY_GLOBAL_TMP_DIR => $config(Config::XML_PATH_TMP_DIR),
52 | PdfOptions::KEY_GLOBAL_CLI_OPTIONS => $withoutNull([
53 | PdfOptions::KEY_CLI_OPTIONS_ESCAPE_ARGS => $config(Config::XML_PATH_ESCAPE_ARGS),
54 | PdfOptions::KEY_CLI_OPTIONS_USE_EXEC => $config(Config::XML_PATH_USE_EXEC),
55 | PdfOptions::KEY_CLI_OPTIONS_XVFB_RUN_OPTIONS => $config(Config::XML_PATH_XVFB_RUN_OPTIONS),
56 | PdfOptions::KEY_CLI_OPTIONS_XVFB_RUN_BINARY => $config(Config::XML_PATH_XVFB_RUN_BINARY),
57 | PdfOptions::KEY_CLI_OPTIONS_USE_XVFB_RUN => $config(Config::XML_PATH_USE_XVFB_RUN),
58 | ])
59 | ]
60 | )
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Model/PdfResponse.php:
--------------------------------------------------------------------------------
1 | pdfOptions = $options;
27 | }
28 |
29 | /**
30 | * We don't actually send a HTTP response, the PdfResponse instance is sent to the Pdf service instead
31 | *
32 | * @return void
33 | */
34 | public function sendResponse()
35 | {
36 | return;
37 | }
38 |
39 | /**
40 | * Not explicitly part of the Response interface but implicitly required by \Magento\Framework\View\Result\Page
41 | *
42 | * @param string $value
43 | * @return $this
44 | */
45 | public function appendBody($value)
46 | {
47 | $this->body .= $value;
48 | return $this;
49 | }
50 |
51 | /**
52 | * @param Medium $medium
53 | */
54 | public function printTo(Medium $medium)
55 | {
56 | $medium->printHtml($this->body, $this->pdfOptions);
57 | }
58 |
59 | /**
60 | * Set HTTP response code
61 | *
62 | * @param int $code
63 | * @return void
64 | */
65 | public function setHttpResponseCode($code)
66 | {
67 | }
68 |
69 | /**
70 | * Get HTTP response code
71 | *
72 | * @return int
73 | */
74 | public function getHttpResponseCode()
75 | {
76 | return 200;
77 | }
78 |
79 | /**
80 | * Set a header
81 | *
82 | * If $replace is true, replaces any headers already defined with that $name.
83 | *
84 | * @param string $name
85 | * @param string $value
86 | * @param boolean $replace
87 | * @return self
88 | */
89 | public function setHeader($name, $value, $replace = false)
90 | {
91 | return $this;
92 | }
93 |
94 | /**
95 | * Get header value by name
96 | *
97 | * Returns first found header by passed name.
98 | * If header with specified name was not found returns false.
99 | *
100 | * @param string $name
101 | * @return \Zend\Http\Header\HeaderInterface|bool
102 | */
103 | public function getHeader($name)
104 | {
105 | return false;
106 | }
107 |
108 | /**
109 | * Remove header by name from header stack
110 | *
111 | * @param string $name
112 | * @return self
113 | */
114 | public function clearHeader($name)
115 | {
116 | return $this;
117 | }
118 |
119 | /**
120 | * Allow granular setting of HTTP response status code, version and phrase
121 | *
122 | * For example, a HTTP response as the following:
123 | * HTTP 200 1.1 Your response has been served
124 | * Can be set with the arguments
125 | * $httpCode = 200
126 | * $version = 1.1
127 | * $phrase = 'Your response has been served'
128 | *
129 | * @param int|string $httpCode
130 | * @param null|int|string $version
131 | * @param null|string $phrase
132 | * @return self
133 | */
134 | public function setStatusHeader($httpCode, $version = null, $phrase = null)
135 | {
136 | return $this;
137 | }
138 |
139 | /**
140 | * Set the response body to the given value
141 | *
142 | * Any previously set contents will be replaced by the new content.
143 | *
144 | * @param string $value
145 | * @return self
146 | */
147 | public function setBody($value)
148 | {
149 | return $this;
150 | }
151 |
152 | /**
153 | * Set redirect URL
154 | *
155 | * Sets Location header and response code. Forces replacement of any prior redirects.
156 | *
157 | * @param string $url
158 | * @param int $code
159 | * @return self
160 | */
161 | public function setRedirect($url, $code = 302)
162 | {
163 | return $this;
164 | }
165 | }
--------------------------------------------------------------------------------
/Model/View/PageResultWithoutHttp.php:
--------------------------------------------------------------------------------
1 | render($response);
22 |
23 | $this->eventManager->dispatch('layout_render_before');
24 | \Magento\Framework\Profiler::stop('layout_render');
25 | \Magento\Framework\Profiler::stop('LAYOUT');
26 | return $this;
27 | }
28 |
29 | /**
30 | * @todo replace $this->pageConfigRenderer with specialized renderer to replace http:// URLs with file:// URLs
31 | * @todo for optimized rendering of local resources
32 | * @todo (similar to \Magento\Developer\Model\View\Page\Config\ClientSideLessCompilation\Renderer)
33 | */
34 | protected function initPageConfigReader()
35 | {
36 | parent::initPageConfigReader();
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/Model/View/PdfResult.php:
--------------------------------------------------------------------------------
1 | pdfResponseFactory = $pdfResponseFactory;
40 | $this->pdfFactory = $pdfFactory;
41 | $this->pdfGlobalOptions = $pdfOptionsFactory->create();
42 | $this->pdfPageOptions = $pdfOptionsFactory->create();
43 | $this->pageResult = $pageResult;
44 | }
45 |
46 | /**
47 | * Set filename for download. If null, Content-Disposition header is not sent,
48 | * i.e. download will not be forced and PDF may be displayed in browser
49 | *
50 | * @param $filename
51 | */
52 | public function setFilename($filename)
53 | {
54 | $this->filename = $filename;
55 | }
56 |
57 | public function addGlobalOptions(Options $options)
58 | {
59 | $this->pdfGlobalOptions = $this->pdfGlobalOptions->merge($options);
60 | }
61 |
62 | public function addPageOptions(Options $options)
63 | {
64 | $this->pdfPageOptions = $this->pdfPageOptions->merge($options);
65 | }
66 |
67 | /**
68 | * Renders directly to HTTP response
69 | *
70 | * @param HttpResponseInterface $response
71 | * @return $this
72 | */
73 | protected function render(HttpResponseInterface $response)
74 | {
75 | $this->preparePdfResponse($response, $this->renderPdf());
76 |
77 | return $this;
78 | }
79 |
80 | /**
81 | * @return PdfResponse
82 | */
83 | public function renderSourceDocument()
84 | {
85 | /** @var PdfResponse $pdfResponse */
86 | $pdfResponse = $this->pdfResponseFactory->create([
87 | PdfResponse::PARAM_OPTIONS => $this->pdfPageOptions
88 | ]);
89 | /*
90 | * As of Magento 2.1, addDefaultHandle() must be called after instantiating a layout result,
91 | * see \Magento\Framework\Controller\ResultFactory::create()
92 | *
93 | * But since it is marked as a temporary solution, this might change in a later Magento release
94 | */
95 | $this->pageResult->addDefaultHandle();
96 |
97 | $this->pageResult->renderNonHttpResult($pdfResponse);
98 | return $pdfResponse;
99 | }
100 |
101 | /**
102 | * @return string
103 | */
104 | protected function renderPdf()
105 | {
106 | $pdf = $this->pdfFactory->create();
107 | $pdf->addOptions($this->pdfGlobalOptions);
108 | $pdfResponse = $this->renderSourceDocument();
109 | $pdf->appendContent($pdfResponse);
110 | $body = $pdf->file()->toString();
111 | return $body;
112 | }
113 |
114 | /**
115 | * @param Framework\App\Response\Http $response
116 | * @param $body
117 | * @return void
118 | */
119 | protected function preparePdfResponse(Framework\App\Response\Http $response, $body)
120 | {
121 | $response->setHeader('Content-type', 'application/pdf', true);
122 | $response->setHeader('Content-Length', \strlen($body));
123 | $response->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true);
124 | $response->setHeader('Last-Modified', \date('r'), true);
125 | if (null !== $this->filename) {
126 | $response->setHeader('Content-Disposition', 'attachment; filename="' . $this->filename . '"', true);
127 | }
128 | $response->appendBody($body);
129 | }
130 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Magento 2 PDF generator
2 |
3 | [](http://www.repostatus.org/#abandoned)
4 |
5 | Magento 2 module to ease the pdf generation using [wkhtmltopdf](https://wkhtmltopdf.org/) features
6 |
7 | ## Installation
8 |
9 | ```
10 | composer require "staempfli/magento2-module-pdf":"~1.0"
11 | ```
12 |
13 | ## Setup
14 |
15 | ### Install wkhtmltopdf
16 | This module needs [wkhtmltopdf](https://wkhtmltopdf.org/) installed on your computer. You can download and install it from here:
17 |
18 | * [https://wkhtmltopdf.org/downloads.html](https://wkhtmltopdf.org/downloads.html)
19 |
20 | NOTE: Do not install it using `apt-get` on Linux systems. See [troubleshooting](#troubleshooting) section for more info.
21 |
22 | ### Module configuration
23 |
24 | `Stores > Configuration > Advanced PDF Generation`
25 |
26 | 
27 |
28 |
29 | ## Usage
30 |
31 | This module can generate a PDF from any `frontControllerAction`
32 |
33 | 1. Create you Controller path with the correspinding Blocks and `.phtml` templates
34 |
35 | ```
36 |
37 |
38 |
39 |
40 |
41 |
42 | ```
43 |
44 | 2. Return pdf instance on Controller
45 |
46 | ```
47 | renderPdfResult();
59 | }
60 |
61 | protected function renderPdfResult()
62 | {
63 | /** @var PdfResult $result */
64 | $result = $this->resultFactory->create(PdfResult::TYPE);
65 | $result->addGlobalOptions(
66 | new PdfOptions(
67 | [
68 | PdfOptions::KEY_GLOBAL_TITLE => __('Return PDF'),
69 | PdfOptions::KEY_PAGE_ENCODING => PdfOptions::ENCODING_UTF_8,
70 | PdfOptions::KEY_GLOBAL_ORIENTATION => PdfOptions::ORIENTATION_PORTRAIT,
71 | PdfOptions::FLAG_PAGE_PRINT_MEDIA_TYPE,
72 | ]
73 | )
74 | );
75 | $result->addPageOptions(
76 | new PdfOptions(
77 | [
78 | PdfOptions::KEY_PAGE_COOKIES => ${'_COOKIE'},
79 | ]
80 | )
81 | );
82 | return $result;
83 | }
84 | }
85 | ```
86 |
87 | ### Header and Footer
88 |
89 | Header and footer html can be added as follows:
90 |
91 | 1. Create your `header.html` and `footer.html` files inside your Module template dir
92 |
93 | ```
94 |
95 |
96 |
97 |
98 | Header text
99 |
100 |
101 | ```
102 | ```
103 |
104 |
105 |
106 |
107 | Footer text
108 |
109 |
110 | ```
111 |
112 | 2. Attach those files to the Controller pdf generation
113 |
114 | ```
115 | templateResolver = $templateResolver;
136 | }
137 |
138 | public function execute()
139 | {
140 | return $this->renderPdfResult();
141 | }
142 |
143 | protected function renderPdfResult()
144 | {
145 | /** @var PdfResult $result */
146 | $result = $this->resultFactory->create(PdfResult::TYPE);
147 | $result->addGlobalOptions(
148 | new PdfOptions(
149 | [
150 | PdfOptions::KEY_GLOBAL_TITLE => __('Return PDF'),
151 | PdfOptions::KEY_PAGE_ENCODING => PdfOptions::ENCODING_UTF_8,
152 | PdfOptions::KEY_GLOBAL_ORIENTATION => PdfOptions::ORIENTATION_PORTRAIT,
153 | PdfOptions::FLAG_PAGE_PRINT_MEDIA_TYPE,
154 | PdfOptions::KEY_PAGE_HEADER_SPACING => "10",
155 | ]
156 | )
157 | );
158 | $result->addPageOptions(
159 | new PdfOptions(
160 | [
161 | PdfOptions::KEY_PAGE_COOKIES => ${'_COOKIE'},
162 | PdfOptions::KEY_PAGE_HEADER_HTML_URL => $this->templateResolver
163 | ->getTemplateFileName('Vendor_Package::pdf/header.html'),
164 | PdfOptions::KEY_PAGE_FOOTER_HTML_URL => $this->templateResolver
165 | ->getTemplateFileName('Vendor_ Package::pdf/footer.html'),
166 | ]
167 | )
168 | );
169 | return $result;
170 | }
171 | }
172 | ```
173 |
174 | **NOTE:** Only the header or only the footer as `html` is not possible, either both or none. Otherwise top and bottom margins are ignored.
175 |
176 | ## Troubleshooting
177 |
178 | ### The switch --print-media, is not support using unpatched qt:
179 |
180 | * `wkhtmltopdf` should not be installed via apt-get. See:
181 | * http://stackoverflow.com/questions/18758589/wkhtmltopdf-installation-error-on-ubuntu
182 |
183 | ### Tiny or very small output on Mac:
184 |
185 | * It seems to be a bug on `wkhtmltopdf` version 0.12.4. It can be fixed by installing 0.12.3
186 | * https://stackoverflow.com/questions/40814680/wkhtmltopdf-generates-tiny-output-on-mac
187 |
188 | ## Requirements
189 | - PHP >= 7.0.*
190 | - Magento >= 2.1.*
191 |
192 | ## Support
193 | If you have any issues with this extension, open an issue on [GitHub](https://github.com/staempfli/magento2-module-pdf/issues).
194 |
195 | ## Contribution
196 | Any contribution is highly appreciated. The best way to contribute code is to open a [pull request on GitHub](https://help.github.com/articles/using-pull-requests).
197 |
198 | ## Developer
199 | Staempfli Webteam, [Fabian Schmengler, integer_net](https://github.com/schmengler) and all other [contributors](https://github.com/staempfli/magento2-module-pdf/contributors)
200 |
201 | ## License
202 | [Open Software License ("OSL") v. 3.0](https://opensource.org/licenses/OSL-3.0)
203 |
204 | ## Copyright
205 | (c) 2018, Stämpfli AG
206 |
--------------------------------------------------------------------------------
/Service/FakePdfEngine.php:
--------------------------------------------------------------------------------
1 | fakePdfFile = new FakePdfFile();
35 | }
36 |
37 | public function addPage($html, Options $options)
38 | {
39 | $this->pages[] = [$html, $options];
40 | }
41 |
42 | public function addCover($html, Options $options)
43 | {
44 | $this->cover = [$html, $options];
45 | }
46 |
47 | public function addTableOfContents(Options $options)
48 | {
49 | $this->tableOfContents = $options;
50 | }
51 |
52 | /**
53 | * @param Options $globalOptions
54 | * @param Medium $cover
55 | * @param Medium[] ...$pages
56 | * @return FakePdfFile
57 | */
58 | public function generatePdf(Options $globalOptions)
59 | {
60 | $this->globalOptions = $globalOptions;
61 | $this->fakePdfFile->isGenerated = true;
62 | return $this->fakePdfFile;
63 | }
64 | }
--------------------------------------------------------------------------------
/Service/FakePdfFile.php:
--------------------------------------------------------------------------------
1 | contents = $contents;
23 | }
24 |
25 | public function saveAs($path)
26 | {
27 | \file_put_contents($path, $this->contents);
28 | }
29 |
30 | public function send()
31 | {
32 | $this->isSent = true;
33 | }
34 |
35 | public function toString()
36 | {
37 | return $this->contents;
38 | }
39 |
40 | /**
41 | * @return bool
42 | */
43 | public function isSent()
44 | {
45 | return $this->isSent;
46 | }
47 | }
--------------------------------------------------------------------------------
/Service/FakeSourceDocument.php:
--------------------------------------------------------------------------------
1 | html = $html;
24 | $this->options = $options;
25 | }
26 |
27 | /**
28 | * @param Medium $medium
29 | * @return void
30 | */
31 | public function printTo(Medium $medium)
32 | {
33 | $medium->printHtml($this->html, $this->options);
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/Service/Pdf.php:
--------------------------------------------------------------------------------
1 | engine = $engine;
26 | $this->options = new PdfOptions();
27 | }
28 |
29 | /**
30 | * Replace options
31 | *
32 | * @param Options $options
33 | */
34 | public function setOptions(Options $options)
35 | {
36 | $this->options = $options;
37 | }
38 |
39 | /**
40 | * Add/override options
41 | *
42 | * @param Options $options
43 | */
44 | public function addOptions(Options $options)
45 | {
46 | $this->options = $this->options->merge($options);
47 | }
48 |
49 | /**
50 | * All options that can be specified for a page object can also be specified for
51 | * a toc, further more the options from the TOC Options section can also be
52 | * applied. The table of content is generated via XSLT which means that it can be
53 | * styled to look however you want it to look. To get an aide of how to do this
54 | * you can dump the default xslt document by supplying the
55 | * --dump-default-toc-xsl, and the outline it works on by supplying
56 | * --dump-outline, see the Outline Options section.
57 | *
58 | * @see http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
59 | * @param Options $tocOptions
60 | */
61 | public function appendTableOfContents(Options $tocOptions)
62 | {
63 | $toc = new PdfTableOfContents($this->engine);
64 | $toc->printToc($tocOptions);
65 | }
66 |
67 | /**
68 | * A page object puts the content of a singe webpage into the output document.
69 | *
70 | * @see http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
71 | * @param SourceDocument $source
72 | */
73 | public function appendContent(SourceDocument $source)
74 | {
75 | $source->printTo(new PdfAppendContent($this->engine));
76 | }
77 |
78 | /**
79 | * A cover object puts the content of a singe webpage into the output document,
80 | * the page does not appear in the table of content, and does not have headers
81 | * and footers.
82 | *
83 | * @see http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
84 | * @param SourceDocument $source
85 | */
86 | public function appendCover(SourceDocument $source)
87 | {
88 | $source->printTo(new PdfCover($this->engine));
89 | }
90 | /**
91 | * @return PdfFile
92 | */
93 | public function file()
94 | {
95 | return $this->engine->generatePdf($this->options);
96 | }
97 | }
--------------------------------------------------------------------------------
/Service/PdfAppendContent.php:
--------------------------------------------------------------------------------
1 | pdfEngine = $pdfEngine;
22 | }
23 |
24 | public function printHtml($html, Options $options)
25 | {
26 | $this->pdfEngine->addPage($html, $options);
27 | }
28 | }
--------------------------------------------------------------------------------
/Service/PdfCover.php:
--------------------------------------------------------------------------------
1 | pdfEngine = $pdfEngine;
22 | }
23 |
24 | public function printHtml($html, Options $options)
25 | {
26 | $this->pdfEngine->addCover($html, $options);
27 | }
28 | }
--------------------------------------------------------------------------------
/Service/PdfOptions.php:
--------------------------------------------------------------------------------
1 | value pair. Example:
16 | *
17 | * [
18 | * PdfOptions::FLAG_PAGE_PRINT_MEDIA_TYPE,
19 | * PdfOptions::KEY_PAGE_NUMBER_OFFSET => -1,
20 | * ]
21 | *
22 | */
23 | final class PdfOptions extends ArrayObject implements Options
24 | {
25 | // global options
26 | const KEY_GLOBAL_BINARY = 'binary';
27 | const KEY_GLOBAL_VERSION9 = 'version9';
28 | const KEY_GLOBAL_TMP_DIR = 'tmpDir';
29 | const KEY_GLOBAL_IGNORE_WARNINGS = 'ignoreWarnings';
30 | /**
31 | * Additional CLI options, as array. See KEY_CLI_OPTIONS_* constants
32 | *
33 | * For more CLI options, refer to the public properties of
34 | *
35 | * \mikehaertl\shellcommand\Command
36 | */
37 | const KEY_GLOBAL_CLI_OPTIONS = 'commandOptions';
38 |
39 | const KEY_CLI_OPTIONS_ESCAPE_ARGS = 'escapeArgs';
40 | const KEY_CLI_OPTIONS_USE_EXEC = 'useExec';
41 | const KEY_CLI_OPTIONS_USE_XVFB_RUN = 'enableXvfb';
42 | const KEY_CLI_OPTIONS_XVFB_RUN_BINARY = 'xvfbRunBinary';
43 | const KEY_CLI_OPTIONS_XVFB_RUN_OPTIONS = 'xvfbRunOptions';
44 |
45 | /**
46 | * The title of the generated pdf file (The title of the first document is used if not specified)
47 | */
48 | const KEY_GLOBAL_TITLE = 'title';
49 | /**
50 | * Set paper size to: A4, Letter, etc. (default A4)
51 | */
52 | const KEY_GLOBAL_PAGE_SIZE = 'page-size';
53 | /**
54 | * Page height in mm, can be used instead of PAGE_SIZE for more control
55 | */
56 | const KEY_GLOBAL_PAGE_HEIGHT = 'page-height';
57 | /**
58 | * Page width in mm, can be used instead of PAGE_SIZE for more control
59 | */
60 | const KEY_GLOBAL_PAGE_WIDTH = 'page-width';
61 | /**
62 | * Set orientation to Landscape or Portrait (default Portrait)
63 | */
64 | const KEY_GLOBAL_ORIENTATION = 'orientation';
65 | /**
66 | * Bottom margin in mm (default 10)
67 | */
68 | const KEY_GLOBAL_MARGIN_BOTTOM = 'margin-bottom';
69 | /**
70 | * Left margin in mm (default 10)
71 | */
72 | const KEY_GLOBAL_MARGIN_LEFT = 'margin-left';
73 | /**
74 | * Right margin in mm (default 10)
75 | */
76 | const KEY_GLOBAL_MARGIN_RIGHT = 'margin-right';
77 | /**
78 | * Top margin in mm (default 10)
79 | */
80 | const KEY_GLOBAL_MARGIN_TOP = 'margin-top';
81 | /**
82 | * Put an outline (bookmarks) into the pdf (default)
83 | */
84 | const FLAG_GLOBAL_OUTLINE = 'outline';
85 | /**
86 | * Do not put an outline (bookmarks) into the pdf
87 | */
88 | const FLAG_GLOBAL_NO_OUTLINE = 'no-outline';
89 | /**
90 | * Set the depth of the outline (default 4)
91 | */
92 | const KEY_GLOBAL_OUTLINE_DEPTH = 'outline-depth';
93 | /**
94 | * Collate when printing multiple copies (default)
95 | */
96 | const FLAG_GLOBAL_COLLATE = 'collate';
97 | /**
98 | * Do not collate when printing multiple copies
99 | */
100 | const FLAG_GLOBAL_NO_COLLATE = 'no-collate';
101 | /**
102 | * Read and write cookies from and to the supplied cookie jar file
103 | */
104 | const KEY_GLOBAL_COOKIE_JAR = 'cookie-jar';
105 | /**
106 | * Number of copies to print into the pdf file (default 1)
107 | */
108 | const KEY_GLOBAL_COPIES = 'copies';
109 | /**
110 | * PDF will be generated in grayscale
111 | */
112 | const FLAG_GLOBAL_GRAYSCALE = 'grayscale';
113 | /**
114 | * When embedding images scale them down to this dpi (default 600)
115 | */
116 | const KEY_GLOBAL_IMAGE_DPI = 'image-dpi';
117 | /**
118 | * When jpeg compressing images use this quality (default 94)
119 | */
120 | const KEY_GLOBAL_IMAGE_QUALITY = 'image-quality';
121 | /**
122 | * Generates lower quality pdf/ps. Useful to shrink the result document space
123 | */
124 | const FLAG_GLOBAL_LOWQUALITY = 'lowquality';
125 | /**
126 | * Do not use lossless compression on pdf objects
127 | */
128 | const FLAG_GLOBAL_NO_PDF_COMPRESSION = 'no-pdf-compression';
129 |
130 | // page options
131 | /**
132 | * HTML header, should be URL or filename
133 | */
134 | const KEY_PAGE_HEADER_HTML_URL = 'header-html';
135 | /**
136 | * Header text printed in the top margin (not visible if margin is 0)
137 | */
138 | const KEY_PAGE_HEADER_TEXT_LEFT = 'header-left';
139 | /**
140 | * Header text printed in the top margin (not visible if margin is 0)
141 | */
142 | const KEY_PAGE_HEADER_TEXT_RIGHT = 'header-right';
143 | /**
144 | * Spacing between header and content in mm (default 0)
145 | */
146 | const KEY_PAGE_HEADER_SPACING = 'header-spacing';
147 | /**
148 | * Display line below the header
149 | */
150 | const FLAG_PAGE_HEADER_LINE = 'header-line';
151 | /**
152 | * Do not display line below the header (default)
153 | */
154 | const FLAG_PAGE_NO_HEADER_LINE = 'no-header-line';
155 | /**
156 | * Centered header text
157 | */
158 | const KEY_PAGE_HEADER_CENTER = 'header-center';
159 | /**
160 | * Set header font name (default Arial)
161 | */
162 | const KEY_PAGE_HEADER_FONT_NAME = 'header-font-name';
163 | /**
164 | * Set header font size (default 12)
165 | */
166 | const KEY_PAGE_HEADER_FONT_SIZE = 'header-font-size';
167 | /**
168 | * HTML footer, should be URL or filename
169 | */
170 | const KEY_PAGE_FOOTER_HTML_URL = 'footer-html';
171 | /**
172 | * Footer text printed in the bottom margin (not visible if margin is 0)
173 | */
174 | const KEY_PAGE_FOOTER_TEXT_LEFT = 'footer-left';
175 | /**
176 | * Footer text printed in the bottom margin (not visible if margin is 0)
177 | */
178 | const KEY_PAGE_FOOTER_TEXT_RIGHT = 'footer-right';
179 | /**
180 | * Spacing between footer and content in mm (default 0)
181 | */
182 | const KEY_PAGE_FOOTER_SPACING = 'footer-spacing';
183 | /**
184 | * Display line above the footer
185 | */
186 | const FLAG_PAGE_FOOTER_LINE = 'footer-line';
187 | /**
188 | * Do not display line above the footer (default
189 | */
190 | const FLAG_PAGE_NO_FOOTER_LINE = 'no-footer-line';
191 | /**
192 | * Centered footer text
193 | */
194 | const KEY_PAGE_FOOTER_CENTER = 'footer-center';
195 | /**
196 | * Set footer font name (default Arial)
197 | */
198 | const KEY_PAGE_FOOTER_FONT_NAME = 'footer-font-name';
199 | /**
200 | * Set footer font size (default 12)
201 | */
202 | const KEY_PAGE_FOOTER_FONT_SIZE = 'footer-font-size';
203 | /**
204 | * Array of additional replacements. ["foo" => "bar"] will replace "[foo]" with "bar"
205 | */
206 | const KEY_PAGE_HEADER_FOOTER_REPLACE_MAP = 'replace';
207 | /**
208 | * Run these additional javascript after the page is done loading (array of JS files)
209 | */
210 | const KEY_PAGE_RUN_SCRIPTS = 'run-script';
211 | /**
212 | * Set additional cookies (array of URL encoded values)
213 | */
214 | const KEY_PAGE_COOKIES = 'cookie';
215 | /**
216 | * Set the default text encoding
217 | */
218 | const KEY_PAGE_ENCODING = 'encoding';
219 | /**
220 | * Use print media-type instead of screen
221 | */
222 | const FLAG_PAGE_PRINT_MEDIA_TYPE = 'print-media-type';
223 | /**
224 | * Do not use print media-type instead of screen (default)
225 | */
226 | const FLAG_PAGE_NO_PRINT_MEDIA_TYPE = 'no-print-media-type';
227 | /**
228 | * HTTP Authentication username
229 | */
230 | const KEY_PAGE_HTTP_AUTH_USER = 'username';
231 | /**
232 | * HTTP Authentication password
233 | */
234 | const KEY_PAGE_HTTP_AUTH_PASSWORD = 'password';
235 | /**
236 | * Allowed conversion of a local file to read in other local files. (default)
237 | */
238 | const FLAG_PAGE_ENABLE_LOCAL_FILE_ACCESS = 'enable-local-file-access';
239 | /**
240 | * Do not allow conversion of a local file to read in other local files,
241 | * unless explicitly allowed with FLAG_PAGE_ALLOW
242 | */
243 | const FLAG_PAGE_DISABLE_LOCAL_FILE_ACCESS = 'disable-local-file-access';
244 | /**
245 | * Allow the file or files from the specified folder to be loaded (array of paths)
246 | */
247 | const FLAG_PAGE_ALLOW = 'allow';
248 | /**
249 | * Adjust the starting page number (default 0)
250 | */
251 | const KEY_PAGE_NUMBER_OFFSET = 'page-offset';
252 |
253 | /**
254 | * Do print background (default)
255 | */
256 | const FLAG_PAGE_BACKGROUND = 'background';
257 | /**
258 | * Do not print background
259 | */
260 | const FLAG_PAGE_NO_BACKGROUND = 'no-background';
261 | /**
262 | * Web cache directory
263 | */
264 | const KEY_PAGE_CACHE_DIR = 'cache-dir';
265 | /**
266 | * Use this SVG file when rendering checked checkboxes
267 | */
268 | const KEY_PAGE_CHECKBOX_CHECKED_SVG = 'checkbox-checked-svg';
269 | /**
270 | * Use this SVG file when rendering unchecked checkboxes
271 | */
272 | const KEY_PAGE_CHECKBOX_SVG = 'checkbox-svg';
273 | /**
274 | * Set additional HTTP headers (array with names as keys and values as values)
275 | */
276 | const KEY_PAGE_CUSTOM_HEADER = 'custom-header';
277 | /**
278 | * Add HTTP headers specified by KEY_PAGE_CUSTOM_HEADER for each resource request.
279 | */
280 | const FLAG_PAGE_CUSTOM_HEADER_PROPAGATION = 'custom-header-propagation';
281 | /**
282 | * Do not add HTTP headers specified by KEY_PAGE_CUSTOM_HEADER for each resource request.
283 | */
284 | const FLAG_PAGE_NO_CUSTOM_HEADER_PROPAGATION = 'no-custom-header-propagation';
285 | /**
286 | * Show javascript debugging output
287 | */
288 | const FLAG_PAGE_DEBUG_JAVASCRIPT = 'debug-javascript';
289 | /**
290 | * Do not show javascript debugging output (default)
291 | */
292 | const FLAG_PAGE_NO_DEBUG_JAVASCRIPT = 'no-debug-javascript';
293 | /**
294 | * Add a default header, with the name of the page to the left, and the page number to the right, this is short for:
295 | * --header-left='[webpage]'
296 | * --header-right='[page]/[toPage]'
297 | * --top 2cm
298 | * --header-line
299 | */
300 | const FLAG_PAGE_DEFAULT_HEADER = 'default-header';
301 | /**
302 | * Do not make links to remote web pages
303 | */
304 | const FLAG_PAGE_DISABLE_EXTERNAL_LINKS = 'disable-external-links';
305 | /**
306 | * Make links to remote web pages (default)
307 | */
308 | const FLAG_PAGE_ENABLE_EXTERNAL_LINKS = 'enable-external-links';
309 | /**
310 | * Do not turn HTML form fields into pdf form fields (default)
311 | */
312 | const FLAG_PAGE_DISABLE_FORMS = 'disable-forms';
313 | /**
314 | * Turn HTML form fields into pdf form fields
315 | */
316 | const FLAG_PAGE_ENABLE_FORMS = 'enable-forms';
317 | /**
318 | * Do load or print images (default)
319 | */
320 | const FLAG_PAGE_IMAGES = 'images';
321 | /**
322 | * Do not load or print images
323 | */
324 | const FLAG_PAGE_NO_IMAGES = 'no-images';
325 | /**
326 | * Do not make local links
327 | */
328 | const FLAG_PAGE_DISABLE_INTERNAL_LINKS = 'disable-internal-links';
329 | /**
330 | * Make local links (default)
331 | */
332 | const FLAG_PAGE_ENABLE_INTERNAL_LINKS = 'enable-internal-links';
333 | /**
334 | * Do not allow web pages to run javascript
335 | */
336 | const FLAG_PAGE_DISABLE_JAVASCRIPT = 'disable-javascript';
337 | /**
338 | * Do allow web pages to run javascript (default)
339 | */
340 | const FLAG_PAGE_ENABLE_JAVASCRIPT = 'enable-javascript';
341 | /**
342 | * Wait some milliseconds for javascript finish (default 200)
343 | */
344 | const KEY_PAGE_JAVASCRIPT_DELAY = 'javascript-delay';
345 | /**
346 | * Specify how to handle pages that fail to load: abort, ignore or skip (default abort)
347 | */
348 | const KEY_PAGE_LOAD_ERROR_HANDLING = 'load-error-handling';
349 | /**
350 | * Specify how to handle media files that fail to load: abort, ignore or skip (default ignore)
351 | */
352 | const KEY_PAGE_LOAD_MEDIA_ERROR_HANDLING = 'load-media-error-handling';
353 | /**
354 | * Minimum font size
355 | */
356 | const KEY_PAGE_MINIMUM_FONT_SIZE = 'minimum-font-size';
357 | /**
358 | * Do not include the page in the table of contents and outlines
359 | */
360 | const FLAG_PAGE_EXCLUDE_FROM_OUTLINE = 'exclude-from-outline';
361 | /**
362 | * Include the page in the table of contents and outlines (default)
363 | */
364 | const FLAG_PAGE_INCLUDE_IN_OUTLINE = 'include-in-outline';
365 | /**
366 | * Disable installed plugins (default)
367 | */
368 | const FLAG_PAGE_DISABLE_PLUGINS = 'disable-plugins';
369 | /**
370 | * Enable installed plugins (plugins will likely not work)
371 | */
372 | const FLAG_PAGE_ENABLE_PLUGINS = 'enable-plugins';
373 | /**
374 | * Add additional post fields (array)
375 | */
376 | const KEY_PAGE_POST = 'post';
377 | /**
378 | * Post additional files (array of paths)
379 | */
380 | const KEY_PAGE_POST_FILE = 'post-file';
381 | /**
382 | * Use a proxy
383 | */
384 | const KEY_PAGE_PROXY = 'proxy';
385 | /**
386 | * Use this SVG file when rendering checked radiobuttons
387 | */
388 | const KEY_PAGE_RADIOBUTTON_CHECKED_SVG = 'radiobutton-checked-svg';
389 | /**
390 | * Use this SVG file when rendering unchecked radiobuttons
391 | */
392 | const KEY_PAGE_RADIOBUTTON_SVG = 'radiobutton-svg';
393 | /**
394 | * Disable the intelligent shrinking strategy used by WebKit that makes the pixel/dpi ratio none constant
395 | */
396 | const FLAG_PAGE_DISABLE_SMART_SHRINKING = 'disable-smart-shrinking';
397 | /**
398 | * Enable the intelligent shrinking strategy used by WebKit that makes the pixel/dpi ratio none constant (default)
399 | */
400 | const FLAG_PAGE_ENABLE_SMART_SHRINKING = 'enable-smart-shrinking';
401 | /**
402 | * Stop slow running javascripts (default)
403 | */
404 | const FLAG_PAGE_STOP_SLOW_SCRIPTS = 'stop-slow-scripts';
405 | /**
406 | * Do not Stop slow running javascripts
407 | */
408 | const FLAG_PAGE_NO_STOP_SLOW_SCRIPTS = 'no-stop-slow-scripts';
409 | /**
410 | * Do not link from section header to toc (default)
411 | */
412 | const FLAG_PAGE_DISABLE_TOC_BACK_LINKS = 'disable-toc-back-links';
413 | /**
414 | * Link from section header to toc
415 | */
416 | const FLAG_PAGE_ENABLE_TOC_BACK_LINKS = 'enable-toc-back-links';
417 | /**
418 | * Specify a user style sheet, to load with every page
419 | */
420 | const KEY_PAGE_USER_STYLE_SHEET = 'user-style-sheet';
421 | /**
422 | * Set viewport size if you have custom scrollbars or css attribute overflow to emulate window size
423 | */
424 | const KEY_PAGE_VIEWPORT_SIZE = 'viewport-size';
425 | /**
426 | * Wait until window.status is equal to this string before rendering page
427 | */
428 | const KEY_PAGE_WINDOW_STATUS = 'window-status';
429 | /**
430 | * Use this zoom factor (default 1)
431 | */
432 | const KEY_PAGE_ZOOM = 'zoom';
433 |
434 | // toc options
435 | /**
436 | * The header text of the toc (default "Table of Contents")
437 | */
438 | const KEY_TOC_HEADER_TEXT = 'toc-header-text';
439 | /**
440 | * Use the supplied xsl style sheet for printing the table of content
441 | */
442 | const KEY_TOC_XSL_STYLE_SHEET = 'xsl-style-sheet';
443 | /**
444 | * For each level of headings in the toc indent by this length (default 1em)
445 | */
446 | const FLAG_TOC_LEVEL_INDENTATION = 'toc-level-indentation';
447 | /**
448 | * Do not use dotted lines in the toc
449 | */
450 | const FLAG_TOC_DISABLE_DOTTED_LINES = 'disable-dotted-lines';
451 | /**
452 | * Do not link from toc to sections
453 | */
454 | const FLAG_TOC_DISABLE_LINKS = 'disable-toc-links';
455 |
456 | // option values
457 | const PAGE_SIZE_DIN_A4 = 'A4';
458 | const PAGE_SIZE_LETTER = 'letter';
459 | const ORIENTATION_PORTRAIT = 'Portrait';
460 | const ORIENTATION_LANDSCAPE = 'Landscape';
461 | const ENCODING_UTF_8 = 'UTF-8';
462 | const LOAD_ON_ERROR_ABORT = 'abort';
463 | const LOAD_ON_ERROR_IGNORE = 'ignore';
464 | const LOAD_ON_ERROR_SKIP = 'skip';
465 | /**
466 | * PdfOptions constructor.
467 | *
468 | * Overridden from ArrayObject for default input parameter. This prevents error
469 | *
470 | * Passed variable is not an array or object, using empty array instead
471 | *
472 | * while used with Magento 2 object manager
473 | *
474 | * @param array $input
475 | * @param int $flags
476 | * @param string $iteratorClass
477 | */
478 | public function __construct($input = [], $flags = 0, $iteratorClass = \ArrayIterator::class)
479 | {
480 | parent::__construct($input, $flags, $iteratorClass);
481 | }
482 |
483 | /**
484 | * @param Options $newOptions
485 | * @return PdfOptions
486 | */
487 | public function merge(Options $newOptions)
488 | {
489 | $merged = clone $this;
490 | foreach ($newOptions as $key => $value) {
491 | $merged[$key] = $value;
492 | }
493 | return $merged;
494 | }
495 |
496 | public function asArray()
497 | {
498 | return (array) $this;
499 | }
500 |
501 | }
--------------------------------------------------------------------------------
/Service/PdfTableOfContents.php:
--------------------------------------------------------------------------------
1 | pdfEngine = $pdfEngine;
21 | }
22 |
23 | public function printToc(Options $options)
24 | {
25 | $this->pdfEngine->addTableOfContents($options);
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/Test/Integration/ConfigTest.php:
--------------------------------------------------------------------------------
1 | uri = 'backend/admin/system_config/edit';
23 | $this->resource = 'Staempfli_Pdf::config_staempfli_pdf';
24 | $this->getRequest()->setParam('section', 'staempfli_pdf');
25 | }
26 |
27 | protected function setUp()
28 | {
29 | parent::setUp();
30 | $this->objectManager = ObjectManager::getInstance();
31 | $this->setUpRequest();
32 | }
33 | /**
34 | * Overridden to make depends annotation work
35 | */
36 | public function testAclHasAccess()
37 | {
38 | parent::testAclHasAccess();
39 | // we do not pass the response as parameter to dependent test because PHPUnit would take ages
40 | // to go through the object tree, looking for mock objects.
41 | self::$lastResponse = $this->getResponse();
42 | }
43 | /**
44 | * Overridden to check for redirect instead of "Forbidden" response
45 | */
46 | public function testAclNoAccess()
47 | {
48 | if ($this->resource === null) {
49 | $this->markTestIncomplete('Acl test is not complete');
50 | }
51 | $this->_objectManager->get('Magento\Framework\Acl\Builder')
52 | ->getAcl()
53 | ->deny(null, $this->resource);
54 | $this->dispatch($this->uri);
55 | $this->assertSame(302, $this->getResponse()->getHttpResponseCode());
56 | $this->assertContains('/index.php/backend/admin/system_config/index/', $this->getResponse()->getHeader('location')->toString());
57 | }
58 | /**
59 | * @depends testAclHasAccess
60 | */
61 | public function testConfigSectionLoads()
62 | {
63 | $response = self::$lastResponse;
64 | $this->assertEquals(200, $response->getStatusCode(), 'HTTP Status Code');
65 | }
66 | }
--------------------------------------------------------------------------------
/Test/Integration/PdfResultTest.php:
--------------------------------------------------------------------------------
1 | objectManager = ObjectManager::getInstance();
19 | $this->pdfResult = $this->objectManager->create(PdfResult::class);
20 | }
21 |
22 | /**
23 | * @magentoAppIsolation enabled
24 | * @magentoAppArea frontend
25 | */
26 | public function testRenderPdfResponse()
27 | {
28 | /** @var HttpResponse $response */
29 | $response = $this->objectManager->create(HttpResponse::class);
30 | $this->objectManager->get(Page::class)->addDefaultHandle();
31 | $this->pdfResult->setFilename('testpdf.pdf');
32 | $this->pdfResult->renderResult($response);
33 |
34 | $this->assertResponseHeader($response, 'Content-type', $this->equalTo('application/pdf'));
35 | $this->assertResponseHeader($response, 'Content-length', $this->greaterThan(1000), 'Content-length header should be at least 1K');
36 | $this->assertResponseHeader($response, 'Cache-Control', $this->equalTo('must-revalidate, post-check=0, pre-check=0'));
37 | $this->assertResponseHeader($response, 'Last-Modified', $this->callback(function($lastModified) {
38 | return \strtotime($lastModified) > \time() - 10;
39 | }), 'Last-Modified header should be about right now');
40 | $this->assertResponseHeader($response, 'Content-Disposition', $this->equalTo('attachment; filename="testpdf.pdf"'));
41 | $this->assertStringStartsWith('%PDF-', $response->getBody(), 'body should contain the PDF header');
42 | }
43 |
44 | /**
45 | * @param HttpResponse $response
46 | * @param string $field
47 | * @param \PHPUnit_Framework_Constraint $constraint
48 | */
49 | private function assertResponseHeader(HttpResponse $response, $field, \PHPUnit_Framework_Constraint $constraint, $message = '')
50 | {
51 | $this->assertNotFalse($response->getHeader($field), "{$field} header should be present");
52 | $this->assertThat($response->getHeader($field)->getFieldValue(), $constraint, $message);
53 | }
54 | }
--------------------------------------------------------------------------------
/Test/Integration/PdfTemplateTest.php:
--------------------------------------------------------------------------------
1 | objectManager = ObjectManager::getInstance();
24 | $this->layout = $this->objectManager->get(Layout::class);
25 | }
26 |
27 | /**
28 | * @magentoAppIsolation enabled
29 | * @magentoAppArea frontend
30 | */
31 | public function testPrint()
32 | {
33 | $fakePdfEngine = new FakePdfEngine();
34 | /** @var PdfTemplate $pdfTemplate */
35 | $pdfTemplate = $this->layout->createBlock(PdfTemplate::class, 'pdftest-container');
36 | $pdfTemplate->addChild('pdftest-element', Text::class, ['text' => 'test']);
37 | $pdfTemplate->printTo(new PdfAppendContent($fakePdfEngine));
38 |
39 | $this->assertEquals(
40 | [['test', new PdfOptions([])]],
41 | $fakePdfEngine->pages
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Test/Integration/WkPdfEngineTest.php:
--------------------------------------------------------------------------------
1 | objectManager = ObjectManager::getInstance();
22 | $this->pdfFactory = $this->objectManager->create(PdfFactory::class);
23 |
24 | $wkhtmltopdfBinary = $this->getWkhtmltopdfBinary();
25 | if (! $this->isExecutable($wkhtmltopdfBinary)) {
26 | $this->markTestSkipped(
27 | sprintf(
28 | 'wkhtmltopdf binary not found at %s. If it is installed in a different location, '.
29 | 'please adjust the configuration.',
30 | $wkhtmltopdfBinary
31 | )
32 | );
33 | }
34 | }
35 | public function testGeneratePdf()
36 | {
37 | $pdf = $this->pdfFactory->create();
38 | $pdf->appendCover(new FakeSourceDocument('BIG COVER', new PdfOptions([])));
39 | $pdf->appendTableOfContents(new PdfOptions([]));
40 | $pdf->appendContent(new FakeSourceDocument('print me!', new PdfOptions([])));
41 | $this->assertStringStartsWith('%PDF-', $pdf->file()->toString(), 'binary string should contain the PDF header');
42 | }
43 |
44 | public function testExceptionOnToString()
45 | {
46 | $this->setExpectedException(WkPdfException::class);
47 | $this->createInvalidPdf()->file()->toString();
48 | }
49 |
50 | public function testExceptionOnSend()
51 | {
52 | $this->setExpectedException(WkPdfException::class);
53 | $this->createInvalidPdf()->file()->send();
54 | }
55 |
56 | public function testExceptionOnSave()
57 | {
58 | $this->setExpectedException(WkPdfException::class);
59 | $this->createInvalidPdf()->file()->saveAs(\tempnam(\sys_get_temp_dir(), 'pdftest'));
60 | }
61 |
62 | /**
63 | * This test saves the generated file in tmp/staempfli_pdf.pdf for manual inspection
64 | */
65 | public function testSaveGeneratedPdfFile()
66 | {
67 | /*
68 | * Direct HTML in header does not work, and with file I get
69 | *
70 | * > Error: Failed loading page file:///tmp/php_tmpfile_wjJkuX
71 | *
72 | * Restrict test to text headers
73 | */
74 | //$headerHtmlFile = new File('I am a HTML header');
75 | $pdf = $this->pdfFactory->create();
76 | $pdf->addOptions(new PdfOptions(
77 | [
78 | PdfOptions::KEY_GLOBAL_TITLE => 'TEST PDF',
79 | PdfOptions::KEY_PAGE_HEADER_SPACING => '10',
80 | PdfOptions::FLAG_PAGE_HEADER_LINE,
81 | PdfOptions::KEY_PAGE_HEADER_TEXT_LEFT => 'HEADER',
82 | PdfOptions::KEY_PAGE_HEADER_TEXT_RIGHT => 'Page [page]',
83 | PdfOptions::KEY_GLOBAL_IGNORE_WARNINGS => true,
84 | PdfOptions::KEY_PAGE_FOOTER_TEXT_LEFT => 'Hello, I am a footer.',
85 | PdfOptions::KEY_PAGE_FOOTER_TEXT_RIGHT => 'I am text based.'
86 | ]
87 | ));
88 | $pdf->appendCover(new FakeSourceDocument('BIG COVER', new PdfOptions([])));
89 | $pdf->appendTableOfContents(new PdfOptions([
90 | ]));
91 | $contentHtml = <<<'HTML'
92 |
93 |
94 |
95 |
96 |
97 | Überschrift
einsÜberschrift
zweiUnterüberschrift
drei
98 | HTML;
99 | $pdf->appendContent(new FakeSourceDocument($contentHtml, new PdfOptions([])));
100 | $pdf->file()->saveAs('tmp/staempflitest.pdf');
101 | $this->assertNotEmpty(\realpath('tmp/staempflitest.pdf'), 'File tmp/staempflitest.pdf should be saved');
102 | $this->assertGreaterThan(10000, \filesize(\realpath('tmp/staempflitest.pdf')), 'File tmp/staempflitest.pdf should be > 10 KB');
103 | }
104 |
105 | /**
106 | * @param $command
107 | * @return bool
108 | * @SuppressWarnings(PHPMD.UnusedLocalVariable) // https://phpmd.org/rules/index.html
109 | */
110 | private function isExecutable($command)
111 | {
112 | exec($command . ' 1> /dev/null 2> /dev/null', $output, $return);
113 | return $return <= 1;
114 | }
115 |
116 | /**
117 | * @return mixed|string
118 | */
119 | private function getWkhtmltopdfBinary()
120 | {
121 | /** @var ScopeConfigInterface $config */
122 | $config = $this->objectManager->get(ScopeConfigInterface::class);
123 | $wkhtmltopdfBinary = $config->getValue(Config::XML_PATH_BINARY);
124 | if (empty($wkhtmltopdfBinary)) {
125 | return 'wkhtmltopdf';
126 | }
127 | return $wkhtmltopdfBinary;
128 | }
129 |
130 | /**
131 | * @return \Staempfli\Pdf\Service\Pdf
132 | */
133 | private function createInvalidPdf()
134 | {
135 | $pdf = $this->pdfFactory->create();
136 | $pdf->setOptions(
137 | new PdfOptions(
138 | [
139 | 'gibberish' => 'pffflllltt',
140 | ]
141 | )
142 | );
143 | return $pdf;
144 | }
145 | }
--------------------------------------------------------------------------------
/Test/Unit/Model/PdfFactoryTest.php:
--------------------------------------------------------------------------------
1 | fakePdfEngine = new FakePdfEngine();
24 | $this->stubConfig = $this->getMockBuilder(ScopeConfigInterface::class)->getMockForAbstractClass();
25 | }
26 |
27 | /**
28 | * @dataProvider dataScopeConfig
29 | * @param array $scopeConfigValueMap
30 | * @param array $expectedOptions
31 | */
32 | public function testOptionsFromConfig(array $scopeConfigValueMap, array $expectedOptions)
33 | {
34 | $this->stubConfig->method('getValue')->willReturnMap($scopeConfigValueMap);
35 | $pdfFactory = new PdfFactory($this->fakePdfEngine, $this->stubConfig);
36 |
37 | $pdfFactory->create()->file();
38 |
39 | $this->assertEquals(
40 | $expectedOptions,
41 | (array) $this->fakePdfEngine->globalOptions
42 | );
43 | }
44 | public static function dataScopeConfig()
45 | {
46 | $default = ScopeConfigInterface::SCOPE_TYPE_DEFAULT;
47 | return [
48 | 'default' => [
49 | 'scope_config_value_map' => [
50 | [Config::XML_PATH_BINARY, $default, null, null],
51 | [Config::XML_PATH_VERSION9, $default, null, null],
52 | [Config::XML_PATH_TMP_DIR, $default, null, null],
53 | [Config::XML_PATH_ESCAPE_ARGS, $default, null, null],
54 | [Config::XML_PATH_USE_EXEC, $default, null, null],
55 | [Config::XML_PATH_USE_XVFB_RUN, $default, null, null],
56 | [Config::XML_PATH_XVFB_RUN_BINARY, $default, null, null],
57 | [Config::XML_PATH_XVFB_RUN_OPTIONS, $default, null, null],
58 | ],
59 | 'expected_options' => [
60 | PdfOptions::KEY_GLOBAL_CLI_OPTIONS => [
61 | ]
62 | ]
63 | ],
64 | 'all' => [
65 | 'scope_config_value_map' => [
66 | [Config::XML_PATH_BINARY, $default, null, '/usr/bin/wkpdf2html'],
67 | [Config::XML_PATH_VERSION9, $default, null, true],
68 | [Config::XML_PATH_TMP_DIR, $default, null, '/tmp/wkpdf'],
69 | [Config::XML_PATH_ESCAPE_ARGS, $default, null, true],
70 | [Config::XML_PATH_USE_EXEC, $default, null, true],
71 | [Config::XML_PATH_USE_XVFB_RUN, $default, null, true],
72 | [Config::XML_PATH_XVFB_RUN_BINARY, $default, null, 'xvfb-run'],
73 | [Config::XML_PATH_XVFB_RUN_OPTIONS, $default, null, '--server-args="-screen 0, 1024x768x24"'],
74 | ],
75 | 'expected_options' => [
76 | PdfOptions::KEY_GLOBAL_BINARY => '/usr/bin/wkpdf2html',
77 | PdfOptions::KEY_GLOBAL_VERSION9 => true,
78 | PdfOptions::KEY_GLOBAL_TMP_DIR => '/tmp/wkpdf',
79 | PdfOptions::KEY_GLOBAL_CLI_OPTIONS => [
80 | PdfOptions::KEY_CLI_OPTIONS_ESCAPE_ARGS => true,
81 | PdfOptions::KEY_CLI_OPTIONS_USE_EXEC => true,
82 | PdfOptions::KEY_CLI_OPTIONS_USE_XVFB_RUN => true,
83 | PdfOptions::KEY_CLI_OPTIONS_XVFB_RUN_BINARY => 'xvfb-run',
84 | PdfOptions::KEY_CLI_OPTIONS_XVFB_RUN_OPTIONS => '--server-args="-screen 0, 1024x768x24"',
85 | ]
86 | ]
87 | ]
88 | ];
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Test/Unit/Model/PdfResultTest.php:
--------------------------------------------------------------------------------
1 | fakePdfEngine = new FakePdfEngine();
28 | $this->stubConfig = $this->getMockBuilder(ScopeConfigInterface::class)->getMockForAbstractClass();
29 | }
30 |
31 | public function testRenderWithOptions()
32 | {
33 | $pageResultMock = $this->getMockBuilder(PageResultWithoutHttp::class)->disableOriginalConstructor()->getMock();
34 | $pageResultMock->expects($this->once())
35 | ->method('renderNonHttpResult')
36 | ->with($this->isInstanceOf(PdfResponse::class))
37 | ->willReturnSelf();
38 | $optionsFactoryMock = $this->getMockBuilder(OptionsFactory::class)->disableOriginalConstructor()->setMethods(['create'])->getMock();
39 | $optionsFactoryMock->method('create')->willReturnCallback(function() { return new PdfOptions(); });
40 | $pdfResult = new PdfResult(
41 | $this->mockPdfResponseFactory(),
42 | new PdfFactory($this->fakePdfEngine, $this->stubConfig),
43 | $optionsFactoryMock,
44 | $pageResultMock
45 | );
46 |
47 | $pdfResult->addGlobalOptions(new PdfOptions(
48 | [
49 | PdfOptions::KEY_GLOBAL_ORIENTATION => PdfOptions::ORIENTATION_LANDSCAPE,
50 | ]
51 | ));
52 | $pdfResult->addPageOptions(new PdfOptions(
53 | [
54 | PdfOptions::KEY_PAGE_COOKIES => ['frontend' => '0123456789abcdef0123456789abcdef'],
55 | ]
56 | ));
57 | $pdfResult->renderResult($this->getMockBuilder(Http::class)->disableOriginalConstructor()->getMock());
58 |
59 | $this->assertEquals(new PdfOptions(
60 | [
61 | PdfOptions::KEY_GLOBAL_CLI_OPTIONS => [],
62 | PdfOptions::KEY_GLOBAL_ORIENTATION => PdfOptions::ORIENTATION_LANDSCAPE,
63 | ]
64 | ), $this->fakePdfEngine->globalOptions);
65 | $this->assertEquals(new PdfOptions(
66 | [
67 | PdfOptions::KEY_PAGE_COOKIES => ['frontend' => '0123456789abcdef0123456789abcdef'],
68 | ]
69 | ), $this->fakePdfEngine->pages[0][1]);
70 | }
71 |
72 | /**
73 | * @return PdfResponseFactory|\PHPUnit_Framework_MockObject_MockObject
74 | */
75 | private function mockPdfResponseFactory()
76 | {
77 | $pdfResponseFactory = $this->getMockBuilder(PdfResponseFactory::class)->disableOriginalConstructor()->setMethods(['create'])->getMock();
78 | $pdfResponseFactory->method('create')->willReturnCallback(
79 | function($data) {
80 | return new PdfResponse($data[PdfResponse::PARAM_OPTIONS]);
81 | }
82 | );
83 | return $pdfResponseFactory;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Test/Unit/Service/FakePdfFileTest.php:
--------------------------------------------------------------------------------
1 | fakePdfFile = new FakePdfFile();
16 | $this->assertFalse($this->fakePdfFile->isSent());
17 | }
18 |
19 | public function testSend()
20 | {
21 | $this->fakePdfFile->send();
22 | $this->assertTrue($this->fakePdfFile->isSent(), 'isSend flag should be set');
23 | }
24 |
25 | public function testToString()
26 | {
27 | $this->fakePdfFile->contents = 'this is a PDF';
28 | $this->assertEquals('this is a PDF', $this->fakePdfFile->toString());
29 | }
30 |
31 | public function testSaveAs()
32 | {
33 | $this->fakePdfFile->contents = 'save this totally real PDF';
34 | $vfsRoot = vfsStream::setUp('root');
35 | $this->fakePdfFile->saveAs(vfsStream::url('root/out.pdf'));
36 | $this->assertTrue($vfsRoot->hasChild('out.pdf'), 'file should be created');
37 | $this->assertInstanceOf(vfsStreamFile::class, $vfsRoot->getChild('out.pdf'));
38 | $this->assertEquals('save this totally real PDF', $vfsRoot->getChild('out.pdf')->getContent());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Test/Unit/Service/FakeSourceDocumentTest.php:
--------------------------------------------------------------------------------
1 | Hello World