Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
53.85% covered (warning)
53.85%
35 / 65
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PdfMetadataExtractor
53.85% covered (warning)
53.85%
35 / 65
50.00% covered (danger)
50.00%
4 / 8
80.63
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFile
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getContent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDocument
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getPageDimensions
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getPageDimensionsWithSmalotPdfParser
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 getPageDimensionsWithPdfInfo
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
5.10
 getPdfVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2020-2026 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Service\File\Pdf;
11
12use OCA\Libresign\Exception\LibresignException;
13use OCA\Libresign\Vendor\Smalot\PdfParser\Document;
14use OCP\Files\File;
15use OCP\ITempManager;
16use Psr\Log\LoggerInterface;
17
18class PdfMetadataExtractor {
19    private string $content = '';
20    private string $fileName = '';
21    private ?Document $document = null;
22
23    public function __construct(
24        private PdfParser $pdfParser,
25        private ITempManager $tempManager,
26        private LoggerInterface $logger,
27    ) {
28    }
29
30    /**
31     * @throws LibresignException
32     */
33    public function setFile(File $file): self {
34        $fileName = $file->getName();
35        try {
36            $this->content = $file->getContent();
37        } catch (\Throwable $e) {
38            throw new LibresignException(
39                sprintf('Unable to read file "%s": %s', $fileName, $e->getMessage())
40            );
41        }
42        if (!$this->content) {
43            throw new LibresignException(sprintf('The file "%s" is empty.', $fileName));
44        }
45        $this->fileName = $fileName;
46        $this->document = null;
47        return $this;
48    }
49
50    private function getContent(): string {
51        if (!$this->content) {
52            throw new LibresignException('File not defined to be parsed.');
53        }
54        return $this->content;
55    }
56
57    private function getDocument(): Document {
58        if (!$this->document) {
59            $content = $this->getContent();
60            $this->document = $this->pdfParser->parse($content, $this->fileName);
61        }
62        return $this->document;
63    }
64
65    /**
66     * @return (array[]|int)[]
67     * @throws LibresignException
68     * @psalm-return array{p: int, d?: non-empty-list<array{w: mixed, h: mixed}>}
69     */
70    public function getPageDimensions(): array {
71        if ($return = $this->getPageDimensionsWithPdfInfo()) {
72            return $return;
73        }
74        return $this->getPageDimensionsWithSmalotPdfParser();
75    }
76
77    private function getPageDimensionsWithSmalotPdfParser(): array {
78        $document = $this->getDocument();
79        $pages = $document->getPages();
80        $output = [
81            'p' => count($pages),
82        ];
83        foreach ($pages as $page) {
84            $details = $page->getDetails();
85            if (!isset($details['MediaBox'])) {
86                $pages = $document->getObjectsByType('Pages');
87                $details = reset($pages)->getHeader()->getDetails();
88            }
89            if (!isset($details['MediaBox']) || !is_numeric($details['MediaBox'][2]) || !is_numeric($details['MediaBox'][3])) {
90                $this->logger->error('Impossible get metadata from this file: Error to get page width and height. If possible, open an issue at github.com/libresign/libresign with the file that you used.');
91                throw new LibresignException('Impossible get metadata from this file.');
92            }
93            $output['d'][] = [
94                'w' => $details['MediaBox'][2],
95                'h' => $details['MediaBox'][3],
96            ];
97        }
98        $pending = $output['p'] - count($output['d']);
99        if ($pending) {
100            for ($i = 0; $i < $pending; $i++) {
101                $output['d'][] = $output['d'][0];
102            }
103        }
104        return $output;
105    }
106
107    private function getPageDimensionsWithPdfInfo(): array {
108        if (shell_exec('which pdfinfo') === null) {
109            return [];
110        }
111        $content = $this->getContent();
112        $filename = $this->tempManager->getTemporaryFile('.pdf');
113        file_put_contents($filename, $content);
114
115        // The output of this command go to STDERR and shell_exec get the STDOUT
116        // With 2>&1 the STRERR is redirected to STDOUT
117        $pdfinfo = shell_exec('pdfinfo ' . $filename . ' -l -1 2>&1');
118        if (!$pdfinfo) {
119            return [];
120        }
121        if (!preg_match_all('/Page +\d+ +size: +(\d+\.?\d*) x (\d+\.?\d*)/', (string)$pdfinfo, $pages)) {
122            return [];
123        }
124        $output = [
125            'p' => count($pages[1]),
126        ];
127        foreach ($pages[1] as $page => $width) {
128            $output['d'][] = [
129                'w' => (float)$width,
130                'h' => (float)$pages[2][$page],
131            ];
132        }
133        return $output;
134    }
135
136    public function getPdfVersion(): string {
137        preg_match('/^%PDF-(?<version>\d+(\.\d+)?)/', $this->getContent(), $match);
138        return $match['version'];
139    }
140}