Test Failed
Push — main ( 961615...d5171d )
by Andreas
12:44 queued 13s
created

PdfcpuWrapper::importMetadata()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 34
rs 8.8333
cc 7
nc 6
nop 2
1
<?php
2
/**
3
 * pdfcpu wrapper
4
 *
5
 * @copyright 2014-2024 Institute of Legal Medicine, Medical University of Innsbruck
6
 * @author Andreas Erhard <[email protected]>
7
 * @license LGPL-3.0-only
8
 * @link http://www.gerichtsmedizin.at/
9
 *
10
 * @package pdftk
11
 */
12
13
namespace Gmi\Toolkit\Pdftk;
14
15
use Symfony\Component\Process\Process;
16
17
use Gmi\Toolkit\Pdftk\Exception\FileNotFoundException;
18
use Gmi\Toolkit\Pdftk\Exception\PdfException;
19
use Gmi\Toolkit\Pdftk\Util\Escaper;
20
use Gmi\Toolkit\Pdftk\Util\FileChecker;
21
use Gmi\Toolkit\Pdftk\Util\ProcessFactory;
22
23
use Exception;
24
25
/**
26
 * Wrapper for pdfcpu.
27
 *
28
 * @internal Only the methods exposed by the interfaces should be accessed from outside.
29
 *
30
 * @psalm-suppress PropertyNotSetInConstructor as $binaryPath is defined and set in the BinaryPathAwareTrait
31
 */
