Ga naar inhoud
getnextpdf.com

Een PDF splitsen en paginabereiken extraheren

Je hebt één PDF en je hebt er meerdere nodig. Dit recipe snijdt één document in meerdere bestanden met de split-interface van Core, NextPDF\Document\PdfSplitter. Je geeft de bron door als een ruwe PDF-bytereeks en beschrijft welke pagina’s je wilt. De splitter parset de bron via de objectgraaf, kopieert de bereikbare objecten van elk aangevraagd bereik naar een verse, hernummerde document met zijn eigen paginaboom en kruisverwijzingstabel, en levert structureel complete PDF’s terug die laden in een conforme lezer.

Dit is de inverse van het samenvoeg-recipe: samenvoegen stelt veel documenten samen tot één, splitsen ontleedt één document in vele. Dezelfde interface dekt de drie taken die je het vaakst nodig hebt:

  • Splitsen op bereiken — produceer één uitvoerdocument per paginabereik dat je benoemt.
  • Splitsen per N pagina’s — knip een lang bestand in segmenten van vaste grootte.
  • Een bereik extraheren — trek één aaneengesloten paginabereik in één document.

Het splitsen verloopt in-process, zonder headless browser of netwerkaanroep. Je hebt Core geïnstalleerd nodig (composer require nextpdf/core:^3) en één leesbare PDF.

Terminal window
composer require nextpdf/core:^3

Een PDF lokaliseert zijn pagina’s via een paginaboom met als wortel een /Pages-knooppunt, en bereikt elk indirect object via zijn kruisverwijzingsgegevens (een tabel of een stream). Je kunt pagina’s niet extraheren door bytes te snijden: één pagina verwijst naar gedeelde lettertypen, afbeeldingen en resource-dictionaries die elders in het bestand leven, en de kruisverwijzings-offsets zouden niet langer geldig zijn.

PdfSplitter doet het echte werk. Voor elk bereik loopt het de objectgraaf af vanaf de aangevraagde pagina-objecten, verzamelt het de bereikbare objectsluiting, hernummert het die objecten naar een verse adresruimte, herbouwt het een document met één paginaboom, en zendt het een echte kruisverwijzingstabel uit volgens de PDF 2.0-structuur (ISO 32000-2:2020, kruisverwijzingstabel §7.5.4, paginaboom §7.7.3). Elke uitvoer is een op zichzelf staand document, geen fragment.

Paginanummers zijn 1-gebaseerd en inclusief. Een bereik is een NextPDF\Document\PageRange-waardeobject: new PageRange(2, 5) betekent de pagina’s 2 tot en met 5. De constructor valideert zijn eigen invarianten — het weigert een start onder 1 of een einde vóór de start door een NextPDF\Exception\PageLayoutException op te werpen — zodat een onmogelijk bereik faalt bij constructie, niet diep in de splitter. PageRange::parse() en PageRange::all() werpen dezelfde PageLayoutException op bij een misvormde specificatie of een niet-positief paginatotaal.

new NextPDF\Document\PdfSplitter() stelt drie methoden beschikbaar. Alle nemen de bron als een ruwe PDF-bytereeks, nooit als een pad.

  • split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult produceert één uitvoerdocument per PageRange in $ranges, op volgorde. De twee begrenzende parameters limiteren de invoergrootte en het aantal bereiken.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult knipt het document in segmenten van vaste grootte van elk $pagesPerSegment pagina’s; het laatste segment bevat de rest.
  • extractPages(string $pdfData, PageRange $range): SplitDocument extraheert één bereik en retourneert dat ene document direct.

split() en splitEvery() retourneren een NextPDF\Document\SplitResult, een readonly-object dat $documents draagt (een lijst met segmenten), $totalPages (pagina’s in de bron) en $sourceSize. Het biedt count(), document(int $index) om een segment op te halen via een nul-gebaseerde index, en totalOutputSize().

