Zum Inhalt springen
getnextpdf.com

Ein PDF aufteilen und Seitenbereiche extrahieren

Sie haben ein PDF und brauchen mehrere. Dieses Recipe zerteilt ein einzelnes Dokument mit der Core-Aufteilungs-Oberfläche NextPDF\Document\PdfSplitter in mehrere Dateien. Sie übergeben die Quelle als rohen PDF-Bytestring und beschreiben, welche Seiten Sie möchten. Der Splitter parst die Quelle über den Objektgraphen, kopiert die erreichbaren Objekte jedes angeforderten Bereichs in ein frisches, neu nummeriertes Dokument mit eigenem Seitenbaum und eigener Querverweistabelle und gibt strukturell vollständige PDFs zurück, die in einem konformen Reader laden.

Dies ist die Umkehrung des Merge-Recipes: Merge fügt viele Dokumente zu einem zusammen, Split zerlegt ein Dokument in viele. Dieselbe Oberfläche deckt die drei Aufgaben ab, die Sie am häufigsten brauchen:

  • Nach Bereichen aufteilen – ein Ausgabedokument je benanntem Seitenbereich erzeugen.
  • Alle N Seiten aufteilen – eine lange Datei in Segmente fester Größe zerteilen.
  • Einen Bereich extrahieren – einen einzelnen zusammenhängenden Seitenbereich in ein Dokument herausziehen.

Die Aufteilung läuft im Prozess, ohne Headless-Browser und ohne Netzwerkaufruf. Sie benötigen eine installierte Core-Version (composer require nextpdf/core:^3) und ein lesbares PDF.

Terminal-Fenster
composer require nextpdf/core:^3

Ein PDF lokalisiert seine Seiten über einen Seitenbaum, der in einem /Pages-Knoten wurzelt, und es erreicht jedes indirekte Objekt über seine Querverweisdaten (eine Tabelle oder einen Stream). Sie können Seiten nicht durch Aufschneiden von Bytes extrahieren: Eine einzelne Seite referenziert gemeinsam genutzte Schriften, Bilder und Ressourcen-Dictionaries, die an anderer Stelle in der Datei liegen, und die Querverweis-Offsets wären nicht mehr gültig.

PdfSplitter erledigt die eigentliche Arbeit. Für jeden Bereich durchläuft er den Objektgraphen von den angeforderten Seitenobjekten aus, sammelt die erreichbare Objekt-Hülle, nummeriert diese Objekte in einen frischen Adressraum um, baut ein Dokument mit einem einzelnen Seitenbaum neu auf und gibt eine echte Querverweistabelle gemäß der PDF-2.0-Struktur aus (ISO 32000-2:2020, Querverweistabelle §7.5.4, Seitenbaum §7.7.3). Jede Ausgabe ist ein eigenständiges Dokument, kein Fragment.

Seitenzahlen sind 1-basiert und inklusiv. Ein Bereich ist ein NextPDF\Document\PageRange-Wertobjekt: new PageRange(2, 5) bedeutet die Seiten 2 bis 5. Der Konstruktor validiert seine eigenen Invarianten – er lehnt einen Start unter 1 oder ein Ende vor dem Start ab, indem er NextPDF\Exception\PageLayoutException auslöst – sodass ein unmöglicher Bereich bei der Konstruktion fehlschlägt, nicht tief im Splitter. PageRange::parse() und PageRange::all() lösen dieselbe PageLayoutException bei einer fehlerhaften Spezifikation oder einer nicht-positiven Seitengesamtzahl aus.

new NextPDF\Document\PdfSplitter() stellt drei Methoden bereit. Alle nehmen die Quelle als rohen PDF-Bytestring entgegen, niemals als Pfad.

  • split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult erzeugt ein Ausgabedokument je PageRange in $ranges, in Reihenfolge. Die beiden Begrenzungsparameter deckeln die Eingabegröße und die Bereichsanzahl.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult zerteilt das Dokument in Segmente fester Größe von je $pagesPerSegment Seiten; das letzte Segment hält den Rest.
  • extractPages(string $pdfData, PageRange $range): SplitDocument extrahiert einen einzelnen Bereich und gibt dieses eine Dokument direkt zurück.

split() und splitEvery() geben ein NextPDF\Document\SplitResult zurück, ein readonly-Objekt, das $documents (eine Liste von Segmenten), $totalPages (Seiten in der Quelle) und $sourceSize trägt. Es bietet count(), document(int $index) zum Abrufen eines Segments über einen nullbasierten Index und totalOutputSize().

