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

PdfcpuWrapperBookmarksHelper::applyBookmarks()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 45
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
c 1
b 0
f 0
dl 0
loc 45
ccs 29
cts 29
cp 1
rs 8.5226
cc 7
nc 16
nop 3
crap 7
1
<?php
2
/**
3
 * bookmarks helper for 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 Gmi\Toolkit\Pdftk\Exception\FileNotFoundException;
16
use Gmi\Toolkit\Pdftk\Exception\PdfException;
17
use Gmi\Toolkit\Pdftk\Util\Escaper;
18
use Gmi\Toolkit\Pdftk\Util\FileChecker;
19
use Gmi\Toolkit\Pdftk\Util\ProcessFactory;
20
21
use Exception;
22
23
/**
24
 * Bookmarks helper for pdfcpu wrapper.
25
 *
26
 * This class encapsulates the complexity of pdfcpu bookmark handling to reduce complexity of the PdfcpuWrapper.
27
 *
28
 * @internal
29
 */
30
class PdfcpuWrapperBookmarksHelper
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
     * @var FileChecker
46
     */
47
    private $fileChecker;
48
49
    /**
50
     * Constructor.
51
     *
52
     * @throws FileNotFoundException
53
     */
54 28
    public function __construct(string $pdftkBinaryPath, ProcessFactory $processFactory)
55
    {
56 28
        $this->binaryPath = $pdftkBinaryPath;
57 28
        $this->processFactory = $processFactory;
58 28
        $this->escaper = new Escaper();
59 28
        $this->fileChecker = new FileChecker();
60 28
    }
61
62
    /**
63
     * @see WrapperInterface::applyBookmarks()
64
     */
65 7
    public function applyBookmarks(Bookmarks $bookmarks, string $infile, string $outfile = null): void
66
    {
67 7
        $temporaryOutFile = false;
68
69 7
        $this->fileChecker->checkPdfFileExists($infile);
70 6
        $bookmarksJson = $this->exportBookmarksToJson($bookmarks);
71 6
        $tempfile = tempnam(sys_get_temp_dir(), 'bookmarks') . '.json';
72 6
        file_put_contents($tempfile, $bookmarksJson);
73
74 6
        if ($outfile === null || $infile === $outfile) {
75 1
            $temporaryOutFile = true;
76 1
            $outfile = tempnam(sys_get_temp_dir(), 'pdf') . '.pdf';
77
        }
78
79 6
        $cmd = sprintf(
80 6
            '%s bookmarks import -replace %s %s %s',
81 6
            $this->getBinary(),
82 6
            $this->escaper->shellArg($infile),
83 6
            $this->escaper->shellArg($tempfile),
84 6
            $this->escaper->shellArg($outfile)
85
        );
86
87 6
        $process = $this->processFactory->createProcess($cmd);
88
89
        try {
90 6
            $process->mustRun();
91 1
        } catch (Exception $e) {
92 1
            $exception = new PdfException(
93 1
                sprintf('Failed to write PDF bookmarks to "%s"! Error: %s', $outfile, $e->getMessage()),
94 1
                0,
95
                $e,
96 1
                $process->getErrorOutput(),
97 1
                $process->getOutput()
98
            );
99
        }
100
101 6
        unlink($tempfile);
102
103 6
        if ($temporaryOutFile && !isset($exception)) {
104 1
            unlink($infile);
105 1
            rename($outfile, $infile);
106
        }
107
108 6
        if (isset($exception)) {
109 1
            throw $exception;
110
        }
111 5
    }
112
113
    /**
114
     * @see WrapperInterface::importBookmarks()
115
     */
116 11
    public function importBookmarks(Bookmarks $bookmarks, string $infile): void
117
    {
118 11
        $tempBookmarksFile = tempnam(sys_get_temp_dir(), 'bookmarks') . '.json';
119
120 11
        $this->fileChecker->checkPdfFileExists($infile);
121
122 10
        $cmd = sprintf(
123 10
            '%s bookmarks export %s %s',
124 10
            $this->getBinary(),
125 10
            $this->escaper->shellArg($infile),
126 10
            $this->escaper->shellArg($tempBookmarksFile)
127
        );
128
129 10
        $process = $this->processFactory->createProcess($cmd);
130
131
        try {
132 10
            $process->mustRun();
133 3
        } catch (Exception $e) {
134 3
            $exception = new PdfException(
135 3
                sprintf('Failed to read bookmarks data from "%s"! Error: %s', $infile, $e->getMessage()),
136 3
                0,
137
                $e,
138 3
                $process->getErrorOutput(),
139 3
                $process->getOutput()
140
            );
141
        }
142
143 10
        if (isset($exception) && false === strpos($process->getErrorOutput(), 'no outlines available')) {
144 1
            @unlink($tempBookmarksFile);
145 1
            throw $exception;
146
        }
147
148 9
        $this->importBookmarksFromJson($bookmarks, @file_get_contents($tempBookmarksFile) ?: '');
149
150 9
        @unlink($tempBookmarksFile);
151 9
    }
