Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
83 / 83
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Parser
100.00% covered (success)
100.00%
83 / 83
100.00% covered (success)
100.00%
10 / 10
35
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%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseData
100.00% covered (success)
100.00%
2 / 2
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
8
 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.0.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     * Get the object streams
62     *
63     * @return array
64     */
65    public function getObjectStreams(): array
66    {
67        return $this->objectStreams;
68    }
69
70    /**
71     * Get the object map
72     *
73     * @return array
74     */
75    public function getObjectMap(): array
76    {
77        return $this->objectMap;
78    }
79
80    /**
81     * Get the document fonts
82     *
83     * @return array
84     */
85    public function getFonts(): array
86    {
87        return $this->fonts;
88    }
89
90    /**
91     * Parse from file
92     *
93     * @param  string $file
94     * @param  mixed  $pages
95     * @throws Exception
96     * @return AbstractDocument
97     */
98    public function parseFile(string $file, mixed $pages = null): AbstractDocument
99    {
100        $this->initFile($file);
101        return $this->parse($pages);
102    }
103
104    /**
105     * Parse from raw data stream
106     *
107     * @param  string $data
108     * @param  mixed  $pages
109     * @throws Exception
110     * @return AbstractDocument
111     */
112    public function parseData(string $data, mixed $pages = null): AbstractDocument
113    {
114        $this->initData($data);
115        return $this->parse($pages);
116    }
117
118    /**
119     * Parse the data stream
120     *
121     * @param  mixed  $pages
122     * @throws Exception
123     * @return AbstractDocument
124     */
125    public function parse(mixed $pages = null): AbstractDocument
126    {
127        try {
128            $extractDoc = new ExtractDocument($this->data);
129            $graph      = Import\ObjectGraphReader::read($extractDoc, 0);
130        } catch (\Pop\Pdf\Extract\Exception $e) {
131            throw new Exception($e->getMessage(), $e->getCode(), $e);
132        }
133
134        $rootObjNum = $graph['nextOffset'] + 1;
135        $nextFree   = $rootObjNum + 1;
136
137        $parent = new PdfObject\ParentObject($graph['topPagesObjNum']);
138        $parent->setKids(self::kidNumbers($graph['topPagesDict']));
139        $parent->setCount(count($graph['pageObjects']));
140        $parent->setImported(true);
141
142        $root = new PdfObject\RootObject($rootObjNum);
143        $root->setParentIndex($graph['topPagesObjNum']);
144        $root->setImported(true);
145
146        $objects                           = $graph['objects'];
147        $objects[$graph['topPagesObjNum']] = $parent;
148        $objects[$rootObjNum]              = $root;
149
150        $doc = new \Pop\Pdf\Document();
151
152        if ($graph['infoDict'] !== null) {
153            $doc->setMetadata(self::metadataFromDict($graph['infoDict']));
154        }
155
156        $info = new PdfObject\InfoObject($nextFree);
157        $info->setImported(true);
158        $objects[$nextFree] = $info;
159
160        $doc->importObjects($objects);
161
162        $pageObjects = $graph['pageObjects'];
163
164        if ($pages !== null) {
165            $pages    = (!is_array($pages)) ? [$pages] : $pages;
166            $kept     = [];
167            $keptKids = [];
168
169            foreach ($pages as $pageNum) {
170                if (isset($pageObjects[$pageNum - 1])) {
171                    $kept[]     = $pageObjects[$pageNum - 1];
172                    $keptKids[] = $pageObjects[$pageNum - 1]->getIndex();
173                }
174            }
175
176            $pageObjects = $kept;
177            $parent->setKids($keptKids);
178            $parent->setCount(count($keptKids));
179        }
180
181        foreach ($pageObjects as $pageObject) {
182            $pg = new \Pop\Pdf\Document\Page($pageObject->getWidth(), $pageObject->getHeight(), $pageObject->getIndex());
183            $pg->importPageObject($pageObject);
184            $doc->addPage($pg);
185        }
186
187        return $doc;
188    }
189
190    /**
191     * Initialize the file and get the data
192     *
193     * @param  string $file
194     * @throws Exception
195     * @return Parser
196     */
197    protected function initFile(string $file): Parser
198    {
199        if (!file_exists($file)) {
200            throw new Exception('Error: That PDF file does not exist.');
201        }
202
203        $this->file = $file;
204        $this->data = file_get_contents($this->file);
205
206        $this->objectStreams = [];
207        $this->objectMap     = [];
208        $this->fonts         = [];
209
210        return $this;
211    }
212
213    /**
214     * Initialize data
215     *
216     * @param  string $data
217     * @return Parser
218     */
219    protected function initData(string $data): Parser
220    {
221        $this->data = $data;
222
223        $this->objectStreams = [];
224        $this->objectMap     = [];
225        $this->fonts         = [];
226
227        return $this;
228    }
229
230    /**
231     * Extract a rewritten Pages node dict's Kids as a flat list of new object numbers
232     *
233     * @param  array $topPagesDict
234     * @return array
235     */
236    protected static function kidNumbers(array $topPagesDict): array
237    {
238        $numbers = [];
239        $kids    = $topPagesDict['Kids'] ?? [];
240
241        if (is_array($kids)) {
242            foreach ($kids as $kid) {
243                if ($kid instanceof Value\Reference) {
244                    $numbers[] = $kid->objNum;
245                }
246            }
247        }
248
249        return $numbers;
250    }
251
252    /**
253     * Build a Document\Metadata from a rewritten Info dict
254     *
255     * @param  array $infoDict
256     * @return Metadata
257     */
258    protected static function metadataFromDict(array $infoDict): Metadata
259    {
260        $metadata = new Metadata();
261
262        if (isset($infoDict['Title']) && is_string($infoDict['Title'])) {
263            $metadata->setTitle($infoDict['Title']);
264        }
265        if (isset($infoDict['Author']) && is_string($infoDict['Author'])) {
266            $metadata->setAuthor($infoDict['Author']);
267        }
268        if (isset($infoDict['Subject']) && is_string($infoDict['Subject'])) {
269            $metadata->setSubject($infoDict['Subject']);
270        }
271        if (isset($infoDict['Creator']) && is_string($infoDict['Creator'])) {
272            $metadata->setCreator($infoDict['Creator']);
273        }
274        if (isset($infoDict['Producer']) && is_string($infoDict['Producer'])) {
275            $metadata->setProducer($infoDict['Producer']);
276        }
277        if (isset($infoDict['CreationDate']) && is_string($infoDict['CreationDate'])) {
278            $metadata->setCreationDate($infoDict['CreationDate']);
279        }
280        if (isset($infoDict['ModDate']) && is_string($infoDict['ModDate'])) {
281            $metadata->setModDate($infoDict['ModDate']);
282        }
283
284        return $metadata;
285    }
286
287}