Jedes Segment sowie der Rückgabewert von extractPages() ist ein NextPDF\Document\SplitDocument: ein readonly-Objekt, das $pdfData (die Segmentbytes), $range, $pageCount, $sizeBytes und den Helfer isValid() bereitstellt. isValid() ist eine enge %PDF-Header-Plausibilitätsprüfung – sie gibt true zurück, wenn die Segmentbytes mit %PDF beginnen – keine Dokumentstruktur- oder Konformitätsvalidierung; sie bestätigt, dass der Splitter ein PDF erzeugt hat, nicht dass die Datei vollständig konform ist.

Sie bauen einen PageRange direkt mit new PageRange($start, $end) oder parsen eine menschenlesbare Spezifikation mit PageRange::parse('1-3,5,7-10'), was eine list<PageRange> zurückgibt, die bereit zur Übergabe an split() ist. PageRange::all($totalPages) gibt einen einzelnen Bereich zurück, der das gesamte Dokument abdeckt.

Dieses Beispiel liest eine Datei und teilt sie in zwei Dokumente auf: die Seiten 1 bis 3 und die Seiten 4 bis 6. Es lässt die Fehlerbehandlung weg, um die Aufrufform zu zeigen; das Produktionsbeispiel weiter unten ergänzt die vollständigen Guards.

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

Dieses eigenständige Programm baut ein kleines mehrseitiges Dokument im Speicher auf, sodass es ohne externe Datei läuft. Es demonstriert alle drei Operationen – nach Bereichen aufteilen, alle N Seiten aufteilen und einen einzelnen Bereich extrahieren. Es validiert und schreibt die nach Bereich erzeugten Segmente sowie das extrahierte Endstück und meldet das nach Größe erzeugte Ergebnis als Zählwert, sodass Sie jede Aufrufform ohne drei nahezu identische Schreibschleifen sehen. Es fängt die Exceptions ab, die die Aufteilungs-Oberfläche auslöst, und wirft jede mit Kontext erneut, statt sie zu verschlucken. Ersetzen Sie die In-Memory-Quelle durch Ihren eigenen file_get_contents()-Lesevorgang oder einen Abruf aus dem Objektspeicher.

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

Erwartete Standardausgabe (Bytegrößen hängen vom Build ab):

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.
  • Die Quelle sind Bytes, kein Pfad. Jede Methode nimmt einen rohen PDF-String entgegen. Lesen Sie die Datei zuerst mit file_get_contents() oder holen Sie die Bytes aus dem Objektspeicher. Die Übergabe eines Pfades führt dazu, dass die Quelle nicht geparst werden kann.
  • Seitenzahlen sind 1-basiert und inklusiv. new PageRange(1, 3) umfasst die Seiten 1, 2 und 3 – drei Seiten. Ein Start unter 1 oder ein Ende vor dem Start löst PageLayoutException aus dem PageRange-Konstruktor selbst aus.
  • Ein Bereich über das Ende hinaus ist ein Fehler, keine Begrenzung. Überschreitet das Ende eines Bereichs die Seitenanzahl der Quelle, löst split() PageLayoutException aus; es kürzt den Bereich niemals stillschweigend auf die letzte Seite. Prüfen Sie zuerst die Seitenanzahl, wenn Ihre Bereiche vom Aufrufer stammen.
  • splitEvery() behält den Rest. Das letzte Segment hält die übrig gebliebenen Seiten, sodass ein 7-seitiges Dokument, das alle 2 Seiten geteilt wird, vier Segmente ergibt: drei mit 2 Seiten und eines mit 1. $pagesPerSegment muss mindestens 1 betragen, sonst erhalten Sie eine InvalidArgumentException.
  • Eine leere Bereichsliste wird abgelehnt. split() mit $ranges === [] löst InvalidArgumentException aus. Bauen Sie mindestens einen Bereich, bevor Sie es aufrufen.
  • Grenzen lösen aus, statt zu kürzen. Das Überschreiten von maxBytes oder maxRanges löst InvalidArgumentException aus. Der Splitter verarbeitet eine überdimensionierte Eingabe niemals teilweise, also stimmen Sie beide Grenzen auf Ihre Arbeitslast ab.
  • Verschlüsselte, signierte und formulartragende Quellen schlagen fail-closed fehl. Eine verschlüsselte Quelle (sie kann ohne den Schlüssel nicht kopiert werden), eine digital signierte Quelle (eine Neupaginierung würde den Byte-Bereich der Signatur ungültig machen) oder eine Quelle, die ein interaktives Formular trägt (die Widgets eines Felds können auf verworfenen Seiten liegen und verwaisen), lösen UnsupportedSourceDocumentException aus. Der Splitter verweigert die Arbeit, statt ein beschädigtes oder kompromittiertes Dokument auszugeben. Das Aufteilen eines Formulardokuments ist eine bekannte Einschränkung dieses Releases.
  • UnsupportedSourceDocumentException liegt im Merge-Namespace. Ihr vollständig qualifizierter Name lautet NextPDF\Document\Merge\UnsupportedSourceDocumentException. Dieser Merge-Pfad auf einer Split-Seite ist kein Copy-and-paste-Fehler: Es ist die einzelne, gemeinsam genutzte Ablehnungs-Exception für Quelldokumente, die sowohl die Merge- als auch die Split-Oberfläche auslöst, wenn eine Quelle nicht sicher kopiert werden kann. Importieren Sie sie aus diesem Namespace.
  • Die Ausgabe ist strukturell frisch, nicht bytestabil. Jedes Segment ist ein neues Dokument mit eigenem Katalog, eigenem Seitenbaum und eigenem Trailer. Zwei Läufe über dieselbe Eingabe sind strukturell gleich, aber nicht garantiert bytegenau identisch – daher das Reproduzierbarkeitsprofil structural.