Elk segment, en de retourwaarde van extractPages(), is een NextPDF\Document\SplitDocument: een readonly-object dat $pdfData (de segmentbytes), $range, $pageCount, $sizeBytes en de hulpmethode isValid() blootlegt. isValid() is een nauwe %PDF-header-sanity-check — het retourneert true wanneer de segmentbytes beginnen met %PDF — geen validatie van documentstructuur of conformiteit; het bevestigt dat de splitter een PDF heeft geproduceerd, niet dat het bestand volledig conform is.

Je bouwt een PageRange direct met new PageRange($start, $end), of parset een mensleesbare specificatie met PageRange::parse('1-3,5,7-10'), die een list<PageRange> retourneert die klaar is om aan split() door te geven. PageRange::all($totalPages) retourneert één bereik dat het hele document dekt.

Dit voorbeeld leest één bestand in en splitst het in twee documenten: de pagina’s 1 tot en met 3, en de pagina’s 4 tot en met 6. Het laat foutafhandeling weg om de aanroepvorm te tonen; het productievoorbeeld hieronder voegt de volledige beveiligingen toe.

<?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());

Dit op zichzelf staande programma bouwt één klein document met meerdere pagina’s in het geheugen, zodat het draait zonder een extern bestand. Het demonstreert alle drie de operaties — splitsen op bereiken, splitsen per N pagina’s en één bereik extraheren. Het valideert en schrijft de segmenten per bereik en de geëxtraheerde staart weg, en rapporteert het resultaat per grootte als een telling, zodat je elke aanroepvorm ziet zonder drie vrijwel identieke schrijflussen. Het vangt de excepties die de split-interface opwerpt en gooit elke opnieuw met context in plaats van die te slikken. Vervang de in-memory bron door je eigen file_get_contents()-lezing of ophaalactie uit objectopslag.

<?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);
}

