├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── RoboFile.php ├── composer.json ├── src ├── Pdf.php └── Pdf │ ├── Contracts │ ├── Backend.php │ └── DocxConverter.php │ ├── Docx │ ├── Backend.php │ ├── Converter │ │ ├── Google.php │ │ ├── LibreOffice.php │ │ └── Unoconv.php │ └── SimpleXMLElement.php │ ├── Html │ ├── Backend.php │ ├── Phantom.js │ ├── Print.css │ ├── Print.js │ ├── isVisible.js │ └── jQuery.js │ └── TempFile.php └── tests ├── PdfGoogleTest.php.disabled ├── PdfLibreOfficeTest.php ├── PdfPhantomJsTest.php ├── PdfUnoconvTest.php ├── environment ├── download.php └── stream.php ├── output └── .gitkeep ├── pdfbox-app-1.8.7.jar └── templates ├── CloneBlock.docx ├── CloneRow.docx ├── Convert.docx ├── DeleteBlock.docx ├── PhantomJs.html ├── ReplaceBlock.docx └── SetValue.docx /.gitignore: -------------------------------------------------------------------------------- 1 | tests/output/* 2 | !tests/output/.gitkeep 3 | vendor 4 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - "5.6" 5 | - "5.5" 6 | - "5.4" 7 | 8 | install: 9 | - "sudo apt-get update -qq" 10 | - "sudo apt-get install -y libreoffice-writer" 11 | - "sudo apt-get install -y unoconv" 12 | - "composer install" 13 | 14 | script: "./vendor/bin/robo test" 15 | 16 | cache: 17 | apt: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Brad Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Looking for maintainers, I no longer do much if any PHP dev, I have moved on, mostly work in dotnet core, node.js & golang these days. If anyone is keen to take over these projects, get in touch - brad@bjc.id.au 2 | 3 | _PS. I know this is one of the more popular repos and I know some of you have been having a few issues with it. I use [athenapdf](https://www.athenapdf.com/) for all my PDF generation needs these days._ 4 | 5 | The Pdf Gear 6 | ================================================================================ 7 | [![Build Status](https://travis-ci.org/phpgearbox/pdf.svg)](https://travis-ci.org/phpgearbox/pdf) 8 | [![Latest Stable Version](https://poser.pugx.org/gears/pdf/v/stable.svg)](https://packagist.org/packages/gears/pdf) 9 | [![Total Downloads](https://poser.pugx.org/gears/pdf/downloads.svg)](https://packagist.org/packages/gears/pdf) 10 | [![License](https://poser.pugx.org/gears/pdf/license.svg)](https://packagist.org/packages/gears/pdf) 11 | 12 | This project started life as a DOCX templating engine. It has now envolved to 13 | also support converting HTML to PDF using a headless version of _webkit_, 14 | [phantomjs](http://phantomjs.org/). 15 | 16 | The DOCX templating is great for documents that end clients update and manage 17 | over time, particularly text heavy documents. For example I use it to auto 18 | generate some legal contracts, where simple replacements are made for attributes 19 | like First Name, Last Name, Company Name & Address. The client, an insurance 20 | company, can provide updated template word documents that might contain subtle 21 | changes to policies & other conditions. 22 | 23 | The HTML to PDF engine is great for cases where greater control over the design 24 | of the document is required. It's also more natural for us programmers, using 25 | standard HTML & CSS, with a splash of Javscript. 26 | 27 | How to Install 28 | -------------------------------------------------------------------------------- 29 | Installation via composer is easy: 30 | 31 | composer require gears/pdf:* 32 | 33 | You will also need to add the following to your root ```composer.json``` file. 34 | 35 | "scripts": 36 | { 37 | "post-install-cmd": ["PhantomInstaller\\Installer::installPhantomJS"], 38 | "post-update-cmd": ["PhantomInstaller\\Installer::installPhantomJS"] 39 | } 40 | 41 | > DOCX: If you are going to be using the DOCX templating you will need to 42 | > install either libre-office-headless or unoconv on your host. 43 | 44 | How to Use, the basics 45 | -------------------------------------------------------------------------------- 46 | Both APIs are accessed through the main ```Pdf``` class. 47 | 48 | To convert a word document into a PDF without any templating: 49 | ```php 50 | $pdf = Gears\Pdf::convert('/path/to/document.docx'); 51 | ``` 52 | 53 | To save the generated PDF to a file: 54 | ```php 55 | Gears\Pdf::convert('/path/to/document.docx', '/path/to/document.pdf'); 56 | ``` 57 | 58 | To convert a html document into a PDF: 59 | ```php 60 | $pdf = Gears\Pdf::convert('/path/to/document.html'); 61 | ``` 62 | 63 | > NOTE: The save to file works just the same for a HTML document. 64 | 65 | DOCX Templating 66 | -------------------------------------------------------------------------------- 67 | By default the DOCX backend defaults to using ```libre-office-headless```, 68 | to use ```unoconv```, override the converter like so: 69 | ```php 70 | $document = new Gears\Pdf('/path/to/document.docx'); 71 | $document->converter = function() 72 | { 73 | return new Gears\Pdf\Docx\Converter\Unoconv(); 74 | }; 75 | $document->save('/path/to/document.pdf'); 76 | ``` 77 | 78 | > NOTE: Currently the HTML backend only uses phantomjs. 79 | 80 | There are several templating methods for the DOCX engine. 81 | The first is setValue, this replaces all instances of 82 | ```${FOO}``` with ```BAR``` 83 | ```php 84 | $document->setValue('FOO', 'BAR'); 85 | ``` 86 | 87 | To clone an entire block of DOCX xml, you surround your block with tags like: 88 | ```${BLOCK_TO_CLONE}``` & ```${/BLOCK_TO_CLONE}```. Whatever content is 89 | contained inside this block will be repeated 3 times in the generated PDF. 90 | ```php 91 | $document->cloneBlock('BLOCK_TO_CLONE', 3); 92 | ``` 93 | 94 | If you need to replace an entire block with custom DOCX xml you can. 95 | But you need to make sure your XML conforms to the DOCX standards. 96 | This is a very low level method and I wouldn't normally use this. 97 | ```php 98 | $document->replaceBlock('BLOCK_TO_REPLACE', ''); 99 | ``` 100 | 101 | To delete an entire block, for example you might have particular 102 | sections of the document that you only want to show to certian users. 103 | ```php 104 | $document->deleteBlock('BLOCK_TO_DELETE'); 105 | ``` 106 | 107 | Finally the last method is useful for adding new rows to tables. 108 | Similar to the ```cloneBlock``` method. You place the tag in first cell 109 | of the table. This row is the one that gets cloned. 110 | ```php 111 | $document->cloneRow('ROW_TO_CLONE', 5); 112 | ``` 113 | 114 | __For more examples please see the [Unit Tests](https://github.com/phpgearbox/pdf/tree/master/tests). 115 | These contain the PHP code to generate the final PDF along with the original DOCX templates.__ 116 | 117 | > NOTE: The HTML to PDF converter does not have these same templating functions. 118 | > Obviously it's just standard HTML that you can template how ever you like. 119 | 120 | HTML PhantomJs Print Environment 121 | -------------------------------------------------------------------------------- 122 | This is still in development and subject to radical change. 123 | So I won't document this section just yet... 124 | 125 | Credits 126 | -------------------------------------------------------------------------------- 127 | The DOCX templating code originally came from 128 | [PHPWord](https://github.com/PHPOffice/PHPWord) 129 | 130 | You may still like to use _PHPWord_ to generate your DOCX documents. 131 | And then use this package to convert the generated document to PDF. 132 | 133 | -------------------------------------------------------------------------------- 134 | Developed by Brad Jones - brad@bjc.id.au 135 | -------------------------------------------------------------------------------- /RoboFile.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | class RoboFile extends Robo\Tasks 15 | { 16 | /** 17 | * Method: test 18 | * ========================================================================= 19 | * This will run our unit / acceptance testing. All the *gears* within 20 | * the **PhpGearBox** utlise PhpUnit as the basis for our testing with the 21 | * addition of the built in PHP Web Server, making the acceptance tests 22 | * almost as portable as standard unit tests. 23 | * 24 | * Just run: ```php ./vendor/bin/robo test``` 25 | * 26 | * Parameters: 27 | * ------------------------------------------------------------------------- 28 | * n/a 29 | * 30 | * Returns: 31 | * ------------------------------------------------------------------------- 32 | * void 33 | */ 34 | public function test() 35 | { 36 | $this->taskCleanDir('./tests/output')->run(); 37 | exit($this->taskPHPUnit()->arg('./tests')->run()->getExitCode()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gears/pdf", 3 | "description": "A PDF builder using HTML or DOCX templates.", 4 | "homepage": "https://github.com/phpgearbox/pdf", 5 | "keywords": ["pdf", "docx", "template", "builder", "converter"], 6 | "license": "MIT", 7 | "autoload": 8 | { 9 | "psr-4": 10 | { 11 | "Gears\\": "src" 12 | } 13 | }, 14 | "require": 15 | { 16 | "gears/di": "*", 17 | "gears/string": "^0.6.0", 18 | "symfony/process": "2.*", 19 | "symfony/filesystem": "2.*", 20 | "jakoch/phantomjs-installer": "1.9.8" 21 | }, 22 | "require-dev": 23 | { 24 | "codegyre/robo": "*", 25 | "phpunit/phpunit": "4.*", 26 | "guzzlehttp/guzzle": "4.*", 27 | "sgh/pdfbox": "dev-master", 28 | "google/apiclient": "1.*", 29 | "symfony/finder": "2.*" 30 | }, 31 | "scripts": 32 | { 33 | "post-install-cmd": ["PhantomInstaller\\Installer::installPhantomJS"], 34 | "post-update-cmd": ["PhantomInstaller\\Installer::installPhantomJS"] 35 | }, 36 | "suggest": 37 | { 38 | "google/apiclient": "Install to support the Google Docx Converter." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Pdf.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use SplFileInfo; 15 | use RuntimeException; 16 | use Gears\String as Str; 17 | use Gears\Di\Container; 18 | use Gears\Pdf\TempFile; 19 | 20 | class Pdf extends Container 21 | { 22 | /** 23 | * This holds an instance of ```Gears\Pdf\TempFile``` 24 | * pointing to the document we will convert to PDF. 25 | */ 26 | protected $document; 27 | 28 | /** 29 | * This holds an instance of ```SplFileInfo``` 30 | * pointing to the original untouched document. 31 | * 32 | * OR 33 | * 34 | * NULL if a HTML string is provided. 35 | */ 36 | protected $originalDocument; 37 | 38 | /** 39 | * The type of source document we are to convert to PDF 40 | * Valid values for this are: ```docx```, ```html``` 41 | */ 42 | private $documentType; 43 | 44 | /** 45 | * This class is simply just a Facade to create a fluent api for the end 46 | * user. This class doesn't do any of the actual converting. Based on the 47 | * document type it will proxy calls to the appropriate backend class. 48 | */ 49 | protected $backend; 50 | 51 | /** 52 | * A closure than returns a configured instance of ```SplFileInfo```. 53 | */ 54 | protected $injectFile; 55 | 56 | /** 57 | * A closure than returns an instance of ```Gears\Pdf\TempFile```. 58 | */ 59 | protected $injectTempFile; 60 | 61 | /** 62 | * Set Container Defaults 63 | * 64 | * This is where we set all our defaults. If you need to customise this 65 | * container this is a good place to look to see what can be configured 66 | * and how to configure it. 67 | */ 68 | protected function setDefaults() 69 | { 70 | $this->file = $this->protect(function($filePath) 71 | { 72 | return new SplFileInfo($filePath); 73 | }); 74 | 75 | $this->tempFile = $this->protect(function($contents, $ext) 76 | { 77 | $file = new TempFile('GearsPdf', $ext); 78 | 79 | $file->setContents($contents); 80 | 81 | return $file; 82 | }); 83 | } 84 | 85 | /** 86 | * Performs some intial Setup. 87 | * 88 | * @param string $document This is either a filepath to a docx or html file. 89 | * Or it may be a HTML string. The HTML string must 90 | * contain a valid DOCTYPE. 91 | * 92 | * @param array $config Further configuration for the di container. 93 | * 94 | * @throws RuntimeException When not of the correct document type. 95 | */ 96 | public function __construct($document, $config = []) 97 | { 98 | // Configure the container 99 | parent::__construct($config); 100 | 101 | // Is the document a file 102 | if (is_file($document)) 103 | { 104 | // So that the save method can save the PDF in the same folder as 105 | // the original source document we need a refrence to it. 106 | $this->originalDocument = $this->file($document); 107 | 108 | // Grab the files extension 109 | $ext = $this->originalDocument->getExtension(); 110 | if ($ext !== 'docx' && $ext !== 'html') 111 | { 112 | throw new RuntimeException('Must be a DOCX or HTML file.'); 113 | } 114 | $this->documentType = $ext; 115 | 116 | // Save the document to a new temp file 117 | // In the case of DOCX files we may make changes to the document 118 | // before converting to PDF so to keep the API consitent lets create 119 | // a the temp file now. 120 | $this->document = $this->tempFile(file_get_contents($document), $ext); 121 | } 122 | 123 | // Check for a HTML string 124 | elseif (Str::contains($document, 'DOCTYPE')) 125 | { 126 | // Again lets save a temp file 127 | $this->document = $this->tempFile($document, 'html'); 128 | 129 | $this->documentType = 'html'; 130 | } 131 | else 132 | { 133 | throw new RuntimeException('Unrecognised document type!'); 134 | } 135 | 136 | // Now create a new backend 137 | $class = '\\Gears\\Pdf\\'.ucfirst($this->documentType).'\\Backend'; 138 | $this->backend = new $class($this->document, $config); 139 | } 140 | 141 | /** 142 | * Shortcut Converter 143 | * 144 | * If all you want to do is convert a document into a pdf, 145 | * this is a shortcut method to do just that. 146 | * 147 | * @param string $document This is either a filepath to a docx or html file. 148 | * Or it may be a HTML string. The HTML string must 149 | * contain a valid DOCTYPE. 150 | * 151 | * @param string $pdf Optionally you may supply an output path of the pdf, 152 | * if not supplied we will create the PDF in the same 153 | * folder as the source document with the same filename. 154 | * If you supplied a HTML string as the document we will 155 | * return the generated PDF bytes. 156 | * 157 | * @return mixed SplFileInfo or PDF Bytes 158 | */ 159 | public static function convert($document, $pdf = null, $config = []) 160 | { 161 | $instance = new static($document, $config); 162 | 163 | if (empty($pdf)) 164 | { 165 | return $instance->backend->generate(); 166 | } 167 | 168 | return $instance->save($pdf); 169 | } 170 | 171 | /** 172 | * Saves the generated PDF. 173 | * 174 | * We call the backend class to generate the PDF for us. 175 | * Then we attempt to save those bytes to a permanent location. 176 | * 177 | * @param string $path If not supplied we will create the PDF in the name 178 | * folder as the source document with the same filename. 179 | * 180 | * @return SplFileInfo 181 | */ 182 | public function save($path = null) 183 | { 184 | $pdf = $this->backend->generate(); 185 | 186 | // If no output path has been supplied save the file 187 | // in the same folder as the original template. 188 | if (is_null($path)) 189 | { 190 | if (is_null($this->originalDocument)) 191 | { 192 | // This will be thrown when someone attemtps to use 193 | // the save method when they have supplied a HTML string. 194 | throw new RuntimeException 195 | ( 196 | 'You must supply a path for us to save the PDF!' 197 | ); 198 | } 199 | 200 | $ext = $this->originalDocument->getExtension(); 201 | $path = Str::s($this->originalDocument->getPathname()); 202 | $path = $path->replace('.'.$ext, '.pdf'); 203 | } 204 | 205 | // Save the pdf to the output path 206 | if (@file_put_contents($path, $pdf) === false) 207 | { 208 | throw new RuntimeException('Failed to write to file "'.$path.'".'); 209 | } 210 | 211 | // Return the location of the saved pdf 212 | return $this->file($path); 213 | } 214 | 215 | /** 216 | * Http Download 217 | * 218 | * If invoked via Apache, PHP-FPM, etc. You may just want to send the PDF 219 | * directly to the browser as a downloadable file. This method will generate 220 | * the PDF and send the appropriate headers for you. 221 | * 222 | * @param string $filename The name of the file that the browser will see. 223 | * 224 | * @param boolean $exit To ensure no extra content is added to the PDF we 225 | * will by default die after outputting it. If you want 226 | * to overide ths behaviour feel free just make sure 227 | * you don't send any extra bytes otherwise your PDF 228 | * will be corrupt. 229 | */ 230 | public function download($filename = 'download.pdf', $exit = true) 231 | { 232 | // Send some headers 233 | header('Content-Type: application/pdf'); 234 | header('Content-Disposition: attachment; filename="'.$filename.'"'); 235 | echo $this->backend->generate(); 236 | if ($exit) exit; 237 | } 238 | 239 | /** 240 | * Http Stream 241 | * 242 | * Unlike the download method this will stream the PDF to the browser. 243 | * ie: It will open inside the browsers PDF reader. 244 | * 245 | * @param boolean $exit To ensure no extra content is added to the PDF we 246 | * will by default die after outputting it. If you want 247 | * to overide ths behaviour feel free just make sure 248 | * you don't send any extra bytes otherwise your PDF 249 | * will be corrupt. 250 | */ 251 | public function stream($exit = true) 252 | { 253 | // Send some headers 254 | header('Content-Type: application/pdf'); 255 | header('Content-Disposition: inline; filename="stream.pdf"'); 256 | echo $this->backend->generate(); 257 | if ($exit) exit; 258 | } 259 | 260 | /** 261 | * Proxy Calls to Backend 262 | * 263 | * Once a source document has been supplied and a backend choosen. 264 | * This will then proxy any unresolved method calls through to backend 265 | * class. 266 | * 267 | * The user can then perform further configuration and custmoistation to 268 | * the backend easily before calling one of the output methods above. 269 | * 270 | * @param string $name 271 | * @param array $args 272 | * @return mixed 273 | */ 274 | public function __call($name, $args) 275 | { 276 | if ($this->offsetExists($name)) 277 | { 278 | return parent::__call($name, $args); 279 | } 280 | else 281 | { 282 | if (empty($this->backend)) 283 | { 284 | throw new RuntimeException('Backend Class not created yet!'); 285 | } 286 | 287 | return call_user_func_array([$this->backend, $name], $args); 288 | } 289 | } 290 | 291 | /** 292 | * Proxy Properties to Backend 293 | * 294 | * Once a source document has been supplied and a backend choosen. 295 | * This will then proxy any unresolved properties through to backend 296 | * class. 297 | * 298 | * The user can then perform further configuration and custmoistation to 299 | * the backend easily before calling one of the output methods above. 300 | * 301 | * @param string $name 302 | * @param mixed $value 303 | * @return void 304 | */ 305 | public function __set($name, $value) 306 | { 307 | if ($this->offsetExists($name)) 308 | { 309 | parent::__set($name, $value); 310 | } 311 | else 312 | { 313 | // Due to the fact that the backend class is not intialised until 314 | // after the main container is configured, we just fail siliently. 315 | if (!empty($this->backend)) 316 | { 317 | call_user_func([$this->backend, '__set'], $name, $value); 318 | } 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Pdf/Contracts/Backend.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use Gears\Pdf\TempFile; 15 | 16 | interface Backend 17 | { 18 | public function __construct(TempFile $document, $config); 19 | public function generate(); 20 | } -------------------------------------------------------------------------------- /src/Pdf/Contracts/DocxConverter.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use Gears\Pdf\TempFile; 15 | 16 | interface DocxConverter 17 | { 18 | public function convertDoc(TempFile $docx); 19 | } -------------------------------------------------------------------------------- /src/Pdf/Docx/Backend.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use ZipArchive; 15 | use RuntimeException; 16 | use Gears\String as Str; 17 | use Gears\Di\Container; 18 | use Gears\Pdf\TempFile; 19 | use Gears\Pdf\Docx\SimpleXMLElement; 20 | use Gears\Pdf\Docx\Converter\LibreOffice; 21 | use Gears\Pdf\Contracts\Backend as BackendInterface; 22 | 23 | class Backend extends Container implements BackendInterface 24 | { 25 | /** 26 | * @var Gears\Pdf\TempFile DOCX document to use as the template for our PDF. 27 | * Set as the first argument of the constructor of 28 | * this class. 29 | */ 30 | protected $template; 31 | 32 | /** 33 | * This is where store the main ```word/document.xml``` of the docx file. 34 | * It will be an instance of ```SimpleXMLElement```. 35 | */ 36 | protected $documentXML; 37 | 38 | /** 39 | * This is where store any header xml. ie: ```word/header1.xml```. 40 | * It will contains instances of ```SimpleXMLElement```. 41 | */ 42 | protected $headerXMLs = []; 43 | 44 | /** 45 | * This is where store any footer xml. ie: ```word/footer1.xml```. 46 | * It will contains instances of ```SimpleXMLElement```. 47 | */ 48 | protected $footerXMLs = []; 49 | 50 | /** 51 | * An instance of ```ZipArchive```. 52 | */ 53 | protected $injectZip; 54 | 55 | /** 56 | * A closure that returns an instance of ```SimpleXMLElement``` 57 | */ 58 | protected $injectXml; 59 | 60 | /** 61 | * This must be supplied before any converstions will take place. 62 | */ 63 | protected $injectConverter; 64 | 65 | /** 66 | * Set Container Defaults 67 | * 68 | * This is where we set all our defaults. If you need to customise this 69 | * container this is a good place to look to see what can be configured 70 | * and how to configure it. 71 | */ 72 | protected function setDefaults() 73 | { 74 | $this->zip = function() 75 | { 76 | return new ZipArchive; 77 | }; 78 | 79 | $this->xml = $this->protect(function($xml) 80 | { 81 | return SimpleXMLElement::fixSplitTags($xml); 82 | }); 83 | 84 | $this->converter = function() 85 | { 86 | return new LibreOffice(); 87 | }; 88 | } 89 | 90 | /** 91 | * Configures this container. 92 | * 93 | * @param TempFile $document The docx file we will convert. 94 | * 95 | * @param array $config Further configuration for this container. 96 | */ 97 | public function __construct(TempFile $document, $config = []) 98 | { 99 | parent::__construct($config); 100 | 101 | $this->template = $document; 102 | 103 | $this->readDocx(); 104 | } 105 | 106 | /** 107 | * Generates the PDF from the DOCX File 108 | * 109 | * @return PDF Bytes 110 | */ 111 | public function generate() 112 | { 113 | $this->writeDocx(); 114 | 115 | return $this->converter->convertDoc($this->template); 116 | } 117 | 118 | /** 119 | * Method: setValue 120 | * ========================================================================= 121 | * Set a Template value. 122 | * 123 | * This will search through all headers and footers 124 | * as well as the main document body. 125 | * 126 | * Parameters: 127 | * ------------------------------------------------------------------------- 128 | * - $search: The tag name to search for. 129 | * - $replace: The value to replace the tag with. 130 | * - $limit: How many times to search for the tag. 131 | * 132 | * Returns: 133 | * ------------------------------------------------------------------------- 134 | * void 135 | */ 136 | public function setValue($search, $replace, $limit = -1) 137 | { 138 | foreach ($this->headerXMLs as $index => $headerXML) 139 | { 140 | $this->headerXMLs[$index] = $this->setValueForPart 141 | ( 142 | $this->headerXMLs[$index], 143 | $search, 144 | $replace, 145 | $limit 146 | ); 147 | } 148 | 149 | $this->documentXML = $this->setValueForPart 150 | ( 151 | $this->documentXML, 152 | $search, 153 | $replace, 154 | $limit 155 | ); 156 | 157 | foreach ($this->footerXMLs as $index => $headerXML) 158 | { 159 | $this->footerXMLs[$index] = $this->setValueForPart 160 | ( 161 | $this->footerXMLs[$index], 162 | $search, 163 | $replace, 164 | $limit 165 | ); 166 | } 167 | } 168 | 169 | /** 170 | * Method: cloneBlock 171 | * ========================================================================= 172 | * Clones a block. 173 | * 174 | * > NOTE: Currently only works in the main body content. 175 | * > Will not work in headers and footers. 176 | * 177 | * Parameters: 178 | * ------------------------------------------------------------------------- 179 | * - $blockname: The name of the block to clone. 180 | * - $clones: How many times do we want to clone the block. 181 | * - $replace: Whether or not to replace the original block with the clones 182 | * 183 | * Returns: 184 | * ------------------------------------------------------------------------- 185 | * void 186 | */ 187 | public function cloneBlock($blockname, $clones = 1, $replace = true) 188 | { 189 | $matches = $this->searchForBlock($this->documentXML, $blockname); 190 | 191 | if (isset($matches[1])) 192 | { 193 | // The xml block to be cloned 194 | $clone = $matches[1]; 195 | 196 | // An array of the cloned blocks of xml 197 | $cloned = []; 198 | 199 | for ($i = 1; $i <= $clones; $i++) 200 | { 201 | // For all tags inside the block we will add an 202 | // incrementing integer to the end of the tag name. 203 | $cloned[] = preg_replace('/\${(.*?)}/','${$1_'.$i.'}', $clone); 204 | } 205 | 206 | if ($replace) 207 | { 208 | $this->documentXML = $this->xml(str_replace 209 | ( 210 | $matches[0], 211 | implode('', $cloned), 212 | $this->documentXML->asXml() 213 | )); 214 | } 215 | } 216 | } 217 | 218 | /** 219 | * Method: replaceBlock 220 | * ========================================================================= 221 | * Replaces a block. This can be used to perform very low level editing to 222 | * the document. The idea being that you will need to actually provide valid 223 | * DOCx XML as the replacement string. 224 | * 225 | * > NOTE: Currently only works in the main body content. 226 | * > Will not work in headers and footers. 227 | * 228 | * Parameters: 229 | * ------------------------------------------------------------------------- 230 | * - $blockname: The name of the block to replace. 231 | * - $replacement: The XML to insert into the document. 232 | * 233 | * Returns: 234 | * ------------------------------------------------------------------------- 235 | * void 236 | */ 237 | public function replaceBlock($blockname, $replacement) 238 | { 239 | $matches = $this->searchForBlock($this->documentXML, $blockname); 240 | 241 | if (isset($matches[1])) 242 | { 243 | $this->documentXML = $this->xml(str_replace 244 | ( 245 | $matches[0], 246 | $replacement, 247 | $this->documentXML->asXml() 248 | )); 249 | } 250 | } 251 | 252 | /** 253 | * Method: deleteBlock 254 | * ========================================================================= 255 | * Delete a block of text. 256 | * 257 | * > NOTE: Currently only works in the main body content. 258 | * > Will not work in headers and footers. 259 | * 260 | * Parameters: 261 | * ------------------------------------------------------------------------- 262 | * - $blockname: The blockname to remove. 263 | * 264 | * Returns: 265 | * ------------------------------------------------------------------------- 266 | * void 267 | */ 268 | public function deleteBlock($blockname) 269 | { 270 | $this->replaceBlock($blockname, ''); 271 | } 272 | 273 | /** 274 | * Method: cloneRow 275 | * ========================================================================= 276 | * Clone a table row in a template document. 277 | * 278 | * > NOTE: Currently only works in the main body content. 279 | * > Will not work in headers and footers. 280 | * 281 | * Parameters: 282 | * ------------------------------------------------------------------------- 283 | * - $search: 284 | * - $numberOfClones: 285 | * 286 | * Returns: 287 | * ------------------------------------------------------------------------- 288 | * 289 | */ 290 | public function cloneRow($search, $numberOfClones) 291 | { 292 | $search = $this->normaliseStartTag($search); 293 | 294 | $xml = $this->documentXML->asXml(); 295 | 296 | if (($tagPos = strpos($xml, $search)) === false) 297 | { 298 | throw new RuntimeException 299 | ( 300 | 'Can not clone row, template variable not found '. 301 | 'or variable contains markup.' 302 | ); 303 | } 304 | 305 | $rowStart = $this->findRowStart($xml, $tagPos); 306 | $rowEnd = $this->findRowEnd($xml, $tagPos); 307 | $xmlRow = Str::slice($xml, $rowStart, $rowEnd); 308 | 309 | // Check if there's a cell spanning multiple rows. 310 | if (preg_match('##', $xmlRow)) 311 | { 312 | // $extraRowStart = $rowEnd; 313 | $extraRowEnd = $rowEnd; 314 | 315 | while (true) 316 | { 317 | $extraRowStart = $this->findRowStart($xml, $extraRowEnd + 1); 318 | $extraRowEnd = $this->findRowEnd($xml, $extraRowEnd + 1); 319 | 320 | // If extraRowEnd is lower then 7, there was no next row found. 321 | if ($extraRowEnd < 7) break; 322 | 323 | // If tmpXmlRow doesn't contain continue, 324 | // this row is no longer part of the spanned row. 325 | $tmpXmlRow = Str::slice($xml, $extraRowStart, $extraRowEnd); 326 | if 327 | ( 328 | !preg_match('##', $tmpXmlRow) && 329 | !preg_match('##', $tmpXmlRow) 330 | ){ 331 | break; 332 | } 333 | 334 | // This row was a spanned row, 335 | // update $rowEnd and search for the next row. 336 | $rowEnd = $extraRowEnd; 337 | } 338 | 339 | $xmlRow = Str::slice($xml, $rowStart, $rowEnd); 340 | } 341 | 342 | $result = Str::slice($xml, 0, $rowStart); 343 | 344 | for ($i = 1; $i <= $numberOfClones; $i++) 345 | { 346 | $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1_' . $i . '}', $xmlRow); 347 | } 348 | 349 | $result .= Str::slice($xml, $rowEnd); 350 | 351 | $this->documentXML = $this->xml($result); 352 | } 353 | 354 | /** 355 | * Method: findRowStart 356 | * ========================================================================= 357 | * Find the start position of the nearest table row before $offset. 358 | * Used by [Method: cloneRow](#) 359 | * 360 | * Parameters: 361 | * ------------------------------------------------------------------------- 362 | * - $xml: The xml string to work with. __STRING not SimpleXMLElement__ 363 | * - $offset: The offset 364 | * 365 | * Returns: 366 | * ------------------------------------------------------------------------- 367 | * int 368 | * 369 | * Throws: 370 | * ------------------------------------------------------------------------- 371 | * - RuntimeException: When start position not found. 372 | */ 373 | protected function findRowStart($xml, $offset) 374 | { 375 | $rowStart = strrpos($xml, '', ((strlen($xml)-$offset)*-1)); 380 | } 381 | 382 | if (!$rowStart) 383 | { 384 | throw new RuntimeException 385 | ( 386 | "Can not find the start position of the row to clone." 387 | ); 388 | } 389 | 390 | return $rowStart; 391 | } 392 | 393 | /** 394 | * Method: findRowEnd 395 | * ========================================================================= 396 | * Find the end position of the nearest table row after $offset 397 | * Used by [Method: cloneRow](#) 398 | * 399 | * Parameters: 400 | * ------------------------------------------------------------------------- 401 | * - $xml: The xml string to work with. __STRING not SimpleXMLElement__ 402 | * - $offset: The offset 403 | * 404 | * Returns: 405 | * ------------------------------------------------------------------------- 406 | * int 407 | */ 408 | protected function findRowEnd($xml, $offset) 409 | { 410 | $rowEnd = strpos($xml, "", $offset) + 7; 411 | return $rowEnd; 412 | } 413 | 414 | /** 415 | * Method: readTempDocument 416 | * ========================================================================= 417 | * This method uses ```ZipArchive``` to open up the temp docx template file. 418 | * It populates the [Property: documentXML](#), [Property: headerXMLs](#) 419 | * & [Property: footerXMLs](#). 420 | * 421 | * Parameters: 422 | * ------------------------------------------------------------------------- 423 | * n/a 424 | * 425 | * Returns: 426 | * ------------------------------------------------------------------------- 427 | * void 428 | */ 429 | protected function readDocx() 430 | { 431 | // Open the document 432 | if ($this->zip->open($this->template) !== true) 433 | { 434 | throw new RuntimeException 435 | ( 436 | 'Failed to open the temp template document!' 437 | ); 438 | } 439 | 440 | // Read in the headers 441 | $index = 1; 442 | while ($this->zip->locateName($this->getHeaderName($index)) !== false) 443 | { 444 | $this->headerXMLs[$index] = $this->xml 445 | ( 446 | $this->zip->getFromName 447 | ( 448 | $this->getHeaderName($index) 449 | ) 450 | ); 451 | 452 | $index++; 453 | } 454 | 455 | // Read in the main body 456 | $this->documentXML = $this->xml 457 | ( 458 | $this->zip->getFromName('word/document.xml') 459 | ); 460 | 461 | // Read in the footers 462 | $index = 1; 463 | while ($this->zip->locateName($this->getFooterName($index)) !== false) 464 | { 465 | $this->footerXMLs[$index] = $this->xml 466 | ( 467 | $this->zip->getFromName 468 | ( 469 | $this->getFooterName($index) 470 | ) 471 | ); 472 | 473 | $index++; 474 | } 475 | } 476 | 477 | /** 478 | * Method: writeTempDocument 479 | * ========================================================================= 480 | * After we have done any searching replacing, we need to write the 481 | * modified XML back into the temporary docx document. 482 | * 483 | * Parameters: 484 | * ------------------------------------------------------------------------- 485 | * n/a 486 | * 487 | * Returns: 488 | * ------------------------------------------------------------------------- 489 | * void 490 | */ 491 | protected function writeDocx() 492 | { 493 | // Write the headers 494 | foreach ($this->headerXMLs as $index => $headerXML) 495 | { 496 | $this->zip->addFromString 497 | ( 498 | $this->getHeaderName($index), 499 | $this->headerXMLs[$index]->asXml() 500 | ); 501 | } 502 | 503 | // Write the main body 504 | $xml = $this->documentXML->asXml(); 505 | $this->zip->addFromString('word/document.xml', $xml); 506 | 507 | // Write the footers 508 | foreach ($this->footerXMLs as $index => $headerXML) 509 | { 510 | $this->zip->addFromString 511 | ( 512 | $this->getFooterName($index), 513 | $this->footerXMLs[$index]->asXml() 514 | ); 515 | } 516 | 517 | // Close zip file 518 | if ($this->zip->close() === false) 519 | { 520 | throw new RuntimeException 521 | ( 522 | 'Could not close the temp template document!' 523 | ); 524 | } 525 | } 526 | 527 | /** 528 | * Method:getHeaderName 529 | * ========================================================================= 530 | * Get the name of the header file for $index. 531 | * 532 | * Parameters: 533 | * ------------------------------------------------------------------------- 534 | * - $index: The number of the header. 535 | * 536 | * Returns: 537 | * ------------------------------------------------------------------------- 538 | * string 539 | */ 540 | protected function getHeaderName($index) 541 | { 542 | return sprintf('word/header%d.xml', $index); 543 | } 544 | 545 | /** 546 | * Method:getFooterName 547 | * ========================================================================= 548 | * Get the name of the footer file for $index. 549 | * 550 | * Parameters: 551 | * ------------------------------------------------------------------------- 552 | * - $index: The number of the footer. 553 | * 554 | * Returns: 555 | * ------------------------------------------------------------------------- 556 | * string 557 | */ 558 | protected function getFooterName($index) 559 | { 560 | return sprintf('word/footer%d.xml', $index); 561 | } 562 | 563 | /** 564 | * Method: setValueForPart 565 | * ========================================================================= 566 | * Find and replace placeholders in the given XML section. 567 | * 568 | * > NOTE: This is not part of the public API. 569 | * 570 | * Paramters: 571 | * ------------------------------------------------------------------------- 572 | * - $xml: The xml string that we are to act on. 573 | * - $search: The tag to search for. ie: ${MYTAG} 574 | * - $replace: A value to replace the tag with. 575 | * - $limit: How many times to do the replacement. 576 | * 577 | * Returns: 578 | * ------------------------------------------------------------------------- 579 | * ```SimpleXMLElement``` 580 | */ 581 | protected function setValueForPart($xml, $search, $replace, $limit) 582 | { 583 | // Make sure the search value contains the tag syntax 584 | $search = $this->normaliseStartTag($search); 585 | 586 | // Make sure the replacement value is encoded correctly. 587 | $replace = htmlspecialchars(Str::toUTF8($replace)); 588 | 589 | // Do the search and replace 590 | return $this->xml(preg_replace 591 | ( 592 | '/'.preg_quote($search, '/').'/u', 593 | $replace, 594 | $xml->asXml(), 595 | $limit 596 | )); 597 | } 598 | 599 | /** 600 | * Method: normaliseStartTag 601 | * ========================================================================= 602 | * This ensures that the start tag contains the correct syntax. 603 | * 604 | * Parameters: 605 | * ------------------------------------------------------------------------- 606 | * - $value: The tag name, with or without curleys. 607 | * 608 | * Returns: 609 | * ------------------------------------------------------------------------- 610 | * string 611 | */ 612 | protected function normaliseStartTag($value) 613 | { 614 | if (substr($value, 0, 2) !== '${' && substr($value, -1) !== '}') 615 | { 616 | $value = '${'.$value.'}'; 617 | } 618 | 619 | return $value; 620 | } 621 | 622 | /** 623 | * Method: normaliseEndTag 624 | * ========================================================================= 625 | * This ensures that the end tag contains the correct syntax. 626 | * 627 | * Parameters: 628 | * ------------------------------------------------------------------------- 629 | * - $value: The tag name, with or without curleys. 630 | * 631 | * Returns: 632 | * ------------------------------------------------------------------------- 633 | * string 634 | */ 635 | protected function normaliseEndTag($value) 636 | { 637 | if (substr($value, 0, 2) !== '${/' && substr($value, -1) !== '}') 638 | { 639 | $value = '${/'.$value.'}'; 640 | } 641 | 642 | return $value; 643 | } 644 | 645 | /** 646 | * Method: getStartAndEndNodes 647 | * ========================================================================= 648 | * Searches the xml with the given blockname and returns 649 | * the corresponding start and end nodes. 650 | * 651 | * Parameters: 652 | * ------------------------------------------------------------------------- 653 | * - $xml: An instance of ```SimpleXMLElement``` 654 | * - $blockname: The name of the block to find. 655 | * 656 | * Returns: 657 | * ------------------------------------------------------------------------- 658 | * array 659 | */ 660 | protected function getStartAndEndNodes($xml, $blockname) 661 | { 662 | // Assume the nodes don't exist 663 | $startNode = false; $endNode = false; 664 | 665 | // Search for the block start and end tags 666 | foreach ($xml->xpath('//w:t') as $node) 667 | { 668 | if (Str::contains($node, $this->normaliseStartTag($blockname))) 669 | { 670 | $startNode = $node; 671 | continue; 672 | } 673 | 674 | if (Str::contains($node, $this->normaliseEndTag($blockname))) 675 | { 676 | $endNode = $node; 677 | break; 678 | } 679 | } 680 | 681 | // Bail out if we couldn't find anything 682 | if ($startNode === false || $endNode === false) return false; 683 | 684 | // Find the parent node for the start tag 685 | $node = $startNode; $startNode = null; 686 | while (is_null($startNode)) 687 | { 688 | $node = $node->xpath('..')[0]; 689 | 690 | if ($node->getName() == 'p') 691 | { 692 | $startNode = $node; 693 | } 694 | } 695 | 696 | // Find the parent node for the end tag 697 | $node = $endNode; $endNode = null; 698 | while (is_null($endNode)) 699 | { 700 | $node = $node->xpath('..')[0]; 701 | 702 | if ($node->getName() == 'p') 703 | { 704 | $endNode = $node; 705 | } 706 | } 707 | 708 | // Return the start and end node 709 | return [$startNode, $endNode]; 710 | } 711 | 712 | /** 713 | * Method: getBlockRegx 714 | * ========================================================================= 715 | * Builds the regular expression to get the 716 | * xml block between the start and end nodes. 717 | * 718 | * Parameters: 719 | * ------------------------------------------------------------------------- 720 | * - $nodes: An array containing the start and end nodes. 721 | * 722 | * Returns: 723 | * ------------------------------------------------------------------------- 724 | * string 725 | */ 726 | protected function getBlockRegx($nodes) 727 | { 728 | $pattern = '/'; 729 | $pattern .= preg_quote($nodes[0]->asXml(), '/'); 730 | $pattern .= '(.*?)'; 731 | $pattern .= preg_quote($nodes[1]->asXml(), '/'); 732 | $pattern .= '/is'; 733 | 734 | return $pattern; 735 | } 736 | 737 | /** 738 | * Method: searchForBlock 739 | * ========================================================================= 740 | * This will search for the xml block between the start and end tags. 741 | * 742 | * Parameters: 743 | * ------------------------------------------------------------------------- 744 | * - $xml: An instance of ```SimpleXMLElement``` 745 | * - $blockname: The name of the block. 746 | * 747 | * Returns: 748 | * ------------------------------------------------------------------------- 749 | * array 750 | */ 751 | protected function searchForBlock($xml, $blockname) 752 | { 753 | // Find the starting and ending tags 754 | $nodes = $this->getStartAndEndNodes($this->documentXML, $blockname); 755 | 756 | // Bail out early 757 | if ($nodes === false) return null; 758 | 759 | // Find the xml in between the nodes 760 | preg_match($this->getBlockRegx($nodes), $xml->asXml(), $matches); 761 | 762 | return $matches; 763 | } 764 | } 765 | -------------------------------------------------------------------------------- /src/Pdf/Docx/Converter/Google.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use RuntimeException; 15 | use Gears\Di\Container; 16 | use Gears\Pdf\TempFile; 17 | use Google_Client as GClient; 18 | use Google_Http_Request as GRequest; 19 | use Google_Auth_AssertionCredentials as GAuth; 20 | use Google_Service_Drive as GDrive; 21 | use Google_Service_Drive_DriveFile as GFile; 22 | use Gears\Pdf\Contracts\DocxConverter; 23 | 24 | class Google extends Container implements DocxConverter 25 | { 26 | /** 27 | * Property: serviceAccountEmail 28 | * ========================================================================= 29 | * This is the email address shown at https://console.developers.google.com/ 30 | * For example it might look like: 31 | * 32 | * 0123456789-abcdefghijklmnopqrstuvxyz@developer.gserviceaccount.com 33 | */ 34 | protected $injectServiceAccountEmail; 35 | 36 | /** 37 | * Property: serviceAccountKeyFile 38 | * ========================================================================= 39 | * When you create a new Service Account at 40 | * https://console.developers.google.com/ 41 | * 42 | * You will get a "P12" private key. This is the location of that file. 43 | */ 44 | protected $injectServiceAccountKeyFile; 45 | 46 | /** 47 | * Property: serviceAccountKey 48 | * ========================================================================= 49 | * This holds the content of the key file. 50 | */ 51 | protected $injectServiceAccountKey; 52 | 53 | /** 54 | * Property: scope 55 | * ========================================================================= 56 | * A scope... just a url that points to google drive I dunno :) 57 | */ 58 | protected $injectScope; 59 | 60 | /** 61 | * Property: mime 62 | * ========================================================================= 63 | * For now we only support docx files. 64 | */ 65 | protected $injectMime; 66 | 67 | /** 68 | * Property: auth 69 | * ========================================================================= 70 | * This provides ```Google_Auth_AssertionCredentials``` 71 | */ 72 | protected $injectAuth; 73 | 74 | /** 75 | * Property: client 76 | * ========================================================================= 77 | * This provides ```Google_Client``` 78 | */ 79 | protected $injectClient; 80 | 81 | /** 82 | * Property: service 83 | * ========================================================================= 84 | * This provides ```Google_Service_Drive``` 85 | */ 86 | protected $injectService; 87 | 88 | /** 89 | * Property: file 90 | * ========================================================================= 91 | * This provides ```Google_Service_Drive_DriveFile``` 92 | */ 93 | protected $injectFile; 94 | 95 | /** 96 | * Method: setDefaults 97 | * ========================================================================= 98 | * This is where we set all our defaults. If you need to customise this 99 | * container this is a good place to look to see what can be configured 100 | * and how to configure it. 101 | * 102 | * Parameters: 103 | * ------------------------------------------------------------------------- 104 | * n/a 105 | * 106 | * Returns: 107 | * ------------------------------------------------------------------------- 108 | * void 109 | */ 110 | protected function setDefaults() 111 | { 112 | $this->scope = ['https://www.googleapis.com/auth/drive']; 113 | 114 | $this->mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; 115 | 116 | $this->serviceAccountKey = function() 117 | { 118 | return file_get_contents($this->serviceAccountKeyFile); 119 | }; 120 | 121 | $this->auth = function() 122 | { 123 | return new GAuth 124 | ( 125 | $this->serviceAccountEmail, 126 | $this->scope, 127 | $this->serviceAccountKey 128 | ); 129 | }; 130 | 131 | $this->client = function() 132 | { 133 | $c = new GClient(); 134 | $c->setAssertionCredentials($this->auth); 135 | return $c; 136 | }; 137 | 138 | $this->service = function() 139 | { 140 | return new GDrive($this->client); 141 | }; 142 | 143 | $this->file = function() 144 | { 145 | $f = new GFile(); 146 | $f->setTitle(time().'.docx'); 147 | $f->setMimeType($this->mime); 148 | return $f; 149 | }; 150 | 151 | $this->request = $this->protect(function($url) 152 | { 153 | return new GRequest($url, 'GET', null, null); 154 | }); 155 | } 156 | 157 | /** 158 | * Method: convertDoc 159 | * ========================================================================= 160 | * This is where we actually do some converting of docx to pdf. 161 | * We use the command line utility unoconv. Which is basically a slightly 162 | * fancier way of using OpenOffice/LibreOffice Headless. 163 | * 164 | * See: http://dag.wiee.rs/home-made/unoconv/ 165 | * 166 | * Parameters: 167 | * ------------------------------------------------------------------------- 168 | * - $docx: This must be an instance of ```SplFileInfo``` 169 | * pointing to the document to convert. 170 | * 171 | * Returns: 172 | * ------------------------------------------------------------------------- 173 | * void 174 | */ 175 | public function convertDoc(TempFile $docx) 176 | { 177 | // Upload the document to google 178 | $gdoc = $this->service->files->insert($this->file, 179 | [ 180 | 'convert' => true, 181 | 'data' => $docx->getContents(), 182 | 'mimeType' => $this->mime, 183 | 'uploadType' => 'multipart' 184 | ]); 185 | 186 | // Now download the pdf 187 | $request = $this->request($gdoc->getExportLinks()['application/pdf']); 188 | $this->client->getAuth()->sign($request); 189 | $response = $this->client->getIo()->makeRequest($request); 190 | 191 | // Delete the uploaded file 192 | $this->service->files->delete($gdoc['id']); 193 | 194 | // Check for errors 195 | if ($response->getResponseHttpCode() != 200) 196 | { 197 | throw new RuntimeException($response->getResponseBody()); 198 | } 199 | 200 | // Return the pdf data 201 | return $response->getResponseBody(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Pdf/Docx/Converter/LibreOffice.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use RuntimeException; 15 | use Gears\Di\Container; 16 | use Gears\String as Str; 17 | use Gears\Pdf\TempFile; 18 | use Symfony\Component\Process\Process; 19 | use Gears\Pdf\Contracts\DocxConverter; 20 | 21 | class LibreOffice extends Container implements DocxConverter 22 | { 23 | /** 24 | * Property: binary 25 | * ========================================================================= 26 | * This stores the location of the libreoffice binary on the local system. 27 | */ 28 | protected $injectBinary; 29 | 30 | /** 31 | * Property: profile 32 | * ========================================================================= 33 | * This stores the location of where libreoffice will create a temp user 34 | * profile. I guess we could use the the current users profile however when 35 | * used via Apache or another webserver this isn't possible. 36 | */ 37 | protected $injectProfile; 38 | 39 | /** 40 | * Property: output 41 | * ========================================================================= 42 | * Unlike unoconv libreoffice headless does not provide the ability to pipe 43 | * the generated pdf to stdout, instead it allows us to set an output folder 44 | * for where the generated PDFs will be saved. 45 | */ 46 | protected $injectOutput; 47 | 48 | /** 49 | * Property: process 50 | * ========================================================================= 51 | * This will return a configured instance of 52 | * ```Symfony\Component\Process\Process``` 53 | */ 54 | protected $injectProcess; 55 | 56 | /** 57 | * Method: setDefaults 58 | * ========================================================================= 59 | * This is where we set all our defaults. If you need to customise this 60 | * container this is a good place to look to see what can be configured 61 | * and how to configure it. 62 | * 63 | * Parameters: 64 | * ------------------------------------------------------------------------- 65 | * n/a 66 | * 67 | * Returns: 68 | * ------------------------------------------------------------------------- 69 | * void 70 | */ 71 | protected function setDefaults() 72 | { 73 | $this->binary = '/usr/bin/libreoffice'; 74 | 75 | $this->profile = '/tmp/gears-pdf-libreoffice'; 76 | 77 | $this->output = '/tmp/gears-pdf-libreoffice/generated'; 78 | 79 | $this->process = $this->protect(function($cmd) 80 | { 81 | return new Process($cmd); 82 | }); 83 | } 84 | 85 | /** 86 | * Method: convertDoc 87 | * ========================================================================= 88 | * This is where we actually do some converting of docx to pdf. 89 | * This converter uses the OpenOffice/LibreOffice Headless capabilities. 90 | * 91 | * Parameters: 92 | * ------------------------------------------------------------------------- 93 | * - $docx: This must be an instance of ```SplFileInfo``` 94 | * pointing to the document to convert. 95 | * 96 | * Returns: 97 | * ------------------------------------------------------------------------- 98 | * void 99 | */ 100 | public function convertDoc(TempFile $docx) 101 | { 102 | if (!is_executable($this->binary)) 103 | { 104 | throw new RuntimeException 105 | ( 106 | 'The libreoffice command ("'.$this->binary.'") '. 107 | 'was not found or is not executable by the current user! ' 108 | ); 109 | } 110 | 111 | // Check to see if the profile dir exists and is writeable 112 | if (is_dir($this->profile) && !is_writable($this->profile)) 113 | { 114 | throw new RuntimeException 115 | ( 116 | 'If libreoffice does not have permissions to the User '. 117 | 'Profile directory ("'.$this->profile.'") the conversion '. 118 | 'will fail!' 119 | ); 120 | } 121 | 122 | // Build the cmd to run 123 | $cmd = 124 | $this->binary.' '. 125 | '--headless '. 126 | '-env:UserInstallation=file://'.$this->profile.' '. 127 | '--convert-to pdf:writer_pdf_Export '. 128 | '--outdir "'.$this->output.'" '. 129 | '"'.$docx->getPathname().'"' 130 | ; 131 | 132 | // Run the command 133 | $process = $this->process($cmd); 134 | $process->run(); 135 | 136 | // Check for errors 137 | if (!$process->isSuccessful()) 138 | { 139 | throw new RuntimeException 140 | ( 141 | $process->getErrorOutput() 142 | ); 143 | } 144 | 145 | // Grab the generated pdf 146 | $pdf = file_get_contents 147 | ( 148 | $this->output.'/'.$docx->getBasename('.docx').'.pdf' 149 | ); 150 | 151 | // Clean up after ourselves 152 | exec('rm -rf '.$this->profile); 153 | 154 | // Finally return the generated pdf 155 | return $pdf; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Pdf/Docx/Converter/Unoconv.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use RuntimeException; 15 | use Gears\Di\Container; 16 | use Gears\String as Str; 17 | use Gears\Pdf\TempFile; 18 | use Symfony\Component\Process\Process; 19 | use Gears\Pdf\Contracts\DocxConverter; 20 | 21 | class Unoconv extends Container implements DocxConverter 22 | { 23 | /** 24 | * Property: binary 25 | * ========================================================================= 26 | * This stores the location of the unoconv binary on the local system. 27 | */ 28 | protected $injectBinary; 29 | 30 | /** 31 | * Property: profile 32 | * ========================================================================= 33 | * This stores the location of where unoconv will use as a user profile. 34 | */ 35 | protected $injectProfile; 36 | 37 | /** 38 | * Property: process 39 | * ========================================================================= 40 | * This will return a configured instance of 41 | * ```Symfony\Component\Process\Process``` 42 | */ 43 | protected $injectProcess; 44 | 45 | /** 46 | * Method: setDefaults 47 | * ========================================================================= 48 | * This is where we set all our defaults. If you need to customise this 49 | * container this is a good place to look to see what can be configured 50 | * and how to configure it. 51 | * 52 | * Parameters: 53 | * ------------------------------------------------------------------------- 54 | * n/a 55 | * 56 | * Returns: 57 | * ------------------------------------------------------------------------- 58 | * void 59 | */ 60 | protected function setDefaults() 61 | { 62 | $this->binary = '/usr/bin/unoconv'; 63 | 64 | $this->profile = '/tmp/gears-pdf-unoconv'; 65 | 66 | $this->process = $this->protect(function($cmd) 67 | { 68 | return new Process($cmd); 69 | }); 70 | } 71 | 72 | /** 73 | * Method: convertDoc 74 | * ========================================================================= 75 | * This is where we actually do some converting of docx to pdf. 76 | * We use the command line utility unoconv. Which is basically a slightly 77 | * fancier way of using OpenOffice/LibreOffice Headless. 78 | * 79 | * See: http://dag.wiee.rs/home-made/unoconv/ 80 | * 81 | * Parameters: 82 | * ------------------------------------------------------------------------- 83 | * - $docx: This must be an instance of ```SplFileInfo``` 84 | * pointing to the document to convert. 85 | * 86 | * Returns: 87 | * ------------------------------------------------------------------------- 88 | * void 89 | */ 90 | public function convertDoc(TempFile $docx) 91 | { 92 | if (!is_executable($this->binary)) 93 | { 94 | throw new RuntimeException 95 | ( 96 | 'The unoconv command ("'.$this->binary.'") '. 97 | 'was not found or is not executable by the current user! ' 98 | ); 99 | } 100 | 101 | // Check to see if the profile dir exists and is writeable 102 | if (is_dir($this->profile) && !is_writable($this->profile)) 103 | { 104 | throw new RuntimeException 105 | ( 106 | 'If unoconv does not have permissions to the User '. 107 | 'Profile directory ("'.$this->profile.'") the conversion '. 108 | 'will fail!' 109 | ); 110 | } 111 | 112 | // Build the unoconv cmd 113 | $cmd = 114 | 'export HOME='.$this->profile.' && '. 115 | $this->binary.' '. 116 | '--stdout '. 117 | '-f pdf '. 118 | '"'.$docx->getPathname().'"' 119 | ; 120 | 121 | // Run the command 122 | $process = $this->process($cmd); 123 | $process->run(); 124 | 125 | // Check for errors 126 | $error = null; 127 | 128 | if (!$process->isSuccessful()) 129 | { 130 | $error = $process->getErrorOutput(); 131 | 132 | // NOTE: For some really odd reason the first time the command runs 133 | // it does not complete successfully. The second time around it 134 | // works fine. It has something to do with the homedir setup... 135 | if (Str::contains($error, 'Error: Unable to connect')) 136 | { 137 | $process->run(); 138 | 139 | if (!$process->isSuccessful()) 140 | { 141 | $error = $process->getErrorOutput(); 142 | } 143 | else 144 | { 145 | $error = null; 146 | } 147 | } 148 | 149 | if (!is_null($error)) throw new RuntimeException($error); 150 | } 151 | 152 | // Clean up after ourselves 153 | exec('rm -rf '.$this->profile); 154 | 155 | // Return the pdf data 156 | return $process->getOutput(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Pdf/Docx/SimpleXMLElement.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use SimpleXMLElement as NativeSimpleXMLElement; 15 | 16 | class SimpleXMLElement extends NativeSimpleXMLElement 17 | { 18 | /** 19 | * Method: fixSplitTags 20 | * ========================================================================= 21 | * If part of the tag is formatted differently we won't get a match. 22 | * Best explained with an example: 23 | * 24 | * ```xml 25 | * 26 | * 27 | * Hello ${tag_ 28 | * 29 | * 30 | * 31 | * 32 | * 33 | * 34 | * 1} 35 | * 36 | * ``` 37 | * 38 | * The above becomes, after running through this method: 39 | * 40 | * ```xml 41 | * 42 | * 43 | * Hello ${tag_1} 44 | * 45 | * ``` 46 | * Parameters: 47 | * ------------------------------------------------------------------------- 48 | * - $xml: A well-formed XML string. 49 | * 50 | * Returns: 51 | * ------------------------------------------------------------------------- 52 | * string 53 | */ 54 | public static function fixSplitTags($xml) 55 | { 56 | preg_match_all('|\$\{([^\}]+)\}|U', $xml, $matches); 57 | 58 | foreach ($matches[0] as $value) 59 | { 60 | $valueCleaned = preg_replace('/<[^>]+>/', '', $value); 61 | $valueCleaned = preg_replace('/<\/[^>]+>/', '', $valueCleaned); 62 | $xml = str_replace($value, $valueCleaned, $xml); 63 | } 64 | 65 | return new static($xml); 66 | } 67 | } -------------------------------------------------------------------------------- /src/Pdf/Html/Backend.php: -------------------------------------------------------------------------------- 1 | > \_\ \ ___/ / __ \| | \/ | ( <_> > < 7 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 8 | // \/|__| \/ \/ \/ \/ \/ 9 | // ----------------------------------------------------------------------------- 10 | // Designed and Developed by Brad Jones 11 | // ----------------------------------------------------------------------------- 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | use RuntimeException; 15 | use Gears\Di\Container; 16 | use Gears\String as Str; 17 | use Gears\Pdf\TempFile; 18 | use Symfony\Component\Process\Process; 19 | use Gears\Pdf\Contracts\Backend as BackendInterface; 20 | 21 | class Backend extends Container implements BackendInterface 22 | { 23 | /** 24 | * @var Gears\Pdf\TempFile The document we will convert to PDF. 25 | */ 26 | protected $document; 27 | 28 | /** 29 | * @var array This will passed through to phantomjs. 30 | * @see http://phantomjs.org/api/webpage/property/paper-size.html 31 | */ 32 | protected $paperSize; 33 | 34 | /** 35 | * @var string The location of the phantomjs binary. 36 | */ 37 | protected $injectBinary; 38 | 39 | /** 40 | * @var string The location of the phantomjs javascript runner. 41 | */ 42 | protected $injectRunner; 43 | 44 | /** 45 | * @var Symfony\Component\Process\Process 46 | */ 47 | protected $injectProcess; 48 | 49 | /** 50 | * @var string Code that we will inject into the html document. 51 | */ 52 | protected $injectPrintFramework; 53 | 54 | /** 55 | * Set Container Defaults 56 | * 57 | * This is where we set all our defaults. If you need to customise this 58 | * container this is a good place to look to see what can be configured 59 | * and how to configure it. 60 | */ 61 | protected function setDefaults() 62 | { 63 | $this->binary = function() 64 | { 65 | if (is_dir(__DIR__.'/../../../vendor')) 66 | { 67 | $bin = __DIR__.'/../../../vendor/bin/phantomjs'; 68 | } 69 | else 70 | { 71 | $bin = __DIR__.'/../../../../bin/phantomjs'; 72 | } 73 | 74 | if (!is_executable($bin)) 75 | { 76 | throw new RuntimeException 77 | ( 78 | 'The phantomjs command ("'.$bin.'") '. 79 | 'was not found or is not executable by the current user! ' 80 | ); 81 | } 82 | 83 | return realpath($bin); 84 | }; 85 | 86 | $this->runner = __DIR__.'/Phantom.js'; 87 | 88 | $this->process = $this->protect(function($cmd) 89 | { 90 | return new Process($cmd); 91 | }); 92 | 93 | $this->tempFile = $this->protect(function() 94 | { 95 | return new TempFile('GearsPdf', 'pdf'); 96 | }); 97 | 98 | $this->printFramework = function() 99 | { 100 | return 101 | ' 102 | 103 | 104 | 105 | 106 | '; 107 | }; 108 | } 109 | 110 | /** 111 | * Configures this container. 112 | * 113 | * @param TempFile $document The html file we will convert. 114 | * 115 | * @param array $config Further configuration for this container. 116 | */ 117 | public function __construct(TempFile $document, $config = []) 118 | { 119 | parent::__construct($config); 120 | 121 | $this->document = $document; 122 | } 123 | 124 | /** 125 | * Generates the PDF from the HTML File 126 | * 127 | * @return PDF Bytes 128 | */ 129 | public function generate() 130 | { 131 | // Inject our print framework 132 | if ($this->printFramework !== false) 133 | { 134 | $this->document->setContents 135 | ( 136 | Str::s($this->document->getContents())->replace 137 | ( 138 | '', 139 | $this->printFramework.'' 140 | ) 141 | ); 142 | } 143 | 144 | // Create a new temp file for the generated pdf 145 | $output_document = $this->tempFile(); 146 | 147 | // Build the cmd to run 148 | $cmd = 149 | $this->binary.' '. 150 | $this->runner.' '. 151 | '--url "file://'.$this->document->getPathname().'" '. 152 | '--output "'.$output_document->getPathname().'" ' 153 | ; 154 | 155 | if (isset($this->paperSize['width'])) 156 | { 157 | $cmd .= '--width "'.$this->paperSize['width'].'" '; 158 | } 159 | 160 | if (isset($this->paperSize['height'])) 161 | { 162 | $cmd .= '--height "'.$this->paperSize['height'].'" '; 163 | } 164 | 165 | // Run the command 166 | $process = $this->process($cmd); 167 | $process->run(); 168 | 169 | // Check for errors 170 | if (!$process->isSuccessful()) 171 | { 172 | throw new RuntimeException 173 | ( 174 | $process->getErrorOutput() 175 | ); 176 | } 177 | 178 | // Return the pdf 179 | return $output_document->getContents(); 180 | } 181 | 182 | /** 183 | * Paper Size Setter 184 | * 185 | * It is easier to work with numbers, so while I understand phantomjs does 186 | * have it's own paper size format and orientation settings. Here we provide 187 | * a basic lookup table to convert A4, A3, Letter, etc... 188 | * Into their metric counterparts. 189 | * 190 | * @param array $size 191 | * 192 | * @return self 193 | */ 194 | public function setPaperSize($size) 195 | { 196 | if (isset($size['format'])) 197 | { 198 | switch (strtolower($size['format'])) 199 | { 200 | case 'a0': 201 | $size['width'] = '841mm'; 202 | $size['height'] = '1189mm'; 203 | break; 204 | 205 | case 'a1': 206 | $size['width'] = '594mm'; 207 | $size['height'] = '841mm'; 208 | break; 209 | 210 | case 'a2': 211 | $size['width'] = '420mm'; 212 | $size['height'] = '594mm'; 213 | break; 214 | 215 | case 'a3': 216 | $size['width'] = '297mm'; 217 | $size['height'] = '420mm'; 218 | break; 219 | 220 | case 'a4': 221 | $size['width'] = '210mm'; 222 | $size['height'] = '297mm'; 223 | break; 224 | 225 | case 'a5': 226 | $size['width'] = '148mm'; 227 | $size['height'] = '210mm'; 228 | break; 229 | 230 | case 'a6': 231 | $size['width'] = '105mm'; 232 | $size['height'] = '148mm'; 233 | break; 234 | 235 | case 'a7': 236 | $size['width'] = '74mm'; 237 | $size['height'] = '105mm'; 238 | break; 239 | 240 | case 'a8': 241 | $size['width'] = '52mm'; 242 | $size['height'] = '74mm'; 243 | break; 244 | 245 | case 'a9': 246 | $size['width'] = '37mm'; 247 | $size['height'] = '52mm'; 248 | break; 249 | 250 | case 'a10': 251 | $size['width'] = '27mm'; 252 | $size['height'] = '37mm'; 253 | break; 254 | 255 | case 'letter': 256 | $size['width'] = '216mm'; 257 | $size['height'] = '280mm'; 258 | break; 259 | 260 | case 'legal': 261 | $size['width'] = '215.9mm'; 262 | $size['height'] = '255.6mm'; 263 | break; 264 | 265 | default: 266 | throw new RuntimeException('Unregcognised Paper Size!'); 267 | break; 268 | } 269 | 270 | // Swap the dimensions around if landscape 271 | if (isset($size['orientation'])) 272 | { 273 | if ($size['orientation'] == 'landscape') 274 | { 275 | $width = $size['width']; 276 | $height = $size['height']; 277 | $size['width'] = $height; 278 | $size['height'] = $width; 279 | } 280 | } 281 | } 282 | 283 | $this->paperSize = $size; 284 | 285 | return $this; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Pdf/Html/Phantom.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // __________ __ ________ __________ 3 | // \______ \ |__ ______ / _____/ ____ _____ ______\______ \ _______ ___ 4 | // | ___/ | \\____ \/ \ ____/ __ \\__ \\_ __ \ | _// _ \ \/ / 5 | // | | | Y \ |_> > \_\ \ ___/ / __ \| | \/ | ( <_> > < 6 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 7 | // \/|__| \/ \/ \/ \/ \/ 8 | // ----------------------------------------------------------------------------- 9 | // Designed and Developed by Brad Jones 10 | // ----------------------------------------------------------------------------- 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | // Import some modules 14 | var 15 | webpage = require('webpage'), 16 | system = require('system') 17 | ; 18 | 19 | // Assign our global exception handler 20 | phantom.onError = whoops; 21 | 22 | // Read in our cli arguments 23 | var args = readArgs 24 | ({ 25 | url: null, 26 | output: null, 27 | width: '210mm', 28 | height: '297mm' 29 | }); 30 | 31 | // Create our page 32 | var page = webpage.create(); 33 | 34 | // Set the paper size 35 | page.paperSize = { width: args.width, height: args.height }; 36 | 37 | // Normalise the paper size 38 | var normalisedPaperSize = normalisePageSize(args.width, args.height); 39 | 40 | // Open up the page 41 | page.open(args.url, function (status) 42 | { 43 | if (status !== 'success') 44 | { 45 | // Not sure why but throwing an exception here makes phantomjs hang. 46 | // The exception gets output to stderr but for some odd reason 47 | // phantom.exit() in whoops appears not to work. 48 | // throw new Error('Unable to load the url: ' + args.url); 49 | 50 | system.stderr.writeLine('Error: Unable to load the url: ' + args.url); 51 | return phantom.exit(1); 52 | } 53 | 54 | // This provides an opportunity for the page to perform dom 55 | // manipulations and calculations before being printed to a PDF. 56 | // We pass through the paper size so that it knows how big the window is. 57 | page.evaluate 58 | ( 59 | function(width, height, version) 60 | { 61 | // NOTE: evaluate is syncronous, so long as beforePrint 62 | // remains syncronous as well, we should be all good. 63 | if (beforePrint) beforePrint(width, height, version); 64 | }, 65 | normalisedPaperSize.width, 66 | normalisedPaperSize.height, 67 | phantom.version.major 68 | ); 69 | 70 | // Save the page to a PDF file 71 | page.render(args.output, {format: 'pdf', quality: '100'}); 72 | 73 | // Shutdown 74 | phantom.exit(); 75 | }); 76 | 77 | //////////////////////////////////////////////////////////////////////////////// 78 | // HELPER FUNCTIONS BELOW HERE 79 | //////////////////////////////////////////////////////////////////////////////// 80 | 81 | /** 82 | * Normalise and Convert the mm Dimensions of the Page into Pixels 83 | * 84 | * There are some major scaling diffrences between phantomjs 1.x and 2.x so 85 | * we convert to pixels ourselves in an attempt to eliminate issues that arise 86 | * from this. The results have been optimised to work with phantomjs 2.x 87 | * 88 | * @see https://github.com/ariya/phantomjs/issues/12936 89 | * 90 | * @param string width The width of the page, includes measurement type. 91 | * @param string height The height of the page, includes measurement type. 92 | * @return object The width and height in pixels of the page. 93 | */ 94 | function normalisePageSize(width, height) 95 | { 96 | // Define the PPI value depending on the version of phantomjs. 97 | if (phantom.version.major == 2) 98 | { 99 | var ppi = 72; 100 | } 101 | else 102 | { 103 | var ppi = 90; 104 | } 105 | 106 | // The dimensions provided contain the measurement type at the end. 107 | // We strictly assume we are dealing with mm. Get with the times America :) 108 | // So lets just strip that away with a simple parseInt call. 109 | width = parseInt(width); 110 | height = parseInt(height); 111 | 112 | // Convert mm to pixels 113 | var mm_to_inch = 25.4; 114 | width = (width / mm_to_inch) * ppi; 115 | height = (height / mm_to_inch) * ppi; 116 | 117 | // Finally return an object with the nromalised sizes. 118 | return { width: width, height: height }; 119 | } 120 | 121 | /** 122 | * Simple Error/Exception Handler 123 | * 124 | * @param string msg The error message to output. 125 | * @param object trace The stack trace 126 | */ 127 | function whoops(msg, trace) 128 | { 129 | var msgStack = []; 130 | 131 | msgStack.push(msg); 132 | msgStack.push(''); 133 | 134 | if (trace && trace.length) 135 | { 136 | msgStack.push('TRACE:'); 137 | 138 | trace.forEach(function(t) 139 | { 140 | msgStack.push 141 | ( 142 | ' -> ' + (t.file || t.sourceURL) + ': ' + t.line + 143 | (t.function ? ' (in function ' + t.function +')' : '') 144 | ); 145 | }); 146 | } 147 | 148 | system.stderr.writeLine(msgStack.join('\n')); 149 | 150 | phantom.exit(1); 151 | }; 152 | 153 | /** 154 | * Reads in the CLI Arguments 155 | * 156 | * This was inspired by: http://goo.gl/hzK0H5 157 | * 158 | * @param object defaults An object of default options. 159 | * Anthing set to null must be provided. 160 | * Anything with a value or set to undefined is optional. 161 | * 162 | * @throws Error When we find an unknown argument. 163 | * 164 | * @return object 165 | */ 166 | function readArgs(defaults) 167 | { 168 | // Loop through the supplied arguments 169 | system.args.forEach(function(arg, index, args) 170 | { 171 | // We only care about arguments that start with a "-" 172 | if (arg[0] != '-') return; 173 | 174 | // Remove any preceding dashes from the argument name 175 | arg = arg.replace(/-/g, ''); 176 | 177 | // Make sure the argument is a valid option 178 | if (!defaults.hasOwnProperty(arg)) 179 | { 180 | throw new Error('Unknown argument: ' + arg); 181 | } 182 | 183 | // Grab the next argument, as this will be the actual value 184 | // eg: phantomjs script.js --option-name option-value 185 | // TODO: support the likes of: --option-name=option-value 186 | var value = args[index+1]; 187 | 188 | // Assign the argument value 189 | defaults[arg] = value; 190 | }); 191 | 192 | // Now make sure we don't have any null values 193 | Object.keys(defaults).forEach(function(key) 194 | { 195 | if (defaults[key] === null) 196 | { 197 | throw new Error('Required argument not supplied: ' + key); 198 | } 199 | }); 200 | 201 | // Finally return our arguments object 202 | return defaults; 203 | } 204 | -------------------------------------------------------------------------------- /src/Pdf/Html/Print.css: -------------------------------------------------------------------------------- 1 | /*////////////////////////////////////////////////////////////////////////////// 2 | // __________ __ ________ __________ 3 | // \______ \ |__ ______ / _____/ ____ _____ ______\______ \ _______ ___ 4 | // | ___/ | \\____ \/ \ ____/ __ \\__ \\_ __ \ | _// _ \ \/ / 5 | // | | | Y \ |_> > \_\ \ ___/ / __ \| | \/ | ( <_> > < 6 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 7 | // \/|__| \/ \/ \/ \/ \/ 8 | // ----------------------------------------------------------------------------- 9 | // Designed and Developed by Brad Jones 10 | // ----------------------------------------------------------------------------- 11 | //////////////////////////////////////////////////////////////////////////////*/ 12 | 13 | /** 14 | * In this custom phantomjs printing environment unexpected margins can be a 15 | * real pain to track down and fix. Every time I ran into some sort of 16 | * unexpected layout behaviour it was usally because of margin so we 17 | * blindly reset everything. 18 | */ 19 | * { margin: 0; } 20 | 21 | /** 22 | * Next another pain in my arse was collapsed margins. 23 | * This will ensure they don't cause any trouble either. 24 | * 25 | * @see http://stackoverflow.com/questions/1762539 26 | */ 27 | * { overflow: auto; } 28 | 29 | /** 30 | * So now lets add some spacing back to the main typographical elements. 31 | */ 32 | h1, h2, h3, h4, h5, h6, p, hr { margin-bottom: 10px; } 33 | 34 | /** 35 | * Ensure all the main page parts or containers are blocks. 36 | * This is crtical for accurate height measurement. 37 | */ 38 | header, main, footer, div { display:block; } 39 | 40 | /** 41 | * Set a sensible default font. 42 | * 43 | * NOTE: You must use phantomjs 2.x to be able to use custom fonts. 44 | */ 45 | body { font-family: sans-serif; } 46 | 47 | /** 48 | * We do not use the phantomjs margin paper size option. 49 | * This allows ultimate control of the layout with CSS. 50 | */ 51 | header { margin-top: 20px; } 52 | header, main, footer { margin-left: 20px; margin-right: 20px; } 53 | footer { margin-top:20px; margin-bottom: 20px; } 54 | 55 | /** 56 | * Ensure every explicity defined page container creates a new page. 57 | */ 58 | .page { page-break-before: always; } 59 | 60 | /** 61 | * Sticky Footers 62 | * 63 | * It goes without saying that the footer of every page should be at the bottom 64 | * regardless of the height of the content in the main container. 65 | */ 66 | .page { position: relative; } 67 | footer { position: absolute; bottom: 0; } 68 | 69 | /** 70 | * Debugging Backgrounds 71 | * 72 | * Theroticaqlly we should be able to just load the HTML Document into a webkit 73 | * browser (ie: Chrome) and debug the layout of the page in a normal fashion 74 | * using the inspector. 75 | * 76 | * However in practise this doesn't really work as there are diffrences between 77 | * phantomjs and chrome. There are obviously diffrences between a screen view 78 | * and a print view. Thus it is easier to enable these styles so we can actually 79 | * see the size of various elements in the final PDF. 80 | * 81 | 82 | .page { background-color: red; } 83 | header { background-color: green; } 84 | main { background-color: chartreuse; } 85 | footer { background-color: blue; } 86 | .to-big-outer { background-color: aqua; } 87 | .to-big-inner { background-color: blueviolet; } */ 88 | -------------------------------------------------------------------------------- /src/Pdf/Html/Print.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // __________ __ ________ __________ 3 | // \______ \ |__ ______ / _____/ ____ _____ ______\______ \ _______ ___ 4 | // | ___/ | \\____ \/ \ ____/ __ \\__ \\_ __ \ | _// _ \ \/ / 5 | // | | | Y \ |_> > \_\ \ ___/ / __ \| | \/ | ( <_> > < 6 | // |____| |___| / __/ \______ /\___ >____ /__| |______ /\____/__/\_ \ 7 | // \/|__| \/ \/ \/ \/ \/ 8 | // ----------------------------------------------------------------------------- 9 | // Designed and Developed by Brad Jones 10 | // ----------------------------------------------------------------------------- 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | /** 14 | * Before Print Event 15 | * 16 | * This is called by phantomjs before printing the HTML document to PDF. 17 | * Allowing us here to make adjustments to the DOM. 18 | * 19 | * IMPORTANT: Please ensure that this method remain syncronous! 20 | * 21 | * @param string width The width of the page, in pixels. 22 | * @param string height The height of the page, in pixels. 23 | * @param int version The major version of phantonjs. 24 | */ 25 | function beforePrint(width, height, version) 26 | { 27 | // For the lazy lets ensure at least one page container exists 28 | ensureBasicPageStructureExists(); 29 | 30 | // Now add default headers and footers 31 | addDefaultHeadersFooters(); 32 | 33 | // Set all the existing pages to the page dimensions provided 34 | $('.page').css({ width: width, height: height }); 35 | 36 | // Now loop through each page and check if it has overflowing content 37 | splitPages(width, height, 0); 38 | 39 | // Create a Table of Contents 40 | createToc(); 41 | 42 | // Now lets set some special span tag values 43 | setSpecialFieldValues(); 44 | } 45 | 46 | /** 47 | * Ensure the Basic Page Structure Exists 48 | * 49 | * Because I know people are lazy, myself included, this function 50 | * will build some missing containers if they are omitted. 51 | */ 52 | function ensureBasicPageStructureExists() 53 | { 54 | // We must have at least one page container 55 | if ($('.page').length == 0) 56 | { 57 | var page = $('
'); 58 | 59 | // The page container must then have a main container 60 | // NOTE:
and