Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.42% covered (warning)
60.42%
58 / 96
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageCopier
60.42% covered (warning)
60.42%
58 / 96
40.00% covered (danger)
40.00%
2 / 5
110.98
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
 copyPages
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 copyPage
87.50% covered (warning)
87.50%
28 / 32
0.00% covered (danger)
0.00%
0 / 1
13.33
 copyIndirectObject
40.74% covered (danger)
40.74%
11 / 27
0.00% covered (danger)
0.00%
0 / 1
10.20
 buildResources
30.77% covered (danger)
30.77%
8 / 26
0.00% covered (danger)
0.00%
0 / 1
69.08
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Toolkit\Internal;
6
7use Phpdftk\Pdf\Core\Content\Resources;
8use Phpdftk\Pdf\Core\Document\Page;
9use Phpdftk\Pdf\Core\File\PdfFileWriter;
10use Phpdftk\Pdf\Core\PdfArray;
11use Phpdftk\Pdf\Core\PdfDictionary;
12use Phpdftk\Pdf\Core\PdfNumber;
13use Phpdftk\Pdf\Core\PdfObject;
14use Phpdftk\Pdf\Core\PdfReference;
15use Phpdftk\Pdf\Core\PdfStream;
16use Phpdftk\Pdf\Reader\PdfReader;
17
18/**
19 * Deep-copies pages from a PdfReader source into a PdfFileWriter target.
20 *
21 * Handles resolving indirect references, re-registering objects,
22 * and maintaining reference integrity.
23 *
24 * @internal
25 */
26final class PageCopier
27{
28    /** @var array<int, int> Maps source object numbers to target object numbers */
29    private array $objectMap = [];
30
31    public function __construct(
32        private readonly PdfReader $reader,
33        private readonly PdfFileWriter $writer,
34    ) {}
35
36    /**
37     * Copy specified pages from source to target.
38     *
39     * @param list<int> $pageIndices 0-based page indices to copy
40     * @param PdfReference $pageTreeRef Reference to the target PageTree
41     * @return list<PdfReference> References to the newly registered Page objects
42     */
43    public function copyPages(array $pageIndices, PdfReference $pageTreeRef): array
44    {
45        $sourcePages = $this->reader->getPages();
46        $pageRefs = [];
47
48        foreach ($pageIndices as $idx) {
49            if (!isset($sourcePages[$idx])) {
50                throw new \OutOfRangeException("Page index $idx out of range");
51            }
52            $pageDict = $sourcePages[$idx];
53            $page = $this->copyPage($pageDict, $pageTreeRef);
54            $this->writer->register($page);
55            $pageRefs[] = new PdfReference($page->objectNumber);
56        }
57
58        return $pageRefs;
59    }
60
61    private function copyPage(PdfDictionary $sourceDict, PdfReference $pageTreeRef): Page
62    {
63        $page = new Page();
64        $page->parent = $pageTreeRef;
65
66        // Copy MediaBox
67        $mediaBox = $sourceDict->get('MediaBox');
68        if ($mediaBox instanceof PdfArray) {
69            $page->mediaBox = $mediaBox;
70        }
71
72        // Copy CropBox
73        $cropBox = $sourceDict->get('CropBox');
74        if ($cropBox instanceof PdfArray) {
75            $page->cropBox = $cropBox;
76        }
77
78        // Copy Rotate
79        $rotate = $sourceDict->get('Rotate');
80        if ($rotate instanceof PdfNumber) {
81            $page->rotate = (int) $rotate->toPdf();
82        }
83
84        // Copy content streams
85        $contents = $sourceDict->get('Contents');
86        if ($contents instanceof PdfReference) {
87            $ref = $this->copyIndirectObject($contents);
88            if ($ref !== null) {
89                $page->contents = [$ref];
90            }
91        } elseif ($contents instanceof PdfArray) {
92            $contentRefs = [];
93            foreach ($contents->items as $ref) {
94                if ($ref instanceof PdfReference) {
95                    $newRef = $this->copyIndirectObject($ref);
96                    if ($newRef !== null) {
97                        $contentRefs[] = $newRef;
98                    }
99                }
100            }
101            $page->contents = $contentRefs;
102        }
103
104        // Copy resources
105        $resources = $sourceDict->get('Resources');
106        if ($resources instanceof PdfDictionary) {
107            $page->resources = $this->buildResources($resources);
108        } elseif ($resources instanceof PdfReference) {
109            $resolved = $this->reader->resolveReference($resources);
110            if ($resolved instanceof PdfDictionary) {
111                $page->resources = $this->buildResources($resolved);
112            }
113        }
114
115        return $page;
116    }
117
118    private function copyIndirectObject(PdfReference $ref): ?PdfReference
119    {
120        // Check if already copied
121        if (isset($this->objectMap[$ref->objectNumber])) {
122            return new PdfReference($this->objectMap[$ref->objectNumber]);
123        }
124
125        $resolved = $this->reader->resolveReference($ref);
126
127        if ($resolved instanceof PdfStream) {
128            // PdfReader returns decoded stream data, so the dictionary's
129            // /Filter and /DecodeParms entries no longer describe the bytes
130            // we're about to write. Strip them â€” PdfStream::toPdf() will
131            // re-set /Length to the unencoded byte count.
132            $newDict = clone $resolved->dictionary;
133            unset($newDict->entries['Filter'], $newDict->entries['DecodeParms'], $newDict->entries['DP']);
134
135            $newStream = new class ($newDict, $resolved->data) extends PdfStream {
136                public function __construct(PdfDictionary $dict, string $data)
137                {
138                    parent::__construct($dict, $data);
139                }
140            };
141            $this->writer->register($newStream);
142            $this->objectMap[$ref->objectNumber] = $newStream->objectNumber;
143            return new PdfReference($newStream->objectNumber);
144        }
145
146        if ($resolved instanceof PdfObject) {
147            $clone = clone $resolved;
148            $clone->objectNumber = 0;
149            $this->writer->register($clone);
150            $this->objectMap[$ref->objectNumber] = $clone->objectNumber;
151            return new PdfReference($clone->objectNumber);
152        }
153
154        if ($resolved instanceof PdfDictionary) {
155            $wrapper = new class ($resolved) extends PdfObject {
156                public function __construct(private readonly PdfDictionary $d) {}
157                public function toPdf(): string
158                {
159                    return $this->d->toPdf();
160                }
161            };
162            $this->writer->register($wrapper);
163            $this->objectMap[$ref->objectNumber] = $wrapper->objectNumber;
164            return new PdfReference($wrapper->objectNumber);
165        }
166
167        return null;
168    }
169
170    private function buildResources(PdfDictionary $res): Resources
171    {
172        $resources = new Resources();
173
174        // Copy Font references
175        $fontDict = $res->get('Font');
176        if ($fontDict instanceof PdfDictionary) {
177            foreach (array_keys($fontDict->entries) as $name) {
178                $ref = $fontDict->entries[$name];
179                if ($ref instanceof PdfReference) {
180                    $newRef = $this->copyIndirectObject($ref);
181                    if ($newRef !== null) {
182                        $resources->font[$name] = $newRef;
183                    }
184                }
185            }
186        }
187
188        // Copy XObject references
189        $xoDict = $res->get('XObject');
190        if ($xoDict instanceof PdfDictionary) {
191            foreach (array_keys($xoDict->entries) as $name) {
192                $ref = $xoDict->entries[$name];
193                if ($ref instanceof PdfReference) {
194                    $newRef = $this->copyIndirectObject($ref);
195                    if ($newRef !== null) {
196                        $resources->xObject[$name] = $newRef;
197                    }
198                }
199            }
200        }
201
202        // Copy ExtGState references
203        $gsDict = $res->get('ExtGState');
204        if ($gsDict instanceof PdfDictionary) {
205            foreach (array_keys($gsDict->entries) as $name) {
206                $ref = $gsDict->entries[$name];
207                if ($ref instanceof PdfReference) {
208                    $newRef = $this->copyIndirectObject($ref);
209                    if ($newRef !== null) {
210                        $resources->extGState[$name] = $newRef;
211                    }
212                }
213            }
214        }
215
216        return $resources;
217    }
218}