Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
PageSlicer
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
16 / 16
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 open
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 openString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 keep
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 keepPages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 keepRange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 remove
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 removePages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reorder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 reverse
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 split
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toBytes
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getVersionWarnings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Toolkit;
6
7use Phpdftk\Pdf\Core\Document\Catalog;
8use Phpdftk\Pdf\Core\Document\PageTree;
9use Phpdftk\Filesystem\LocalFilesystem;
10use Phpdftk\Pdf\Core\File\PdfFileWriter;
11use Phpdftk\Pdf\Core\PdfReference;
12use Phpdftk\Pdf\Reader\PdfReader;
13use Phpdftk\Pdf\Toolkit\Internal\PageCopier;
14
15/**
16 * Extract, reorder, remove, and split pages from a PDF.
17 *
18 * Uses PdfFileWriter (full rewrite) since page tree restructuring
19 * cannot be done incrementally.
20 *
21 * Usage:
22 *   PageSlicer::open('large.pdf')
23 *       ->keepRange(1, 5)
24 *       ->save('first-five.pdf');
25 *
26 *   PageSlicer::open('report.pdf')
27 *       ->reorder(3, 1, 2)
28 *       ->save('reordered.pdf');
29 *
30 * @api
31 */
32final class PageSlicer
33{
34    private string $originalBytes;
35
36    /** @var ?list<int> 0-based page indices to output (null = not set yet) */
37    private ?array $selectedIndices = null;
38
39    /** @var list<string> */
40    private array $lastVersionWarnings = [];
41
42    private function __construct(
43        private readonly PdfReader $reader,
44        string $originalBytes,
45    ) {
46        $this->originalBytes = $originalBytes;
47    }
48
49    public static function open(string $path, string $password = ''): self
50    {
51        $bytes = LocalFilesystem::readFile($path);
52        return new self(PdfReader::fromString($bytes, $password), $bytes);
53    }
54
55    public static function openString(string $pdfBytes, string $password = ''): self
56    {
57        return new self(PdfReader::fromString($pdfBytes, $password), $pdfBytes);
58    }
59
60    // -----------------------------------------------------------------------
61    // Extract
62    // -----------------------------------------------------------------------
63
64    public function keep(PageSelector $pages): self
65    {
66        $this->selectedIndices = $pages->resolve($this->reader->getPageCount());
67        return $this;
68    }
69
70    public function keepPages(int ...$pageNumbers): self
71    {
72        return $this->keep(PageSelector::pages(...$pageNumbers));
73    }
74
75    public function keepRange(int $from, int $to): self
76    {
77        return $this->keep(PageSelector::range($from, $to));
78    }
79
80    // -----------------------------------------------------------------------
81    // Remove
82    // -----------------------------------------------------------------------
83
84    public function remove(PageSelector $pages): self
85    {
86        $total = $this->reader->getPageCount();
87        $removeIndices = $pages->resolve($total);
88        $this->selectedIndices = array_values(array_diff(range(0, $total - 1), $removeIndices));
89        return $this;
90    }
91
92    public function removePages(int ...$pageNumbers): self
93    {
94        return $this->remove(PageSelector::pages(...$pageNumbers));
95    }
96
97    // -----------------------------------------------------------------------
98    // Reorder
99    // -----------------------------------------------------------------------
100
101    /**
102     * Reorder pages. Arguments are 1-based page numbers in desired order.
103     */
104    public function reorder(int ...$pageOrder): self
105    {
106        $this->selectedIndices = array_map(fn(int $n) => $n - 1, $pageOrder);
107        return $this;
108    }
109
110    public function reverse(): self
111    {
112        $total = $this->reader->getPageCount();
113        $this->selectedIndices = array_reverse(range(0, $total - 1));
114        return $this;
115    }
116
117    // -----------------------------------------------------------------------
118    // Split
119    // -----------------------------------------------------------------------
120
121    /**
122     * Split the PDF at a given page number.
123     *
124     * @param int $atPage 1-based page number where the split occurs.
125     *                     Pages 1..(atPage-1) go to first result,
126     *                     pages atPage..end go to second result.
127     * @return array{string, string} Two PDF byte strings
128     */
129    public function split(int $atPage): array
130    {
131        $total = $this->reader->getPageCount();
132        $first = clone $this;
133        $first->selectedIndices = range(0, $atPage - 2);
134        $second = clone $this;
135        $second->selectedIndices = range($atPage - 1, $total - 1);
136        return [$first->toBytes(), $second->toBytes()];
137    }
138
139    // -----------------------------------------------------------------------
140    // Output
141    // -----------------------------------------------------------------------
142
143    public function save(string $path): void
144    {
145        LocalFilesystem::writeFile($path, $this->toBytes(), createDirectories: true);
146    }
147
148    public function toBytes(): string
149    {
150        $indices = $this->selectedIndices ?? range(0, $this->reader->getPageCount() - 1);
151
152        $fw = new PdfFileWriter();
153        $catalog = new Catalog();
154        $fw->setCatalog($catalog);
155
156        $pageTree = new PageTree();
157        $fw->register($pageTree);
158        $catalog->pages = new PdfReference($pageTree->objectNumber);
159
160        $copier = new PageCopier($this->reader, $fw);
161        $pageRefs = $copier->copyPages($indices, new PdfReference($pageTree->objectNumber));
162
163        $pageTree->kids = $pageRefs;
164        $pageTree->count = count($pageRefs);
165
166        $result = $fw->generate();
167        $this->lastVersionWarnings = $fw->getVersionWarnings();
168        return $result;
169    }
170
171    // -----------------------------------------------------------------------
172    // Info
173    // -----------------------------------------------------------------------
174
175    /** @return list<string> */
176    public function getVersionWarnings(): array
177    {
178        return $this->lastVersionWarnings;
179    }
180
181    public function getPageCount(): int
182    {
183        return $this->reader->getPageCount();
184    }
185
186    public function getReader(): PdfReader
187    {
188        return $this->reader;
189    }
190}