Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
85 / 85
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Parser
100.00% covered (success)
100.00%
85 / 85
100.00% covered (success)
100.00%
10 / 10
36
100.00% covered (success)
100.00%
1 / 1
 getObjectStreams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getObjectMap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFonts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
9
 initFile
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 initData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 kidNumbers
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 metadataFromDict
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
15
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Build;
16
17use Pop\Pdf\Document\AbstractDocument;
18use Pop\Pdf\Document\Metadata;
19use Pop\Pdf\Extract\Document as ExtractDocument;
20use Pop\Pdf\Extract\Value;
21
22/**
23 * Pdf parser class
24 *
25 * @category   Pop
26 * @package    Pop\Pdf
27 * @author     Nick Sagona, III <nick@popphp.org>
28 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
29 * @license    https://www.popphp.org/license     New BSD License
30 * @version    6.2.0
31 */
32class Parser extends AbstractParser
33{
34
35    /**
36     * Parsed object data streams - retained only for public API
37     * compatibility (getObjectStreams()); no longer populated under the
38     * Extract\Document-based implementation, since no consumer depends on
39     * its contents (only its array type).
40     * @var array
41     */
42    protected array $objectStreams = [];
43
44    /**
45     * Object map - retained only for public API compatibility
46     * (getObjectMap()); see $objectStreams.
47     * @var array
48     */
49    protected array $objectMap = [];
50
51    /**
52     * Document fonts - retained only for public API compatibility
53     * (getFonts()); font resources are now carried per-page via each
54     * translated PageObject's own structured font references instead of
55     * this document-wide bag.
56     * @var array
57     */
58    protected array $fonts = [];
59
60    /**
61     * Password used to decrypt an encrypted source PDF, if any
62     * @var ?string
63     */
64    protected ?string $password = null;
65
66    /**
67     * Get the object streams
68     *
69     * @return array
70     */
71    public function getObjectStreams(): array
72    {
73        return $this->objectStreams;
74    }
75
76    /**
77     * Get the object map
78     *
79     * @return array
80     */
81    public function getObjectMap(): array
82    {
83        return $this->objectMap;
84    }
85
86    /**
87     * Get the document fonts
88     *
89     * @return array
90     */
91    public function getFonts(): array
92    {
93        return $this->fonts;
94    }
95
96    /**
97     * Parse from file
98     *
99     * @param  string  $file
100     * @param  mixed   $pages
101     * @param  ?string $password
102     * @throws Exception
103     * @return AbstractDocument
104     */
105    public function parseFile(string $file, mixed $pages = null, ?string $password = null): AbstractDocument
106    {
107        $this->password = $password;
108        $this->initFile($file);
109        return $this->parse($pages);
110    }
111
112    /**
113     * Parse from raw data stream
114     *
115     * @param  string  $data
116     * @param  mixed   $pages
117     * @param  ?string $password
118     * @throws Exception
119     * @return AbstractDocument
120     */
121    public function parseData(string $data, mixed $pages = null, ?string $password = null): AbstractDocument
122    {
123        $this->password = $password;
124        $this->initData($data);
125        return $this->parse($pages);
126    }
127
128    /**
129     * Parse the data stream
130     *
131     * @param  mixed  $pages
132     * @throws Exception
133     * @return AbstractDocument
134     */
135    public function parse(mixed $pages = null): AbstractDocument
136    {
137        try {
138            $extractDoc = new ExtractDocument($this->data, $this->password);
139            $graph      = Import\ObjectGraphReader::read($extractDoc, 0);
140        } catch (\Pop\Pdf\Extract\Exception $e) {
141            throw new Exception($e->getMessage(), $e->getCode(), $e);
142        }
143
144        $rootObjNum = $graph['nextOffset'] + 1;
145        $nextFree   = $rootObjNum + 1;
146
147        $parent = new PdfObject\ParentObject($graph['topPagesObjNum']);
148        $parent->setKids(self::kidNumbers($graph['topPagesDict']));
149        $parent->setCount(count($graph['pageObjects']));
150        $parent->setImported(true);
151
152        $root = new PdfObject\RootObject($rootObjNum);
153        $root->setParentIndex($graph['topPagesObjNum']);
154        $root->setImported(true);
155
156        $objects                           = $graph['objects'];
157        $objects[$graph['topPagesObjNum']] = $parent;
158        $objects[$rootObjNum]              = $root;
159
160        $doc = new \Pop\Pdf\Document();
161
162        // A source PDF that encrypts its STRINGS (any non-/Identity /StrF -
163        // including this library's own /StrF /StdCF output) hands back
164        // /Info values that are still raw AES ciphertext, because nothing in
165        // Extract\* decrypts strings. Those bytes are not metadata in any
166        // useful sense, so they are dropped rather than carried into
167        // Document\Metadata and re-emitted as this document's title/author/
168        // dates.
169        if (($graph['infoDict'] !== null) && !$extractDoc->hasEncryptedStrings()) {
170            $doc->setMetadata(self::metadataFromDict($graph['infoDict']));
171        }
172
173        $info = new PdfObject\InfoObject($nextFree);
174        $info->setImported(true);
175        $objects[$nextFree] = $info;
176
177        $doc->importObjects($objects);
178
179        $pageObjects = $graph['pageObjects'];
180
181        if ($pages !== null) {
182            $pages    = (!is_array($pages)) ? [$pages] : $pages;
183            $kept     = [];
184            $keptKids = [];
185
186            foreach ($pages as $pageNum) {
187                if (isset($pageObjects[$pageNum - 1])) {
188                    $kept[]     = $pageObjects[$pageNum - 1];
189                    $keptKids[] = $pageObjects[$pageNum - 1]->getIndex();
190                }
191            }
192
193            $pageObjects = $kept;
194            $parent->setKids($keptKids);
195            $parent->setCount(count($keptKids));
196        }
197
198        foreach ($pageObjects as $pageObject) {
199            $pg = new \Pop\Pdf\Document\Page($pageObject->getWidth(), $pageObject->getHeight(), $pageObject->getIndex());
200            $pg->importPageObject($pageObject);
201            $doc->addPage($pg);
202        }
203
204        return $doc;
205    }
206
207    /**
208     * Initialize the file and get the data
209     *
210     * @param  string $file
211     * @throws Exception
212     * @return Parser
213     */
214    protected function initFile(string $file): Parser
215    {
216        if (!file_exists($file)) {
217            throw new Exception('Error: That PDF file does not exist.');
218        }
219
220        $this->file = $file;
221        $this->data = file_get_contents($this->file);
222
223        $this->objectStreams = [];
224        $this->objectMap     = [];
225        $this->fonts         = [];
226
227        return $this;
228    }
229
230    /**
231     * Initialize data
232     *
233     * @param  string $data
234     * @return Parser
235     */
236    protected function initData(string $data): Parser
237    {
238        $this->data = $data;
239
240        $this->objectStreams = [];
241        $this->objectMap     = [];
242        $this->fonts         = [];
243
244        return $this;
245    }
246
247    /**
248     * Extract a rewritten Pages node dict's Kids as a flat list of new object numbers
249     *
250     * @param  array $topPagesDict
251     * @return array
252     */
253    protected static function kidNumbers(array $topPagesDict): array
254    {
255        $numbers = [];
256        $kids    = $topPagesDict['Kids'] ?? [];
257
258        if (is_array($kids)) {
259            foreach ($kids as $kid) {
260                if ($kid instanceof Value\Reference) {
261                    $numbers[] = $kid->objNum;
262                }
263            }
264        }
265
266        return $numbers;
267    }
268
269    /**
270     * Build a Document\Metadata from a rewritten Info dict
271     *
272     * @param  array $infoDict
273     * @return Metadata
274     */
275    protected static function metadataFromDict(array $infoDict): Metadata
276    {
277        $metadata = new Metadata();
278
279        if (isset($infoDict['Title']) && is_string($infoDict['Title'])) {
280            $metadata->setTitle($infoDict['Title']);
281        }
282        if (isset($infoDict['Author']) && is_string($infoDict['Author'])) {
283            $metadata->setAuthor($infoDict['Author']);
284        }
285        if (isset($infoDict['Subject']) && is_string($infoDict['Subject'])) {
286            $metadata->setSubject($infoDict['Subject']);
287        }
288        if (isset($infoDict['Creator']) && is_string($infoDict['Creator'])) {
289            $metadata->setCreator($infoDict['Creator']);
290        }
291        if (isset($infoDict['Producer']) && is_string($infoDict['Producer'])) {
292            $metadata->setProducer($infoDict['Producer']);
293        }
294        if (isset($infoDict['CreationDate']) && is_string($infoDict['CreationDate'])) {
295            $metadata->setCreationDate($infoDict['CreationDate']);
296        }
297        if (isset($infoDict['ModDate']) && is_string($infoDict['ModDate'])) {
298            $metadata->setModDate($infoDict['ModDate']);
299        }
300
301        return $metadata;
302    }
303
304}