├── src ├── Exceptions │ ├── FontException.php │ ├── TextException.php │ ├── ColorException.php │ ├── ImageException.php │ ├── MeasureException.php │ ├── TableException.php │ ├── DocumentException.php │ ├── PdfWriterException.php │ ├── CoordinateException.php │ └── DifferentLocationException.php ├── Facades │ └── Pdf.php ├── Concerns │ ├── FromTemplate.php │ ├── WithColors.php │ ├── DifferentFontsLocation.php │ ├── DifferentExportLocation.php │ ├── DifferentTemplateLocation.php │ ├── WithPreview.php │ └── WithDraw.php ├── WriterComponents │ ├── WriterComponent.php │ └── Table.php ├── Commands │ ├── stubs │ │ └── document.standard.stub │ └── DocumentMakeCommand.php ├── Helpers │ └── MeasureCalculator.php ├── PdflibServiceProvider.php ├── Files │ └── FileManager.php ├── Pdf.php └── Writers │ ├── PdfWriter.php │ └── PdflibPdfWriter.php ├── .styleci.yml ├── phpunit.xml.dist ├── LICENSE.md ├── composer.json ├── config └── pdf.php └── README.md /src/Exceptions/FontException.php: -------------------------------------------------------------------------------- 1 | newPage(); 18 | 19 | $writer->useFont('Arial', 10) 20 | ->writeText('Start something great'); 21 | } 22 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ./tests/ 9 | 10 | 11 | 12 | 13 | ./src 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) contoweb AG 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. -------------------------------------------------------------------------------- /src/Commands/DocumentMakeCommand.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 23 | $this->getConfigFile(), 24 | 'pdf' 25 | ); 26 | 27 | $writerClass = config('pdf.writer') ?? PdflibPdfWriter::class; 28 | 29 | $this->app->bind(PdfWriter::class, function () use ($writerClass) { 30 | $writer = new $writerClass( 31 | config('pdf.license'), 32 | config('pdf.creator', 'Laravel') 33 | ); 34 | 35 | if ($writer instanceof PdfWriter !== true) { 36 | throw new PdfWriterException('Writer must implement PdfWriter interface'); 37 | } 38 | 39 | return $writer; 40 | }); 41 | 42 | $this->app->bind('pdf', function () { 43 | return new Pdf( 44 | $this->app->make(PdfWriter::class) 45 | ); 46 | }); 47 | 48 | $this->app->alias('pdf', Pdf::class); 49 | 50 | $this->commands([DocumentMakeCommand::class]); 51 | } 52 | 53 | /** 54 | * Bootstrap services. 55 | * 56 | * @return void 57 | */ 58 | public function boot() 59 | { 60 | /* Todo: Lumen setup */ 61 | 62 | if ($this->app->runningInConsole()) { 63 | $this->publishes([ 64 | $this->getConfigFile() => config_path('pdf.php'), 65 | ], 'config'); 66 | } 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | protected function getConfigFile(): string 73 | { 74 | return __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'pdf.php'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/pdf.php: -------------------------------------------------------------------------------- 1 | '', 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Measurement 22 | |-------------------------------------------------------------------------- 23 | | 24 | | In which unit the package should position your elements. 25 | | You can choose between "mm" or "pt" 26 | | 27 | */ 28 | 'measurement' => [ 29 | 'unit' => 'mm', 30 | ], 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Fonts 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Define fonts location. 38 | | Use OTF fonts for best result. 39 | | 40 | */ 41 | 'fonts' => [ 42 | 'disk' => 'local', 43 | 'path' => '', 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Templates 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Define the location of your PDF templates. 52 | | 53 | */ 54 | 'templates' => [ 55 | 'disk' => 'local', 56 | 'path' => '', 57 | ], 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Exports 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Define the location of your generated PDFs. 65 | | 66 | */ 67 | 'exports' => [ 68 | 'disk' => 'local', 69 | 'path' => '', 70 | ], 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | PDF writer 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Define the writer class. It must implement the PdfWriter interface. 78 | | 79 | */ 80 | 'writer' => PdflibPdfWriter::class, 81 | ]; 82 | -------------------------------------------------------------------------------- /src/Files/FileManager.php: -------------------------------------------------------------------------------- 1 | document = $document; 24 | } 25 | 26 | /** 27 | * Return the pdf export path. 28 | * 29 | * @param string $fileName 30 | * @return string 31 | * 32 | * @throws DifferentLocationException 33 | */ 34 | public function exportPath($fileName) 35 | { 36 | if ($this->document instanceof DifferentExportLocation) { 37 | $location = $this->document->exportLocation(); 38 | 39 | return $this->absolutPathForDifferentLocation($location, $fileName); 40 | } 41 | 42 | return self::absolutePath( 43 | config('pdf.exports.disk', 'local'), 44 | config('pdf.exports.path', ''), 45 | $fileName 46 | ); 47 | } 48 | 49 | /** 50 | * Return the template path. 51 | * 52 | * @param string $fileName 53 | * @return string 54 | * 55 | * @throws DifferentLocationException 56 | */ 57 | public function templatePath($fileName) 58 | { 59 | if ($this->document instanceof DifferentTemplateLocation) { 60 | $location = $this->document->templateLocation(); 61 | 62 | return $this->absolutPathForDifferentLocation($location, $fileName); 63 | } 64 | 65 | return self::absolutePath( 66 | config('pdf.templates.disk', 'local'), 67 | config('pdf.templates.path', ''), 68 | $fileName 69 | ); 70 | } 71 | 72 | /** 73 | * Return the path of a font. 74 | * 75 | * @param $name 76 | * @param string $type 77 | * @return string 78 | * 79 | * @throws DifferentLocationException 80 | */ 81 | public function fontPath($name, $type = null) 82 | { 83 | $fileName = $name . '.' . ($type ?: 'ttf'); 84 | 85 | if ($this->document instanceof DifferentFontsLocation) { 86 | $location = $this->document->fontsLocation(); 87 | 88 | return $this->absolutPathForDifferentLocation($location, $fileName); 89 | } 90 | 91 | return self::absolutePath( 92 | config('pdf.fonts.disk', 'local'), 93 | config('pdf.fonts.path', ''), 94 | $fileName 95 | ); 96 | } 97 | 98 | /** 99 | * Return the fonts location. 100 | * 101 | * @return string 102 | * 103 | * @throws DifferentLocationException 104 | */ 105 | public function fontsDirectory() 106 | { 107 | if ($this->document instanceof DifferentFontsLocation) { 108 | $location = $this->document->fontsLocation(); 109 | 110 | return $this->absolutPathForDifferentLocation($location, null); 111 | } 112 | 113 | return self::absolutePath( 114 | config('pdf.fonts.disk', 'local'), 115 | config('pdf.fonts.path', '') 116 | ); 117 | } 118 | 119 | /** 120 | * Absolute path to the file. 121 | * 122 | * @param string $disk 123 | * @param string $prefix 124 | * @param string|null $fileName 125 | * @return string 126 | */ 127 | protected static function absolutePath($disk, $prefix, $fileName = null) 128 | { 129 | if ($prefix === null) { 130 | $prefix = ''; 131 | } 132 | 133 | $path = Storage::disk($disk)->path($prefix); 134 | 135 | if ($prefix !== '') { 136 | $path .= DIRECTORY_SEPARATOR; 137 | } 138 | 139 | return $path . $fileName; 140 | } 141 | 142 | /** 143 | * Get the absolut path for document's different location. 144 | * 145 | * @param $location 146 | * @param $fileName 147 | * @return string 148 | * 149 | * @throws DifferentLocationException 150 | */ 151 | protected function absolutPathForDifferentLocation($location, $fileName) 152 | { 153 | if ( 154 | array_key_exists('disk', $location) === false || 155 | array_key_exists('path', $location) === false 156 | ) { 157 | throw new DifferentLocationException('Invalid different location parameters provided.'); 158 | } 159 | 160 | return self::absolutePath( 161 | $location['disk'], 162 | $location['path'], 163 | $fileName 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Pdf.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 40 | } 41 | 42 | /** 43 | * @param WithDraw $document 44 | * @param string $fileName 45 | * @return Pdf 46 | * 47 | * @throws Exception 48 | */ 49 | public function store(WithDraw $document, string $fileName): Pdf 50 | { 51 | $this->document = $document; 52 | $this->fileName = $fileName; 53 | 54 | if ($document instanceof ShouldQueue) { 55 | // Working on it... 56 | } 57 | 58 | $this->create(); 59 | 60 | return $this; 61 | } 62 | 63 | // /** 64 | // * @param $document 65 | // * @param $fileName 66 | // * @return Pdf 67 | // * @throws Exception 68 | // */ 69 | // public function download($document, $fileName) 70 | // { 71 | // // Working on it... 72 | // } 73 | 74 | /** 75 | * @param string|null $fileName 76 | * @return true 77 | * 78 | * @throws Exception 79 | */ 80 | public function withPreview(?string $fileName = null): bool 81 | { 82 | $mainMode = $this->previewMode; 83 | 84 | $this->inPreviewMode(); 85 | 86 | $this->previewFileName($fileName); 87 | 88 | $this->create(); 89 | 90 | // Switch back to the main mode. 91 | $this->previewMode = $mainMode; 92 | 93 | return true; 94 | } 95 | 96 | /** 97 | * @return $this 98 | */ 99 | public function inPreviewMode(): static 100 | { 101 | $this->previewMode = true; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @return $this 108 | */ 109 | public function inOriginalMode(): static 110 | { 111 | $this->previewMode = false; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Creates the pdf document(s). 118 | * 119 | * @return void 120 | * 121 | * @throws Exception 122 | */ 123 | public function create(): void 124 | { 125 | $fileManager = new FileManager($this->document); 126 | 127 | $this->writer->defineFontSearchPath($fileManager->fontsDirectory()); 128 | 129 | $this->writer->beginDocument($fileManager->exportPath($this->fileName)); 130 | 131 | if ($this->document instanceof FromTemplate) { 132 | $template = $this->document->template(); 133 | 134 | if ($this->document instanceof WithPreview) { 135 | if ($this->previewMode === false) { 136 | $this->applyOffset(); 137 | } 138 | 139 | if ($this->previewMode === true) { 140 | $this->writer->disableOffset(); 141 | 142 | $template = $this->document->previewTemplate(); 143 | } 144 | } 145 | 146 | $this->writer->loadTemplate($fileManager->templatePath($template)); 147 | } 148 | 149 | if ($this->document instanceof WithColors) { 150 | foreach ($this->document->colors() as $name => $color) { 151 | $this->writer->loadColor($name, $color); 152 | } 153 | } 154 | 155 | if ($this->document instanceof WithDraw) { 156 | foreach ($this->document->fonts() as $name => $settings) { 157 | if (is_int($name)) { 158 | $name = $settings; 159 | $settings = []; 160 | } 161 | 162 | $this->writer->loadFont( 163 | $name, 164 | array_key_exists('encoding', $settings) ? $settings['encoding'] : null, 165 | array_key_exists('optlist', $settings) ? $settings['optlist'] : null 166 | ); 167 | } 168 | 169 | $this->document->draw($this->writer); 170 | } 171 | 172 | if ($this->document instanceof FromTemplate) { 173 | $this->writer->closeTemplate(); 174 | } 175 | 176 | $this->writer->finishDocument(); 177 | } 178 | 179 | /** 180 | * @param $fileName 181 | * @return void 182 | */ 183 | private function previewFileName($fileName = null): void 184 | { 185 | if ($fileName) { 186 | $this->fileName = $fileName; 187 | } else { 188 | // Extend file name before extension 189 | $extensionPos = strrpos($this->fileName, '.'); 190 | $this->fileName = substr($this->fileName, 0, $extensionPos) . '_preview' . substr($this->fileName, $extensionPos); 191 | } 192 | } 193 | 194 | /** 195 | * Apply the defined document offset. 196 | * 197 | * @retrun void 198 | * 199 | * @throws MeasureException 200 | */ 201 | private function applyOffset(): void 202 | { 203 | $offsetArray = array_change_key_case($this->document->offset()); 204 | 205 | if (array_key_exists('x', $offsetArray)) { 206 | $this->writer->setXOffset($offsetArray['x'], config('pdf.measurement.unit', 'pt')); 207 | } else { 208 | throw new MeasureException('No X offset defined.'); 209 | } 210 | 211 | if (array_key_exists('y', $offsetArray)) { 212 | $this->writer->setYOffset($offsetArray['y'], config('pdf.measurement.unit', 'pt')); 213 | } else { 214 | throw new MeasureException('No Y offset defined.'); 215 | } 216 | 217 | $this->writer->enableOffset(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/WriterComponents/Table.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 51 | $this->pdflibTable = 0; 52 | } 53 | 54 | /** 55 | * Set items for table. 56 | * 57 | * @param array $items 58 | */ 59 | public function setItems(array $items) 60 | { 61 | $this->items = $items; 62 | } 63 | 64 | /** 65 | * Add a header to table. 66 | * 67 | * @param array $names 68 | * @param string|null $font 69 | * @param int|null $fontSize 70 | * @param string|null $position 71 | * @return $this 72 | * 73 | * @throws \Contoweb\Pdflib\Exceptions\MeasureException 74 | */ 75 | public function withHeader($names, $font = null, $fontSize = null, $position = null) 76 | { 77 | foreach ($this->columns as $key=>$tableColumn) { 78 | array_push( 79 | $this->headers, 80 | [ 81 | 'name' => $names, 82 | 'width' => MeasureCalculator::calculateToPt($tableColumn['width'], $tableColumn['unit']), 83 | 'unit' => $tableColumn['unit'] ?: config('pdf.measurement.unit', 'pt'), 84 | 'font' => $font ?: 'Arial', 85 | 'fontsize' => $fontSize ?: 10, 86 | 'position' => $position ?: 'left bottom', 87 | ] 88 | ); 89 | } 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Add a column for the given table. 96 | * 97 | * @param int $columnWidth 98 | * @param string|null $unit 99 | * @param string|null $font 100 | * @param int|null $fontSize 101 | * @param int|string $position 102 | * @return $this 103 | * 104 | * @throws \Contoweb\Pdflib\Exceptions\MeasureException 105 | */ 106 | public function addColumn($columnWidth, $unit = null, $font = null, $fontSize = null, $position = null) 107 | { 108 | array_push( 109 | $this->columns, 110 | [ 111 | 'width' => MeasureCalculator::calculateToPt($columnWidth, $unit), 112 | 'unit' => $unit ?: config('pdf.measurement.unit', 'pt'), 113 | 'font' => $font ?: 'Arial', 114 | 'fontsize' => $fontSize ?: 10, 115 | 'position' => $position ?: 'left bottom', 116 | ] 117 | ); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Add a cell to the given table. 124 | * 125 | * @param array $table 126 | * @param int|null $column 127 | * @param int|null $row 128 | * @param string|null $name 129 | * @param string|null $optlist 130 | * @return $this 131 | * 132 | * @throws TableException 133 | */ 134 | public function addCell($table, $column, $row, $name, $optlist = null) 135 | { 136 | $this->pdflibTable = $this->writer->add_table_cell($table, $column, $row, $name, $optlist); 137 | 138 | if ($this->pdflibTable == 0) { 139 | throw new TableException('Error adding cell: ' . $this->writer->get_errmsg()); 140 | } 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * Draw the given table. 147 | * 148 | * @param string|null $optlist 149 | * @return PdflibPdfWriter 150 | * 151 | * @throws \Contoweb\Pdflib\Exceptions\MeasureException|TableException 152 | */ 153 | public function place($optlist = null) 154 | { 155 | if ($optlist === null) { 156 | if (count($this->headers) > 0) { 157 | $headerCount = '1'; 158 | } else { 159 | $headerCount = '0'; 160 | } 161 | $optlist = 'header=' . $headerCount . ' footer=0 stroke={ {line=horother linewidth=0.3}}'; 162 | } 163 | 164 | // Start the table with row 1 165 | $row = 1; 166 | $col = 1; 167 | 168 | if ($this->headers) { 169 | foreach ($this->headers as $index => $tableHeader) { 170 | $this->addCell( 171 | $this->pdflibTable, 172 | $col++, 173 | $row, 174 | isset(array_values($tableHeader['name'])[$index]) ? array_values($tableHeader['name'])[$index] : '', 175 | 'fittextline={font=' . 176 | $this->writer->getFonts()[$tableHeader['font']] . 177 | ' fontsize=' . $tableHeader['fontsize'] . 178 | ' position={' . $tableHeader['position'] . '} 179 | }' . 180 | ' colwidth=' . $tableHeader['width'] 181 | ); 182 | } 183 | 184 | $row++; 185 | $col = 1; 186 | } 187 | 188 | for ($itemno = 1; $itemno <= count($this->items); $itemno++, $row) { 189 | foreach ($this->columns as $index => $column) { 190 | $this->addCell( 191 | $this->pdflibTable, 192 | $col++, 193 | $row, 194 | isset(array_values($this->items[$itemno - 1])[$index]) ? array_values($this->items[$itemno - 1])[$index] : '', 195 | 'fittextline={font=' . 196 | $this->writer->getFonts()[$column['font']] . 197 | ' fontsize=' . $column['fontsize'] . 198 | ' position={' . $column['position'] . '} 199 | }' . 200 | ' colwidth=' . $column['width'] 201 | ); 202 | } 203 | $row++; 204 | $col = 1; 205 | } 206 | 207 | $this->fitTable( 208 | $this->pdflibTable, 209 | MeasureCalculator::calculateToPt($this->writer->getXPosition(), 'pt'), 210 | 0, 211 | $this->writer->getPageSize('width'), 212 | MeasureCalculator::calculateToPt($this->writer->getYPosition(), 'pt'), 213 | $optlist 214 | ); 215 | 216 | // reset set table-data for next table 217 | $this->items = []; 218 | $this->columns = []; 219 | $this->headers = []; 220 | $this->pdflibTable = 0; 221 | 222 | return $this->writer; 223 | } 224 | 225 | /** 226 | * Fit the given table. 227 | * 228 | * @param array $table 229 | * @param int $lowerLeftX 230 | * @param int $lowerLeftY 231 | * @param int $upperRightX 232 | * @param int $upperRightY 233 | * @param string $optlist 234 | * @return string 235 | * 236 | * @throws TableException 237 | */ 238 | private function fitTable($table, $lowerLeftX, $lowerLeftY, $upperRightX, $upperRightY, $optlist) 239 | { 240 | $result = $this->writer->fit_table($table, $lowerLeftX, $lowerLeftY, $upperRightX, $upperRightY, $optlist); 241 | 242 | if ($result == '_error') { 243 | throw new TableException("Couldn't place table : " . $this->writer->get_errmsg()); 244 | } 245 | 246 | return $result; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Writers/PdfWriter.php: -------------------------------------------------------------------------------- 1 | Contoweb\Pdflib\Facades\Pdf::class, 75 | ``` 76 | 77 | ## Usage 78 | 79 | To create a new document, you can use the `make:document` command: 80 | ```shell 81 | php artisan make:document MarketingDocument 82 | ``` 83 | 84 | The new document file can be found in the `app/Documents` directory. 85 | 86 | > app\Documents\MarketingDocument 87 | 88 | Ìn a final step, generating a PDF is as easy as: 89 | 90 | ```php 91 | 92 | use App\Documents\MarketingDocument; 93 | use Contoweb\Pdflib\Facades\Pdf; 94 | use App\Http\Controllers\Controller; 95 | 96 | class MarketingController extends Controller 97 | { 98 | public function storeDocument() 99 | { 100 | return Pdf::store(new MarketingDocument, 'marketing.pdf'); 101 | } 102 | } 103 | ``` 104 | 105 | You can find your document in your configured export path then! 106 | But first, let us take a look how to write a simple PDF. 107 | 108 | ### Quick start 109 | Within your document file, you have a boilerplated method `draw()`: 110 | 111 | ```php 112 | public function draw(Writer $writer) 113 | { 114 | $writer->newPage(); 115 | $writer->useFont('Arial', 12); 116 | $writer->writeText('Start something great...'); 117 | } 118 | ``` 119 | 120 | Here you actually write the document's content. As you can see, a small example is already boilerplated: 121 | 122 | 1. Create a new page in the document. 123 | 2. Use an available font from the `fonts()` method. 124 | 3. Write the text. 125 | 126 | ### Create a page 127 | To create a new page, you can use 128 | ```php 129 | $writer->newPage(); 130 | ``` 131 | 132 | You can optionally define the width and height of your document by passing the parameters. 133 | 134 | ```php 135 | $writer->newPage(210, 297); // A4 portrait format 136 | ``` 137 | 138 | #### Using a template 139 | In most cases, you want to write dynamic content on a already designed PDF. 140 | To use a PDF template, use the `FromTemplate` concern and define the template PDF in a new `template()` function: 141 | 142 | ```php 143 | namespace App\Documents; 144 | 145 | use Contoweb\Pdflib\Concerns\FromTemplate; 146 | use Contoweb\Pdflib\Concerns\WithDraw; 147 | use Contoweb\Pdflib\Writers\PdfWriter as Writer; 148 | 149 | class MarketingDocument implements FromTemplate, WithDraw 150 | { 151 | public function template(): string { 152 | return 'template.pdf'; 153 | } 154 | 155 | // ... 156 | 157 | public function draw(Writer $writer) 158 | { 159 | $writer->newPage()->fromTemplatePage(1); 160 | } 161 | } 162 | ``` 163 | 164 | Now, your first page is using the page 1 from `template.pdf`. 165 | As you can see, you don't need to define a page size since it's using the template's size. 166 | Don't forget to configure your templates location in the configuration file. 167 | 168 | ##### Preview and print PDF 169 | If you're aware of (professional) print-ready PDFs, you may know that your print PDF isn't the same as the user finally sees. 170 | 171 | ![pdf-bleed](https://user-images.githubusercontent.com/13394801/65696401-6e6fdc00-e079-11e9-96fa-86e9d40d6aa1.jpg) 172 | 173 | There is a bleed box, crop marks and so on. For this case, you can use `WithPreview` combined with the `FromTemplate` concern. 174 | While your original template includes all the boxes and marks, your preview PDF is a preview of the final document. 175 | 176 | This requires you to add a `previewTemplate()` and `offset()` method. 177 | 178 | ```php 179 | namespace App\Documents; 180 | 181 | use Contoweb\Pdflib\Concerns\FromTemplate; 182 | use Contoweb\Pdflib\Concerns\WithDraw; 183 | use Contoweb\Pdflib\Concerns\WithPreview; 184 | use Contoweb\Pdflib\Writers\PdfWriter as Writer; 185 | 186 | class MarketingDocument implements FromTemplate, WithDraw, WithPreview 187 | { 188 | public function template(): string { 189 | return 'print.pdf'; 190 | } 191 | 192 | public function previewTemplate(): string 193 | { 194 | return 'preview.pdf'; 195 | } 196 | 197 | public function offset(): array 198 | { 199 | return [ 200 | 'x' => 20, 201 | 'y' => 20, 202 | ]; 203 | } 204 | 205 | // 206 | } 207 | ``` 208 | 209 | The `offset()` method defines the offset from the print PDF to the preview PDF (see image above). 210 | 211 | Now you can generate the preview PDF with: 212 | ```php 213 | return Pdf::inPreviewMode()->store(new MarketingDocument, 'marketing.pdf'); 214 | ``` 215 | 216 | You can also generate the print and preview PDF in one step: 217 | ```php 218 | return Pdf::store(new MarketingDocument, 'marketing.pdf')->withPreview(); 219 | ``` 220 | 221 | The preview PDF will be automatically named to `<>_preview.pdf`. 222 | You can override this by passing the name in `->withPreview('othername.pdf')`. 223 | 224 | ### Navigate on the page 225 | 226 | To tell PDFlib where your elements should be placed, you have to set the `X` and `Y` position of your "cursor". 227 | 228 | ```php 229 | $writer->setPosition(10, 100); 230 | 231 | // only X axis 232 | $writer->setXPosition(10); 233 | 234 | //only Y axis 235 | $writer->setYPosition(100); 236 | 237 | ``` 238 | 239 | In the configuration file, you can define which measure unit is used for positioning. You can choose between `mm` or `pt`. 240 | 241 | > **Note**: It may be confusing in the beginning, but PDFlib Y axis are measured from the bottom. 242 | So position 0 0 is in the left bottom corner, not the left top corner. 243 | 244 | ### Write text 245 | 246 | To write text, you can simply use: 247 | 248 | ```php 249 | $writer->writeTextLine('your text'); 250 | 251 | // or 252 | 253 | $writer->writeText('your text'); 254 | 255 | ``` 256 | 257 | Don't forget to set the cursor position and use the right font before writing text. 258 | Since the package extends PDFlib, you also can pass PDFlib options as a second parameter. 259 | 260 | > You only have to use `writeText` when placing two text blocks next to each other. 261 | Behind the scenes, `wirteText()` uses PDFlibs `show()` method, while `wirteTextLine()` uses the mostly used PDFlib method `fit_text_line()`. 262 | 263 | If you want to go to the next line, instead of reposition your cursor every time, you can use: 264 | ```php 265 | $writer->nextLine(); 266 | ``` 267 | To use a custom line spacing instead of 1.0, just pass it as a parameter or set the line spacing with: 268 | ```php 269 | $writer->setLineSpacing(2.0); 270 | ``` 271 | 272 | 273 | #### Fonts 274 | The boilerplate document loads `Arial` as an example font, but we don't provide a font file in the fonts folder. 275 | In this case, PDFlib tries to load it from your host fonts. 276 | You may want to use custom fonts and want ensure that your server is able to load it. 277 | So it's highly recommended to place the font files (currently .ttf and .otf is supported) inside your configured font location (see `pdf.php` configuration). 278 | 279 | As a next step, you have to make the fonts available in your document. For TrueType fonts, just use the file name without the extension to auto-load the font: 280 | ```php 281 | public function fonts(): array 282 | { 283 | return ['OpenSans-Regular']; 284 | } 285 | ``` 286 | 287 | An underlying font file like `OpenSans-Regular.ttf` has to be available in your fonts location. 288 | 289 | Now you can use the font in your document by it's name: 290 | 291 | ```php 292 | public function draw(Writer $writer) 293 | { 294 | $writer->newPage(); 295 | $writer->useFont('OpenSans-Regular', 12) 296 | ->writeText('This text is written with Open Sans font...'); 297 | } 298 | ``` 299 | 300 | You can also overwrite default font encoding and option list: 301 | 302 | ```php 303 | public function fonts(): array 304 | { 305 | return [ 306 | 'OpenSans-Regular' => [ 307 | 'encoding' => 'ansi', 308 | 'optlist' => '' 309 | ], 310 | ]; 311 | } 312 | ``` 313 | 314 | #### Colors 315 | If you need to colorize your text, you can use the ```WithColor``` concern. This requires you to define custom colors: 316 | ```php 317 | public function colors(): array 318 | { 319 | return [ 320 | 'orange-rgb' => ['rgb', 255, 165, 0], 321 | 'blue-cmyk' => ['cmyk', 100, 100, 0, 0], 322 | ]; 323 | } 324 | ``` 325 | 326 | You can use the color with: 327 | 328 | ```php 329 | $writer->useColor('orange-rgb'); 330 | ``` 331 | or as a parameter when using a font: 332 | ```php 333 | $writer->useFont('OpenSans-Regular', 12, 'blue-cmyk'); 334 | ``` 335 | 336 | ### Tables 337 | 338 | To write a table you can follow this example: 339 | 340 | ```php 341 | $items = [ 342 | ['first_name' => 'John', 'last_name' => 'Doe'], 343 | ['first_name' => 'Jane','last_name' => 'Doe'], 344 | ]; 345 | 346 | $table = $writer 347 | ->setPosition(10, 150) 348 | ->newTable($items); 349 | 350 | $table 351 | ->addColumn(50) 352 | ->addColumn(50) 353 | ->withHeader(['First name', 'Last name']) 354 | ->place("stroke={ {line=horother linewidth=0}}") 355 | ; 356 | ``` 357 | 358 | ### Images 359 | You can place images with: 360 | ```php 361 | $writer->drawImage('/path/to/the/image.jpeg', 150, 100); 362 | ``` 363 | This places an image with and resize it to 150x100. 364 | 365 | Since loading rounded images is just a pain in PDFlib, you can use the method: 366 | ```php 367 | $writer->circleImage('/path/to/the/image.jpeg', 100); 368 | ``` 369 | 370 | ### PDFlib functions 371 | Since this package extending PDFlib, you can use the whole PDFlib toolkit. 372 | The [PDFlib Cookbook](https://www.pdflib.com/pdflib-cookbook/) helps a lot, even to understand this package. 373 | 374 | ## Extending 375 | This package is just a basic beginning of wrapping PDFlib. 376 | Since PDFlib brings so much more functionality, we have to put the focus on the most used functions in the beginning. 377 | 378 | You're welcome to PR your ideas! 379 | 380 | ## Customization 381 | If you want to use a filesystem disk / path other than the config in a specific document, 382 | you can use the following concerns: 383 | 384 | - `Contoweb\Pdflib\Concerns\DifferentExportLocation`: Custom export location 385 | - `Contoweb\Pdflib\Concerns\DifferentFontsLocation`: Custom location for fonts 386 | - `Contoweb\Pdflib\Concerns\DifferentTemplateLocation`: Custom template location 387 | 388 | The storage and path are defined the same way as in the config file: 389 | 390 | ```php 391 | public function exportLocation(): array 392 | { 393 | return [ 394 | 'disk' => 'other', 395 | 'path' => null, 396 | ]; 397 | } 398 | 399 | public function fontsLocation(): array 400 | { 401 | return [ 402 | 'disk' => 'other', 403 | 'path' => 'custom-font-directory', 404 | ]; 405 | } 406 | 407 | public function templateLocation(): array 408 | { 409 | return [ 410 | 'disk' => 'other', 411 | 'path' => 'custom-template-directory', 412 | ]; 413 | } 414 | ``` 415 | 416 | ## License 417 | 418 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 419 | -------------------------------------------------------------------------------- /src/Writers/PdflibPdfWriter.php: -------------------------------------------------------------------------------- 1 | set_info('Creator', $creator); 113 | 114 | if ($license) { 115 | $this->set_option('license=' . $license); 116 | } 117 | 118 | $this->set_option('errorpolicy=return'); 119 | $this->set_option('stringformat=utf8'); 120 | } 121 | 122 | /** 123 | * @param $searchPath 124 | * @return $this 125 | */ 126 | public function defineFontSearchPath($searchPath) 127 | { 128 | $this->set_option('searchpath={' . rtrim($searchPath, DIRECTORY_SEPARATOR) . '}'); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function beginDocument($path, $optlist = null) 137 | { 138 | if ($this->begin_document($path, '') == 0) { 139 | throw new DocumentException('Error: ' . $this->get_errmsg()); 140 | } 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function finishDocument() 149 | { 150 | if ($this->siteOpen) { 151 | $this->end_page_ext(''); 152 | $this->siteOpen = false; 153 | } 154 | 155 | $this->end_document(''); 156 | 157 | $this->imageCache = []; 158 | 159 | return true; 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function newPage($width = 50, $height = 50, $optlist = null) 166 | { 167 | if ($this->siteOpen) { 168 | $this->end_page_ext(''); 169 | } 170 | 171 | $this->siteOpen = true; 172 | 173 | $this->begin_page_ext( 174 | MeasureCalculator::calculateToPt($width), 175 | MeasureCalculator::calculateToPt($height), 176 | $optlist ?: ''); 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * {@inheritdoc} 183 | */ 184 | public function getPageSize($side = 'width') 185 | { 186 | return $this->get_option('page' . $side, ''); 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function loadTemplate($absolutPath, $optlist = null) 193 | { 194 | $this->template = $this->open_pdi_document( 195 | $absolutPath, 196 | $optlist ?: '' 197 | ); 198 | 199 | if ($this->template == 0) { 200 | throw new DocumentException('Error: ' . $this->get_errmsg()); 201 | } 202 | 203 | return true; 204 | } 205 | 206 | /** 207 | * {@inheritdoc} 208 | */ 209 | public function fromTemplatePage($pageNumber) 210 | { 211 | $page = $this->open_pdi_page($this->template, $pageNumber, 'cloneboxes'); 212 | $this->fit_pdi_page($page, 0, 0, 'adjustpage cloneboxes'); 213 | $this->close_pdi_page($page); 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * {@inheritdoc} 220 | */ 221 | public function closeTemplate() 222 | { 223 | $this->close_pdi_document($this->template); 224 | 225 | return true; 226 | } 227 | 228 | /** 229 | * {@inheritdoc} 230 | */ 231 | public function loadColor($name, array $color) 232 | { 233 | array_unshift($color, 'fill'); 234 | 235 | // Divide all color definitions to convert it for PDFLib. 236 | $color = array_map(function ($definition) use ($color) { 237 | if (is_numeric($definition)) { 238 | if ($color[1] === 'cmyk') { 239 | return $definition / 100; 240 | } 241 | 242 | if ($color[1] === 'rgb') { 243 | return $definition / 255; 244 | } 245 | } 246 | 247 | return $definition; 248 | }, $color); 249 | 250 | // This allows to define rgb colors with only three parameters. 251 | if (! array_key_exists(5, $color)) { 252 | $color[5] = 0; 253 | } 254 | 255 | $this->colors[$name] = $color; 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * {@inheritdoc} 262 | */ 263 | public function useColor($name) 264 | { 265 | if (array_key_exists($name, $this->colors)) { 266 | try { 267 | call_user_func_array([$this, 'setcolor'], $this->colors[$name]); 268 | } catch (Exception $e) { 269 | throw new ColorException($e); 270 | } 271 | } else { 272 | throw new ColorException('Color "' . $name . '" not defined.'); 273 | } 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * {@inheritdoc} 280 | */ 281 | public function loadFont($name, $encoding = null, $optlist = null) 282 | { 283 | $this->fonts[$name] = $this->load_font($name, $encoding ?: 'unicode', $optlist ?: 'embedding'); 284 | 285 | if ($this->fonts[$name] == 0) { 286 | throw new FontException('Error: ' . $this->get_errmsg()); 287 | } 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * {@inheritdoc} 294 | */ 295 | public function useFont($name, $size, $color = null) 296 | { 297 | if (array_key_exists($name, $this->fonts)) { 298 | $this->setfont($this->fonts[$name], $size); 299 | } else { 300 | throw new FontException('Font "' . $name . '" not loaded.'); 301 | } 302 | 303 | $this->fontSize = $size; 304 | 305 | if ($color) { 306 | $this->useColor($color); 307 | } 308 | 309 | return $this; 310 | } 311 | 312 | /** 313 | * {@inheritdoc} 314 | */ 315 | public function getFonts() 316 | { 317 | return $this->fonts; 318 | } 319 | 320 | /** 321 | * Get the current font as an integer. 322 | * 323 | * @return int 324 | */ 325 | public function getCurrentFont() 326 | { 327 | return (int) $this->get_option('font', ''); 328 | } 329 | 330 | /** 331 | * {@inheritdoc} 332 | */ 333 | public function writeText($text) 334 | { 335 | $this->set_text_pos($this->xPos, $this->yPos); 336 | $this->show($text); 337 | 338 | return $this; 339 | } 340 | 341 | /** 342 | * {@inheritdoc} 343 | */ 344 | public function writeTextLine($text, $optlist = '') 345 | { 346 | $this->fit_textline($text, $this->xPos, $this->yPos, $optlist); 347 | 348 | return $this; 349 | } 350 | 351 | /** 352 | * {@inheritdoc} 353 | */ 354 | public function nextLine(?float $spacing = null) 355 | { 356 | $spacing = $spacing ?: $this->spacing; 357 | 358 | $this->setYPosition($this->yPos - ($this->fontSize * $spacing), 'pt', true); 359 | 360 | return $this; 361 | } 362 | 363 | /** 364 | * Set the line offset. 365 | * 366 | * @param float $spacing 367 | * @return $this 368 | */ 369 | public function setLineSpacing(float $spacing): static 370 | { 371 | $this->spacing = $spacing; 372 | 373 | return $this; 374 | } 375 | 376 | /** 377 | * {@inheritdoc} 378 | */ 379 | public function getTextWidth($text, $font = null, $fontSize = null, $unit = null) 380 | { 381 | $textWidth = MeasureCalculator::calculateToUnit( 382 | $this->stringwidth($text, $font ? $this->load_font($font, 'unicode', 'embedding') : $this->getCurrentFont(), $fontSize ?? $this->fontSize), 383 | $unit ?: config('pdf.measurement.unit', 'pt'), 384 | 'pt' 385 | ); 386 | 387 | return $textWidth; 388 | } 389 | 390 | /** 391 | * {@inheritdoc} 392 | */ 393 | public function drawImage($imagePath, $width, $height, $loadOptions = null, $fitOptions = null) 394 | { 395 | $image = $this->preloadImage($imagePath, $loadOptions); 396 | 397 | if (strpos($imagePath, '.pdf') || strpos($imagePath, '.svg')) { 398 | // vector images 399 | $fitObjectMethod = 'fit_graphics'; 400 | } else { 401 | // pixel images 402 | $fitObjectMethod = 'fit_image'; 403 | } 404 | 405 | $this->{$fitObjectMethod}( 406 | $image, 407 | MeasureCalculator::calculateToPt($this->xPos, 'pt'), 408 | MeasureCalculator::calculateToPt($this->yPos, 'pt'), 409 | $fitOptions ?: 'boxsize {' . MeasureCalculator::calculateToPt($width) . ' ' . MeasureCalculator::calculateToPt($height) . '} position left fitmethod=meet' 410 | ); 411 | 412 | return $this; 413 | } 414 | 415 | /** 416 | * {@inheritdoc} 417 | */ 418 | public function circleImage($imagePath, $size, $loadOptions = null) 419 | { 420 | $this->save(); 421 | 422 | $width = $size; 423 | $height = $size; 424 | $radius = $size / 2; 425 | 426 | // Set curves of the circle 427 | $this->moveto($this->xPos + $radius, $this->yPos); 428 | $this->lineto($this->xPos + $width - $radius, $this->yPos); 429 | $this->arc($this->xPos + $width - $radius, $this->yPos + $radius, $radius, 270, 360); 430 | $this->lineto($this->xPos + $width, $this->yPos + $height - $radius); 431 | $this->arc($this->xPos + $width - $radius, $this->yPos + $height - $radius, $radius, 0, 90); 432 | $this->lineto($this->xPos + $radius, $this->yPos + $height); 433 | $this->arc($this->xPos + $radius, $this->yPos + $height - $radius, $radius, 90, 180); 434 | $this->lineto($this->xPos, $this->yPos + $radius); 435 | $this->arc($this->xPos + $radius, $this->yPos + $radius, $radius, 180, 270); 436 | 437 | // Set the rounded corners from the code above 438 | $this->clip(); 439 | 440 | // Load image 441 | $image = $this->preloadImage($imagePath, $loadOptions); 442 | 443 | // Fit the image into the circle 444 | $this->fit_image($image, 445 | MeasureCalculator::calculateToPt($this->xPos, 'pt'), 446 | MeasureCalculator::calculateToPt($this->yPos, 'pt'), 447 | 'boxsize {' . MeasureCalculator::calculateToPt($width) . ' ' . MeasureCalculator::calculateToPt($height) . '} position center fitmethod=meet'); 448 | 449 | // Close image and restore original clipping (no clipping) 450 | $this->close_image($image); 451 | 452 | // Restore the state without rounded corners 453 | $this->restore(); 454 | 455 | return $this; 456 | } 457 | 458 | /** 459 | * {@inheritdoc} 460 | */ 461 | public function drawRectangle($width, $height) 462 | { 463 | $this->rect($this->xPos, $this->yPos, $width, $height); 464 | $this->fill(); 465 | } 466 | 467 | /** 468 | * {@inheritdoc} 469 | */ 470 | public function drawLine($xFrom, $xTo, $yFrom, $yTo, $lineWidth = 0.3, $unit = null) 471 | { 472 | $this->setlinewidth($lineWidth); 473 | $this->moveto(MeasureCalculator::calculateToPt($xFrom, $unit), MeasureCalculator::calculateToPt($yFrom, $unit)); 474 | $this->lineto(MeasureCalculator::calculateToPt($xTo, $unit), MeasureCalculator::calculateToPt($yTo, $unit)); 475 | $this->stroke(); 476 | } 477 | 478 | /** 479 | * {@inheritdoc} 480 | */ 481 | public function setPosition($x, $y, $unit = null) 482 | { 483 | $this->setXPosition($x, $unit); 484 | $this->setYPosition($y, $unit); 485 | 486 | return $this; 487 | } 488 | 489 | /** 490 | * {@inheritdoc} 491 | */ 492 | public function setXPosition($measure, $unit = null, $ignoreOffset = false) 493 | { 494 | $measure = MeasureCalculator::calculateToPt($measure, $unit); 495 | 496 | if ($this->useOffset && $ignoreOffset === false) { 497 | $measure += $this->xOffset; 498 | } 499 | 500 | $this->xPos = $measure; 501 | 502 | return $this; 503 | } 504 | 505 | /** 506 | * {@inheritdoc} 507 | */ 508 | public function getXPosition($unit = null) 509 | { 510 | return MeasureCalculator::calculateToUnit( 511 | $this->xPos, 512 | $unit ?: config('pdf.measurement.unit', 'pt'), 513 | 'pt' 514 | ); 515 | } 516 | 517 | /** 518 | * {@inheritdoc} 519 | */ 520 | public function setYPosition($measure, $unit = null, $ignoreOffset = false) 521 | { 522 | $measure = MeasureCalculator::calculateToPt($measure, $unit); 523 | 524 | if ($this->useOffset && $ignoreOffset === false) { 525 | $measure += $this->yOffset; 526 | } 527 | 528 | $this->yPos = $measure; 529 | 530 | return $this; 531 | } 532 | 533 | /** 534 | * {@inheritdoc} 535 | */ 536 | public function getYPosition($unit = null) 537 | { 538 | return MeasureCalculator::calculateToUnit( 539 | $this->yPos, 540 | $unit ?: config('pdf.measurement.unit', 'pt'), 541 | 'pt' 542 | ); 543 | } 544 | 545 | /** 546 | * {@inheritdoc} 547 | */ 548 | public function getElementPosition($infobox, $corner) 549 | { 550 | if ($this->info_matchbox($infobox, 1, 'exists') == 1) { 551 | return $this->info_matchbox($infobox, 1, $corner); 552 | } else { 553 | throw new CoordinateException('Error: ' . $this->get_errmsg()); 554 | } 555 | } 556 | 557 | /** 558 | * {@inheritdoc} 559 | */ 560 | public function getElementSize($element, $dimension = 'width') 561 | { 562 | return $this->info_table($element, $dimension); 563 | } 564 | 565 | /** 566 | * {@inheritdoc} 567 | */ 568 | public function setXOffset($measure, $unit = null) 569 | { 570 | $measure = MeasureCalculator::calculateToPt($measure, $unit); 571 | $this->xOffset = $measure; 572 | 573 | return $this; 574 | } 575 | 576 | /** 577 | * {@inheritdoc} 578 | */ 579 | public function setYOffset($measure, $unit = null) 580 | { 581 | $measure = MeasureCalculator::calculateToPt($measure, $unit); 582 | $this->yOffset = $measure; 583 | 584 | return $this; 585 | } 586 | 587 | /** 588 | * {@inheritdoc} 589 | */ 590 | public function enableOffset() 591 | { 592 | $this->useOffset = true; 593 | 594 | return $this; 595 | } 596 | 597 | /** 598 | * {@inheritdoc} 599 | */ 600 | public function disableOffset() 601 | { 602 | $this->useOffset = false; 603 | 604 | return $this; 605 | } 606 | 607 | /** 608 | * Loads existing or new image. 609 | * 610 | * @param $imagePath 611 | * @param $loadOptions 612 | * @return int 613 | * 614 | * @throws ImageException 615 | */ 616 | protected function preloadImage($imagePath, $loadOptions) 617 | { 618 | // We're using the PDFLib image index so the same image is only embedded one time in the PDF. 619 | if (array_key_exists($imagePath, $this->imageCache)) { 620 | $image = $this->imageCache[$imagePath]; 621 | } else { 622 | if (strpos($imagePath, '.pdf') || strpos($imagePath, '.svg')) { 623 | // vector images 624 | $image = $this->load_graphics('auto', $imagePath, $loadOptions ?: ''); 625 | } else { 626 | // pixel images 627 | $image = $this->load_image('auto', $imagePath, $loadOptions ?: ''); 628 | } 629 | $this->imageCache[$imagePath] = $image; 630 | } 631 | 632 | if ($image <= 0) { 633 | throw new ImageException($this->get_errmsg()); 634 | } 635 | 636 | return $image; 637 | } 638 | 639 | /** 640 | * {@inheritdoc} 641 | */ 642 | public function addTextflow($textflow, $title, $optlist = null) 643 | { 644 | $textflow = $this->add_textflow($textflow, $title, $optlist); 645 | 646 | if ($textflow == 0) { 647 | throw new TextException('Error: ' . $this->get_errmsg()); 648 | } 649 | 650 | return $textflow; 651 | } 652 | 653 | /** 654 | * {@inheritdoc} 655 | */ 656 | public function newTable($items) 657 | { 658 | $table = new Table($this); 659 | 660 | $table->setItems($items); 661 | 662 | return $table; 663 | } 664 | } 665 | --------------------------------------------------------------------------------