PDF-Seiten mit PDFtk und Python zusammenführen und extrahieren
Die Operation cat von PDFtk kann PDFs zusammenführen, Seitenbereiche
extrahieren oder Seiten neu anordnen, ohne sie zu rastern. Diese Anleitung nutzt PDFtk Java, um zwei
kleine Beispieldokumente zusammenzuführen, Seiten aus dem Ergebnis auszuwählen und das Zusammenführen
mit Python zu automatisieren. Das Python-Skript zum Zusammenführen gibt bei Fehlern einen Exit-Status
ungleich null zurück und ersetzt keine bestehenden Ausgabedateien.
Systemanforderungen
Die folgenden Befehle wurden unter Linux mit Bash 5.3.15, OpenJDK 21.0.12.1, Python 3.14.7,
qpdf 12.4.1 und Poppler 26.08.0 getestet. Stellen Sie vor Beginn sicher, dass
java, python3, qpdf,
pdftotext, curl und sha256sum
in Ihrem PATH verfügbar sind. Dies sind die getesteten Versionen,
keine Zusage, dass jede ältere Version funktioniert. Poppler stellt pdftotext
bereit; qpdf prüft die PDF-Struktur unabhängig von PDFtk.
Verwenden Sie für dieses lokale Beispiel vertrauenswürdige, unverschlüsselte PDFs. Wenn ein Parser ein PDF akzeptiert, belegt das nicht, dass es harmlos, visuell intakt oder frei von aktiven Inhalten ist. Verarbeiten Sie nicht vertrauenswürdige Dokumente in einer angemessen isolierten Umgebung. Verwenden Sie dieses Skript nicht als Sicherheitsfilter.
PDFtk installieren
PDFtk Java ist eine Portierung von PDFtk, nicht die ursprüngliche native Programmdatei von
PDFtk Server. Die
versionsgebundene Installationsanleitung
führt Distributionspakete und eine eigenständige JAR-Datei mit allen Abhängigkeiten auf. Wir verwenden
diese JAR-Datei, damit jeder Aufruf Version 3.3.3 auswählt und nicht irgendeine vom Paketmanager
bereitgestellte Version von pdftk. Diese Anleitung nutzt Linux-Shell-Befehle;
Installationsprogramme für macOS oder Windows werden nicht behandelt.
Fügen Sie diesen Block in Bash ein, während Sie sich in einem beschreibbaren Verzeichnis befinden.
Er erstellt ein neues Verzeichnis pdf-workflow; Ihre Shell bleibt im ursprünglichen
Verzeichnis. Ein bereits vorhandenes Verzeichnis, ein fehlgeschlagener Download oder eine abweichende
Prüfsumme stoppt den Block. Löschen Sie kein bestehendes Projekt, damit der Block erfolgreich läuft.
(
set -eu
mkdir pdf-workflow
cd pdf-workflow
curl -fsSLo pdftk-java-3.3.3-all.jar \
https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v3.3.3/pdftk-all.jar
printf '%s %s\n' \
a694d49bd03e1edd4c23b3ba808bc221eb8a8ccfe7bfd2a0a884b2b2fb425188 \
pdftk-java-3.3.3-all.jar | sha256sum -c -
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar --version
)
Der Hash legt die heruntergeladenen Bytes fest; er ist keine Herausgebersignatur. Die Versionsausgabe sollte PDFtk Java 3.3.3 ausweisen. Eine fehlgeschlagene Einrichtung kann das neue Verzeichnis und die heruntergeladene Datei zurücklassen. Prüfen Sie beide, bevor Sie einen neuen Speicherort wählen.
Grundlegende PDF-Operationen
Speichern Sie Folgendes als pdf-workflow/make_samples.py, um reproduzierbare Eingaben zu erhalten.
Es verwendet nur die Python-Standardbibliothek, um ein zweiseitiges PDF mit den Beschriftungen
ALPHA ONE und ALPHA TWO sowie ein einseitiges PDF mit der
Beschriftung BETA ONE zu erstellen. Bereits vorhandene Beispieldateinamen werden
abgelehnt, statt die Dateien zu ersetzen.
from pathlib import Path
def write_pdf(path, labels):
page_ids = [4 + 2 * index for index in range(len(labels))]
kids = ' '.join(f'{page_id} 0 R' for page_id in page_ids)
objects = [
b'<< /Type /Catalog /Pages 2 0 R >>',
f'<< /Type /Pages /Count {len(labels)} /Kids [{kids}] >>'.encode(),
b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
]
for page_id, label in zip(page_ids, labels):
stream = f'BT /F1 24 Tf 40 180 Td ({label}) Tj ET\n'.encode('ascii')
objects.append(
f'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 360 240] '
f'/Resources << /Font << /F1 3 0 R >> >> '
f'/Contents {page_id + 1} 0 R >>'.encode()
)
objects.append(f'<< /Length {len(stream)} >>\nstream\n'.encode()
+ stream + b'endstream')
data = bytearray(b'%PDF-1.4\n')
offsets = [0]
for object_id, payload in enumerate(objects, start=1):
offsets.append(len(data))
data.extend(f'{object_id} 0 obj\n'.encode() + payload + b'\nendobj\n')
xref = len(data)
data.extend(f'xref\n0 {len(offsets)}\n0000000000 65535 f \n'.encode())
for offset in offsets[1:]:
data.extend(f'{offset:010d} 00000 n \n'.encode())
data.extend(f'trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\n'
f'startxref\n{xref}\n%%EOF\n'.encode())
with path.open('xb') as output:
output.write(data)
write_pdf(Path('input-a.pdf'), ['ALPHA ONE', 'ALPHA TWO'])
write_pdf(Path('input-b.pdf'), ['BETA ONE'])
PDFs zusammenführen
Führen Sie dies im selben übergeordneten Verzeichnis wie die Einrichtung aus.
A und B sind Eingabe-Handles;
A1-end B1-end umfasst alle Seiten von A, gefolgt von allen Seiten von B. Das
PDFtk-Handbuch dokumentiert Seitenbereiche und Handles.
Ersetzen Sie bei eigenen Dokumenten die Eingabenamen und prüfen Sie zuerst deren Seitenzahlen.
(
set -eu
cd pdf-workflow
python3 make_samples.py
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar A=input-a.pdf B=input-b.pdf \
cat A1-end B1-end output combined.pdf dont_ask
)
Die systemnahen Befehle verwenden bewusst dont_ask: Sie laufen ohne Rückfragen
und überschreiben die benannten Ausgabedateien. Es sind aufeinanderfolgende Beispielbefehle,
keine abgesicherten Wrapper zur Bereitstellung der Ergebnisse. Ein Fehler kann eine unvollständige
Ausgabedatei hinterlassen. Geben Sie keine Dateien an, die Sie behalten müssen. Das folgende
Python-Skript zum Zusammenführen verfolgt eine andere Strategie und ersetzt keine Dateien.
Wenn Sie den gesamten Block zum Zusammenführen erneut ausführen, stoppt er bei den bereits
vorhandenen Beispieldateien; nach diesem Fehler arbeitet er nicht mit veralteten Eingaben weiter.
PDFs aufteilen
Extrahieren Sie Seiten aus dem oben zusammengeführten PDF und ordnen Sie sie neu an. Die
Seitennummerierung beginnt bei eins. cat 3 1 wählt die dritte Seite, gefolgt
von der ersten; cat 1-2 würde einen zusammenhängenden Bereich auswählen.
(
set -eu
cd pdf-workflow
java -Xms16m -Xmx256m -XX:ActiveProcessorCount=2 -XX:-UsePerfData \
-Djava.io.tmpdir="$PWD" -Duser.home="$PWD" \
-jar pdftk-java-3.3.3-all.jar combined.pdf cat 3 1 output selected.pdf dont_ask
)
Prüfen Sie beide tatsächlichen Ausgaben, nicht nur den Exit-Status des erzeugenden Programms:
(
set -eu
cd pdf-workflow
qpdf --check combined.pdf >/dev/null
test "$(qpdf --show-npages combined.pdf)" = 3
printf 'Merged pages:\n'
pdftotext -layout combined.pdf -
qpdf --check selected.pdf >/dev/null
test "$(qpdf --show-npages selected.pdf)" = 2
printf 'Selected pages:\n'
pdftotext -layout selected.pdf -
)
Der zusammengeführte Text sollte ALPHA ONE, ALPHA TWO,
dann BETA ONE lauten; der ausgewählte Text sollte
BETA ONE, dann ALPHA ONE lauten. Öffnen Sie beide PDFs
in einem Anzeigeprogramm, um ihr Erscheinungsbild zu prüfen. Diese Text- und Strukturprüfungen
belegen nicht, dass bei beliebigen Dokumenten Formulare, Anmerkungen, Signaturen oder jedes visuelle
Detail erhalten bleiben. Reine Bildscans können keinen extrahierbaren Text enthalten.
PDF-Dateigröße optimieren
Die Option compress von PDFtk stellt die
Komprimierung der Seitenstreams wieder her. Sie reduziert weder die Bildauflösung noch garantiert
sie ein kleineres PDF. Auch das Zusammenführen „standardisiert“ ein Dokument nicht. Bildkomprimierung
erfordert einen separaten Workflow zum Neuschreiben mit eigenen Qualitätsprüfungen. Daher enthält
dieses Beispiel keinen Konvertierungsschritt mit Ghostscript.
Integrationsbeispiel
Speichern Sie dies als pdf-workflow/merge_pdfs.py. Es akzeptiert eine JAR-Datei, einen neuen
Ausgabedateinamen und eine oder mehrere Eingaben. Setzen Sie -- vor das
JAR-Argument, wie im folgenden Aufruf, damit der Argumentparser von Python führende Bindestriche in
Dateinamen wörtlich behandelt. Das Skript löst Eingabepfade auf, bevor es sie als separate Argumente
an den Unterprozess übergibt; es baut aus Ihren Dateinamen keinen Shell-Befehl.
Dieses lokale Linux-Skript lehnt Eingaben mit einer Gesamtgröße über 100 MiB ab, prüft jede Eingabe
mit qpdf und legt das zusammengeführte PDF vorläufig im Zielverzeichnis ab. Nach der Prüfung dieses
PDFs nutzt es os.link, um das Ergebnis
bereitzustellen, ohne eine bestehende Zieldatei zu ersetzen. Das Dateisystem muss Hardlinks
unterstützen. Das Skript verspricht weder Beständigkeit bei Abstürzen noch einen sicheren Betrieb,
wenn jemand während der Verarbeitung das Verzeichnis oder die Eingaben böswillig verändert.
import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='Merge PDFs without replacing an output.')
parser.add_argument('jar', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('inputs', type=Path, nargs='+')
args = parser.parse_args()
destination = args.output.absolute()
if os.path.lexists(destination):
raise FileExistsError(f'Output already exists: {destination}')
jar = args.jar.resolve(strict=True)
inputs = [path.resolve(strict=True) for path in args.inputs]
if not jar.is_file() or any(not path.is_file() for path in inputs):
raise ValueError('The JAR and inputs must be regular files.')
if sum(path.stat().st_size for path in inputs) > 100 * 1024 * 1024:
raise ValueError('Combined inputs exceed 100 MiB.')
for path in inputs:
subprocess.run(['qpdf', '--check', str(path)], check=True,
capture_output=True, timeout=60)
with tempfile.TemporaryDirectory(prefix='.pdf-merge-', dir=destination.parent) as staging:
staged = Path(staging) / 'merged.pdf'
command = [
'java', '-Xms16m', '-Xmx256m', '-XX:ActiveProcessorCount=2', '-XX:-UsePerfData',
f'-Djava.io.tmpdir={staging}', f'-Duser.home={staging}', '-jar', str(jar),
*map(str, inputs), 'cat', 'output', str(staged), 'dont_ask',
]
subprocess.run(command, check=True, capture_output=True, timeout=60)
subprocess.run(['qpdf', '--check', str(staged)], check=True,
capture_output=True, timeout=60)
os.link(staged, destination)
print(f'Created {destination.name}')
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, subprocess.SubprocessError) as error:
print(f'Cannot merge PDFs: {error}', file=sys.stderr)
sys.exit(1)
Führen Sie es mit denselben Beispieleingaben aus. Die Ausgabe enthält drei Seiten in derselben
Reihenfolge wie combined.pdf:
(
set -eu
cd pdf-workflow
python3 merge_pdfs.py -- pdftk-java-3.3.3-all.jar merged-from-python.pdf input-a.pdf input-b.pdf
qpdf --check merged-from-python.pdf >/dev/null
test "$(qpdf --show-npages merged-from-python.pdf)" = 3
pdftotext -layout merged-from-python.pdf -
)
Fehlende Dateien, ungültige PDFs, zu große Eingaben, eine fehlende Programmdatei, eine
Zeitüberschreitung oder eine fehlgeschlagene Bereitstellung führen zu einem Status ungleich null,
ohne ein erfolgreiches Zusammenführen zu melden. Ein zweiter Aufruf lehnt
merged-from-python.pdf ab und lässt dessen Bytes unverändert. Fehler bei den Vorabprüfungen
erzeugen keine vorläufige Ausgabe; bei gewöhnlichen Verarbeitungsfehlern wird das temporäre
Verzeichnis entfernt. Ein erzwungener Abbruch kann dieses private Verzeichnis zurücklassen.
Für jeden nativen Unterprozess gilt ein Zeitlimit von 60 Sekunden, nicht ein Zeitlimit von
60 Sekunden für das gesamte Zusammenführen. Warnungen von qpdf gelten hier als Fehler. Prüfen Sie
Ihre Quelldatei, statt stillschweigend ein repariertes Dokument zu akzeptieren.
Häufige Probleme beheben
Speicherbezogene Fehler
Die Prüfung auf 100 MiB ist eine Zulassungsregel für Eingaben, keine RAM-Schätzung. Auch die
PDF-Komplexität, Dekomprimierung und Speicherzuweisungen von Java außerhalb des Heaps spielen eine
Rolle. -Xmx256m begrenzt den Java-Heap, nicht den gesamten Prozessspeicher,
und garantiert nicht, dass eine akzeptierte Eingabe vollständig verarbeitet wird. Es gibt keine
automatische Stapelbildung: Ein zu großes Dokument wird abgelehnt, statt in einen zu großen Stapel
aufgenommen zu werden oder einen leeren Stapel vorangestellt zu bekommen.
Dateizugriffsfehler
Prüfen Sie, ob die Eingaben und die JAR-Datei lesbar sind und ob das übergeordnete Verzeichnis der Ausgabe existiert und beschreibbar ist. Wählen Sie eine neue Zieldatei, wenn bereits eine existiert; das Python-Skript löscht sie nicht, selbst wenn ein anderer Schritt fehlschlägt. Lockern Sie Dateiberechtigungen nicht pauschal, nur damit das Zusammenführen funktioniert. Führen Sie bei einem qpdf- oder PDFtk-Fehler den jeweiligen Befehl lokal mit der betroffenen Eingabe aus, um die Diagnosemeldung zu prüfen.
Verarbeiten Sie zunächst jeweils nur einen Job. Kleinere Eingabedateien allein belegen weder einen begrenzten Speicherbedarf noch einen höheren Durchsatz. Fügen Sie Parallelverarbeitung erst hinzu, nachdem Sie Ihre eigene Arbeitslast und die verfügbaren Ressourcen gemessen haben.
Sicherheitsaspekte
Sicherer Umgang mit PDFs
Der Modus encrypt_128bit von PDFtk
verwendet das ältere RC4, nicht modernes AES. Verwenden Sie ihn nicht zum Schutz vertraulicher
Dokumente. Ein Eigentümerpasswort allein erzwingt keine Passworteingabe beim Öffnen eines PDFs;
das Benutzerpasswort ist der separate Mechanismus dafür. Lesen Sie die
Passwortoptionen, wenn Sie einen älteren Workflow pflegen,
statt Passwörter in Befehlsargumente oder Skripte zu kopieren.
PROMPT steht zur interaktiven Passworteingabe bereit; dieses nicht
interaktive Skript zum Zusammenführen verarbeitet keine passwortgeschützten Eingaben.
Dateiberechtigungen verwalten
Druck- und Änderungsbeschränkungen von PDFs hängen davon ab, dass das Leseprogramm sie beachtet. Sie bilden keine Grenze für die Zugriffskontrolle. Beschränken Sie den Zugriff auf die Dateien selbst; das Skript zum Zusammenführen entfernt weder sensible Texte noch Metadaten und legt keine Speicher- oder Freigaberichtlinien einer Organisation fest.
Fazit
Verwenden Sie explizite Seitenbereiche, wenn die Reihenfolge wichtig ist, prüfen Sie das Ergebnis und entscheiden Sie vor der Automatisierung, ob eine Ausgabedatei ersetzt werden darf. Beim PDFtk-Java-Projekt finden Sie weitere Operationen, sobald dieser kleine Workflow zu Ihren Anforderungen passt.
