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