├── 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 | 
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 |
--------------------------------------------------------------------------------