Test Failed
Push — feature/pdfcpu ( 14c728 )
by Andreas
09:40
created

PdftkWrapper::setBinary()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * PDFtk wrapper
4
 *
5
 * @copyright 2014-2024 Institute of Legal Medicine, Medical University of Innsbruck
6
 * @author Martin Pircher <[email protected]>
7
 * @author Andreas Erhard <[email protected]>
8
 * @license LGPL-3.0-only
9
 * @link http://www.gerichtsmedizin.at/
10
 *
11
 * @package pdftk
12
 */
13
14
namespace Gmi\Toolkit\Pdftk;
15
16
use Symfony\Component\Process\Process;
17
18
use Gmi\Toolkit\Pdftk\Exception\FileNotFoundException;
19
use Gmi\Toolkit\Pdftk\Exception\PdfException;
20
use Gmi\Toolkit\Pdftk\Util\Escaper;
21
use Gmi\Toolkit\Pdftk\Util\ProcessFactory;
22
23
use Exception;
24
25
/**
26
 * Wrapper for PDFtk.
27
 *
28
 * @internal Only the methods exposed by the interfaces should be accessed from outside.
29
 */
30
class PdftkWrapper implements WrapperInterface, BinaryPathAwareInterface
31
{
32
    use BinaryPathAwareTrait;
33
34
    /**
35
     * @var ProcessFactory
36
     */
37
    private $processFactory;
38
39
    /**
40
     * @var Escaper
41
     */
42
    private $escaper;
43
44
    /**
45
     * Constructor.
46
     *
47
     * @throws FileNotFoundException
48
     */
49
    public function __construct(string $pdftkBinary = null, ProcessFactory $processFactory = null)
50
    {
51
        $this->setBinary($pdftkBinary ?: $this->guessBinary(PHP_OS));
52
        $this->processFactory = $processFactory ?: new ProcessFactory();
53
        $this->escaper = new Escaper();
54
    }
55
56
    /**
57
     * Guesses the pdftk binary path based on the operating system.
58
     */
59
    public function guessBinary(string $operatingSystemString): string
60
    {
61
        if (strtoupper(substr($operatingSystemString, 0, 3)) === 'WIN') {
62
            $binary = 'C:\\Program Files (x86)\\PDFtk Server\\bin\\pdftk.exe';
63
        } else {
64
            $binary = '/usr/bin/pdftk';
65
        }
66
67
        return $binary;
68
    }
69
70
    /**
71
     * {@inheritDoc}
72
     */
73
    public function join(array $filePaths, string $outfile): void
74
    {
75
        $esc = $this->escaper;
76
77
        $filePathsEscaped = array_map(function (string $filePath) use ($esc) {
78
            return $esc->shellArg($filePath);
79
        }, $filePaths);
80
81
        $fileList = implode(' ', $filePathsEscaped);
82
83
        $commandLine = sprintf('%s %s cat output %s', $this->getBinary(), $fileList, $esc->shellArg($outfile));
84
85
        /**
86
         * @var Process
87
         */
88
        $process = $this->processFactory->createProcess($commandLine);
89
90
        try {
91
            $process->mustRun();
92
        } catch (Exception $e) {
93
            throw new PdfException($e->getMessage(), 0, $e, $process->getErrorOutput(), $process->getOutput());
94
        }
95
96
        $process->getOutput();
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     */
102
    public function split(string $infile, array $mapping, string $outputFolder = null): void
103
    {
104
        $commandLines = $this->buildSplitCommandLines($infile, $mapping, $outputFolder);
105
106
        foreach ($commandLines as $commandLine) {
107
            $process = $this->processFactory->createProcess($commandLine);
108
            try {
109
                $process->mustRun();
110
            } catch (Exception $e) {
111
                throw new PdfException($e->getMessage(), 0, $e, $process->getErrorOutput(), $process->getOutput());
112
            }
113
        }
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119
    public function reorder(string $infile, array $order, string $outfile = null): void
120
    {
121
        $pageNumbers = implode(' ', $order);
122
123
        $temporaryOutFile = false;
124
125
        if ($outfile === null || $infile === $outfile) {
126
            $temporaryOutFile = true;
127
            $outfile = tempnam(sys_get_temp_dir(), 'pdf');
128
        }
129
130
        $esc = $this->escaper;
131
132
        $commandLine = sprintf(
133
            '%s %s cat %s output %s',
134
            $this->getBinary(),
135
            $esc->shellArg($infile),
136
            $pageNumbers,
137
            $esc->shellArg($outfile)
138
        );
139
140
        /**
141
         * @var Process
142
         */
143
        $process = $this->processFactory->createProcess($commandLine);
144
145
        try {
146
            $process->mustRun();
147
        } catch (Exception $e) {
148
            throw new PdfException(
149
                sprintf('Failed to reorder PDF "%s"! Error: %s', $infile, $e->getMessage()),
150
                0,
151
                $e,
152
                $process->getErrorOutput(),
153
                $process->getOutput()
154
            );
155
        }
156
157
        if ($temporaryOutFile) {
158
            unlink($infile);
159
            rename($outfile, $infile);
160
        }
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     */
166
    public function applyBookmarks(Bookmarks $bookmarks, string $infile, string $outfile = null): self
167
    {
168
        $this->updatePdfDataFromDump($infile, $this->buildBookmarksBlock($bookmarks), $outfile);
169
170
        return $this;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function importBookmarks(Bookmarks $bookmarks, string $infile): self
177
    {
178
        $dump = $this->getPdfDataDump($infile);
179
        $this->importBookmarksFromDump($bookmarks, $dump);
180
181
        return $this;
182
    }
183
184
    /**
185
     * {@inheritDoc}
186
     */
187
    public function importPages(Pages $pages, string $infile): self
188
    {
189
        $dump = $this->getPdfDataDump($infile);
190
        $this->importPagesFromDump($pages, $dump);
191
192
        return $this;
193
    }
194
195
    /**
196
     * {@inheritDoc}
197
     */
198
    public function applyMetadata(Metadata $metadata, string $infile, string $outfile = null): self
199
    {
200
        $metadataBlock = $this->buildMetadataBlock($metadata);
201
202
        $this->updatePdfDataFromDump($infile, $metadataBlock, $outfile);
203
204
        return $this;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210
    public function importMetadata(Metadata $metadata, string $infile): self
211
    {
212
        $dump = $this->getPdfDataDump($infile);
213
        $this->importMetadataFromDump($metadata, $dump);
214
215
        return $this;
216
    }
217
218
    /**
219
     * Get data dump.
220
     *
221
     * @throws PdfException
222
     */
223
    public function getPdfDataDump(string $pdf): string
224
    {
225
        if (!file_exists($pdf)) {
226
            throw new FileNotFoundException(sprintf('PDF "%s" not found!', $pdf));
227
        }
228
229
        $esc = $this->escaper;
230
231
        $tempfile = tempnam(sys_get_temp_dir(), 'pdf');
232
        $cmd = sprintf(
233
            '%s %s dump_data_utf8 output %s',
234
            $this->getBinary(),
235
            $esc->shellArg($pdf),
236
            $esc->shellArg($tempfile)
237
        );
238
239
        $process = $this->processFactory->createProcess($cmd);
240
241
        try {
242
            $process->mustRun();
243
        } catch (Exception $e) {
244
            $exception = new PdfException(
245
                sprintf('Failed to read PDF data from "%s"! Error: %s', $pdf, $e->getMessage()),
246
                0,
247
                $e,
248
                $process->getErrorOutput(),
249
                $process->getOutput()
250
            );
251
        }
252
253
        $dump = file_get_contents($tempfile);
254
        unlink($tempfile);
255
256
        if (isset($exception)) {
257
            throw $exception;
258
        }
259
260
        return $dump;
261
    }
262
263
    /**
264
     * Update PDF data from dump.
265
     *
266
     * @param string $pdf     input file
267
     * @param string $data    dump data or filename of containing dump data
268
     * @param string $outfile output file (is input when null)
269
     *
270
     * @throws PdfException
271
     */
272
    public function updatePdfDataFromDump(string $pdf, string $data, string $outfile = null): void
273
    {
274
        $temporaryOutFile = false;
275
276
        if (!file_exists($pdf)) {
277
            throw new FileNotFoundException(sprintf('PDF "%s" not found', $pdf));
278
        }
279
280
        if ($outfile === null || $pdf === $outfile) {
281
            $temporaryOutFile = true;
282
            $outfile = tempnam(sys_get_temp_dir(), 'pdf');
283
        }
284
285
        $tempfile = tempnam(sys_get_temp_dir(), 'pdf');
286
        file_put_contents($tempfile, $data);
287
288
        $esc = $this->escaper;
289
290
        $cmd = sprintf(
291
            '%s %s update_info_utf8 %s output %s',
292
            $this->getBinary(),
293
            $esc->shellArg($pdf),
294
            $esc->shellArg($tempfile),
295
            $esc->shellArg($outfile)
296
        );
297
298
        $process = $this->processFactory->createProcess($cmd);
299
300
        try {
301
            $process->mustRun();
302
        } catch (Exception $e) {
303
            $exception = new PdfException(
304
                sprintf('Failed to write PDF data to "%s"! Error: %s', $outfile, $e->getMessage()),
305
                0,
306
                $e,
307
                $process->getErrorOutput(),
308
                $process->getOutput()
309
            );
310
        }
311
312
        unlink($tempfile);
313
314
        if ($temporaryOutFile && !isset($exception)) {
315
            unlink($pdf);
316
            rename($outfile, $pdf);
317
        }
318
319
        if (isset($exception)) {
320
            throw $exception;
321
        }
322
    }
323
324
    /**
325
     * Builds the pdftk command lines for splitting.
326
     *
327
     * @return string[]
328
     */
329
    private function buildSplitCommandLines(string $inputFile, array $mapping, string $outputFolder = null): array
330
    {
331
        $commandLines = [];
332
        $esc = $this->escaper;
333
334
        foreach ($mapping as $filename => $pages) {
335
            if ($outputFolder) {
336
                $target = sprintf('%s/%s', $outputFolder, $filename);
337
            } else {
338
                $target = $filename;
339
            }
340
341
            $commandLines[] = sprintf(
342
                '%s %s cat %s output %s',
343
                $this->getBinary(),
344
                $esc->shellArg($inputFile),
345
                implode(' ', $pages),
346
                $esc->shellArg($target)
347
            );
348
        }
349
350
        return $commandLines;
351
    }
352
353
    /**
354
     * Imports bookmarks from a pdftk dump.
355
     */
356
    private function importBookmarksFromDump(Bookmarks $bookmarks, string $dump): self
357
    {
358
        $matches = [];
359
        $regex = '/BookmarkBegin\nBookmarkTitle: (?<title>.+)\n' .
360
                 'BookmarkLevel: (?<level>[0-9]+)\nBookmarkPageNumber: (?<page>[0-9]+)/';
361
        preg_match_all($regex, $dump, $matches, PREG_SET_ORDER);
362
363
        foreach ($matches as $bm) {
364
            $bookmark = new Bookmark();
365
            $bookmark
366
                ->setTitle($bm['title'])
367
                ->setPageNumber((int) $bm['page'])
368
                ->setLevel((int) $bm['level'])
369
            ;
370
371
            $bookmarks->add($bookmark);
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378
     * Builds an Bookmark string for all bookmarks.
379
     */
380
    private function buildBookmarksBlock(Bookmarks $bookmarks): string
381
    {
382
        $result = '';
383
384
        foreach ($bookmarks->all() as $bookmark) {
385
            $result .= $this->buildBookmarkBlock($bookmark);
386
        }
387
388
        return $result;
389
    }
390
391
    /**
392
     * Builds an Bookmark string for a single bookmark.
393
     */
394
    private function buildBookmarkBlock(Bookmark $bookmark): string
395
    {
396
        return
397
            'BookmarkBegin' . PHP_EOL .
398
            'BookmarkTitle: ' . $bookmark->getTitle() . PHP_EOL .
399
            'BookmarkLevel: ' . $bookmark->getLevel() . PHP_EOL .
400
            'BookmarkPageNumber: ' . $bookmark->getPageNumber() . PHP_EOL;
401
    }
402
403
    /**
404
     * Imports page meta data from a pdftk dump.
405
     */
406
    public function importPagesFromDump(Pages $pages, string $dump): self
407
    {
408
        $matches = [];
409
        $regex = '/PageMediaBegin\nPageMediaNumber: (?<page>.+)\nPageMediaRotation: (?<rotation>[0-9]+)\n' .
410
                 'PageMediaRect: .*\n' .
411
                 'PageMediaDimensions: (?<dim>(([0-9]\,)?[0-9]+(\.[0-9]+)?) (([0-9]\,)?[0-9]+(\.[0-9]+)?))/';
412
        preg_match_all($regex, $dump, $matches, PREG_SET_ORDER);
413
414
        foreach ($matches as $p) {
415
            $page = new Page();
416
417
            $dimensions = explode(' ', $p['dim']);
418
419
            $page
420
                ->setPageNumber((int) $p['page'])
421
                ->setRotation((int) $p['rotation'])
422
                ->setWidth((float) str_replace(',', '', $dimensions[0]))
423
                ->setHeight((float) str_replace(',', '', $dimensions[1]))
424
            ;
425
426
            $pages->add($page);
427
        }
428
429
        return $this;
430
    }
431
432
    /**
433
     * Imports PDF metadata from a pdftk dump.
434
     */
435
    public function importMetadataFromDump(Metadata $metadata, string $dump): self
436
    {
437
        $matches = [];
438
        $regex = '/InfoBegin??\r?\nInfoKey: (?<key>.*)??\r?\nInfoValue: (?<value>.*)??\r?\n/';
439
        preg_match_all($regex, $dump, $matches, PREG_SET_ORDER);
440
441
        foreach ($matches as $meta) {
442
            $metadata->set($meta['key'], $meta['value']);
443
        }
444
445
        return $this;
446
    }
447
448
    /**
449
     * Builds an Metadata string for all metadata entries.
450
     */
451
    public function buildMetadataBlock(Metadata $metadata): string
452
    {
453
        $result = '';
454
455
        foreach ($metadata->all() as $key => $value) {
456
            $result .= 'InfoBegin' . PHP_EOL;
457
            $result .= 'InfoKey: ' . $key . PHP_EOL;
458
            $result .= 'InfoValue: ' . (string) $value . PHP_EOL;
459
        }
460
461
        return $result;
462
    }
463
}
464