PerfectVector
Von Irene Kim

Ein SVG-Wasserzeichen mit Python in eine PDF einfügen

Füge mit svglib, ReportLab und pypdf ein SVG-Wasserzeichen in eine bestehende PDF ein. Prüfe Deckkraft, beschnittene Seiten, Drehung, erhaltenen Text und Überlagerungsreihenfolge.

12 Min. Lesezeit
Auf dieser Seite

Um einer bestehenden PDF mit Python ein SVG-Wasserzeichen hinzuzufügen, wandle die SVG in eine ReportLab-Zeichnung um, rendere diese auf eine kleine PDF-Überlagerung und füge die Überlagerung anschließend mit pypdf in jede Originalseite ein. Bestimme Größe und Position anhand der sichtbaren CropBox jeder Seite und normalisiere vor dem Zusammenführen die Seitendrehung.

Verwende over=True, wenn das Zeichen über einem deckenden Seitenhintergrund sichtbar bleiben muss. Auch ein durchscheinendes Zeichen kann oben liegen. Bei over=False kommt dieselbe Grafik unter den vorhandenen Inhalt und kann hinter einer gefüllten Seite verschwinden.

Das vollständige Beispiel unten erstellt seine eigene SVG und eine dreiseitige Quell-PDF. Es enthält unterschiedliche Seitengrößen, eine CropBox mit Ursprung ungleich null und eine gedrehte Seite. Das Ergebnis ist eine neue PDF. Behalte die Originaldatei separat.

Wähle zuerst den Dokumentvorgang

AusgangspunktSinnvoller Ablauf
Eine bestehende PDF benötigt auf jeder Seite ein SVG-EmblemSVG-Überlagerung rendern und wie unten in die bestehenden Seiten einfügen
Eine neue PDF wird von Grund auf erstelltDen ReportLab-Ablauf für SVG-Zeichnungen direkt verwenden
Eine vorhandene SVG enthält bereits die richtige GrafikDiese native Vektorquelle behalten; Nachzeichnen ist unnötig
Ein Logo liegt nur als Rasterbild vorZuerst geeignete Grafik vorbereiten, dann das PDF-Zusammenführen separat testen

pypdfs Wasserzeichenanleitung unterscheidet Inhalte über und unter einer Seite. „Wasserzeichen“ bezeichnet hier das sichtbare Emblem; over bestimmt seine tatsächliche Stapelreihenfolge. Dies ist weder ein Sicherheitszeichen noch Manipulationsschutz oder ein Ablauf für signierte Dokumente.

1. Beginne mit einer kleinen, unterstützten SVG

Dieser Test verwendet zwei gefüllte Pfade: einen dreieckigen Rahmen mit offener Mitte und drei Balken. Beide Pfade tragen fill-opacity="0.25". Text, Filter, eingebettete Bilder oder externe Ressourcen sind nicht enthalten.

Diese enge Eingrenzung ist wichtig. svglib unterstützt eine nützliche SVG-Teilmenge, doch zu seinen dokumentierten Einschränkungen gehören begrenzte CSS-Unterstützung und nicht unterstützte Funktionen wie Masken und foreignObject. Eine im Browser richtig aussehende SVG muss nach der Umwandlung weiterhin geprüft werden. Enthält eine Datei Rasterinhalte, lies vor der Annahme eines vollständig vektorbasierten Ergebnisses, wie du eingebettete Bilder in SVG findest.

Eine erste Version dieser Beispieldatei enthielt neben dem Rahmen SVG-Text. Im getesteten Stack wurde der umgewandelte Text vollständig deckend gezeichnet, während der Pfad 0.25 Deckkraft behielt. Ich ersetzte den Text durch eigene Pfadgrafik und testete den Ersatz. Das endgültige Ergebnis belegt diesen reinen Pfadfall, nicht korrekte Deckkraft für jede SVG-Text- oder Stilkombination.

2. Führe das vollständige Beispiel aus

Die getestete Umgebung bestand aus Python 3.12.14, svglib 2.2.0, ReportLab 4.4.9 und pypdf 6.10.0. Die aktuelle pypdf-Dokumentation beschreibt auch neuere Veröffentlichungen. Halte deshalb getestete Versionen und Dokumentationsversion auseinander.

Installiere diese Versionen in einer separaten Umgebung:

