Test Failed
Push — feature/pdfcpu ( 859fd3...f782de )
by Andreas
13:47
created

PdfcpuWrapper::buildBookmarksArrayForTree()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 6

Importance

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