PDF-Dokumente in PHP mit FPDI und FPDF zusammenführen
Um PDFs in PHP zusammenzuführen, importieren Sie jede Quellseite mit FPDI und erstellen mit FPDF eine passende Ausgabeseite. Dieses Kommandozeilenbeispiel führt lokale PDFs in der angegebenen Reihenfolge zusammen, erhält unterschiedliche Seitengrößen und schlägt fehl, wenn eine angeforderte Eingabe nicht importiert werden kann. Sie erzeugen zwei Beispiel-PDFs, führen sie zusammen und prüfen die drei resultierenden Seiten.
Voraussetzungen
Verwenden Sie PHP 8.3–8.5, Composer 2 und ein beschreibbares lokales Verzeichnis. Diese Anleitung wurde in einer Bash-Shell unter Linux mit PHP 8.5.10, Composer 2.10.3, FPDI 2.6.8 und FPDF 1.9.0 getestet.
Aktivieren Sie die Erweiterungen GD und Zlib in der PHP-CLI, bevor Sie Abhängigkeiten installieren.
Beide sind im Composer-Manifest von FPDF angegeben;
auch FPDI benötigt Zlib. Prüfen Sie die CLI-Konfiguration mit
php --ini und die geladenen Erweiterungen mit php -m.
Ein Webserver kann eine andere PHP-Konfiguration verwenden.
Installieren Sie die Poppler-Befehle pdfinfo und
pdftotext, wenn Sie die unabhängigen Prüfungen weiter unten ausführen möchten.
Sie untersuchen das Ergebnis und sind keine Abhängigkeiten des PHP-Skripts zum Zusammenführen.
FPDI und FPDF installieren
Beginnen Sie in einem übergeordneten Verzeichnis, in dem Sie ein neues Projekt namens
pdf-merge-demo erstellen möchten:
mkdir pdf-merge-demo &&
cd pdf-merge-demo &&
composer require --no-interaction 'setasign/fpdf:1.9.0' 'setasign/fpdi:2.6.8'
Die Verkettung mit && stoppt, wenn das Erstellen des Verzeichnisses oder der
Wechsel dorthin fehlschlägt. Falls das Projekt bereits existiert, wählen Sie einen anderen Namen;
löschen Sie es nicht, um die Einrichtung erneut auszuführen. Fahren Sie erst fort, wenn Composer
erfolgreich abgeschlossen wurde, und behalten Sie die erzeugte Datei composer.lock
für reproduzierbare Installationen. Speichern Sie die folgenden Skripte in
pdf-merge-demo und führen Sie alle weiteren Befehle aus diesem Verzeichnis aus.
Beispiel-PDFs erzeugen
Speichern Sie dies als samples.php. Das Skript erstellt
a.pdf mit einer A4-Seite im Hochformat und einer A3-Seite im Querformat sowie
b.pdf mit einer US-Letter-Seite im Hochformat. Die Beschriftungen erleichtern
die Prüfung der Seitenfolge. Wenn Sie diesen Beispielgenerator erneut ausführen, ersetzt er
a.pdf und b.pdf im Verzeichnis dieser Anleitung.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$samples = [
'a.pdf' => [['P', 'A4', 'A1'], ['L', 'A3', 'A2']],
'b.pdf' => [['P', 'Letter', 'B1']],
];
foreach ($samples as $name => $pages) {
$pdf = new FPDF();
foreach ($pages as [$orientation, $format, $label]) {
$pdf->AddPage($orientation, $format);
$pdf->SetFont('Helvetica', '', 20);
$pdf->Cell(0, 10, $label);
}
$pdf->Output('F', __DIR__ . '/' . $name);
}
php samples.php
Vorhandene PDFs mit FPDI importieren
FPDI erstellt aus importierten Seiteninhalten ein neues Dokument. Die
Methode setSourceFile()
liefert die Seitenanzahl der Quelle zurück. Für jede Seite ruft die folgende Schleife zum
Zusammenführen importPage() auf, ermittelt die Abmessungen mit
getTemplateSize() und übergibt diese sowie die Ausrichtung an
AddPage(). Ein Aufruf von AddPage() ohne diese Argumente
würde stattdessen die Standardseite im A4-Hochformat verwenden.
Standardmäßig verwendet importPage() die CropBox, die sichtbare Seitengrenze,
und greift bei Bedarf auf die MediaBox zurück. Die Ausgabe entspricht diesem importierten Bereich,
einschließlich der Drehung der Quellseite; separate Begrenzungsrahmen für die Druckproduktion bleiben
nicht erhalten. Siehe die
Implementierung der Seitengrenzen in FPDI.
Beliebig viele PDFs zusammenführen
Speichern Sie dieses vollständige Skript als merge.php.
mergeMany() gibt bei Erfolg die Anzahl der Ausgabeseiten zurück und löst bei einem
Fehler eine Ausnahme aus. Es überspringt keine Eingabe. Die CLI fängt Fehler ab, schreibt eine
Fehlermeldung in die Standardfehlerausgabe und beendet sich mit dem Status
1.
Für die Ausgabe gelten andere Regeln als beim Beispielgenerator, dessen Ergebnisse ersetzt werden
dürfen: Beim Zusammenführen wird das Überschreiben eines vorhandenen Ziels verweigert, auch wenn
es eine Eingabedatei ist. Alle Seiten werden vollständig importiert und serialisiert, bevor die
Ausgabedatei geöffnet wird. Der Dateimodus xb
von PHP erstellt ausschließlich eine neue Binärdatei. Ein erneuter Durchlauf kann daher kein früheres
Ergebnis kürzen.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use setasign\Fpdi\Fpdi;
function mergeMany(array $files, string $out): int
{
if ($files === []) {
throw new InvalidArgumentException('Provide at least one input PDF.');
}
$pdf = new Fpdi();
foreach ($files as $file) {
if (!is_file($file) || !is_readable($file)) {
throw new RuntimeException("Cannot read input PDF: $file");
}
$pageCount = $pdf->setSourceFile($file);
if ($pageCount < 1) {
throw new RuntimeException("Input PDF has no pages: $file");
}
for ($page = 1; $page <= $pageCount; $page++) {
$template = $pdf->importPage($page);
$size = $pdf->getTemplateSize($template);
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($template);
}
}
// Serialization can fail too; do it before creating the destination.
$bytes = $pdf->Output('S');
$handle = @fopen($out, 'xb');
if ($handle === false) {
throw new RuntimeException("Cannot create output: $out (exists or is not writable).");
}
try {
if (fwrite($handle, $bytes) !== strlen($bytes) || !fflush($handle)) {
throw new RuntimeException("Could not write the complete PDF: $out");
}
} catch (Throwable $error) {
fclose($handle);
unlink($out);
throw $error;
}
fclose($handle);
return $pdf->PageNo();
}
try {
if ($argc < 2) {
throw new InvalidArgumentException('Usage: php merge.php OUTPUT.pdf INPUT.pdf ...');
}
$pages = mergeMany(array_slice($argv, 2), $argv[1]);
fwrite(STDOUT, "Merged $pages pages into {$argv[1]}\n");
} catch (Throwable $error) {
fwrite(STDERR, 'Merge failed: ' . $error->getMessage() . "\n");
exit(1);
}
Output('S') gibt PDF-Bytes als Zeichenfolge zurück.
Bei Fehlern beim Import oder bei der Serialisierung bleibt das Ziel unberührt. Ein erkannter
Schreibfehler entfernt die neue, unvollständige Ausgabedatei. Dieses lokale Skript ist kein
absturzsicherer Veröffentlichungsmechanismus: Eine Unterbrechung während des Schreibens kann eine
unvollständige Datei hinterlassen. Weiterverarbeitende Prozesse sollten daher warten, bis das Skript
erfolgreich beendet wurde.
Zwei PDFs zusammenführen
Führen Sie das Skript mit dem Ziel als erstem Argument aus, gefolgt von den Eingaben in der gewünschten Reihenfolge:
php merge.php merged.pdf a.pdf b.pdf
Es sollte Merged 3 pages into merged.pdf ausgeben und sich mit dem Status
0 beenden. Für weitere Eingabepfade verwenden Sie denselben Befehl;
setzen Sie Pfade mit Leerzeichen in Anführungszeichen. Um diese Beispiele anders anzuordnen,
schreiben Sie das Ergebnis in eine separate Datei:
php merge.php reversed.pdf b.pdf a.pdf
Die Beschriftungen in reversed.pdf sollten B1,
A1 und A2 sein. Die Quelldateien bleiben
unverändert.
Ergebnis und Fehlerverhalten prüfen
pdfinfo -f 1 -l 3 merged.pdf &&
pdftotext -layout merged.pdf -
Erwarten Sie Pages: 3, die folgenden Seitenabmessungen und die Beschriftungen
A1, A2 und B1 in
dieser Reihenfolge. Ein PDF-Punkt entspricht 1/72 Zoll; kleine Rundungsabweichungen sind normal.
| Seite | Beschriftung | Format | Breite × Höhe in Punkten |
|---|---|---|---|
| 1 | A1 | A4-Hochformat | 595,28 × 841,89 |
| 2 | A2 | A3-Querformat | 1190,55 × 841,89 |
| 3 | B1 | US-Letter-Hochformat | 612 × 792 |
Eine fehlende Eingabe muss die gesamte Anfrage scheitern lassen, selbst wenn davor und danach gültige
Eingaben stehen. Lassen Sie missing.pdf fehlen und verwenden Sie ein neues Ziel:
php merge.php incomplete.pdf a.pdf missing.pdf b.pdf
Dieser Aufruf endet mit dem Status 1, meldet
Cannot read input PDF: missing.pdf und erstellt keine Datei incomplete.pdf. Eine nicht
lesbare Datei, ein Verzeichnis als Eingabe oder ein fehlerhaftes PDF führen ebenfalls zum
Fehlschlag. Wird nur ein Ausgabepfad angegeben, schlägt der Aufruf fehl, weil die Eingabeliste leer
ist. Eine Wiederholung des erfolgreichen Befehls zum Zusammenführen schlägt fehl, weil
merged.pdf existiert; die Bytes dieser Datei bleiben unverändert. Wählen Sie einen
neuen Ausgabenamen, um beide Versionen zu behalten.
Was der freie Parser erhalten kann
Hier werden statische Seiten importiert. Dieses Beispiel kopiert keine interaktiven Formularfelder,
Annotationen, anklickbaren Links, Lesezeichen, Ebenen oder Dokumentaktionen. FPDI kann optional
Link-Annotationen für externe URIs mit importPage() über dessen Parameter
importExternalLinks importieren. Dieser hat hier jedoch den Standardwert
false. Die Option stellt weder Formulare noch die interne Navigation oder
andere Annotationen wieder her.
Der freie Parser lehnt außerdem verschlüsselte/passwortgeschützte PDFs und PDFs mit komprimierten Querverweis-Streams oder Objekt-Streams ab. Gewöhnliche komprimierte Seiteninhalte werden unterstützt, wie in den FPDF-Beispielen. Die PDF-Versionsnummer allein sagt nicht aus, ob die nicht unterstützten Strukturen vorhanden sind. Diese Einschränkungen sind unter Einschränkungen von FPDI dokumentiert.
Wenn Sie die Fehlermeldung zu nicht unterstützter Komprimierung sehen, beschaffen Sie einen kompatiblen Export oder evaluieren Sie das optionale FPDI PDF-Parser-Add-on von Setasign. Eine Erweiterung des Parsers macht aus einem Import von Seiteninhalten keine Zusammenführung, die sämtliche interaktiven Funktionen des Dokuments erhält. Wählen Sie ein Werkzeug zum Zusammenführen auf Dokumentebene, wenn diese Funktionen benötigt werden.
Arbeitsspeicher für große Dateien verwalten
FPDF erstellt die Ausgabe im Arbeitsspeicher. Dieses Skript hält zudem die von
Output('S') zurückgegebene Zeichenfolge während des Speicherns im Arbeitsspeicher.
Auch wenn jeweils nur eine Seite verarbeitet wird, erfolgt die Zusammenführung daher nicht als
Streaming. Messen Sie den maximalen Speicherbedarf mit repräsentativen Eingaben, bevor Sie ein
PHP-Speicherlimit oder eine Auftragsgröße wählen; allein anhand der Anzahl der PDFs lässt sich kein
verlässliches Limit bestimmen.
Wenn Sie das FPDI-Objekt nach einem Auftrag freigeben, können dessen Ressourcen frei werden. Die Garbage Collection nach dem Zusammenführen kann den maximalen Speicherbedarf dieses Auftrags jedoch nicht verringern. Geben Sie bei einem dauerhaft laufenden Worker die Objekte jedes abgeschlossenen Auftrags frei, bevor Sie den nächsten starten.
