Lewati ke konten
getnextpdf.com

Memecah PDF dan mengekstrak rentang halaman

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.

Terminal window
composer require nextpdf/core:^3

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.

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): SplitResult menghasilkan satu dokumen keluaran per PageRange di $ranges, secara berurutan. Kedua parameter pembatas membatasi ukuran input dan jumlah rentang.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult memotong dokumen menjadi segmen berukuran tetap masing-masing $pagesPerSegment halaman; segmen terakhir menampung sisanya.
  • extractPages(string $pdfData, PageRange $range): SplitDocument mengekstrak 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 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());

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.
  • 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 memunculkan PageLayoutException dari constructor PageRange itu sendiri.
  • Sebuah rentang melewati akhir adalah galat, bukan clamp. Jika end sebuah rentang melebihi jumlah halaman sumber, split() memunculkan PageLayoutException; 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. $pagesPerSegment harus minimal 1, jika tidak Anda mendapatkan InvalidArgumentException.
  • List rentang kosong ditolak. split() dengan $ranges === [] memunculkan InvalidArgumentException. Bangun setidaknya satu rentang sebelum memanggilnya.
  • Batas memunculkan galat alih-alih memangkas. Melampaui maxBytes atau maxRanges memunculkan InvalidArgumentException. 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.
  • UnsupportedSourceDocumentException berada di bawah namespace Merge. Nama fully-qualified-nya adalah NextPDF\Document\Merge\UnsupportedSourceDocumentException. Path Merge itu 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.

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.

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. maxBytes dan maxRanges adalah 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.

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.