Ein PDF aufteilen und Seitenbereiche extrahieren
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“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.
Installation
Abschnitt betitelt „Installation“composer require nextpdf/core:^3Konzeptioneller Überblick
Abschnitt betitelt „Konzeptioneller Überblick“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.
API-Oberfläche
Abschnitt betitelt „API-Oberfläche“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): SplitResulterzeugt ein Ausgabedokument jePageRangein$ranges, in Reihenfolge. Die beiden Begrenzungsparameter deckeln die Eingabegröße und die Bereichsanzahl.splitEvery(string $pdfData, int $pagesPerSegment): SplitResultzerteilt das Dokument in Segmente fester Größe von je$pagesPerSegmentSeiten; das letzte Segment hält den Rest.extractPages(string $pdfData, PageRange $range): SplitDocumentextrahiert 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.
Codebeispiel — Schnellstart
Abschnitt betitelt „Codebeispiel — Schnellstart“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());Codebeispiel — Produktion
Abschnitt betitelt „Codebeispiel — Produktion“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.Grenzfälle & Stolperfallen
Abschnitt betitelt „Grenzfälle & Stolperfallen“- 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östPageLayoutExceptionaus demPageRange-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()PageLayoutExceptionaus; 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.$pagesPerSegmentmuss mindestens 1 betragen, sonst erhalten Sie eineInvalidArgumentException.- Eine leere Bereichsliste wird abgelehnt.
split()mit$ranges === []löstInvalidArgumentExceptionaus. Bauen Sie mindestens einen Bereich, bevor Sie es aufrufen. - Grenzen lösen aus, statt zu kürzen. Das Überschreiten von
maxBytesodermaxRangeslöstInvalidArgumentExceptionaus. 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
UnsupportedSourceDocumentExceptionaus. Der Splitter verweigert die Arbeit, statt ein beschädigtes oder kompromittiertes Dokument auszugeben. Das Aufteilen eines Formulardokuments ist eine bekannte Einschränkung dieses Releases. UnsupportedSourceDocumentExceptionliegt imMerge-Namespace. Ihr vollständig qualifizierter Name lautetNextPDF\Document\Merge\UnsupportedSourceDocumentException. DieserMerge-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.
Performance
Abschnitt betitelt „Performance“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.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“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.
maxBytesundmaxRangessind 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.
Konformität
Abschnitt betitelt „Konformität“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.
Siehe auch
Abschnitt betitelt „Siehe auch“- Referenz des Dokumentmoduls – die vollständige Split-, Merge- und Dokumentteil-Oberfläche.
- Externe PDFs zusammenführen – das umgekehrte Recipe: viele Dokumente zu einem zusammenfügen.
- Ein PDF parsen und inspizieren – nicht vertrauenswürdige Eingaben triagieren, bevor Sie sie aufteilen.
- Exception-bewusste Fehlerbehandlung
– die NextPDF-Exception-Hierarchie hinter
PageLayoutExceptionundUnsupportedSourceDocumentException.