32
class PdfcpuWrapper implements WrapperInterface, BinaryPathAwareInterface
33
{
34
    use BinaryPathAwareTrait;
35
36
    private const SUPPORTED_METADATA_ATTRIBUTES = [
37
        'Title', 'Keywords', 'Subject', 'Author', 'Creator', 'Producer', 'CreationDate', 'ModificationDate',
38
    ];
39
40
    /**
41
     * @var ProcessFactory
42
     */
43
    private $processFactory;
44
45
    /**
46
     * @var Escaper
47
     */
48
    private $escaper;
49
50
    /**
51
     * @var FileChecker
52
     */
53
    private $fileChecker;
54
55
    /**
56
     * @var PdfcpuWrapperBookmarksHelper
57
     */
58
    private $bookmarksHelper;
59
60
    /**
61
     * Constructor.
62
     *
63
     * @throws FileNotFoundException
64
     */
65
    public function __construct(string $pdftkBinary = null, ProcessFactory $processFactory = null)
66
    {
67
        $this->setBinary($pdftkBinary ?: $this->guessBinary(PHP_OS));
68
        $this->processFactory = $processFactory ?: new ProcessFactory();
69
        $this->escaper = new Escaper();
70
        $this->fileChecker = new FileChecker();
71
        $this->bookmarksHelper = new PdfcpuWrapperBookmarksHelper($this->getBinary(false), $this->processFactory);
72
    }
73
74
    /**
75
     * Guesses the pdfcpu binary path based on the operating system.
76
     */
77
    public function guessBinary(string $operatingSystemString): string
78
    {
79
        if (strtoupper(substr($operatingSystemString, 0, 3)) === 'WIN') {
80
            $binary = 'C:\\Program Files\\pdfcpu\\pdfcpu.exe';
81
        } else {
82
            $binary = '/usr/bin/pdfcpu';
83
        }
84
85
        return $binary;
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91
    public function join(array $filePaths, string $outfile): void
92
    {
93
        $esc = $this->escaper;
94
95
        $filePathsEscaped = array_map(function (string $filePath) use ($esc) {
96
            return $esc->shellArg($filePath);
97
        }, $filePaths);
98
99
        $fileList = implode(' ', $filePathsEscaped);
100
101
        $commandLine = sprintf('%s merge %s %s', $this->getBinary(), $esc->shellArg($outfile), $fileList);
102
103
        /**
104
         * @var Process
105
         */
106
        $process = $this->processFactory->createProcess($commandLine);
107
108
        try {
109
            $process->mustRun();
110
        } catch (Exception $e) {
111
            throw new PdfException($e->getMessage(), 0, $e, $process->getErrorOutput(), $process->getOutput());
112
        }
113
114
        $process->getOutput();
115
    }
116
117
    /**
118
     * {@inheritDoc}
119
     */
120
    public function split(string $infile, array $mapping, string $outputFolder = null): void
121
    {
122
        $esc = $this->escaper;
123
124
        foreach ($mapping as $filename => $pages) {
125
            if ($outputFolder) {
126
                $target = sprintf('%s/%s', $outputFolder, $filename);
127
            } else {
128
                $target = $filename;
129
            }
130
131
            $commandLine = sprintf(
132
                '%s collect -pages %s %s %s',
133
                $this->getBinary(),
134
                implode(',', $pages),
135
                $esc->shellArg($infile),
136
                $esc->shellArg($target)
137
            );
138
139
            $process = $this->processFactory->createProcess($commandLine);
140
141
            try {
142
                $process->mustRun();
143
            } catch (Exception $e) {
144
                throw new PdfException($e->getMessage(), 0, $e, $process->getErrorOutput(), $process->getOutput());
145
            }
146
        }
147
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152
    public function reorder(string $infile, array $order, string $outfile = null): void
153
    {
154
        $temporaryOutFile = false;
155
156
        if ($outfile === null || $infile === $outfile) {
157
            $temporaryOutFile = true;
158
            $outfile = tempnam(sys_get_temp_dir(), 'pdf') . '.pdf';
159
        }
160
161
        $esc = $this->escaper;
162
163
        $commandLine = sprintf(
164
            '%s collect -pages %s %s %s',
165
            $this->getBinary(),
166
            implode(',', $order),
167
            $esc->shellArg($infile),
168
            $esc->shellArg($outfile)
169
        );
170
171
        $process = $this->processFactory->createProcess($commandLine);
172
173
        try {
174
            $process->mustRun();
175
        } catch (Exception $e) {
176
            throw new PdfException(
177
                sprintf('Failed to reorder PDF "%s"! Error: %s', $infile, $e->getMessage()),
178
                0,
179
                $e,
180
                $process->getErrorOutput(),
181
                $process->getOutput()
182
            );
183
        }
184
185
        if ($temporaryOutFile) {
186
            unlink($infile);
187
            rename($outfile, $infile);
188
        }
189
    }
190
191
    /**
192
     * {@inheritDoc}
193
     */
194
    public function applyBookmarks(Bookmarks $bookmarks, string $infile, string $outfile = null): self
195
    {
196
        $this->bookmarksHelper->applyBookmarks($bookmarks, $infile, $outfile);
197
198
        return $this;
199
    }
200
201
    /**
202
     * {@inheritDoc}
203
     */
204
    public function importBookmarks(Bookmarks $bookmarks, string $infile): self
205
    {
206
        $this->bookmarksHelper->importBookmarks($bookmarks, $infile);
207
208
        return $this;
209
    }
210
211
    /**
212
     * {@inheritDoc}
213
     */
214
    public function importPages(Pages $pages, string $infile): self
215
    {
216
        $this->fileChecker->checkPdfFileExists($infile);
217
218
        $cmd = sprintf('%s info -pages 1- -j %s', $this->getBinary(), $this->escaper->shellArg($infile));
219
220
        $process = $this->processFactory->createProcess($cmd);
221
222
        try {
223
            $process->mustRun();
224
        } catch (Exception $e) {
225
            $exception = new PdfException(
226
                sprintf('Failed to read pages data from "%s"! Error: %s', $infile, $e->getMessage()),
227
                0,
228
                $e,
229
                $process->getErrorOutput(),
230
                $process->getOutput()
231
            );
232
233
            throw $exception;
234
        }
235
236
        /**
237
         * Remove invalid JSON (useless line with the page numbers at the beginning)
238
         * @todo Remove when pdfcpu does not emit the extra pages line before JSON anymore
239
         */
240
        $outputCleaned = preg_replace('/^pages: (\d,?)+$/mu', '', $process->getOutput());
241
        $infoRaw = json_decode($outputCleaned, true);
242
243
        $pageBoundaries = $infoRaw['infos'][0]['pageBoundaries'];
244
245
        // the page numbers in the JSON are strings, not numbers and sorted as strings, ensure natural sort
246
        ksort($pageBoundaries, SORT_NATURAL);
247
248
        foreach ($pageBoundaries as $pageNumber => $pageInfo) {
249
            $page = new Page();
250
251
            $page
252
                ->setPageNumber((int) $pageNumber)
253
                ->setRotation((int) $pageInfo['rot'])
254
                ->setWidth((float) $pageInfo['mediaBox']['rect']['ur']['x'])
255
                ->setHeight((float) $pageInfo['mediaBox']['rect']['ur']['y'])
256
            ;
257
258
            $pages->add($page);
259
        }
260
261
        return $this;
262
    }
263
264
    /**
265
     * {@inheritDoc}
266
     */
267
    public function applyMetadata(Metadata $metadata, string $infile, string $outfile = null): self
268
    {
269
        $temporaryOutFile = false;
270
271
        $this->fileChecker->checkPdfFileExists($infile);
272
273
        $properties = [];
274
        foreach ($metadata->all() as $key => $value) {
275
            $properties[] = sprintf('%s=%s', $key, $this->escaper->shellArg($value));
276
        }
277
278
        $propArgs = implode(' ', $properties);
279
280
        if ($outfile === null || $infile === $outfile) {
281
            $temporaryOutFile = true;
282
            $outfile = tempnam(sys_get_temp_dir(), 'pdf') . '.pdf';
283
        }
284
285
        copy($infile, $outfile);
286
287
        $cmd = sprintf('%s properties add %s %s', $this->getBinary(), $this->escaper->shellArg($outfile), $propArgs);
288
        $process = $this->processFactory->createProcess($cmd);
289
290
        try {
291
            $process->mustRun();
292
        } catch (Exception $e) {
293
            $exception = new PdfException(
294
                sprintf('Failed to write PDF metadata to "%s"! Error: %s', $outfile, $e->getMessage()),
295
                0,
296
                $e,
297
                $process->getErrorOutput(),
298
                $process->getOutput()
299
            );
300
        }
301
302
        if ($temporaryOutFile && !isset($exception)) {
303
            unlink($infile);
304
            rename($outfile, $infile);
305
        }
306
307
        if (isset($exception)) {
308
            throw $exception;
309
        }
310
311
        return $this;
312
    }
313
314
    /**
315
     * {@inheritDoc}
316
     */
317
    public function importMetadata(Metadata $metadata, string $infile): self
318
    {
319
        $cmd = sprintf('%s info -j %s', $this->getBinary(), $this->escaper->shellArg($infile));
320
321
        $process = $this->processFactory->createProcess($cmd);
322
323
        try {
324
            $process->mustRun();
325
        } catch (Exception $e) {
326
            throw new PdfException(
327
                sprintf('Failed to read metadata data from "%s"! Error: %s', $infile, $e->getMessage()),
328
                0,
329
                $e,
330
                $process->getErrorOutput(),
331
                $process->getOutput()
332
            );
333
        }
334
335
        $raw = json_decode($process->getOutput(), true);
336
        $metadataArray = $raw['infos'][0];
337
338
        foreach (self::SUPPORTED_METADATA_ATTRIBUTES as $attribute) {
339
            $attributeNormalized = lcfirst($attribute);
340
341
            if ($attributeNormalized === 'keywords' && isset($metadataArray['keywords'])) {
342
                $metadataArray['keywords'] = implode(', ', $metadataArray['keywords']);
343
            }
344
345
            if (isset($metadataArray[$attributeNormalized]) && '' !== trim($metadataArray[$attributeNormalized])) {
346
                $metadata->set($attribute, $metadataArray[$attributeNormalized]);
347
            }
348
        }
349
350
        return $this;
351
    }
352
}
353