Memecah PDF dan mengekstrak rentang halaman
Sekilas pandang
Bagian berjudul “Sekilas pandang”Anda punya satu PDF, dan Anda membutuhkan beberapa. Resep ini memotong satu
dokumen menjadi beberapa berkas dengan permukaan split Core,
NextPDF\Document\PdfSplitter. Anda meneruskan sumber sebagai string byte PDF
mentah dan menjelaskan halaman mana yang Anda inginkan. Splitter mengurai sumber
melalui object graph, menyalin object yang terjangkau untuk setiap rentang yang
diminta ke dalam dokumen baru yang dinomori ulang dengan page tree dan tabel
cross-reference-nya sendiri, lalu mengembalikan PDF yang lengkap secara struktural
yang dapat dimuat di pembaca yang sesuai.
Ini adalah kebalikan dari resep merge: merge menyusun banyak dokumen menjadi satu, split menguraikan satu dokumen menjadi banyak. Permukaan yang sama mencakup tiga tugas yang paling sering Anda butuhkan:
- Pecah berdasarkan rentang — hasilkan satu dokumen keluaran per rentang halaman yang Anda namai.
- Pecah setiap N halaman — potong berkas panjang menjadi segmen berukuran tetap.
- Ekstrak sebuah rentang — tarik satu rentang halaman yang berurutan menjadi satu dokumen.
Split berjalan dalam proses, tanpa headless browser atau panggilan jaringan.
Anda memerlukan Core terpasang (composer require nextpdf/core:^3) dan satu PDF
yang dapat dibaca.
Pemasangan
Bagian berjudul “Pemasangan”composer require nextpdf/core:^3Ikhtisar konseptual
Bagian berjudul “Ikhtisar konseptual”Sebuah PDF menemukan halamannya melalui sebuah page tree yang berakar pada node
/Pages, dan ia menjangkau setiap indirect object melalui data
cross-reference-nya (sebuah tabel atau sebuah stream). Anda tidak dapat
mengekstrak halaman dengan memotong byte: sebuah halaman tunggal mereferensikan
font, gambar, dan resource dictionary bersama yang berada di tempat lain dalam
berkas, dan offset cross-reference-nya tidak akan valid lagi.
PdfSplitter melakukan pekerjaan sebenarnya. Untuk setiap rentang, ia menelusuri
object graph dari object halaman yang diminta, mengumpulkan closure object yang
terjangkau, menomori ulang object-object tersebut ke ruang alamat baru, membangun
ulang sebuah dokumen ber-page-tree tunggal, dan memancarkan tabel cross-reference
nyata sesuai struktur PDF 2.0 (ISO 32000-2:2020, tabel cross-reference §7.5.4,
page tree §7.7.3). Setiap keluaran adalah dokumen yang berdiri sendiri, bukan
fragmen.
Nomor halaman berbasis 1 dan inklusif. Sebuah rentang adalah value object
NextPDF\Document\PageRange: new PageRange(2, 5) berarti halaman 2 hingga 5.
Constructor memvalidasi invarian-nya sendiri — ia menolak start di bawah 1 atau
end sebelum start dengan memunculkan NextPDF\Exception\PageLayoutException —
sehingga rentang yang mustahil gagal pada saat konstruksi, bukan jauh di dalam
splitter. PageRange::parse() dan PageRange::all() memunculkan
PageLayoutException yang sama pada spesifikasi yang cacat atau total halaman
yang non-positif.
Permukaan API
Bagian berjudul “Permukaan API”new NextPDF\Document\PdfSplitter() mengekspos tiga metode. Semuanya mengambil
sumber sebagai string byte PDF mentah, tidak pernah sebuah path.
split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResultmenghasilkan satu dokumen keluaran perPageRangedi$ranges, secara berurutan. Kedua parameter pembatas membatasi ukuran input dan jumlah rentang.splitEvery(string $pdfData, int $pagesPerSegment): SplitResultmemotong dokumen menjadi segmen berukuran tetap masing-masing$pagesPerSegmenthalaman; segmen terakhir menampung sisanya.extractPages(string $pdfData, PageRange $range): SplitDocumentmengekstrak satu rentang dan mengembalikan dokumen itu secara langsung.
split() dan splitEvery() mengembalikan sebuah
NextPDF\Document\SplitResult, sebuah object readonly yang membawa
$documents (sebuah list segmen), $totalPages (halaman pada sumber), dan
$sourceSize. Ia menawarkan count(), document(int $index) untuk mengambil
sebuah segmen berdasarkan indeks berbasis nol, dan totalOutputSize().
Setiap segmen, dan nilai kembalian dari extractPages(), adalah sebuah
NextPDF\Document\SplitDocument: sebuah object readonly yang mengekspos
$pdfData (byte segmen), $range, $pageCount, $sizeBytes, dan helper
isValid(). isValid() adalah pemeriksaan kewarasan header %PDF yang sempit —
ia mengembalikan true ketika byte segmen diawali dengan %PDF — bukan validasi
struktur dokumen atau konformitas; ia mengonfirmasi bahwa splitter menghasilkan
sebuah PDF, bukan bahwa berkas sepenuhnya sesuai.
Anda membangun sebuah PageRange secara langsung dengan
new PageRange($start, $end), atau mengurai sebuah spesifikasi yang mudah dibaca
manusia dengan PageRange::parse('1-3,5,7-10'), yang mengembalikan sebuah
list<PageRange> yang siap diteruskan ke split().
PageRange::all($totalPages) mengembalikan satu rentang yang mencakup seluruh
dokumen.
Contoh kode — Mulai cepat
Bagian berjudul “Contoh kode — Mulai cepat”Contoh ini membaca satu berkas dan memecahnya menjadi dua dokumen: halaman 1 hingga 3, dan halaman 4 hingga 6. Contoh ini menghilangkan penanganan galat untuk menunjukkan bentuk pemanggilan; contoh produksi di bawah menambahkan guard selengkapnya.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->split( file_get_contents(__DIR__ . '/report.pdf'), [ new PageRange(1, 3), new PageRange(4, 6), ],);
foreach ($result->documents as $i => $segment) { file_put_contents(__DIR__ . sprintf('/part-%d.pdf', $i + 1), $segment->pdfData);}
printf("Split %d-page source into %d document(s).\n", $result->totalPages, $result->count());Contoh kode — Produksi
Bagian berjudul “Contoh kode — Produksi”Program mandiri ini membangun satu dokumen multi-halaman kecil di memori,
sehingga ia berjalan tanpa berkas eksternal. Program ini mendemonstrasikan
ketiga operasi — pecah berdasarkan rentang, pecah setiap N halaman, dan ekstrak
satu rentang. Program ini memvalidasi dan menulis segmen per-rentang serta ekor
yang diekstrak, dan melaporkan hasil per-ukuran sebagai sebuah jumlah, sehingga
Anda melihat setiap bentuk pemanggilan tanpa tiga loop penulisan yang hampir
identik. Program ini menangkap exception yang dimunculkan oleh permukaan split
dan melempar ulang masing-masing dengan konteks alih-alih menelannya. Ganti
sumber in-memory dengan pembacaan file_get_contents() milik Anda sendiri atau
pengambilan object-storage.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use InvalidArgumentException;use NextPDF\Core\Document;use NextPDF\Document\Merge\UnsupportedSourceDocumentException;use NextPDF\Document\PageRange;use NextPDF\Document\PdfSplitter;use NextPDF\Document\SplitDocument;use NextPDF\Exception\PageLayoutException;
/** * Build a tiny labelled multi-page PDF so the program is self-contained. * * In your own code, replace this with a read of the PDF you want to split, * for example file_get_contents($path). */function buildSample(int $pages): string{ $doc = Document::createStandalone(); $doc->setTitle('Split sample');
for ($page = 1; $page <= $pages; $page++) { $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, sprintf('Source page %d', $page), newLine: true); }
return $doc->getPdfData();}
$source = buildSample(7);
$splitter = new PdfSplitter();
try { // 1. Split into named ranges: one output per PageRange, in order. $byRange = $splitter->split( $source, PageRange::parse('1-3,4-6'), maxBytes: 50_000_000, maxRanges: 100, );
// 2. Split every 2 pages: segments of [1-2], [3-4], [5-6], [7] (remainder). $bySize = $splitter->splitEvery($source, 2);
// 3. Extract a single range as one document. $tail = $splitter->extractPages($source, new PageRange(7, 7));} catch (InvalidArgumentException $e) { // Raised on an oversized input, an empty range list, or too many ranges. throw new RuntimeException('Split rejected its input: ' . $e->getMessage(), previous: $e);} catch (PageLayoutException $e) { // Raised when a range exceeds the source page count, and also by the // PageRange constructor / PageRange::parse() on an invalid or malformed range. throw new RuntimeException( sprintf('Range out of bounds (page %d): %s', $e->getPageNumber(), $e->getConstraint()), previous: $e, );} catch (UnsupportedSourceDocumentException $e) { // Raised fail-closed on an encrypted, signed, or form-bearing source. throw new RuntimeException('Source cannot be split: ' . $e->getMessage(), previous: $e);}
printf( "Source has %d page(s). By-range produced %d doc(s); by-size produced %d doc(s).\n", $byRange->totalPages, $byRange->count(), $bySize->count(),);
foreach ($byRange->documents as $i => $segment) { emitSegment(sprintf('range-%d', $i + 1), $segment);}
emitSegment('tail', $tail);
/** * Validate a segment and write it to the cookbook side-channel directory, * or to the script directory by default. */function emitSegment(string $name, SplitDocument $segment): void{ if (!$segment->isValid()) { throw new RuntimeException(sprintf('Segment "%s" failed its %%PDF header check.', $name)); }
$dir = getenv('NEXTPDF_COOKBOOK_OUTPUT'); $dir = $dir !== false && $dir !== '' ? $dir : __DIR__; $path = sprintf('%s/%s.pdf', rtrim($dir, '/'), $name);
if (file_put_contents($path, $segment->pdfData) === false) { throw new RuntimeException(sprintf('Could not write segment to "%s".', $path)); }
printf("Wrote %s: pages %d-%d, %d bytes.\n", $name, $segment->range->start, $segment->range->end, $segment->sizeBytes);}Keluaran standar yang diharapkan (ukuran byte bergantung pada build):
Source has 7 page(s). By-range produced 2 doc(s); by-size produced 4 doc(s).Wrote range-1: pages 1-3, <n> bytes.Wrote range-2: pages 4-6, <n> bytes.Wrote tail: pages 7-7, <n> bytes.Kasus tepi & jebakan
Bagian berjudul “Kasus tepi & jebakan”- Sumber adalah byte, bukan path. Setiap metode mengambil string PDF mentah.
Baca berkas dengan
file_get_contents()terlebih dahulu, atau ambil byte dari object storage. Meneruskan sebuah path membuat sumber gagal diurai. - Nomor halaman berbasis 1 dan inklusif.
new PageRange(1, 3)mencakup halaman 1, 2, dan 3 — tiga halaman. Sebuah start di bawah 1 atau end sebelum start memunculkanPageLayoutExceptiondari constructorPageRangeitu sendiri. - Sebuah rentang melewati akhir adalah galat, bukan clamp. Jika end sebuah
rentang melebihi jumlah halaman sumber,
split()memunculkanPageLayoutException; ia tidak pernah memangkas rentang secara diam-diam ke halaman terakhir. Periksa jumlah halaman terlebih dahulu jika rentang Anda berasal dari pemanggil. splitEvery()mempertahankan sisanya. Segmen terakhir menampung halaman apa pun yang tersisa, sehingga dokumen 7 halaman yang dipecah setiap 2 halaman menghasilkan empat segmen: tiga berisi 2 halaman dan satu berisi 1 halaman.$pagesPerSegmentharus minimal 1, jika tidak Anda mendapatkanInvalidArgumentException.- List rentang kosong ditolak.
split()dengan$ranges === []memunculkanInvalidArgumentException. Bangun setidaknya satu rentang sebelum memanggilnya. - Batas memunculkan galat alih-alih memangkas. Melampaui
maxBytesataumaxRangesmemunculkanInvalidArgumentException. Splitter tidak pernah memproses sebagian input yang berukuran berlebih, jadi setel kedua batas untuk beban kerja Anda. - Sumber terenkripsi, ditandatangani, dan membawa form gagal tertutup.
Sumber terenkripsi (tidak dapat disalin tanpa kunci), sumber yang
ditandatangani secara digital (penomoran ulang halaman akan membatalkan byte
range tanda tangan), atau sumber yang membawa form interaktif (widget sebuah
field mungkin berada di halaman yang dibuang dan menjadi yatim) memunculkan
UnsupportedSourceDocumentException. Splitter menolak alih-alih memancarkan dokumen yang rusak atau terkompromi. Memecah dokumen form adalah keterbatasan yang diketahui pada rilis ini. UnsupportedSourceDocumentExceptionberada di bawah namespaceMerge. Nama fully-qualified-nya adalahNextPDF\Document\Merge\UnsupportedSourceDocumentException. PathMergeitu pada halaman split bukan kesalahan salin/tempel: ia adalah satu-satunya exception penolakan source-document yang dipakai bersama, yang dimunculkan baik oleh permukaan merge maupun split ketika sebuah sumber tidak dapat disalin secara aman. Impor dari namespace tersebut.- Keluaran segar secara struktural, bukan stabil secara byte. Setiap segmen
adalah dokumen baru dengan catalog, page tree, dan trailer-nya sendiri. Dua
proses atas input yang sama setara secara struktural, tetapi tidak dijamin
identik secara byte — karena itulah profil reproducibility
structural.
Performa
Bagian berjudul “Performa”Pemecahan bersifat linear terhadap jumlah halaman yang disalin di seluruh
rentang. Mengurai sumber dan menyalin closure object setiap rentang, bukan
pembukuan splitter itu sendiri, mendominasi pekerjaan. Sumber ditahan di memori
sebagai sebuah string, dan byte setiap segmen ditahan hingga Anda menulisnya,
sehingga memori puncak mengikuti ukuran sumber ditambah rentang terbesar yang
Anda hasilkan. Guard maxBytes menjaga sisi sumber dari puncak itu tetap
terbatas. Untuk pipeline bervolume tinggi, setel maxBytes dan maxRanges ke
nilai terkecil yang dibutuhkan beban kerja Anda, sehingga input yang cacat atau
berukuran berlebih gagal cepat alih-alih menghabiskan memori.
Catatan keamanan
Bagian berjudul “Catatan keamanan”Split berjalan dalam proses; tidak ada byte dokumen yang meninggalkan host, dan tidak ada panggilan jaringan yang dilakukan. Perlakukan setiap PDF sumber sebagai input yang tidak tepercaya:
- Jaga batas tetap ketat.
maxBytesdanmaxRangesadalah lini pertahanan pertama Anda terhadap input denial-of-service. Untuk permukaan apa pun yang menerima unggahan, setel keduanya ke plafon nyata Anda, bukan default yang longgar. - Lakukan triase sebelum memecah. Sumber yang terenkripsi atau ditandatangani gagal tertutup, tetapi Anda dapat mendeteksi kondisi tersebut lebih awal. Jalankan input yang tidak tepercaya melalui inspector Core terlebih dahulu. Lihat Mengurai dan memeriksa PDF untuk pemindaian terbatas yang menandai enkripsi, tanda tangan, dan penanda risiko sebelum pemrosesan yang lebih berat.
- Jangan pernah menyisipkan input pengguna ke dalam path. Resep ini menulis ke direktori tetap atau side-channel cookbook. Turunkan path keluaran dan nama segmen dari nilai yang dikendalikan server, tidak pernah dari sebuah field permintaan, untuk menghindari path traversal.
- Tidak ada rahasia di keluaran. Jangan menulis berkas segmen ke lokasi, atau dengan nama, yang mengekspos pengenal internal ke klien yang seharusnya tidak melihatnya.
Konformitas
Bagian berjudul “Konformitas”Resep ini tidak membuat klaim standar normatif tersendiri. Ia menguraikan satu
dokumen melalui permukaan split Core dan memeriksa kewarasan setiap segmen dengan
pemeriksaan header %PDF SplitDocument::isValid() — sebuah pemeriksaan
keberadaan bahwa splitter memancarkan sebuah PDF, bukan validasi konformitas atau
struktur dokumen. Struktur page-tree dan cross-reference yang dibangun ulang oleh
PdfSplitter untuk setiap segmen adalah struktur PDF 2.0 yang dijelaskan dalam
referensi /modules/core/document/ (ISO 32000-2:2020, tabel cross-reference
§7.5.4, page tree §7.7.3). Untuk pembacaan struktural atas dokumen input atau
keluaran mana pun, termasuk versi, jumlah halaman, enkripsi, dan flag tanda
tangan, gunakan inspector Core yang didokumentasikan dalam
Mengurai dan memeriksa PDF.
Lihat juga
Bagian berjudul “Lihat juga”- Referensi modul Document — permukaan split, merge, dan document-part selengkapnya.
- Menggabungkan PDF eksternal — resep kebalikannya: menyusun banyak dokumen menjadi satu.
- Mengurai dan memeriksa PDF — lakukan triase input yang tidak tepercaya sebelum Anda memecahnya.
- Penanganan galat yang sadar exception
— hierarki exception NextPDF di balik
PageLayoutExceptiondanUnsupportedSourceDocumentException.