python -m venv .venv
source .venv/bin/activate
python -m pip install svglib==2.2.0 reportlab==4.4.9 pypdf==6.10.0

Speichere Folgendes als watermark_demo.py. Es enthält die gesamte Eingabegrafik und erstellt alle drei PDF-Dateien. Du musst deshalb keine fehlende Beispiel-PDF oder -SVG herunterladen.

from io import BytesIO
from pathlib import Path
 
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.graphics import renderPDF
from reportlab.lib.colors import HexColor
from reportlab.pdfgen import canvas
from svglib.svglib import svg2rlg
 
SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="240" height="80"
             viewBox="0 0 240 80">
  <path fill="#315d6f" fill-opacity="0.25" fill-rule="nonzero"
        d="M10 60 L45 8 L80 60 Z M31 48 L59 48 L45 27 Z"/>
  <path fill="#315d6f" fill-opacity="0.25"
        d="M95 23 H216 V31 H95 Z M95 39 H190 V47 H95 Z
           M95 55 H205 V63 H95 Z"/>
</svg>'''
 
 
def make_demo_source(destination):
    stream = BytesIO()
    document = canvas.Canvas(stream, pagesize=(420, 300))
    specifications = [
        (420, 300, '#f5eddb', 'SOURCE PAGE ONE'),
        (300, 420, '#ffffff', 'SOURCE PAGE TWO'),
        (360, 260, '#e8eef0', 'SOURCE PAGE THREE')
    ]
    for width, height, background, label in specifications:
        document.setPageSize((width, height))
        document.setFillColor(HexColor(background))
        document.rect(0, 0, width, height, fill=1, stroke=0)
        document.setFillColor(HexColor('#183a42'))
        document.setFont('Helvetica-Bold', 16)
        document.drawString(28, height - 40, label)
        document.setFont('Helvetica', 10)
        document.drawString(28, height - 62, 'Original text remains in the PDF content.')
        document.drawString(28, 28, 'Original vector line and opaque page background')
        document.setStrokeColor(HexColor('#c47b44'))
        document.setLineWidth(3)
        document.line(28, 48, width - 28, 48)
        document.showPage()
    document.save()
    reader = PdfReader(stream)
    reader.pages[1].cropbox = RectangleObject([10, 20, 290, 400])
    reader.pages[2].rotate(90)
    writer = PdfWriter()
    for page in reader.pages:
        writer.add_page(page)
    writer.write(destination)
 
 
def make_overlay(svg_path, width, height):
    drawing = svg2rlg(str(svg_path))
    if drawing is None or drawing.width <= 0 or drawing.height <= 0:
        raise ValueError('The SVG did not produce a usable drawing')
    scale = width * 0.65 / drawing.width
    left = (width - drawing.width * scale) / 2
    bottom = (height - drawing.height * scale) / 2
    drawing.scale(scale, scale)
    stream = BytesIO()
    overlay = canvas.Canvas(stream, pagesize=(width, height))
    renderPDF.draw(drawing, overlay, left, bottom)
    overlay.save()
    return PdfReader(stream).pages[0]
 
 
def add_artwork(source_path, svg_path, output_path, *, over):
    reader = PdfReader(source_path)
    if reader.is_encrypted:
        raise ValueError('Encrypted inputs are outside this example')
    if reader.get_fields():
        raise ValueError('Form handling is outside this example')
    writer = PdfWriter()
    writer.clone_document_from_reader(reader)
    # Cloning in pypdf 6.10.0 does not retain the source header version.
    # This overlay uses transparency, introduced in PDF 1.4.
    writer.pdf_header = max(reader.pdf_header, '%PDF-1.4')
    for page in writer.pages:
        if page.rotation:
            page.transfer_rotation_to_content()
        box = page.cropbox
        width, height = float(box.width), float(box.height)
        overlay = make_overlay(svg_path, width, height)
        page.merge_transformed_page(
            overlay,
            Transformation().translate(float(box.left), float(box.bottom)),
            over=over,
            expand=False
        )
    writer.write(output_path)
 
 
if __name__ == '__main__':
    output = Path('watermark-output')
    output.mkdir(exist_ok=True)
    artwork = output / 'emblem.svg'
    artwork.write_text(SVG)
    source = output / 'source.pdf'
    make_demo_source(source)
    add_artwork(source, artwork, output / 'overlay-top.pdf', over=True)
    add_artwork(source, artwork, output / 'underlay.pdf', over=False)

Führe es aus:

python watermark_demo.py

Öffne watermark-output/source.pdf, overlay-top.pdf und underlay.pdf. Seite eins sollte das Emblem in der oberen Überlagerung zeigen, in der darunterliegenden Version aber verbergen, weil ihr ursprünglicher Hintergrund deckend ist. Die beiden anderen Seiten prüfen Beschnitt und Drehung, statt dieselbe Seitengeometrie zu wiederholen.

Behalte für deine eigene gewöhnliche, unsignierte PDF make_overlay() und add_artwork() und rufe anschließend Folgendes auf:

add_artwork(
    Path("original.pdf"),
    Path("emblem.svg"),
    Path("watermarked.pdf"),
    over=True
)

Der Writer behält die Quell-Headerversion mit mindestens PDF 1.4 bei, weil die Überlagerung Transparenz verwendet. In pypdf 6.10.0 kann das reine Klonen des Dokuments den standardmäßigen PDF-1.3-Header des Writers hinterlassen, selbst wenn die Quelle eine spätere Version angibt. Die ausdrückliche Zuweisung verhindert diese Herabstufung; sie ist keine Konformitätsprüfung. Die Dokumentation zum Writer-Header empfiehlt eine Version, die alle in der Datei verwendeten Funktionen unterstützt.

Die Schutzprüfungen weisen verschlüsselte Eingaben und erkannte Formularfelder zurück. Sie sind kein vollständiges Kompatibilitäts- oder Signaturaudit. Signierte Dokumente, Formulare, Anmerkungen, Verschlüsselung und dokumentspezifische Anforderungen benötigen einen separaten Ablauf und eine Prüfung, bevor du eine funktionierende Datei ersetzt.

3. Platziere Grafiken in den sichtbaren Seitenkoordinaten

Ein PDF-Punkt entspricht 1/72 Zoll. In dieser svglib-Version ergeben die einheitenlosen SVG-Maße 240 × 80 eine Zeichnung von 180 × 60 Punkt: Ein CSS-Pixel entspricht 0.75 Punkt. Der Code liest drawing.width und drawing.height, statt anzunehmen, dass SVG-Pixel bereits PDF-Punkten entsprechen.

Das Beispiel macht den SVG-Ansichtsbereich 65% so breit wie jede sichtbare Seite, behält sein Seitenverhältnis und zentriert ihn. Es zentriert das deklarierte Zeichenrechteck, nicht die visuelle Masse des Emblems. Die 65% sind eine Layoutentscheidung des Beispiels, kein Wasserzeichenstandard. Diese nur auf der Breite beruhende Skalierung passt zum hier gezeigten breiten Emblem. Ein hoher Ersatz kann über die sichtbare Seitenhöhe hinausragen. Begrenze beim Anpassen der Funktion den Maßstab durch verfügbare Breite und Höhe und prüfe jede Seite vor der Verwendung deiner eigenen PDF.

SeiteZustand der QuelleSichtbare Ausgabe-CropBox in PunktNominelle SVG-Ansichtsbereichsbreite
1420 × 300 große Querformatseite0, 0 bis 420, 300273pt
2300 × 420 große Seite, nach innen beschnitten10, 20 bis 290, 400182pt
3360 × 260 große Seite mit 90° DrehungNach Normalisierung 0, 0 bis 260, 360169pt

Der Aufruf transfer_rotation_to_content() überträgt die Drehung in Inhalt und Seitenboxen. Er richtet die ursprünglichen Wörter nicht relativ zum Betrachter gerade aus. Der Originaltext auf Seite drei bleibt so ausgerichtet wie in der angezeigten Quellansicht, während das neue Emblem in den normalisierten sichtbaren Koordinaten platziert wird. Die pypdf-Seiten-API empfiehlt die Übertragung der Drehung vor dem Zusammenführen von Seiten.

Die Überlagerung beginnt an ihrem eigenen Ursprung. Durch Verschieben um cropbox.left und cropbox.bottom liegt sie im sichtbaren Zielbereich, auch wenn die CropBox nicht bei null beginnt. expand=False verhindert, dass das Zusammenführen die Seitenboxen vergrößert.

4. Prüfe die gespeicherte PDF, nicht nur die SVG-Vorschau

Tatsächlich gerenderter PDF-Vergleich mit verborgener Unterlegung, sichtbarer durchscheinender oberer Überlagerung, beschnittener Hochformatseite und gedrehter Quellseite mit zentriertem Vektoremblem
Tatsächliche Poppler-Renderings der gespeicherten PDFs. Die ursprünglichen Seitenhintergründe verbergen die Unterlegung; die obere Überlagerung bleibt sichtbar. Die letzten beiden Ansichten prüfen einen Beschnittursprung ungleich null und eine ursprünglich gedrehte Seite.

Ich öffnete die gespeicherten PDFs erneut, prüfte Inhaltsströme, Seitenboxen, Drehung, extrahierten Text und Bildressourcen und renderte anschließend die sichtbaren CropBoxen mit Poppler 26.02 bei 96dpi. Die abschließende Dateiprüfung bestand 40 Kontrollen. Ein Browservergleich prüfte außerdem gespeicherte Dateihashes, geladene Renderings und 12 Pixelproben in nativer Auflösung.

Prüfung dieses BeispielsBeobachtetes Ergebnis
Seitenzahl und bekannter OriginaltextAlle drei Seiten und ihr erwarteter Quelltext blieben in beiden Ausgaben erhalten
ÜberlagerungsgeometrieZwei gefüllte Pfade, 18 geprüfte Eckpunkte, Alpha für Füllungen von 0.25 auf jeder Seite
Hinzugefügte BildressourcenKeine in diesen ursprünglichen Quell- und Ausgabeseiten
Platzierung und SeitenboxenEntsprachen den deklarierten CropBoxen und den Berechnungen der gespeicherten Transformationen; Ausgabedrehung war null
Rahmen- und offene-Mitte-ProbenRahmenpixel der oberen Überlagerung entsprachen der erwarteten Mischung innerhalb von ±2 RGB-Werten; offene Mitten behielten den Seitenhintergrund
Unterlegung bei deckendem HintergrundDie Renderings von Quelle und Unterlegung der ersten Seite unterschieden sich bei 96dpi in null Pixeln

Die Platzierungsprüfung scheiterte zunächst an einem strengen Vergleich mit idealen Gleitkommakoordinaten bei 0.00001pt Toleranz. ReportLab speichert Transformationsmatrizen mit endlicher Genauigkeit. Ich behielt dieses fehlgeschlagene Ergebnis bei, berechnete unabhängig die erwarteten gespeicherten Matrizen samt Rundungsgrenze und verglich die gespeicherten Eckpunkte damit. Die größte beobachtete Abweichung von Idealkoordinaten betrug etwa 0.000054pt und lag innerhalb der hergeleiteten Grenze von 0.000086pt. Das prüft die visuell zentrierte Platzierung in diesen Dateien; es ist keine allgemeine Präzisionsgarantie.

Für eine schnelle lokale Sichtprüfung mit installiertem Poppler:

pdftoppm -cropbox -r 96 -png watermark-output/overlay-top.pdf watermark-output/top
pdftoppm -cropbox -r 96 -png watermark-output/underlay.pdf watermark-output/below

Prüfe jede Seite mit sinnvoller Vergrößerung. Wähle oder extrahiere außerdem erwarteten Originaltext, prüfe die Seitenmaße und untersuche, ob das Wasserzeichen Pfade blieb oder Bilder einführte. Ein scharf wirkendes Bildschirmfoto allein belegt diese Dateieigenschaften nicht. Dieser Test maß weder Druck noch Viewerkompatibilität, Dateigrößeneinsparungen oder Widerstand gegen das Entfernen des Wasserzeichens.

Behebe den Fehler im richtigen Schritt

SymptomPrüfen oder ändern
Das Zeichen verschwindetVergleiche over=False mit over=True und suche nach deckenden Originalhintergründen
Das Zeichen sitzt auf einer beschnittenen Seite falschVerwende Größe und Ursprung der CropBox statt einer fest codierten Seitengröße
Gedrehte Seiten zeigen das Zeichen in unerwarteter RichtungNormalisiere die Drehung vor dem Berechnen der sichtbaren Maße und prüfe die resultierende Ansicht
Deckkraft oder Stil verändern sichVereinfache die SVG, prüfe den Farbauftragszustand der umgewandelten PDF und teste genau diese Datei erneut
Ein fotoartiger oder pixeliger Bereich bleibtPrüfe die SVG auf eingebettete Bilder; eine SVG-Hülle macht ein Bitmap nicht zu Pfaden
Text, Formulare oder Dokumentverhalten verändern sichHalte an und prüfe die tatsächlichen Dokumentfunktionen, statt anzunehmen, dass ein erfolgreiches Zusammenführen alles erhalten hat

Die Original-PDF bleibt Seiteninhalt; das Skript rastert keine vollständigen Seiten. Das beweist nicht, dass jede Eingabe-PDF eine beliebige Zusammenführung unverändert übersteht. Verwende eine Kopie und definiere vor einer größeren Stapelverarbeitung die Inhalte und Dokumentfunktionen, die weiter funktionieren müssen.

Bereite fehlende Grafik vor dem Zusammenführen vor

Wenn ein vorhandenes Logo bereits eine saubere SVG ist, behalte es. Ist nur ein geeignetes flächiges Rasterlogo übrig, kann PerfectVectors Logovektorisierer vor diesem Python-Schritt bearbeitbare Grafiken vorbereiten. Prüfe gewonnene Silhouette, Löcher und kleine Details und wiederhole anschließend SVG-Umwandlung und Prüfungen der gespeicherten PDF. Das Beispiel hier verwendete eigene native Pfade und führte keine PerfectVector-Konvertierung durch. Vektorisierung führt keine Seiten zusammen, validiert keine Signaturen und garantiert keine svglib-Kompatibilität.

Häufige Fragen

Kann pypdf ein SVG-Wasserzeichen direkt lesen? Dieses Beispiel wandelt SVG zuerst mit svglib und ReportLab in eine PDF-Überlagerung um. pypdf führt anschließend PDF-Seiteninhalte zusammen; es interpretiert in diesem Ablauf nicht die SVG-Quelle.

Warum verschwindet ein Wasserzeichen hinter meiner PDF-Seite? Eine Unterlegung kann von einem bestehenden deckenden Hintergrund verdeckt werden. Verwende over=True für eine sichtbare obere Überlagerung und lege die Grafikdeckkraft separat fest. Prüfe das tatsächliche Ergebnis auf jeder Seite.

Erhält dies Vektorgrafiken und Originaltext? Das getestete reine Pfademblem blieb als gefüllte Pfade erhalten, und erwarteter Originaltext blieb auf allen drei Seiten extrahierbar. SVG-Funktionen und PDF-Dokumentfunktionen außerhalb dieses Beispiels benötigen eine separate Prüfung.

Bleibt eine digitale Signatur beim Neuschreiben der PDF erhalten? Verwende dieses Beispiel nicht als Ablauf für signierte Dokumente. Es schreibt die Datei neu und testet weder Signaturerhalt noch Gültigkeit. Behalte das signierte Original und nutze einen zu seinen Signaturanforderungen passenden Ablauf.

Quellen

  1. pypdf: Stempel oder Wasserzeichen hinzufügen — Überlagerungs- und Unterlegungsreihenfolge, Zusammenführen und Drehungshinweise.
  2. pypdf: PageObject-API — Seitenboxen, Drehungsübertragung und transformiertes Zusammenführen.
  3. svglib-Projektdokumentation — SVG-zu-ReportLab-Umwandlung, Einheiten und Grenzen unterstützter Funktionen.
  4. ReportLab-Grafikanleitung — Zeichenobjekte und PDF-Rendering im ReportLab-Grafiksystem.
  5. pypdf 6.10.0: PDF-Headerversion — deklariert eine PDF-Version, die die verwendeten Ausgabefunktionen unterstützt.

Behalte native Grafiken, wenn du sie hast. Ist nur ein geeignetes Rasterlogo übrig, bereite mit PerfectVector eine bearbeitbare SVG vor, prüfe die Grafik und verifiziere die neu zusammengeführte PDF anschließend separat.

Mehr aus dem Blog

PerfectVector

Starte mit einer sauberen SVG,
die leichter zu bearbeiten ist

Keine Kreditkarte nötig