Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.17% covered (warning)
89.17%
107 / 120
42.86% covered (danger)
42.86%
9 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReaderDocumentInspector
89.17% covered (warning)
89.17%
107 / 120
42.86% covered (danger)
42.86%
9 / 21
69.21
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
 getCatalog
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getInfo
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
7.04
 getPages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFonts
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 hasEncryption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasXmpMetadata
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getXmpBytes
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 hasOutputIntents
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 hasOutputIntentWithIccProfile
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 hasTransparency
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 hasJavaScript
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 hasEmbeddedFiles
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 getRegisteredObjects
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hasThreeDAnnotations
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getThreeDStreams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 hasRasterOnlyContent
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getImageXObjects
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 hasInteractiveForms
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasMultimediaContent
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
7.05
 getReferenceXObjects
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Conformance\Inspection;
6
7use Phpdftk\Pdf\Core\Action\JavaScriptAction;
8use Phpdftk\Pdf\Core\Annotation\Annotation;
9use Phpdftk\Pdf\Core\Annotation\MovieAnnotation;
10use Phpdftk\Pdf\Core\Annotation\RichMediaAnnotation;
11use Phpdftk\Pdf\Core\Annotation\ScreenAnnotation;
12use Phpdftk\Pdf\Core\Annotation\SoundAnnotation;
13use Phpdftk\Pdf\Core\Annotation\ThreeDAnnotation;
14use Phpdftk\Pdf\Core\Document\Catalog;
15use Phpdftk\Pdf\Core\Document\Info;
16use Phpdftk\Pdf\Core\Document\Page;
17use Phpdftk\Pdf\Core\Font\Font;
18use Phpdftk\Pdf\Core\Font\Type0Font;
19use Phpdftk\Pdf\Core\Graphics\XObject\FormXObject;
20use Phpdftk\Pdf\Core\Graphics\XObject\ImageXObject;
21use Phpdftk\Pdf\Core\Interactive\Form\AcroForm;
22use Phpdftk\Pdf\Core\Multimedia\MediaRendition;
23use Phpdftk\Pdf\Core\PdfDictionary;
24use Phpdftk\Pdf\Core\PdfName;
25use Phpdftk\Pdf\Core\PdfObject;
26use Phpdftk\Pdf\Core\PdfReference;
27use Phpdftk\Pdf\Core\PdfStream;
28use Phpdftk\Pdf\Core\PdfString;
29use Phpdftk\Pdf\Core\ThreeD\ThreeDStream;
30use Phpdftk\Pdf\Reader\PdfReader;
31
32/**
33 * Inspects a parsed PDF (via PdfReader) for conformance validation.
34 *
35 * Uses the reader's hydration API to obtain typed objects where possible,
36 * and falls back to raw dictionary inspection for fields not yet hydrated.
37 */
38final class ReaderDocumentInspector implements DocumentInspector
39{
40    private ?Catalog $typedCatalog = null;
41    private ?Info $typedInfo = null;
42
43    public function __construct(
44        private readonly PdfReader $reader,
45    ) {}
46
47    public function getCatalog(): Catalog
48    {
49        if ($this->typedCatalog === null) {
50            $this->typedCatalog = $this->reader->getTypedCatalog();
51        }
52        return $this->typedCatalog;
53    }
54
55    public function getInfo(): ?Info
56    {
57        if ($this->typedInfo !== null) {
58            return $this->typedInfo;
59        }
60
61        $infoDict = $this->reader->getInfo();
62        if ($infoDict === null) {
63            return null;
64        }
65
66        // Build a typed Info from the raw dictionary
67        $info = new Info();
68        $info->objectNumber = 0;
69        $info->generationNumber = 0;
70
71        $title = $infoDict->get('Title');
72        if ($title instanceof PdfString) {
73            $info->title = $title;
74        }
75        $author = $infoDict->get('Author');
76        if ($author instanceof PdfString) {
77            $info->author = $author;
78        }
79        $producer = $infoDict->get('Producer');
80        if ($producer instanceof PdfString) {
81            $info->producer = $producer;
82        }
83        $trapped = $infoDict->get('Trapped');
84        if ($trapped instanceof PdfName) {
85            $info->trapped = $trapped;
86        }
87
88        $this->typedInfo = $info;
89        return $info;
90    }
91
92    public function getPages(): iterable
93    {
94        return $this->reader->getTypedPages();
95    }
96
97    public function getFonts(): iterable
98    {
99        // Walk all objects looking for Font and Type0Font instances
100        $resolver = $this->reader->getResolver();
101        foreach ($resolver->getObjectNumbers() as $objNum) {
102            $obj = $this->reader->getTypedObject($objNum);
103            if ($obj instanceof Font || $obj instanceof Type0Font) {
104                yield $obj;
105            }
106        }
107    }
108
109    public function hasEncryption(): bool
110    {
111        return $this->reader->getTrailer()->has('Encrypt');
112    }
113
114    public function hasXmpMetadata(): bool
115    {
116        $catalogDict = $this->reader->getCatalog();
117        return $catalogDict->has('Metadata');
118    }
119
120    public function getXmpBytes(): ?string
121    {
122        $catalogDict = $this->reader->getCatalog();
123        $metaRef = $catalogDict->get('Metadata');
124        if (!$metaRef instanceof PdfReference) {
125            return null;
126        }
127
128        $obj = $this->reader->getResolver()->resolveReference($metaRef);
129        if ($obj instanceof PdfStream) {
130            return $obj->data;
131        }
132
133        return null;
134    }
135
136    public function hasOutputIntents(): bool
137    {
138        $catalogDict = $this->reader->getCatalog();
139        $oi = $catalogDict->get('OutputIntents');
140        if ($oi instanceof \Phpdftk\Pdf\Core\PdfArray) {
141            return count($oi->items) > 0;
142        }
143        return false;
144    }
145
146    public function hasOutputIntentWithIccProfile(): bool
147    {
148        $catalogDict = $this->reader->getCatalog();
149        $oi = $catalogDict->get('OutputIntents');
150        if (!$oi instanceof \Phpdftk\Pdf\Core\PdfArray) {
151            return false;
152        }
153
154        $resolver = $this->reader->getResolver();
155        foreach ($oi->items as $item) {
156            $dict = $item;
157            if ($item instanceof PdfReference) {
158                $dict = $resolver->resolveReference($item);
159            }
160            if ($dict instanceof PdfDictionary && $dict->has('DestOutputProfile')) {
161                return true;
162            }
163        }
164
165        return false;
166    }
167
168    public function hasTransparency(): bool
169    {
170        foreach ($this->getPages() as $page) {
171            if ($page->group !== null) {
172                return true;
173            }
174        }
175        return false;
176    }
177
178    public function hasJavaScript(): bool
179    {
180        $resolver = $this->reader->getResolver();
181        foreach ($resolver->getObjectNumbers() as $objNum) {
182            $obj = $this->reader->getTypedObject($objNum);
183            if ($obj instanceof JavaScriptAction) {
184                return true;
185            }
186        }
187        return false;
188    }
189
190    public function hasEmbeddedFiles(): bool
191    {
192        $catalogDict = $this->reader->getCatalog();
193        if (!$catalogDict->has('Names')) {
194            return false;
195        }
196
197        $namesRef = $catalogDict->get('Names');
198        if ($namesRef instanceof PdfReference) {
199            $names = $this->reader->getResolver()->resolveReference($namesRef);
200            if ($names instanceof PdfDictionary) {
201                return $names->has('EmbeddedFiles');
202            }
203        }
204
205        return false;
206    }
207
208    public function getRegisteredObjects(): iterable
209    {
210        $resolver = $this->reader->getResolver();
211        foreach ($resolver->getObjectNumbers() as $objNum) {
212            $obj = $this->reader->getTypedObject($objNum);
213            if ($obj instanceof PdfObject) {
214                yield $obj;
215            }
216        }
217    }
218
219    public function hasThreeDAnnotations(): bool
220    {
221        $resolver = $this->reader->getResolver();
222        foreach ($resolver->getObjectNumbers() as $objNum) {
223            $obj = $this->reader->getTypedObject($objNum);
224            if ($obj instanceof ThreeDAnnotation) {
225                return true;
226            }
227        }
228        return false;
229    }
230
231    public function getThreeDStreams(): iterable
232    {
233        $resolver = $this->reader->getResolver();
234        foreach ($resolver->getObjectNumbers() as $objNum) {
235            $obj = $this->reader->getTypedObject($objNum);
236            if ($obj instanceof ThreeDStream) {
237                yield $obj;
238            }
239        }
240    }
241
242    public function hasRasterOnlyContent(): bool
243    {
244        // Heuristic: if any fonts exist in the document, content is not raster-only
245        foreach ($this->getFonts() as $_) {
246            return false;
247        }
248        return true;
249    }
250
251    public function getImageXObjects(): iterable
252    {
253        $resolver = $this->reader->getResolver();
254        foreach ($resolver->getObjectNumbers() as $objNum) {
255            $obj = $this->reader->getTypedObject($objNum);
256            if ($obj instanceof ImageXObject) {
257                yield $obj;
258            }
259        }
260    }
261
262    public function hasInteractiveForms(): bool
263    {
264        $catalogDict = $this->reader->getCatalog();
265        return $catalogDict->has('AcroForm');
266    }
267
268    public function hasMultimediaContent(): bool
269    {
270        $resolver = $this->reader->getResolver();
271        foreach ($resolver->getObjectNumbers() as $objNum) {
272            $obj = $this->reader->getTypedObject($objNum);
273            if ($obj instanceof MovieAnnotation
274                || $obj instanceof SoundAnnotation
275                || $obj instanceof ScreenAnnotation
276                || $obj instanceof RichMediaAnnotation
277                || $obj instanceof MediaRendition
278            ) {
279                return true;
280            }
281        }
282        return false;
283    }
284
285    public function getReferenceXObjects(): iterable
286    {
287        $resolver = $this->reader->getResolver();
288        foreach ($resolver->getObjectNumbers() as $objNum) {
289            $obj = $this->reader->getTypedObject($objNum);
290            if ($obj instanceof FormXObject && $obj->ref !== null) {
291                yield $obj;
292            }
293        }
294    }
295}