Verwachte standaarduitvoer (bytegroottes hangen af van de 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.
  • De bron is bytes, geen pad. Elke methode neemt een ruwe PDF-string. Lees het bestand eerst in met file_get_contents(), of haal de bytes uit objectopslag. Het doorgeven van een pad zorgt dat de bron niet parset.
  • Paginanummers zijn 1-gebaseerd en inclusief. new PageRange(1, 3) dekt de pagina’s 1, 2 en 3 — drie pagina’s. Een start onder 1 of een einde vóór de start werpt PageLayoutException op vanuit de PageRange-constructor zelf.
  • Een bereik voorbij het einde is een fout, geen clamp. Als het einde van een bereik het paginaaantal van de bron overschrijdt, werpt split() PageLayoutException op; het trimt het bereik nooit stilzwijgend naar de laatste pagina. Inspecteer het paginaaantal eerst als je bereiken door de aanroeper worden geleverd.
  • splitEvery() behoudt de rest. Het laatste segment bevat welke pagina’s er ook overblijven, dus een document van 7 pagina’s dat elke 2 pagina’s wordt gesplitst, levert vier segmenten op: drie van 2 pagina’s en één van 1. $pagesPerSegment moet ten minste 1 zijn, anders krijg je een InvalidArgumentException.
  • Een lege bereiklijst wordt geweigerd. split() met $ranges === [] werpt InvalidArgumentException op. Bouw ten minste één bereik voordat je het aanroept.
  • Grenzen werpen op in plaats van af te kappen. Het overschrijden van maxBytes of maxRanges werpt InvalidArgumentException op. De splitter verwerkt een te grote invoer nooit gedeeltelijk, dus stem beide grenzen af op je werklast.
  • Versleutelde, ondertekende en formulierdragende bronnen falen gesloten. Een versleutelde bron (die kan niet worden gekopieerd zonder de sleutel), een digitaal ondertekende bron (herpagineren zou de byterange van de handtekening ongeldig maken), of een bron die een interactief formulier draagt (de widgets van een veld kunnen op weggelaten pagina’s staan en verweesd raken) werpen UnsupportedSourceDocumentException op. De splitter weigert liever dan een beschadigd of gecompromitteerd document uit te zenden. Een formulierdocument splitsen is een bekende beperking van deze release.
  • UnsupportedSourceDocumentException leeft onder de Merge-namespace. De volledig gekwalificeerde naam is NextPDF\Document\Merge\UnsupportedSourceDocumentException. Dat Merge-pad op een split-pagina is geen kopieer/plak-fout: het is de enige, gedeelde exceptie voor brondocumentweigering die zowel de merge- als de split-interface opwerpen wanneer een bron niet veilig kan worden gekopieerd. Importeer hem uit die namespace.
  • Uitvoer is structureel vers, niet byte-stabiel. Elk segment is een nieuw document met zijn eigen catalogus, paginaboom en trailer. Twee runs over dezelfde invoer zijn structureel gelijk, maar niet gegarandeerd byte-identiek — vandaar het reproduceerbaarheidsprofiel structural.

Splitsen is lineair in het aantal pagina’s dat over alle bereiken wordt gekopieerd. Het parsen van de bron en het kopiëren van de objectsluiting van elk bereik, niet de eigen administratie van de splitter, domineren het werk. De bron wordt als een string in het geheugen gehouden, en de bytes van elk segment worden vastgehouden totdat je ze wegschrijft, dus de piekgeheugengebruik volgt de brongrootte plus het grootste bereik dat je produceert. De maxBytes-guard houdt de bronkant van die piek begrensd. Stel voor pijplijnen met hoog volume maxBytes en maxRanges in op de kleinste waarden die je werklast nodig heeft, zodat een misvormde of te grote invoer snel faalt in plaats van het geheugen uit te putten.

Het splitsen verloopt in-process; geen documentbytes verlaten de host, en er wordt geen netwerkaanroep gedaan. Behandel elke bron-PDF als niet-vertrouwde invoer:

  • Houd de grenzen strak. maxBytes en maxRanges zijn je eerste verdedigingslinie tegen denial-of-service-invoer. Stel ze voor elke interface die uploads accepteert in op je echte plafond, niet op de royale standaardwaarden.
  • Triage voordat je splitst. Een bron die versleuteld of ondertekend is, faalt gesloten, maar je kunt die condities eerder detecteren. Voer niet-vertrouwde invoer eerst door de Core-inspector. Zie Een PDF parsen en inspecteren voor een begrensde scan die encryptie, handtekeningen en risicomarkeringen markeert vóór zwaardere verwerking.
  • Interpoleer nooit gebruikersinvoer in een pad. Dit recipe schrijft naar een vaste map of het cookbook-zijkanaal. Leid uitvoerpaden en segmentnamen af uit servergecontroleerde waarden, nooit uit een requestveld, om path traversal te voorkomen.
  • Geen secrets in de uitvoer. Schrijf geen segmentbestanden naar een locatie, of met een naam, die interne identifiers blootstelt aan een client die ze niet zou mogen zien.

Dit recipe doet geen eigen normatieve standaardenclaim. Het ontleedt één document via de split-interface van Core en sanity-checkt elk segment met de %PDF-header-check van SplitDocument::isValid() — een aanwezigheidscheck dat de splitter een PDF heeft uitgezonden, geen validatie van conformiteit of documentstructuur. De paginaboom- en kruisverwijzingsstructuren die PdfSplitter voor elk segment herbouwt, zijn de PDF 2.0-structuren die zijn beschreven in de referentie /modules/core/document/ (ISO 32000-2:2020, kruisverwijzingstabel §7.5.4, paginaboom §7.7.3). Gebruik voor een structurele lezing van een willekeurig invoer- of uitvoerdocument, inclusief versie, paginaaantal, encryptie- en handtekeningvlaggen, de Core-inspector die is gedocumenteerd in Een PDF parsen en inspecteren.