Die Aufteilung ist linear in der Anzahl der über alle Bereiche kopierten Seiten. Das Parsen der Quelle und das Kopieren der Objekt-Hülle jedes Bereichs – nicht die eigene Buchführung des Splitters – dominieren die Arbeit. Die Quelle wird als String im Speicher gehalten, und die Bytes jedes Segments werden gehalten, bis Sie sie schreiben, sodass die Speicherspitze der Quellgröße plus dem größten erzeugten Bereich folgt. Der maxBytes-Guard hält die Quellseite dieser Spitze begrenzt. Setzen Sie für Pipelines mit hohem Volumen maxBytes und maxRanges auf die kleinsten Werte, die Ihre Arbeitslast benötigt, damit eine fehlerhafte oder überdimensionierte Eingabe schnell fehlschlägt, statt den Speicher zu erschöpfen.

Die Aufteilung läuft im Prozess; keine Dokumentbytes verlassen den Host und es wird kein Netzwerkaufruf getätigt. Behandeln Sie jedes Quell-PDF als nicht vertrauenswürdige Eingabe:

  • Halten Sie die Grenzen eng. maxBytes und maxRanges sind Ihre erste Verteidigungslinie gegen Denial-of-Service-Eingaben. Setzen Sie sie für jede Oberfläche, die Uploads annimmt, auf Ihre reale Obergrenze, nicht auf die großzügigen Standardwerte.
  • Triagieren Sie, bevor Sie aufteilen. Eine verschlüsselte oder signierte Quelle schlägt fail-closed fehl, aber Sie können diese Bedingungen früher erkennen. Lassen Sie nicht vertrauenswürdige Eingaben zuerst durch den Core-Inspector laufen. Siehe Ein PDF parsen und inspizieren für einen begrenzten Scan, der Verschlüsselung, Signaturen und Risikomarker vor schwererer Verarbeitung kennzeichnet.
  • Interpolieren Sie niemals Benutzereingaben in einen Pfad. Dieses Recipe schreibt in ein festes Verzeichnis oder den Cookbook-Seitenkanal. Leiten Sie Ausgabepfade und Segmentnamen aus servergesteuerten Werten ab, niemals aus einem Anfragefeld, um Path-Traversal zu vermeiden.
  • Keine Geheimnisse in der Ausgabe. Schreiben Sie Segmentdateien nicht an einen Ort oder mit einem Namen, der interne Bezeichner einem Client offenlegt, der sie nicht sehen sollte.

Dieses Recipe erhebt keinen eigenen normativen Standardanspruch. Es zerlegt ein Dokument über die Core-Aufteilungs-Oberfläche und prüft jedes Segment mit der %PDF-Header-Prüfung von SplitDocument::isValid() auf Plausibilität – eine Vorhandenseinsprüfung, dass der Splitter ein PDF ausgegeben hat, keine Konformitäts- oder Dokumentstrukturvalidierung. Die Seitenbaum- und Querverweisstrukturen, die PdfSplitter für jedes Segment neu aufbaut, sind die PDF-2.0-Strukturen, die in der Referenz /modules/core/document/ beschrieben sind (ISO 32000-2:2020, Querverweistabelle §7.5.4, Seitenbaum §7.7.3). Für eine strukturelle Lesart eines beliebigen Eingabe- oder Ausgabedokuments, einschließlich Version, Seitenanzahl, Verschlüsselung und Signatur-Flags, verwenden Sie den Core-Inspector, dokumentiert in Ein PDF parsen und inspizieren.