├── .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 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
dompdfwkhtmltopdfprince
Requires command-line utilitiesnoyesyes
Supports modern CSSnoyesyes
Supports modern JSnoyesyes
Produces vector filesyesyesyes
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 "
{$header}
"; 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 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
dompdfwkhtmltopdfprince
Requires command-line utilitiesnoyesyes
Supports modern CSSnoyesyes
Supports modern JSnoyesyes
Produces vector filesyesyesyes
116 | 117 | 118 | 119 | --------------------------------------------------------------------------------