Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.62% covered (success)
97.62%
41 / 42
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PdfMerger
97.62% covered (success)
97.62%
41 / 42
90.00% covered (success)
90.00%
9 / 10
15
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
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addPages
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSourceCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTotalPageCount
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 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%
23 / 23
100.00% covered (success)
100.00%
1 / 1
4
 getVersionWarnings
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 * Combine multiple PDFs into one document.
17 *
18 * Usage:
19 *   PdfMerger::create()
20 *       ->addFile('chapter1.pdf')
21 *       ->addFile('chapter2.pdf')
22 *       ->save('book.pdf');
23 *
24 * @api
25 */
26final class PdfMerger
27{
28    /** @var list<array{reader: PdfReader, pages: ?PageSelector}> */
29    private array $sources = [];
30
31    /** @var list<string> */
32    private array $lastVersionWarnings = [];
33
34    private function __construct() {}
35
36    public static function create(): self
37    {
38        return new self();
39    }
40
41    // -----------------------------------------------------------------------
42    // Add sources
43    // -----------------------------------------------------------------------
44
45    public function addFile(string $path, string $password = ''): self
46    {
47        $bytes = LocalFilesystem::readFile($path);
48        $this->sources[] = ['reader' => PdfReader::fromString($bytes, $password), 'pages' => null];
49        return $this;
50    }
51
52    public function addString(string $pdfBytes, string $password = ''): self
53    {
54        $this->sources[] = ['reader' => PdfReader::fromString($pdfBytes, $password), 'pages' => null];
55        return $this;
56    }
57
58    public function addPages(string $path, PageSelector $pages, string $password = ''): self
59    {
60        $bytes = LocalFilesystem::readFile($path);
61        $this->sources[] = ['reader' => PdfReader::fromString($bytes, $password), 'pages' => $pages];
62        return $this;
63    }
64
65    // -----------------------------------------------------------------------
66    // Info
67    // -----------------------------------------------------------------------
68
69    public function getSourceCount(): int
70    {
71        return count($this->sources);
72    }
73
74    public function getTotalPageCount(): int
75    {
76        $total = 0;
77        foreach ($this->sources as $source) {
78            if ($source['pages'] !== null) {
79                $total += count($source['pages']->resolve($source['reader']->getPageCount()));
80            } else {
81                $total += $source['reader']->getPageCount();
82            }
83        }
84        return $total;
85    }
86
87    // -----------------------------------------------------------------------
88    // Output
89    // -----------------------------------------------------------------------
90
91    public function save(string $path): void
92    {
93        LocalFilesystem::writeFile($path, $this->toBytes(), createDirectories: true);
94    }
95
96    /**
97     * Build the merged PDF.
98     *
99     * Creates a new PdfFileWriter with a fresh page tree, then uses PageCopier
100     * for each source to deep-copy pages and their dependent objects (fonts,
101     * images, ExtGState, etc.) with reference remapping so cross-document
102     * object numbers do not collide.
103     */
104    public function toBytes(): string
105    {
106        if (empty($this->sources)) {
107            throw new \RuntimeException('No source PDFs added');
108        }
109
110        $fw = new PdfFileWriter();
111        $catalog = new Catalog();
112        $fw->setCatalog($catalog);
113
114        $pageTree = new PageTree();
115        $fw->register($pageTree);
116        $catalog->pages = new PdfReference($pageTree->objectNumber);
117
118        $allPageRefs = [];
119
120        foreach ($this->sources as $source) {
121            $reader = $source['reader'];
122            $pageCount = $reader->getPageCount();
123
124            if ($source['pages'] !== null) {
125                $indices = $source['pages']->resolve($pageCount);
126            } else {
127                $indices = range(0, $pageCount - 1);
128            }
129
130            $copier = new PageCopier($reader, $fw);
131            $pageRefs = $copier->copyPages($indices, new PdfReference($pageTree->objectNumber));
132            $allPageRefs = array_merge($allPageRefs, $pageRefs);
133        }
134
135        $pageTree->kids = $allPageRefs;
136        $pageTree->count = count($allPageRefs);
137
138        $result = $fw->generate();
139        $this->lastVersionWarnings = $fw->getVersionWarnings();
140        return $result;
141    }
142
143    /** @return list<string> */
144    public function getVersionWarnings(): array
145    {
146        return $this->lastVersionWarnings;
147    }
148}