├── .gitignore
├── code-of-conduct.md
├── composer.json
├── examples
├── .gitignore
├── async-amp-dom.php
├── async-react-webkit.php
├── async.php
├── sample.html
├── sync-amp-dom.php
└── sync-react-webkit.php
├── header
├── license.md
├── phpunit.xml
├── pre-commit
├── readme.md
├── scrutinizer.yml
├── src
├── Decorator.php
├── Driver.php
├── Driver
│ ├── BaseDriver.php
│ ├── DomDriver.php
│ ├── PrinceDriver.php
│ ├── SyncDriver.php
│ ├── Traits
│ │ ├── AppendsFooterTrait.php
│ │ ├── AppendsHeaderTrait.php
│ │ └── AppendsTrait.php
│ └── WebkitDriver.php
├── Factory.php
├── Runner.php
└── Runner
│ ├── AmpRunner.php
│ └── ReactRunner.php
└── tests
├── .gitignore
├── DomDriverTest.php
├── FactoryTest.php
├── PrinceDriverTest.php
├── RunnerTest.php
├── WebkitDriverTest.php
└── fixtures
└── sample.html
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/code-of-conduct.md:
--------------------------------------------------------------------------------
1 | # Our Pledge
2 |
3 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
4 |
5 | # Our Standards
6 |
7 | Examples of behavior that contributes to creating a positive environment include:
8 |
9 | * Using welcoming and inclusive language
10 | * Being respectful of differing viewpoints and experiences
11 | * Gracefully accepting constructive criticism
12 | * Focusing on what is best for the community
13 | * Showing empathy towards other community members
14 |
15 | Examples of unacceptable behavior by participants include:
16 |
17 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
18 | * Trolling, insulting/derogatory comments, and personal or political attacks
19 | * Public or private harassment
20 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
21 | * Other conduct which could reasonably be considered inappropriate in a professional setting
22 |
23 | # Our Responsibilities
24 |
25 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
26 |
27 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
28 |
29 | # Scope
30 |
31 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
32 |
33 | # Enforcement
34 |
35 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cgpitt@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
36 |
37 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
38 |
39 | # Attribution
40 |
41 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
42 |
43 | [homepage]: http://contributor-covenant.org
44 | [version]: http://contributor-covenant.org/version/1/4/
45 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "asyncphp/paper",
3 | "description": "Hassle-free HTML to PDF conversion abstraction library",
4 | "license": "MIT",
5 | "require": {
6 | "php": "^5.6|^7.0",
7 | "symfony/dom-crawler": "^3.2",
8 | "symfony/css-selector": "^3.2"
9 | },
10 | "require-dev": {
11 | "dompdf/dompdf": "^0.7.0",
12 | "friendsofphp/php-cs-fixer": "^2.0",
13 | "phpunit/phpunit": "^5.7",
14 | "amphp/loop": "dev-master",
15 | "amphp/parallel": "dev-master",
16 | "async-interop/event-loop": "^0.5.0",
17 | "react/event-loop": "^0.4.2",
18 | "react/child-process": "^0.4.1",
19 | "jeremeamia/superclosure": "^2.3"
20 | },
21 | "autoload": {
22 | "psr-4": {
23 | "AsyncPHP\\Paper\\": "src"
24 | }
25 | },
26 | "autoload-dev": {
27 | "classmap": [
28 | "tests"
29 | ]
30 | },
31 | "suggest": {
32 | "amphp/loop": "Needed for Amp Runner",
33 | "amphp/parallel": "Needed for Amp Runner",
34 | "async-interop/event-loop": "Needed for Amp Runner",
35 | "dompdf/dompdf": "Needed for DOMPDF Driver",
36 | "prince": "Needed for Prince Driver",
37 | "react/event-loop": "Needed for React Runner",
38 | "react/child-process": "Needed for React Runner",
39 | "jeremeamia/superclosure": "Needed for React Runner",
40 | "wkhtmltopdf": "Needed for WKHTMLtoPDF Driver"
41 | },
42 | "minimum-stability": "dev",
43 | "prefer-stable": true
44 | }
45 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | *.pdf
2 |
--------------------------------------------------------------------------------
/examples/async-amp-dom.php:
--------------------------------------------------------------------------------
1 | createDriver([
17 | "driver" => "dom",
18 | ]);
19 |
20 | $runner = $factory->createRunner([
21 | "runner" => "amp",
22 | ]);
23 |
24 | // this is an AsyncInterop\Promise...
25 | $promise = $driver
26 | ->body($sample)
27 | ->size("A4")
28 | ->orientation("portrait")
29 | ->dpi(300)
30 | ->render($runner);
31 |
32 | $results = yield $promise;
33 |
34 | // this too could be async...
35 | file_put_contents(__DIR__ . "/async-amp-dom.pdf", $results);
36 | }));
37 |
--------------------------------------------------------------------------------
/examples/async-react-webkit.php:
--------------------------------------------------------------------------------
1 | createDriver([
16 | "driver" => "webkit",
17 | ]);
18 |
19 | $runner = $factory->createRunner([
20 | "runner" => "react",
21 | ]);
22 |
23 | $loop = EventLoopFactory::create();
24 |
25 | // this is a React\ChildProcess\Process...
26 | $process = $driver
27 | ->body($sample)
28 | ->size("A4")
29 | ->orientation("portrait")
30 | ->dpi(300)
31 | ->render($runner);
32 |
33 | $process->on("exit", function() use ($loop) {
34 | $loop->stop();
35 | });
36 |
37 | $loop->addTimer(0.001, function($timer) use ($process) {
38 | $process->start($timer->getLoop());
39 |
40 | $process->stdout->on("data", function($output) {
41 | // this too could be async...
42 | file_put_contents(__DIR__ . "/async-react-webkit.pdf", $output);
43 | });
44 | });
45 |
46 | $loop->run();
47 |
--------------------------------------------------------------------------------
/examples/async.php:
--------------------------------------------------------------------------------
1 | createDriver([
21 | "driver" => "webkit",
22 | ]);
23 |
24 | $runner = $factory->createRunner([
25 | "runner" => "amp",
26 | ]);
27 |
28 | $promise = $driver
29 | ->body($sample)
30 | ->size("A4")
31 | ->orientation("portrait")
32 | ->dpi(300)
33 | ->render($runner);
34 |
35 | $results = yield $promise;
36 |
37 | print "done" . PHP_EOL;
38 | Loop::stop();
39 | }));
40 |
--------------------------------------------------------------------------------
/examples/sample.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
43 |
44 |
45 | Paper
46 | Hassle-free HTML to PDF conversion abstraction library.
47 | Installation
48 | composer require asyncphp/paper
49 | Usage
50 | // async
51 |
52 | $async = new DomDriver();
53 |
54 | $result = yield $async
55 | ->html($sample)
56 | ->size("B5")
57 | ->orientation("landscape")
58 | ->dpi(300)
59 | ->render();
60 |
61 | // sync
62 |
63 | $sync = new SyncDriver(new DomDriver());
64 |
65 | $result = $sync
66 | ->html($sample)
67 | ->size("B5")
68 | ->orientation("landscape")
69 | ->dpi(300)
70 | ->render();
71 | Supported drivers
72 |
73 |
74 |
75 | dompdf
76 | wkhtmltopdf
77 | prince
78 |
79 |
80 | Requires command-line utilities
81 | no
82 | yes
83 | yes
84 |
85 |
86 | Supports modern CSS
87 | no
88 | yes
89 | yes
90 |
91 |
92 | Supports modern JS
93 | no
94 | yes
95 | yes
96 |
97 |
98 | Produces vector files
99 | yes
100 | yes
101 | yes
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/examples/sync-amp-dom.php:
--------------------------------------------------------------------------------
1 | createDriver([
15 | "sync" => true,
16 | "driver" => "dom",
17 | ]);
18 |
19 | $runner = $factory->createRunner([
20 | "runner" => "amp",
21 | ]);
22 |
23 | $results = $driver
24 | ->body($sample)
25 | ->size("A4")
26 | ->orientation("portrait")
27 | ->dpi(300)
28 | ->render($runner);
29 |
30 | file_put_contents(__DIR__ . "/sync-amp-dom.pdf", $results);
31 |
--------------------------------------------------------------------------------
/examples/sync-react-webkit.php:
--------------------------------------------------------------------------------
1 | createDriver([
15 | "sync" => true,
16 | "driver" => "webkit",
17 | ]);
18 |
19 | $runner = $factory->createRunner([
20 | "runner" => "react",
21 | ]);
22 |
23 | $results = $driver
24 | ->body($sample)
25 | ->size("A4")
26 | ->orientation("portrait")
27 | ->dpi(300)
28 | ->render($runner);
29 |
30 | file_put_contents(__DIR__ . "/sync-react-webkit.pdf", $results);
31 |
--------------------------------------------------------------------------------
/header:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asyncphp/paper/40e1decdcecf6b4924e018d8531a7e903c7bb1e5/header
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | Copyright Christopher Pitt
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 | src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 | html($sample)
28 | ->size("A4")
29 | ->orientation("portrait")
30 | ->dpi(300)
31 | ->render($runner);
32 |
33 | $results = yield $promise;
34 | }));
35 | ```
36 |
37 | However, it's must easier to use the factory, to create pre-configured drivers:
38 |
39 | ```php
40 | use AsyncPHP\Paper\Factory;
41 |
42 | $config = [
43 | "driver" => "dom",
44 |
45 | "dom" => [
46 | "options" => [
47 | "fontDir" => __DIR__ . "/fonts",
48 | // https://github.com/dompdf/dompdf/blob/master/src/Options.php
49 | ],
50 | ],
51 |
52 | "prince" => [
53 | "binary" => "/opt/prince/bin/prince",
54 | "tempPath" => __DIR__,
55 | "options" => [
56 | "--no-compress",
57 | "--http-timeout" => 10,
58 | // https://www.princexml.com/doc/command-line/#command-line
59 | ],
60 | ],
61 |
62 | "webkit" => [
63 | "binary" => "/usr/local/bin/wkhtmltopdf",
64 | "tempPath" => __DIR__,
65 | "options" => [
66 | "--grayscale",
67 | "--javascript-delay" => 500,
68 | // http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
69 | ],
70 | ],
71 |
72 | "runner" => "amp",
73 | ];
74 |
75 | $factory = new Factory();
76 | $driver = $factory->createDriver($config);
77 | $runner = $factory->createRunner($config);
78 |
79 | yield $driver->html($sample)->render($runner);
80 | ```
81 |
82 | Paper takes an async-first approach. Operations, like rendering PDF files, are particularly suited to parallel processing architecture. You may be stuck rending PDF files in a synchronous architecture, in which case you can use the `SyncDriver` decorator:
83 |
84 | ```php
85 | $driver = new SyncDriver(new DomDriver());
86 |
87 | // ...or with the factory
88 |
89 | $driver = $factory->createDriver([
90 | "driver" => "dom",
91 | "sync" => true,
92 | ]);
93 | ```
94 |
95 | ## Drivers
96 |
97 | Here's a list of the drivers to currently support:
98 |
99 | ### [DOMPDF](http://dompdf.github.io)
100 |
101 | * Requires command-line utilities: **no**
102 | * Supports modern CSS: **no**
103 | * Supports modern JS: **no**
104 | * Produces vector files: **yes**
105 | * Open + free: **yes**
106 |
107 | ### [WKHTMLtoDPF](http://wkhtmltopdf.org)
108 |
109 | * Requires command-line utilities: **yes**
110 | * Supports modern CSS: **yes**
111 | * Supports modern JS: **yes**
112 | * Produces vector files: **yes**
113 | * Open + free: **yes**
114 |
115 | ### [Prince](https://www.princexml.com)
116 |
117 | * Requires command-line utilities: **yes**
118 | * Supports modern CSS: **yes**
119 | * Supports modern JS: **yes**
120 | * Produces vector files: **yes**
121 | * Open + free: **no**
122 |
123 | ## Runners
124 |
125 | Paper supports [Amp](https://github.com/amphp) and [React](https://github.com/reactphp), to package and run the async code. You have to install one of the following library groups:
126 |
127 | ### [Amp](examples/async-amp-dom.php)
128 |
129 | ```
130 | composer require amphp/loop
131 | composer require amphp/parallel
132 | composer require async-interop/event-loop
133 | ```
134 |
135 | ### [React](examples/async-react-webkit.php)
136 |
137 | ```
138 | composer require react/event-loop
139 | composer require react/child-process
140 | composer require jeremeamia/superclosure
141 | ```
142 |
143 | Take a look at the [examples](examples) folder to find out more about using the async drivers.
144 |
145 | ## Roadmap
146 |
147 | * Setters for default margin
148 | * Setters for header HTML
149 | * Setters for footer HTML
150 | * More drivers (especially [DocRaptor](https://docraptor.com) – a SaaS version of Prince)
151 |
152 | ## Versioning
153 |
154 | This library follows [Semver](http://semver.org). According to Semver, you will be able to upgrade to any minor or patch version of this library without any breaking changes to the public API. Semver also requires that we clearly define the public API for this library.
155 |
156 | All methods, with `public` visibility, are part of the public API. All other methods are not part of the public API. Where possible, we'll try to keep `protected` methods backwards-compatible in minor/patch versions, but if you're overriding methods then please test your work before upgrading.
157 |
--------------------------------------------------------------------------------
/scrutinizer.yml:
--------------------------------------------------------------------------------
1 | inherit: true
2 |
3 | filter:
4 | paths: [src/*, tests/*]
5 |
6 | checks:
7 | php:
8 | code_rating: true
9 | duplication: true
10 |
--------------------------------------------------------------------------------
/src/Decorator.php:
--------------------------------------------------------------------------------
1 | access("header", $header);
53 | }
54 |
55 | /**
56 | * Works as a universal getter and setter.
57 | *
58 | * @param string $key
59 | * @param mixed $value
60 | */
61 | private function access($key, $value = null)
62 | {
63 | if (is_null($value)) {
64 | return $this->$key;
65 | }
66 |
67 | $this->$key = $value;
68 | return $this;
69 | }
70 |
71 | /**
72 | * @inheritdoc
73 | *
74 | * @param null|string $body
75 | *
76 | * @return string|static
77 | */
78 | public function body($body = null)
79 | {
80 | return $this->access("body", $body);
81 | }
82 |
83 | /**
84 | * @inheritdoc
85 | *
86 | * @param null|string $footer
87 | *
88 | * @return string|static
89 | */
90 | public function footer($footer = null)
91 | {
92 | return $this->access("footer", $footer);
93 | }
94 |
95 | /**
96 | * @inheritdoc
97 | *
98 | * @param null|string $size
99 | *
100 | * @return string|static
101 | */
102 | public function size($size = null)
103 | {
104 | return $this->access("size", $size);
105 | }
106 |
107 | /**
108 | * @inheritdoc
109 | *
110 | * @param null|string $orientation
111 | *
112 | * @return string|static
113 | */
114 | public function orientation($orientation = null)
115 | {
116 | return $this->access("orientation", $orientation);
117 | }
118 |
119 | /**
120 | * @inheritdoc
121 | *
122 | * @param null|int $dpi
123 | *
124 | * @return int|static
125 | */
126 | public function dpi($dpi = null)
127 | {
128 | return $this->access("dpi", $dpi);
129 | }
130 |
131 | /**
132 | * Get context variables for parallel execution.
133 | *
134 | * @return array
135 | */
136 | protected function data()
137 | {
138 | $html = $this->appends(
139 | $this->body, "head", ""
140 | );
141 |
142 | if ($this->header) {
143 | $html = $this->appendsHeader($html, $this->header);
144 | }
145 |
146 | if ($this->footer) {
147 | $html = $this->appendsFooter($html, $this->footer);
148 | }
149 |
150 | return [
151 | "header" => $this->header,
152 | "body" => $this->body,
153 | "footer" => $this->footer,
154 | "html" => $html,
155 | "size" => $this->size,
156 | "orientation" => $this->orientation,
157 | "dpi" => $this->dpi,
158 | ];
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Driver/DomDriver.php:
--------------------------------------------------------------------------------
1 | options = $options;
24 | }
25 |
26 | /**
27 | * @inheritdoc
28 | *
29 | * @param Runner $runner
30 | *
31 | * @return mixed
32 | */
33 | public function render(Runner $runner)
34 | {
35 | $data = $this->data();
36 | $custom = $this->options;
37 |
38 | return $runner->run(function() use ($data, $custom) {
39 | $options = new Options();
40 | $options->set("isJavascriptEnabled", true);
41 | $options->set("isPhpEnabled", false);
42 | $options->set("isHtml5ParserEnabled", true);
43 | $options->set("dpi", $data["dpi"]);
44 |
45 | foreach ($custom as $key => $value) {
46 | $options->set($key, $value);
47 | }
48 |
49 | $engine = new Dompdf($options);
50 | $engine->setPaper($data["size"], $data["orientation"]);
51 | $engine->loadHtml($data["html"]);
52 | $engine->render();
53 |
54 | return $engine->output();
55 | });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Driver/PrinceDriver.php:
--------------------------------------------------------------------------------
1 | binaryPath = $binaryPath;
35 | $this->tempPath = $tempPath;
36 | $this->options = $options;
37 | }
38 |
39 | /**
40 | * @inheritdoc
41 | *
42 | * @param Runner $runner
43 | *
44 | * @return mixed
45 | */
46 | public function render(Runner $runner)
47 | {
48 | $data = $this->data();
49 |
50 | $data["html"] = $this->appends(
51 | $data["html"], "head",
52 | ""
53 | );
54 |
55 | $hash = md5(spl_object_hash(new StdClass) . $data["html"]);
56 |
57 | $tempPath = rtrim($this->tempPath, "/");
58 |
59 | $binary = $this->binaryPath;
60 | $input = "{$tempPath}/{$hash}.html";
61 | $styles = "{$tempPath}/{$hash}.css";
62 | $output = "{$tempPath}/{$hash}.pdf";
63 | $custom = $this->options;
64 |
65 | return $runner->run(function() use ($data, $binary, $input, $styles, $output, $custom) {
66 | file_put_contents($input, $data["html"]);
67 | file_put_contents($styles, "@page{size:{$data["size"]} {$data["orientation"]}}");
68 |
69 | $options = "";
70 |
71 | foreach ($custom as $key => $value) {
72 | if (is_string($key)) {
73 | $options .= " {$key} {$value}";
74 | } else {
75 | $options .= " {$value}";
76 | }
77 | }
78 |
79 | exec("
80 | {$binary} \
81 | -s {$styles} \
82 | --css-dpi={$data["dpi"]} \
83 | --javascript \
84 | --no-warn-css \
85 | {$options} \
86 | {$input} -o {$output}
87 | > /dev/null 2>/dev/null
88 | ");
89 |
90 | $contents = file_get_contents($output);
91 |
92 | unlink($input);
93 | unlink($styles);
94 | unlink($output);
95 |
96 | return $contents;
97 | });
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Driver/SyncDriver.php:
--------------------------------------------------------------------------------
1 | decorated = $decorated;
27 | }
28 |
29 | /**
30 | * @inheritdoc
31 | *
32 | * @param null|string $header
33 | *
34 | * @return string|static
35 | */
36 | public function header($header = null)
37 | {
38 | return $this->access("header", $header);
39 | }
40 |
41 | /**
42 | * Works as a universal getter and setter.
43 | *
44 | * @param string $key
45 | * @param mixed $value
46 | */
47 | private function access($key, $value = null)
48 | {
49 | if (is_null($value)) {
50 | return $this->decorated->$key();
51 | }
52 |
53 | $this->decorated->$key($value);
54 | return $this;
55 | }
56 |
57 | /**
58 | * @inheritdoc
59 | *
60 | * @param null|string $body
61 | *
62 | * @return string|static
63 | */
64 | public function body($body = null)
65 | {
66 | return $this->access("body", $body);
67 | }
68 |
69 | /**
70 | * @inheritdoc
71 | *
72 | * @param null|string $footer
73 | *
74 | * @return string|static
75 | */
76 | public function footer($footer = null)
77 | {
78 | return $this->access("footer", $footer);
79 | }
80 |
81 | /**
82 | * @inheritdoc
83 | *
84 | * @param null|string $size
85 | *
86 | * @return string|static
87 | */
88 | public function size($size = null)
89 | {
90 | return $this->access("size", $size);
91 | }
92 |
93 | /**
94 | * @inheritdoc
95 | *
96 | * @param null|string $orientation
97 | *
98 | * @return string|static
99 | */
100 | public function orientation($orientation = null)
101 | {
102 | return $this->access("orientation", $orientation);
103 | }
104 |
105 | /**
106 | * @inheritdoc
107 | *
108 | * @param null|int $dpi
109 | *
110 | * @return int|static
111 | */
112 | public function dpi($dpi = null)
113 | {
114 | return $this->access("dpi", $dpi);
115 | }
116 |
117 | /**
118 | * @inheritdoc
119 | *
120 | * @param Runner $runner
121 | *
122 | * @return null|string
123 | */
124 | public function render(Runner $runner)
125 | {
126 | if ($runner instanceof AmpRunner) {
127 | return $this->renderWithAmp($runner);
128 | }
129 |
130 | if ($runner instanceof ReactRunner) {
131 | return $this->renderWithReact($runner);
132 | }
133 |
134 | return null;
135 | }
136 |
137 | /**
138 | * Run the render step using Amp classes.
139 | *
140 | * @param Runner $runner
141 | *
142 | * @return null|string
143 | */
144 | private function renderWithAmp(Runner $runner)
145 | {
146 | $result = null;
147 |
148 | Loop::execute(Amp\wrap(function() use (&$result, &$runner) {
149 | $result = yield $this->decorated->render($runner);
150 | }));
151 |
152 | return $result;
153 | }
154 |
155 | /**
156 | * Run the render step using React classes.
157 | *
158 | * @param Runner $runner
159 | *
160 | * @return null|string
161 | */
162 | private function renderWithReact(Runner $runner)
163 | {
164 | $result = null;
165 |
166 | $loop = Factory::create();
167 | $process = $this->decorated->render($runner);
168 |
169 | $process->on("exit", function() use ($loop) {
170 | $loop->stop();
171 | });
172 |
173 | $loop->addTimer(0.001, function($timer) use ($process, &$result) {
174 | $process->start($timer->getLoop());
175 |
176 | $process->stdout->on("data", function($output) use (&$result) {
177 | $result = $output;
178 | });
179 | });
180 |
181 | $loop->run();
182 |
183 | return $result;
184 | }
185 |
186 | /**
187 | * @inheritdoc
188 | *
189 | * @return Driver
190 | */
191 | public function decorated()
192 | {
193 | return $this->decorated;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/Driver/Traits/AppendsFooterTrait.php:
--------------------------------------------------------------------------------
1 | appends($document, "body", $this->footerHtml($footer));
18 | $document = $this->appends($document, "head", $this->footerCss());
19 |
20 | return $document;
21 | }
22 |
23 | /**
24 | * Returns the HTML to be applied for the footer.
25 | *
26 | * @param string $footer
27 | *
28 | * @return string
29 | */
30 | protected function footerHtml($footer)
31 | {
32 | return "";
33 | }
34 |
35 | /**
36 | * Returns the CSS to be applied for the footer.
37 | *
38 | * @return string
39 | */
40 | protected function footerCss()
41 | {
42 | return "";
43 | }
44 |
45 | /**
46 | * Appends some HTML to an element, selected out of an HTML string.
47 | *
48 | * @param string $document
49 | * @param string $selector
50 | * @param string $content
51 | *
52 | * @return string
53 | */
54 | protected abstract function appends($document, $selector, $content);
55 | }
56 |
--------------------------------------------------------------------------------
/src/Driver/Traits/AppendsHeaderTrait.php:
--------------------------------------------------------------------------------
1 | appends($document, "body", $this->headerHtml($header));
18 | $document = $this->appends($document, "head", $this->headerCss());
19 |
20 | return $document;
21 | }
22 |
23 | /**
24 | * Returns the HTML to be applied for the header.
25 | *
26 | * @param string $header
27 | *
28 | * @return string
29 | */
30 | protected function headerHtml($header)
31 | {
32 | return "";
33 | }
34 |
35 | /**
36 | * Returns the CSS to be applied for the header.
37 | *
38 | * @return string
39 | */
40 | protected function headerCss()
41 | {
42 | return "";
43 | }
44 |
45 | /**
46 | * Appends some HTML to an element, selected out of an HTML string.
47 | *
48 | * @param string $document
49 | * @param string $selector
50 | * @param string $content
51 | *
52 | * @return string
53 | */
54 | protected abstract function appends($document, $selector, $content);
55 | }
56 |
--------------------------------------------------------------------------------
/src/Driver/Traits/AppendsTrait.php:
--------------------------------------------------------------------------------
1 | filter($selector)->each(function($crawler, $i) use ($content) {
25 | $node = $crawler->getNode(0);
26 |
27 | $fragment = $node->ownerDocument->createDocumentFragment();
28 | $fragment->appendXML($content);
29 |
30 | $imported = $node->ownerDocument->importNode($fragment);
31 |
32 | if ($node->firstChild) {
33 | $node->insertBefore($imported, $node->firstChild);
34 | } else {
35 | $node->appendChild($imported);
36 | }
37 | });
38 |
39 | $html = $crawler->getNode(0);
40 | return $html->ownerDocument->saveHTML($html);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Driver/WebkitDriver.php:
--------------------------------------------------------------------------------
1 | binaryPath = $binaryPath;
35 | $this->tempPath = $tempPath;
36 | $this->options = $options;
37 | }
38 |
39 | /**
40 | * @inheritdoc
41 | *
42 | * @param Runner $runner
43 | *
44 | * @return mixed
45 | */
46 | public function render(Runner $runner)
47 | {
48 | $data = $this->data();
49 |
50 | $hash = md5(spl_object_hash(new StdClass) . $data["html"]);
51 |
52 | $tempPath = rtrim($this->tempPath, "/");
53 |
54 | $binary = $this->binaryPath;
55 | $input = "{$tempPath}/{$hash}-body.html";
56 | $output = "{$tempPath}/{$hash}.pdf";
57 | $custom = $this->options;
58 |
59 | return $runner->run(function() use ($data, $binary, $input, $output, $custom) {
60 | file_put_contents($input, $data["html"]);
61 |
62 | $orientation = "Portrait";
63 |
64 | if ($data["orientation"] === "landscape") {
65 | $orientation = "Landscape";
66 | }
67 |
68 | $options = "";
69 |
70 | foreach ($custom as $key => $value) {
71 | if (is_string($key)) {
72 | $options .= " {$key} {$value}";
73 | } else {
74 | $options .= " {$value}";
75 | }
76 | }
77 |
78 | exec("
79 | {$binary} \
80 | --page-size {$data["size"]} \
81 | --orientation {$orientation} \
82 | --dpi {$data["dpi"]} \
83 | --disable-smart-shrinking \
84 | --load-error-handling 'ignore' \
85 | --load-media-error-handling 'ignore' \
86 | {$options} \
87 | {$input} {$output} \
88 | > /dev/null 2> /dev/null
89 | ");
90 |
91 | $contents = file_get_contents($output);
92 |
93 | unlink($input);
94 | unlink($output);
95 |
96 | return $contents;
97 | });
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Factory.php:
--------------------------------------------------------------------------------
1 | "dom"])
24 | {
25 | $driver = null;
26 |
27 | if ($options["driver"] === "dom") {
28 | $driver = new DomDriver(
29 | !empty($options["dom"]["options"]) ? $options["dom"]["options"] : []
30 | );
31 | }
32 |
33 | if ($options["driver"] === "prince") {
34 | $driver = new PrinceDriver(
35 | !empty($options["prince"]["binary"]) ? $options["prince"]["binary"] : "prince",
36 | !empty($options["prince"]["tempPath"]) ? $options["prince"]["tempPath"] : __DIR__,
37 | !empty($options["prince"]["options"]) ? $options["prince"]["options"] : []
38 | );
39 | }
40 |
41 | if ($options["driver"] === "webkit") {
42 | $driver = new WebkitDriver(
43 | !empty($options["webkit"]["binary"]) ? $options["webkit"]["binary"] : "wkhtmltopdf",
44 | !empty($options["webkit"]["tempPath"]) ? $options["webkit"]["tempPath"] : __DIR__,
45 | !empty($options["webkit"]["options"]) ? $options["webkit"]["options"] : []
46 | );
47 | }
48 |
49 | if (empty($options["sync"])) {
50 | return $driver;
51 | }
52 |
53 | if ($driver !== null) {
54 | return new SyncDriver($driver);
55 | }
56 |
57 | return null;
58 | }
59 |
60 | /**
61 | * Creates a runner instance, based on provided config options.
62 | *
63 | * @param array $options
64 | *
65 | * @return null|Runner
66 | */
67 | public function createRunner(array $options = ["runner" => "amp"])
68 | {
69 | $runner = null;
70 |
71 | if ($options["runner"] === "amp") {
72 | $runner = new AmpRunner();
73 | }
74 |
75 | if ($options["runner"] === "react") {
76 | $runner = new ReactRunner();
77 | }
78 |
79 | return $runner;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Runner.php:
--------------------------------------------------------------------------------
1 | join();
26 | }
27 |
28 | if (Thread::supported()) {
29 | return Thread::spawn($deferred)->join();
30 | }
31 |
32 | return null;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Runner/ReactRunner.php:
--------------------------------------------------------------------------------
1 | autoload();
22 |
23 | $serializer = new Serializer();
24 | $serialized = base64_encode($serializer->serialize($deferred));
25 |
26 | $raw = "
27 | require_once '{$autoload}';
28 |
29 | \$serializer = new SuperClosure\Serializer();
30 | \$serialized = base64_decode('{$serialized}');
31 |
32 | return call_user_func(
33 | \$serializer->unserialize(\$serialized)
34 | );
35 | ";
36 |
37 | $encoded = addslashes(base64_encode($raw));
38 |
39 | return new Process("exec php -r 'print eval(base64_decode(\"{$encoded}\"));'");
40 | }
41 |
42 | /**
43 | * Return the path to the class autoloader.
44 | *
45 | * @return string
46 | */
47 | private function autoload()
48 | {
49 | if (file_exists(__DIR__ . "/../../vendor/autoload.php")) {
50 | return realpath(__DIR__ . "/../../vendor/autoload.php");
51 | }
52 |
53 | if (file_exists(__DIR__ . "/../../../../autoload.php")) {
54 | return realpath(__DIR__ . "/../../../../autoload.php");
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | *.pdf
2 |
--------------------------------------------------------------------------------
/tests/DomDriverTest.php:
--------------------------------------------------------------------------------
1 | header("this is the header")
22 | ->body(file_get_contents(__DIR__ . "/fixtures/sample.html"))
23 | ->footer("this is the footer")
24 | ->size("A4")
25 | ->orientation("portrait")
26 | ->dpi(300)
27 | ->render($runner);
28 |
29 | file_put_contents(__DIR__ . "/test-dom.pdf", $result);
30 |
31 | exec("diff-pdf -v " . __DIR__ . "/test-dom.pdf " . __DIR__ . "/fixtures/test-dom.pdf", $output);
32 |
33 | foreach ($output as $line) {
34 | if (stristr($line, "differs")) {
35 | $this->fail();
36 | }
37 | }
38 |
39 | unlink(__DIR__ . "/test-dom.pdf");
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/FactoryTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(DomDriver::class, $factory->createDriver(["driver" => "dom"]));
19 | $this->assertInstanceOf(PrinceDriver::class, $factory->createDriver(["driver" => "prince"]));
20 | $this->assertInstanceOf(WebkitDriver::class, $factory->createDriver(["driver" => "webkit"]));
21 |
22 | $this->assertInstanceOf(SyncDriver::class, $factory->createDriver(["driver" => "dom", "sync" => true]));
23 |
24 | $this->assertInstanceOf(DomDriver::class, $factory->createDriver(["driver" => "dom", "sync" => true])->decorated());
25 | $this->assertInstanceOf(PrinceDriver::class, $factory->createDriver(["driver" => "prince", "sync" => true])->decorated());
26 | $this->assertInstanceOf(WebkitDriver::class, $factory->createDriver(["driver" => "webkit", "sync" => true])->decorated());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/PrinceDriverTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped("
18 | It appears you don't have the prince installed or in your path. Better to
19 | skip the test, in that case.
20 | ");
21 | }
22 |
23 | error_reporting(E_ERROR | E_PARSE);
24 |
25 | $driver = new SyncDriver(new PrinceDriver($binary = "/opt/prince/bin/prince", $temp = __DIR__));
26 |
27 | $runner = new AmpRunner();
28 |
29 | $result = $driver
30 | ->header("this is the header")
31 | ->body(file_get_contents(__DIR__ . "/fixtures/sample.html"))
32 | ->footer("this is the footer")
33 | ->size("A4")
34 | ->orientation("portrait")
35 | ->dpi(300)
36 | ->render($runner);
37 |
38 | file_put_contents(__DIR__ . "/test-prince.pdf", $result);
39 |
40 | exec("diff-pdf -v " . __DIR__ . "/test-prince.pdf " . __DIR__ . "/fixtures/test-prince.pdf", $output);
41 |
42 | foreach ($output as $line) {
43 | if (stristr($line, "differs")) {
44 | $this->fail();
45 | }
46 | }
47 |
48 | unlink(__DIR__ . "/test-prince.pdf");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/RunnerTest.php:
--------------------------------------------------------------------------------
1 | body($sample)
26 | ->size("A4")
27 | ->orientation("portrait")
28 | ->dpi(300)
29 | ->render($ampRunner);
30 |
31 | file_put_contents(__DIR__ . "/test-amp.pdf", $ampResult);
32 |
33 | $reactResult = $driver
34 | ->body($sample)
35 | ->size("A4")
36 | ->orientation("portrait")
37 | ->dpi(300)
38 | ->render($reactRunner);
39 |
40 | file_put_contents(__DIR__ . "/test-react.pdf", $reactResult);
41 |
42 | exec("diff-pdf -v " . __DIR__ . "/test-amp.pdf " . __DIR__ . "/test-react.pdf", $output);
43 |
44 | foreach ($output as $line) {
45 | if (stristr($line, "differs")) {
46 | $this->fail();
47 | }
48 | }
49 |
50 | unlink(__DIR__ . "/test-amp.pdf");
51 | unlink(__DIR__ . "/test-react.pdf");
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/WebkitDriverTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped("
18 | It appears you don't have the wkhtmltopdf installed or in your path. Better to
19 | skip the test, in that case.
20 | ");
21 | }
22 |
23 | error_reporting(E_ERROR | E_PARSE);
24 |
25 | $driver = new SyncDriver(new WebkitDriver($binary = "/usr/local/bin/wkhtmltopdf", $temp = __DIR__));
26 |
27 | $runner = new AmpRunner();
28 |
29 | $result = $driver
30 | ->header("this is the header")
31 | ->body(file_get_contents(__DIR__ . "/fixtures/sample.html"))
32 | ->footer("this is the footer")
33 | ->size("A4")
34 | ->orientation("portrait")
35 | ->dpi(300)
36 | ->render($runner);
37 |
38 | file_put_contents(__DIR__ . "/test-webkit.pdf", $result);
39 |
40 | exec("diff-pdf -v " . __DIR__ . "/test-webkit.pdf " . __DIR__ . "/fixtures/test-webkit.pdf", $output);
41 |
42 | foreach ($output as $line) {
43 | if (stristr($line, "differs")) {
44 | $this->fail();
45 | }
46 | }
47 |
48 | unlink(__DIR__ . "/test-webkit.pdf");
49 | }
50 |
51 | /**
52 | * Remove unique dates from PDF content, so it can be compared.
53 | */
54 | private function scrubDates($content): string
55 | {
56 | $content = preg_replace("/\/CreationDate[^\\n]+/", "", $content);
57 | $content = preg_replace("/\/ModDate[^\\n]+/", "", $content);
58 |
59 | return $content;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/fixtures/sample.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
55 |
56 |
57 | Paper
58 | Hassle-free HTML to PDF conversion abstraction library.
59 | Installation
60 | composer require asyncphp/paper
61 | Usage
62 | // async
63 |
64 | $async = new DomDriver();
65 |
66 | $result = yield $async
67 | ->html($sample)
68 | ->size("B5")
69 | ->orientation("landscape")
70 | ->dpi(300)
71 | ->render();
72 |
73 | // sync
74 |
75 | $sync = new SyncDriver(new DomDriver());
76 |
77 | $result = $sync
78 | ->html($sample)
79 | ->size("B5")
80 | ->orientation("landscape")
81 | ->dpi(300)
82 | ->render();
83 | Supported drivers
84 |
85 |
86 |
87 | dompdf
88 | wkhtmltopdf
89 | prince
90 |
91 |
92 | Requires command-line utilities
93 | no
94 | yes
95 | yes
96 |
97 |
98 | Supports modern CSS
99 | no
100 | yes
101 | yes
102 |
103 |
104 | Supports modern JS
105 | no
106 | yes
107 | yes
108 |
109 |
110 | Produces vector files
111 | yes
112 | yes
113 | yes
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------