152
153
    /**
154
     * Imports bookmarks from a pdfcpu bookmark JSON file.
155
     */
156 9
    private function importBookmarksFromJson(Bookmarks $bookmarks, string $json): void
157
    {
158 9
        $raw = json_decode($json, true);
159 9
        $bookmarksArray = $raw['bookmarks'] ?? [];
160
161 9
        $this->parseBookmarksTree($bookmarks, $bookmarksArray);
162 9
    }
163
164
    /**
165
     * Recursively traverse the bookmarks array and add the bookmarks appropriately.
166
     */
167 9
    private function parseBookmarksTree(Bookmarks $bookmarks, array $arr, int $level = 1): void
168
    {
169 9
        foreach ($arr as $current) {
170 6
            $bookmark = new Bookmark();
171
172
            $bookmark
173 6
                ->setTitle($current['title'])
174 6
                ->setPageNumber($current['page'])
175 6
                ->setLevel($level)
176
            ;
177
178 6
            $bookmarks->add($bookmark);
179
180 6
            if (isset($current['kids'])) {
181 4
                $this->parseBookmarksTree($bookmarks, $current['kids'], $level + 1);
182
            }
183
        }
184 9
    }
185
186
    /**
187
     * Exports bookmarks to a pdfcpu bookmark JSON file.
188
     */
189 6
    private function exportBookmarksToJson(Bookmarks $bookmarks): string
190
    {
191 6
        $bookmarksRecursiveArray = $this->buildBookmarksTree($this->buildBookmarksArrayForTree($bookmarks));
192
193 6
        return json_encode(['bookmarks' => $bookmarksRecursiveArray], JSON_PRETTY_PRINT);
194
    }
195
196
    /**
197
     * Recursively build the JSON tree based on the normalized bookmarks array.
198
     */
199 6
    private function buildBookmarksTree(array $bookmarksArray, int $parentId = null): array
200
    {
201 6
        $result = [];
202
203 6
        foreach ($bookmarksArray as $bookmarkItem) {
204 4
            if ($bookmarkItem['__parent'] === $parentId) {
205 4
                $children = $this->buildBookmarksTree($bookmarksArray, $bookmarkItem['__id']);
206 4
                if ($children) {
207 2
                    $bookmarkItem['kids'] = $children;
208
                }
209
210 4
                foreach ($bookmarkItem as $key => $value) {
211 4
                    if (strpos($key, "__") === 0) {
212 4
                        unset($bookmarkItem[$key]);
213
                    }
214
                }
215
216 4
                $result[] = $bookmarkItem;
217
            }
218
        }
219
220 6
        return $result;
221
    }
222
223
    /**
224
     * Builds an array with additional entries prefixed with "__" for level, id and parent id.
225
     */
226 6
    private function buildBookmarksArrayForTree(Bookmarks $bookmarks): array
227
    {
228 6
        $bookmarksArray = [];
229
230 6
        $b = $bookmarks->all();
231 6
        $bookmarksCount = count($b);
232
233 6
        $indexParent = null;
234
235 6
        for ($i = 0; $i < $bookmarksCount; $i++) {
236 4
            $bookmark = $b[$i];
237 4
            $prevBookmark = $b[$i - 1] ?? null;
238
239
            // bookmark has a higher level (is deeper down) than the previous one
240 4
            if ($prevBookmark && $prevBookmark->getLevel() < $bookmark->getLevel()) {
241 2
                $indexParent = $i - 1;
242
            // bookmark has a lower level (is higher up) than the previous one
243 4
            } elseif ($prevBookmark && $prevBookmark->getLevel() > $bookmark->getLevel()) {
244 1
                $indexParent = $this->getLastParentId($bookmarksArray, $bookmark->getLevel()) ?? null;
245
            }
246
247 4
            $bookmarksArray[] = [
248 4
                'title' => $bookmark->getTitle(),
249 4
                'page' => $bookmark->getPageNumber(),
250 4
                '__level' => $bookmark->getLevel(),
251 4
                '__id' => $i,
252 4
                '__parent' => $indexParent,
253
            ];
254
        }
255
256 6
        return $bookmarksArray;
257
    }
258
259
    /**
260
     * Returns the id of the last bookmark with a lower level than the provided current level.
261
     */
262 1
    private function getLastParentId(array $bookmarksArray, int $currentLevel): ?int
263
    {
264 1
        for ($j = count($bookmarksArray) - 1; $j >= 0; $j--) {
265 1
            if ($bookmarksArray[$j]['__level'] < $currentLevel) {
266 1
                return $bookmarksArray[$j]['__id'];
267
            }
268
        }
269
270 1
        return null;
271
    }
272
}
273