Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
42 / 42 |
|
100.00% |
16 / 16 |
CRAP | |
100.00% |
1 / 1 |
| PageSlicer | |
100.00% |
42 / 42 |
|
100.00% |
16 / 16 |
16 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| open | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| openString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| keep | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| keepPages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| keepRange | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| remove | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| removePages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| reorder | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| reverse | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| split | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| save | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| toBytes | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| getVersionWarnings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getPageCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getReader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Phpdftk\Pdf\Toolkit; |
| 6 | |
| 7 | use Phpdftk\Pdf\Core\Document\Catalog; |
| 8 | use Phpdftk\Pdf\Core\Document\PageTree; |
| 9 | use Phpdftk\Filesystem\LocalFilesystem; |
| 10 | use Phpdftk\Pdf\Core\File\PdfFileWriter; |
| 11 | use Phpdftk\Pdf\Core\PdfReference; |
| 12 | use Phpdftk\Pdf\Reader\PdfReader; |
| 13 | use 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 | */ |
| 32 | final 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 | } |