Passed
Branch main (d5171d)
by Andreas
26:52 queued 08:26
created

PdftkWrapper::applyBookmarks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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