Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.89% covered (warning)
75.89%
321 / 423
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
PdfHydrator
75.89% covered (warning)
75.89%
321 / 423
66.67% covered (warning)
66.67%
8 / 12
362.32
0.00% covered (danger)
0.00%
0 / 1
 registerType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerSubtype
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerActionType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hydrate
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
13
 resolveClass
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 resolveActionClass
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 construct
84.00% covered (warning)
84.00%
21 / 25
0.00% covered (danger)
0.00%
0 / 1
10.41
 extractConstructorArg
52.27% covered (warning)
52.27%
23 / 44
0.00% covered (danger)
0.00%
0 / 1
176.90
 coerce
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
12.04
 getKeyMap
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 getOverrides
51.90% covered (warning)
51.90%
82 / 158
0.00% covered (danger)
0.00%
0 / 1
162.66
 registerDefaults
100.00% covered (success)
100.00%
126 / 126
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Core\File;
6
7use Phpdftk\Pdf\Core\PdfArray;
8use Phpdftk\Pdf\Core\PdfBoolean;
9use Phpdftk\Pdf\Core\PdfDictionary;
10use Phpdftk\Pdf\Core\PdfName;
11use Phpdftk\Pdf\Core\PdfNumber;
12use Phpdftk\Pdf\Core\PdfObject;
13use Phpdftk\Pdf\Core\PdfReference;
14use Phpdftk\Pdf\Core\Serializable;
15
16/**
17 * Hydrates raw `PdfDictionary` objects (from the reader) into typed
18 * `PdfObject` subclasses (used by the writer).
19 *
20 * The hydrator uses a `/Type` → class registry and maps PDF dictionary
21 * keys to PHP property names using a key→property map built from each
22 * class's known field mappings.
23 *
24 * Values are assigned as-is — `PdfReference` values stay as references
25 * (lazy resolution), and inline dicts/arrays are preserved. Deep
26 * hydration (recursively hydrating nested objects) is opt-in via
27 * the resolver.
28 */
29final class PdfHydrator
30{
31    /**
32     * Registry mapping PDF /Type values to PHP class names.
33     *
34     * @var array<string, class-string<PdfObject>>
35     */
36    private static array $typeMap = [];
37
38    /**
39     * Registry mapping /Type+/Subtype to PHP class names for types
40     * that share a /Type value (e.g., Annot, Font, XObject, Pattern).
41     *
42     * @var array<string, array<string, class-string<PdfObject>>>
43     */
44    private static array $subtypeMap = [];
45
46    /**
47     * Registry mapping action /S values to PHP class names.
48     *
49     * @var array<string, class-string<PdfObject>>
50     */
51    private static array $actionTypeMap = [];
52
53    /**
54     * Cache of key→property maps per class.
55     *
56     * @var array<class-string, array<string, string>>
57     */
58    private static array $keyMapCache = [];
59
60    /**
61     * Register a PdfObject subclass for a given /Type value.
62     *
63     * @param class-string<PdfObject> $className
64     */
65    public static function registerType(string $pdfType, string $className): void
66    {
67        self::$typeMap[$pdfType] = $className;
68    }
69
70    /**
71     * Register a PdfObject subclass for a /Type + /Subtype combination.
72     *
73     * @param class-string<PdfObject> $className
74     */
75    public static function registerSubtype(string $pdfType, string $subtype, string $className): void
76    {
77        self::$subtypeMap[$pdfType][$subtype] = $className;
78    }
79
80    /**
81     * Register a PdfObject subclass for an action /S value.
82     *
83     * @param class-string<PdfObject> $className
84     */
85    public static function registerActionType(string $actionType, string $className): void
86    {
87        self::$actionTypeMap[$actionType] = $className;
88    }
89
90    /**
91     * Hydrate a raw PdfDictionary into a typed PdfObject.
92     *
93     * If the dictionary has a /Type entry that maps to a registered class,
94     * that class is instantiated and its properties populated. Otherwise,
95     * the raw dictionary is returned unchanged.
96     *
97     * @param int $objectNumber Object number to assign to the hydrated object
98     * @param int $generationNumber Generation number
99     */
100    public static function hydrate(
101        PdfDictionary $dict,
102        int $objectNumber = 0,
103        int $generationNumber = 0,
104    ): PdfObject|PdfDictionary {
105        $type = $dict->get('Type');
106        $typeName = $type instanceof PdfName ? $type->value : null;
107
108        if ($typeName === null) {
109            // Check if this is an action dict (has /S but no /Type — common in real PDFs)
110            $className = self::resolveActionClass($dict);
111            if ($className === null) {
112                return $dict;
113            }
114        } else {
115            // Resolve the target class — check subtype map first for shared /Type values
116            $className = self::resolveClass($typeName, $dict);
117        }
118
119        // For /Type /Action, also try action dispatch if subtype/type lookup failed
120        if ($className === null && $typeName === 'Action') {
121            $className = self::resolveActionClass($dict);
122        }
123        if ($className === null) {
124            return $dict;
125        }
126
127        // Construct the object — some classes require constructor args from the dict
128        $object = self::construct($className, $dict);
129        if ($object === null) {
130            return $dict;
131        }
132
133        $object->objectNumber = $objectNumber;
134        $object->generationNumber = $generationNumber;
135
136        $keyMap = self::getKeyMap($className);
137
138        foreach ($dict->entries as $key => $value) {
139            if ($key === 'Type') {
140                continue;
141            }
142
143            $propertyName = $keyMap[$key] ?? null;
144            if ($propertyName !== null && property_exists($object, $propertyName)) {
145                try {
146                    $object->$propertyName = self::coerce($value, $object, $propertyName);
147                } catch (\TypeError) {
148                    // Value type incompatible with property — skip
149                    // (e.g., PdfDictionary for a typed Resources property)
150                }
151            }
152        }
153
154        return $object;
155    }
156
157    /**
158     * Resolve the target class for a given /Type, considering /Subtype.
159     *
160     * @return class-string<PdfObject>|null
161     */
162    private static function resolveClass(string $typeName, PdfDictionary $dict): ?string
163    {
164        // Check subtype map first for types with multiple implementations
165        if (isset(self::$subtypeMap[$typeName])) {
166            $subtype = $dict->get('Subtype');
167            if ($subtype instanceof PdfName && isset(self::$subtypeMap[$typeName][$subtype->value])) {
168                return self::$subtypeMap[$typeName][$subtype->value];
169            }
170            // Fall through to base type map if no subtype match
171        }
172
173        return self::$typeMap[$typeName] ?? null;
174    }
175
176    /**
177     * Resolve an action class from the /S key in the dictionary.
178     *
179     * @return class-string<PdfObject>|null
180     */
181    private static function resolveActionClass(PdfDictionary $dict): ?string
182    {
183        $s = $dict->get('S');
184        if ($s instanceof PdfName && isset(self::$actionTypeMap[$s->value])) {
185            return self::$actionTypeMap[$s->value];
186        }
187        return null;
188    }
189
190    /**
191     * Construct a PdfObject, extracting required constructor args from the dict.
192     */
193    private static function construct(string $className, PdfDictionary $dict): ?PdfObject
194    {
195        $ref = new \ReflectionClass($className);
196        if ($ref->isAbstract()) {
197            return null;
198        }
199
200        $constructor = $ref->getConstructor();
201        if ($constructor === null) {
202            return new $className();
203        }
204
205        // Check if all parameters have defaults
206        $params = $constructor->getParameters();
207        $allOptional = true;
208        foreach ($params as $param) {
209            if (!$param->isOptional()) {
210                $allOptional = false;
211                break;
212            }
213        }
214        if ($allOptional) {
215            return new $className();
216        }
217
218        // Try to extract required args from the dictionary
219        $args = [];
220        foreach ($params as $param) {
221            if ($param->isOptional()) {
222                break; // Once we hit optional params, stop — let defaults apply
223            }
224            $value = self::extractConstructorArg($param, $dict);
225            if ($value === null) {
226                return null; // Can't satisfy required arg — bail
227            }
228            $args[] = $value;
229        }
230
231        try {
232            return new $className(...$args);
233        } catch (\Throwable) {
234            return null;
235        }
236    }
237
238    /**
239     * Try to extract a constructor argument value from the dictionary.
240     */
241    private static function extractConstructorArg(\ReflectionParameter $param, PdfDictionary $dict): mixed
242    {
243        $name = $param->getName();
244        $type = $param->getType();
245        $typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null;
246
247        // Map common constructor param names to PDF keys
248        $pdfKey = match ($name) {
249            'title' => 'Title',
250            'fontName' => 'FontName',
251            'rect' => 'Rect',
252            'subtype' => 'Subtype',
253            'structureType' => 'S',
254            'name' => 'Name',
255            'externalName' => 'XN',
256            'sampleRate' => 'R',
257            'transformMethod' => 'TransformMethod',
258            'outputConditionIdentifier' => 'OutputConditionIdentifier',
259            'dPartRootNode' => 'DPartRootNode',
260            'parent' => 'Parent',
261            // Action constructor params
262            'dest' => 'D',
263            'uri' => 'URI',
264            'js' => 'JS',
265            'n' => 'N',
266            'f' => 'F',
267            't' => 'T',
268            'hide' => 'H',
269            'sound' => 'Sound',
270            'ta' => 'TA',
271            'state' => 'State',
272            'trans' => 'Trans',
273            'bBox' => 'BBox',
274            'baseFontName' => 'BaseFont',
275            default => ucfirst($name),
276        };
277
278        $value = $dict->get($pdfKey);
279        if ($value === null) {
280            return null;
281        }
282
283        // Coerce to expected type
284        if ($typeName === 'string') {
285            if ($value instanceof PdfName) {
286                return $value->value;
287            }
288            if ($value instanceof \Phpdftk\Pdf\Core\PdfString) {
289                return $value->value;
290            }
291            return (string) $value;
292        }
293
294        if ($typeName === 'int' && $value instanceof PdfNumber) {
295            return (int) $value->toPdf();
296        }
297
298        if ($typeName === 'float' && $value instanceof PdfNumber) {
299            return (float) $value->toPdf();
300        }
301
302        return $value;
303    }
304
305    /**
306     * Coerce a raw PDF value to match the declared PHP property type.
307     *
308     * Handles common mismatches like PdfNumber → int/float,
309     * PdfArray → array, PdfReference in single-value array slots, etc.
310     */
311    private static function coerce(mixed $value, object $object, string $propertyName): mixed
312    {
313        $ref = new \ReflectionProperty($object, $propertyName);
314        $type = $ref->getType();
315
316        if (!$type instanceof \ReflectionNamedType) {
317            return $value;
318        }
319
320        $typeName = $type->getName();
321
322        // PdfNumber → int
323        if ($typeName === 'int' && $value instanceof PdfNumber) {
324            return (int) $value->toPdf();
325        }
326
327        // PdfNumber → float
328        if ($typeName === 'float' && $value instanceof PdfNumber) {
329            return (float) $value->toPdf();
330        }
331
332        // PdfBoolean → bool
333        if ($typeName === 'bool' && $value instanceof PdfBoolean) {
334            return $value->value;
335        }
336
337        // PdfArray → array (extract items)
338        if ($typeName === 'array' && $value instanceof PdfArray) {
339            return $value->items;
340        }
341
342        // Single PdfReference assigned to an array property (e.g., /Contents with 1 ref)
343        if ($typeName === 'array' && $value instanceof PdfReference) {
344            return [$value];
345        }
346
347        return $value;
348    }
349
350    /**
351     * Build or retrieve the PDF key → PHP property map for a class.
352     *
353     * Uses reflection to discover all public properties, then maps
354     * each property name to its corresponding PDF key using the
355     * standard naming convention: lcfirst(PdfKey) = phpProperty.
356     *
357     * Special cases (where the convention doesn't apply) are handled
358     * by explicit overrides.
359     *
360     * @param class-string $className
361     * @return array<string, string> PDF key → property name
362     */
363    private static function getKeyMap(string $className): array
364    {
365        if (isset(self::$keyMapCache[$className])) {
366            return self::$keyMapCache[$className];
367        }
368
369        $map = [];
370        $ref = new \ReflectionClass($className);
371
372        foreach ($ref->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
373            $propName = $prop->getName();
374
375            // Skip PdfObject base class properties
376            if ($propName === 'objectNumber' || $propName === 'generationNumber') {
377                continue;
378            }
379            // Skip PdfStream properties
380            if ($propName === 'dictionary' || $propName === 'data') {
381                continue;
382            }
383
384            // Convert camelCase property to PDF key (ucfirst)
385            $pdfKey = ucfirst($propName);
386
387            $map[$pdfKey] = $propName;
388        }
389
390        // Apply known special-case overrides where the naming convention
391        // doesn't match (PDF key differs from ucfirst(property))
392        $overrides = self::getOverrides($className);
393        foreach ($overrides as $pdfKey => $propName) {
394            $map[$pdfKey] = $propName;
395        }
396
397        self::$keyMapCache[$className] = $map;
398        return $map;
399    }
400
401    /**
402     * Return special-case PDF key → property overrides for a class.
403     *
404     * @return array<string, string>
405     */
406    private static function getOverrides(string $className): array
407    {
408        // Class-specific overrides where PDF key ≠ ucfirst(propertyName)
409        $shortName = (new \ReflectionClass($className))->getShortName();
410
411        return match ($shortName) {
412            'Page' => [
413                'Trans' => 'transition',
414                'AA' => 'aa',
415                'PZ' => 'pz',
416                'B' => 'b',
417                'AF' => 'af',
418                'ID' => 'id',
419                'VP' => 'vp',
420                'DPart' => 'dPart',
421            ],
422            'Catalog' => [
423                'AA' => 'aa',
424                'URI' => 'uri',
425                'AF' => 'af',
426                'DSS' => 'dss',
427                'DPartRoot' => 'dPartRoot',
428            ],
429            'PageTree' => [
430                'Kids' => 'kids',
431            ],
432            'Info' => [
433                'ModDate' => 'modDate',
434            ],
435            'FontDescriptor' => [
436                'ItalicAngle' => 'italicAngle',
437                'StemV' => 'stemV',
438                'StemH' => 'stemH',
439                'CapHeight' => 'capHeight',
440                'XHeight' => 'xHeight',
441                'AvgWidth' => 'avgWidth',
442                'MaxWidth' => 'maxWidth',
443                'MissingWidth' => 'missingWidth',
444                'FontFile' => 'fontFile',
445                'FontFile2' => 'fontFile2',
446                'FontFile3' => 'fontFile3',
447                'FontBBox' => 'fontBBox',
448                'FontName' => 'fontName',
449                'FontFamily' => 'fontFamily',
450                'FontStretch' => 'fontStretch',
451                'FontWeight' => 'fontWeight',
452            ],
453            'OutlineItem' => [
454                'A' => 'a',
455                'C' => 'c',
456                'F' => 'f',
457                'SE' => 'se',
458            ],
459            'OutputIntent' => [
460                'S' => 's',
461            ],
462            'StructTreeRoot' => [
463                'K' => 'k',
464            ],
465            'StructElem' => [
466                'S' => 's',
467                'P' => 'p',
468                'K' => 'k',
469                'A' => 'a',
470                'C' => 'c',
471                'R' => 'r',
472                'T' => 't',
473                'E' => 'e',
474                'ID' => 'id',
475            ],
476            'EncryptDictionary' => [
477                'CF' => 'cf',
478                'O' => 'o',
479                'U' => 'u',
480                'OE' => 'oe',
481                'UE' => 'ue',
482                'P' => 'p',
483                'R' => 'r',
484                'V' => 'v',
485            ],
486            'OCG' => [
487                'Name' => 'name',
488            ],
489            'ExtGState' => [
490                'LW' => 'lw',
491                'LC' => 'lc',
492                'LJ' => 'lj',
493                'ML' => 'ml',
494                'D' => 'd',
495                'RI' => 'ri',
496                'OP' => 'op',
497                'op' => 'opLower',
498                'OPM' => 'opm',
499                'FL' => 'fl',
500                'SM' => 'sm',
501                'SA' => 'sa',
502                'BM' => 'bm',
503                'SMask' => 'sMask',
504                'CA' => 'ca',
505                'ca' => 'caLower',
506                'AIS' => 'ais',
507                'TK' => 'tk',
508                'BG' => 'bg',
509                'BG2' => 'bg2',
510                'UCR' => 'ucr',
511                'UCR2' => 'ucr2',
512                'TR' => 'tr',
513                'TR2' => 'tr2',
514                'HT' => 'ht',
515                'HTO' => 'hto',
516            ],
517            'SignatureValue', 'DocTimeStamp' => [
518                'M' => 'm',
519            ],
520            'ThreeDBackground' => [
521                'CS' => 'cs',
522                'C' => 'c',
523                'EA' => 'ea',
524            ],
525            'ThreeDCrossSection' => [
526                'C' => 'c',
527                'O' => 'o',
528                'PC' => 'pc',
529                'PO' => 'po',
530                'IV' => 'iv',
531                'IC' => 'ic',
532                'ST' => 'st',
533            ],
534            'ThreeDView' => [
535                'XN' => 'xn',
536                'IN' => 'in',
537                'MS' => 'ms',
538                'C2W' => 'c2w',
539                'CO' => 'co',
540                'P' => 'p',
541                'O' => 'o',
542                'BG' => 'bg',
543                'RM' => 'rm',
544                'LS' => 'ls',
545                'SA' => 'sa',
546            ],
547            'ThreeDStream' => [
548                'VA' => 'va',
549                'DV' => 'dv',
550                'AN' => 'an',
551            ],
552            // Actions — map /S and single-letter PDF keys to property names
553            'GoToAction' => ['D' => 'dest'],
554            'GoToRAction' => ['D' => 'dest', 'F' => 'f'],
555            'GoToEAction' => ['D' => 'd', 'F' => 'f', 'T' => 't'],
556            'URIAction' => ['URI' => 'uri'],
557            'JavaScriptAction' => ['JS' => 'js'],
558            'NamedAction' => ['N' => 'n'],
559            'HideAction' => ['T' => 't', 'H' => 'h'],
560            'SoundAction' => ['Sound' => 'sound'],
561            'SubmitFormAction' => ['F' => 'f'],
562            'ImportDataAction' => ['F' => 'f'],
563            'SetOCGStateAction' => ['State' => 'state'],
564            'GoTo3DViewAction' => ['TA' => 'ta', 'V' => 'v'],
565            'TransAction' => ['Trans' => 'trans'],
566            'RenditionAction' => ['R' => 'r', 'AN' => 'an', 'OP' => 'op'],
567            'MovieAction' => ['T' => 't'],
568            default => [],
569        };
570    }
571
572    /**
573     * Auto-discover and register all PdfObject subclasses that define
574     * a PDF_TYPE constant. Call once at bootstrap.
575     */
576    public static function registerDefaults(): void
577    {
578        if (!empty(self::$typeMap)) {
579            return;
580        }
581
582        // ---------------------------------------------------------------
583        // Unique /Type classes (one class per /Type value)
584        // ---------------------------------------------------------------
585        $uniqueTypes = [
586            // Document structure
587            \Phpdftk\Pdf\Core\Document\Catalog::class,
588            \Phpdftk\Pdf\Core\Document\Page::class,
589            \Phpdftk\Pdf\Core\Document\PageTree::class,
590            \Phpdftk\Pdf\Core\Document\Outline::class,
591            \Phpdftk\Pdf\Core\Document\OutlineItem::class,
592            \Phpdftk\Pdf\Core\Document\PageLabel::class,
593            \Phpdftk\Pdf\Core\Document\OutputIntent::class,
594            \Phpdftk\Pdf\Core\Document\StructTreeRoot::class,
595            \Phpdftk\Pdf\Core\Document\StructElem::class,
596            \Phpdftk\Pdf\Core\Document\OCG::class,
597            \Phpdftk\Pdf\Core\Document\OCMD::class,
598            \Phpdftk\Pdf\Core\Document\Collection::class,
599            \Phpdftk\Pdf\Core\Document\CollectionSchema::class,
600            \Phpdftk\Pdf\Core\Document\Thread::class,
601            \Phpdftk\Pdf\Core\Document\Bead::class,
602            \Phpdftk\Pdf\Core\Document\ObjectRef::class,
603            \Phpdftk\Pdf\Core\Document\DPartRoot::class,
604            \Phpdftk\Pdf\Core\Document\DPart::class,
605            \Phpdftk\Pdf\Core\Document\Requirement::class,
606            \Phpdftk\Pdf\Core\Document\RequirementHandler::class,
607            \Phpdftk\Pdf\Core\Document\MetadataStream::class,
608            \Phpdftk\Pdf\Core\Document\CrossReferenceStream::class,
609            \Phpdftk\Pdf\Core\Document\ObjectStream::class,
610            // Font
611            \Phpdftk\Pdf\Core\Font\FontDescriptor::class,
612            \Phpdftk\Pdf\Core\Font\Encoding::class,
613            \Phpdftk\Pdf\Core\Font\CMapStream::class,
614            // FileSpec
615            \Phpdftk\Pdf\Core\FileSpec\FileSpec::class,
616            \Phpdftk\Pdf\Core\FileSpec\EmbeddedFile::class,
617            // Security
618            \Phpdftk\Pdf\Core\Security\EncryptDictionary::class,
619            // Graphics
620            \Phpdftk\Pdf\Core\Graphics\ExtGState::class,
621            // Interactive — forms
622            \Phpdftk\Pdf\Core\Interactive\Form\SigFieldLock::class,
623            \Phpdftk\Pdf\Core\Interactive\Form\SeedValueDictionary::class,
624            // Interactive — signatures
625            \Phpdftk\Pdf\Core\Interactive\Signature\SignatureValue::class,
626            \Phpdftk\Pdf\Core\Interactive\Signature\DocTimeStamp::class,
627            \Phpdftk\Pdf\Core\Interactive\Signature\SignatureReference::class,
628            // Multimedia
629            \Phpdftk\Pdf\Core\Multimedia\Sound::class,
630            \Phpdftk\Pdf\Core\Multimedia\MediaPlayParams::class,
631            \Phpdftk\Pdf\Core\Multimedia\MediaScreenParams::class,
632            \Phpdftk\Pdf\Core\Multimedia\MediaCriteria::class,
633            \Phpdftk\Pdf\Core\Multimedia\Navigator::class,
634            // 3D
635            \Phpdftk\Pdf\Core\ThreeD\ThreeDStream::class,
636            \Phpdftk\Pdf\Core\ThreeD\ThreeDView::class,
637            \Phpdftk\Pdf\Core\ThreeD\ThreeDBackground::class,
638            \Phpdftk\Pdf\Core\ThreeD\ThreeDRenderMode::class,
639            \Phpdftk\Pdf\Core\ThreeD\ThreeDLightingScheme::class,
640            \Phpdftk\Pdf\Core\ThreeD\ThreeDCrossSection::class,
641            \Phpdftk\Pdf\Core\ThreeD\ThreeDNode::class,
642            \Phpdftk\Pdf\Core\ThreeD\ThreeDMeasure::class,
643        ];
644
645        foreach ($uniqueTypes as $class) {
646            if (defined("$class::PDF_TYPE")) {
647                self::registerType($class::PDF_TYPE, $class);
648            }
649        }
650
651        // ---------------------------------------------------------------
652        // Shared /Type classes — dispatched by /Subtype
653        // ---------------------------------------------------------------
654
655        // /Type /Annot — concrete annotation subtypes
656        $annotationSubtypes = [
657            'Text' => \Phpdftk\Pdf\Core\Annotation\TextAnnotation::class,
658            'Link' => \Phpdftk\Pdf\Core\Annotation\LinkAnnotation::class,
659            'FreeText' => \Phpdftk\Pdf\Core\Annotation\FreeTextAnnotation::class,
660            'Highlight' => \Phpdftk\Pdf\Core\Annotation\HighlightAnnotation::class,
661            'Underline' => \Phpdftk\Pdf\Core\Annotation\UnderlineAnnotation::class,
662            'Squiggly' => \Phpdftk\Pdf\Core\Annotation\SquigglyAnnotation::class,
663            'StrikeOut' => \Phpdftk\Pdf\Core\Annotation\StrikeOutAnnotation::class,
664            'Stamp' => \Phpdftk\Pdf\Core\Annotation\StampAnnotation::class,
665            'Ink' => \Phpdftk\Pdf\Core\Annotation\InkAnnotation::class,
666            'Popup' => \Phpdftk\Pdf\Core\Annotation\PopupAnnotation::class,
667            'Widget' => \Phpdftk\Pdf\Core\Annotation\WidgetAnnotation::class,
668            'Line' => \Phpdftk\Pdf\Core\Annotation\LineAnnotation::class,
669            'Square' => \Phpdftk\Pdf\Core\Annotation\SquareAnnotation::class,
670            'Circle' => \Phpdftk\Pdf\Core\Annotation\CircleAnnotation::class,
671            'Polygon' => \Phpdftk\Pdf\Core\Annotation\PolygonAnnotation::class,
672            'PolyLine' => \Phpdftk\Pdf\Core\Annotation\PolyLineAnnotation::class,
673            'Caret' => \Phpdftk\Pdf\Core\Annotation\CaretAnnotation::class,
674            'FileAttachment' => \Phpdftk\Pdf\Core\Annotation\FileAttachmentAnnotation::class,
675            'Sound' => \Phpdftk\Pdf\Core\Annotation\SoundAnnotation::class,
676            'Movie' => \Phpdftk\Pdf\Core\Annotation\MovieAnnotation::class,
677            'Screen' => \Phpdftk\Pdf\Core\Annotation\ScreenAnnotation::class,
678            'PrinterMark' => \Phpdftk\Pdf\Core\Annotation\PrinterMarkAnnotation::class,
679            'TrapNet' => \Phpdftk\Pdf\Core\Annotation\TrapNetAnnotation::class,
680            'Watermark' => \Phpdftk\Pdf\Core\Annotation\WatermarkAnnotation::class,
681            '3D' => \Phpdftk\Pdf\Core\Annotation\ThreeDAnnotation::class,
682            'Redact' => \Phpdftk\Pdf\Core\Annotation\RedactAnnotation::class,
683            'Projection' => \Phpdftk\Pdf\Core\Annotation\ProjectionAnnotation::class,
684            'RichMedia' => \Phpdftk\Pdf\Core\Annotation\RichMediaAnnotation::class,
685        ];
686        foreach ($annotationSubtypes as $subtype => $class) {
687            self::registerSubtype('Annot', $subtype, $class);
688        }
689
690        // /Type /Font — font subtypes
691        self::registerSubtype('Font', 'Type1', \Phpdftk\Pdf\Core\Font\Type1Font::class);
692        self::registerSubtype('Font', 'TrueType', \Phpdftk\Pdf\Core\Font\TrueTypeFont::class);
693        self::registerSubtype('Font', 'Type0', \Phpdftk\Pdf\Core\Font\Type0Font::class);
694        self::registerSubtype('Font', 'Type3', \Phpdftk\Pdf\Core\Font\Type3Font::class);
695        self::registerSubtype('Font', 'MMType1', \Phpdftk\Pdf\Core\Font\MMType1Font::class);
696        self::registerSubtype('Font', 'CIDFontType0', \Phpdftk\Pdf\Core\Font\CIDFontType0Font::class);
697        self::registerSubtype('Font', 'CIDFontType2', \Phpdftk\Pdf\Core\Font\CIDFontType2Font::class);
698
699        // /Type /XObject
700        self::registerSubtype('XObject', 'Image', \Phpdftk\Pdf\Core\Graphics\XObject\ImageXObject::class);
701        self::registerSubtype('XObject', 'Form', \Phpdftk\Pdf\Core\Graphics\XObject\FormXObject::class);
702        self::registerSubtype('XObject', 'PS', \Phpdftk\Pdf\Core\Graphics\XObject\PostScriptXObject::class);
703
704        // /Type /Rendition
705        self::registerSubtype('Rendition', 'MR', \Phpdftk\Pdf\Core\Multimedia\MediaRendition::class);
706        self::registerSubtype('Rendition', 'SR', \Phpdftk\Pdf\Core\Multimedia\SelectorRendition::class);
707
708        // /Type /MediaClip
709        self::registerSubtype('MediaClip', 'MCD', \Phpdftk\Pdf\Core\Multimedia\MediaClipData::class);
710        self::registerSubtype('MediaClip', 'MCS', \Phpdftk\Pdf\Core\Multimedia\MediaClipSection::class);
711
712        // ---------------------------------------------------------------
713        // Actions — dispatched by /S (not /Type + /Subtype)
714        // ---------------------------------------------------------------
715        $actionTypes = [
716            'GoTo'              => \Phpdftk\Pdf\Core\Action\GoToAction::class,
717            'GoToR'             => \Phpdftk\Pdf\Core\Action\GoToRAction::class,
718            'GoToE'             => \Phpdftk\Pdf\Core\Action\GoToEAction::class,
719            'GoToDP'            => \Phpdftk\Pdf\Core\Action\GoToDPAction::class,
720            'GoTo3DView'        => \Phpdftk\Pdf\Core\Action\GoTo3DViewAction::class,
721            'Launch'            => \Phpdftk\Pdf\Core\Action\LaunchAction::class,
722            'Thread'            => \Phpdftk\Pdf\Core\Action\ThreadAction::class,
723            'URI'               => \Phpdftk\Pdf\Core\Action\URIAction::class,
724            'Sound'             => \Phpdftk\Pdf\Core\Action\SoundAction::class,
725            'Movie'             => \Phpdftk\Pdf\Core\Action\MovieAction::class,
726            'Hide'              => \Phpdftk\Pdf\Core\Action\HideAction::class,
727            'Named'             => \Phpdftk\Pdf\Core\Action\NamedAction::class,
728            'SubmitForm'        => \Phpdftk\Pdf\Core\Action\SubmitFormAction::class,
729            'ResetForm'         => \Phpdftk\Pdf\Core\Action\ResetFormAction::class,
730            'ImportData'        => \Phpdftk\Pdf\Core\Action\ImportDataAction::class,
731            'SetOCGState'       => \Phpdftk\Pdf\Core\Action\SetOCGStateAction::class,
732            'Rendition'         => \Phpdftk\Pdf\Core\Action\RenditionAction::class,
733            'Trans'             => \Phpdftk\Pdf\Core\Action\TransAction::class,
734            'JavaScript'        => \Phpdftk\Pdf\Core\Action\JavaScriptAction::class,
735            'RichMediaExecute'  => \Phpdftk\Pdf\Core\Action\RichMediaExecuteAction::class,
736        ];
737        foreach ($actionTypes as $sValue => $class) {
738            self::registerActionType($sValue, $class);
739        }
740
741        // DSS (Document Security Store — PDF 2.0)
742        self::registerType('DSS', \Phpdftk\Pdf\Core\Document\DSS::class);
743    }
744}