PerfectVector
Di Irene Kim

Aggiungi una filigrana SVG a un PDF esistente con Python

Unisci una filigrana SVG a un PDF con svglib, ReportLab e pypdf. Verifica opacità, pagine ritagliate, rotazione, testo conservato e ordine di sovrapposizione.

13 min di lettura
In questa pagina

Per aggiungere una filigrana SVG a un PDF esistente in Python, converti l'SVG in un disegno ReportLab, renderizzalo su un piccolo PDF da sovrapporre e uniscilo a ogni pagina originale con pypdf. Dimensiona e posiziona la sovrapposizione dal riquadro di ritaglio visibile di ogni pagina e normalizza la rotazione prima dell'unione.

Usa over=True quando il segno deve rimanere visibile sopra uno sfondo opaco della pagina. Anche un segno traslucido può stare sopra. Con over=False, la stessa grafica va sotto il contenuto esistente e può scomparire dietro una pagina riempita.

L'esempio completo sotto crea il proprio SVG e un PDF sorgente di tre pagine. Include dimensioni diverse, un riquadro di ritaglio con origine diversa da zero e una pagina ruotata. Il risultato è un nuovo PDF; conserva separatamente l'originale.

Scegli prima l'operazione sul documento

Punto di partenzaFlusso utile
Un PDF esistente richiede un emblema SVG su ogni paginaRenderizzare una sovrapposizione SVG e unirla alle pagine esistenti, come sotto
Un nuovo PDF viene composto da zeroUsare direttamente il flusso di disegno SVG con ReportLab
Un SVG esistente contiene già la grafica correttaConservare quel sorgente vettoriale nativo; il ricalco non serve
Un logo esiste soltanto come immagine rasterPreparare prima la grafica adatta, poi provare separatamente l'unione PDF

La guida alle filigrane di pypdf distingue il contenuto posto sopra e sotto una pagina. Qui “filigrana” indica l'emblema visibile; over ne determina l'effettivo ordine di sovrapposizione. Non è un marchio di sicurezza, un metodo antimanomissione o un flusso per documenti firmati.

1. Parti da un SVG piccolo e supportato

Questa prova usa due tracciati riempiti: una cornice triangolare con centro aperto e tre barre. Entrambi i tracciati hanno fill-opacity="0.25". Non ci sono testo, filtri, immagini incorporate o risorse esterne.

Questo input limitato conta. svglib supporta un sottoinsieme utile di SVG, ma i suoi limiti documentati includono supporto CSS ridotto e funzionalità non supportate come maschere e foreignObject. Un SVG corretto nel browser va comunque verificato dopo la conversione. Se contiene raster, leggi come trovare immagini incorporate negli SVG prima di trattare il risultato come interamente vettoriale.

Una prima versione del campione conteneva testo SVG oltre alla cornice. Nello stack provato, il testo convertito veniva dipinto a piena opacità mentre il tracciato manteneva opacità 0,25. Ho sostituito quel testo con grafica originale a tracciati e verificato la sostituzione. Il risultato finale dimostra questo caso di soli tracciati; non dimostra l'opacità corretta per ogni combinazione di testo o stile SVG.

2. Esegui l'esempio completo

L'ambiente provato era Python 3.12.14, svglib 2.2.0, ReportLab 4.4.9 e pypdf 6.10.0. La documentazione attuale di pypdf descrive anche versioni più recenti: tieni quindi distinte le versioni provate da quella della documentazione.

In un ambiente separato, installa queste versioni:

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

Salva quanto segue come watermark_demo.py. Include tutta la grafica di input e crea tutti e tre i PDF: non mancano PDF o SVG di esempio da scaricare.

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)

Eseguilo:

python watermark_demo.py

Apri watermark-output/source.pdf, overlay-top.pdf e underlay.pdf. La prima pagina dovrebbe mostrare l'emblema nell'output sovrapposto ma nasconderlo in quello sottostante, perché lo sfondo originale è opaco. Le altre due pagine verificano ritaglio e rotazione anziché ripetere la stessa geometria di pagina.

Per un tuo normale PDF non firmato, mantieni make_overlay() e add_artwork(), poi chiama:

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

Il writer conserva la versione dell'intestazione sorgente con un minimo di PDF 1.4, perché la sovrapposizione usa trasparenza. In pypdf 6.10.0, la sola clonazione del documento può lasciare l'intestazione predefinita PDF 1.3 del writer, anche se il sorgente dichiara una versione successiva. L'assegnazione esplicita evita quel declassamento; non è una verifica di conformità. La documentazione dell'intestazione del writer raccomanda una versione che supporti tutte le funzionalità usate nel file.

Le protezioni rifiutano input cifrati e campi modulo rilevati. Non sono un controllo completo di compatibilità o firme. Documenti firmati, moduli, annotazioni, cifratura e requisiti specifici richiedono un flusso e una convalida separati prima di sostituire qualsiasi file in uso.

3. Posiziona la grafica nelle coordinate visibili della pagina

Un punto PDF è 1/72 di pollice. In questa versione di svglib, le dimensioni SVG 240 × 80 senza unità producono un disegno da 180 × 60 punti: un pixel CSS corrisponde a 0,75 punti. Il codice legge drawing.width e drawing.height invece di presumere che i pixel SVG equivalgano già ai punti PDF.

L'esempio porta il viewport SVG al 65% della larghezza visibile di ogni pagina, ne conserva il rapporto d'aspetto e centra quel viewport. Centra il rettangolo dichiarato del disegno, non la massa visiva dell'emblema. Il 65% è una scelta di impaginazione dell'esempio, non uno standard per filigrane. Questa scala basata sulla sola larghezza è adatta all'emblema largo mostrato qui. Una sostituzione alta può superare l'altezza visibile della pagina; quando adatti la funzione, limita la scala in base sia alla larghezza sia all'altezza disponibili e ispeziona ogni pagina prima di usare un tuo PDF.

PaginaCondizione del sorgenteRiquadro di ritaglio visibile dell'output, in puntiLarghezza nominale del viewport SVG
1Pagina orizzontale 420 × 300Da 0, 0 a 420, 300273pt
2Pagina 300 × 420, ritagliata verso l'internoDa 10, 20 a 290, 400182pt
3Pagina 360 × 260 con rotazione di 90°Da 0, 0 a 260, 360 dopo la normalizzazione169pt

Chiamare transfer_rotation_to_content() trasferisce la rotazione nel contenuto e nei riquadri della pagina. Non raddrizza le parole originali rispetto a chi guarda. Il testo originale della terza pagina mantiene l'orientamento della vista sorgente, mentre il nuovo emblema viene collocato nelle coordinate visibili normalizzate. L'API delle pagine di pypdf raccomanda di trasferire la rotazione prima dell'unione delle pagine.

La sovrapposizione parte dalla propria origine. Traslarla di cropbox.left e cropbox.bottom la posiziona nella regione visibile della destinazione anche quando il riquadro di ritaglio non parte da zero. expand=False impedisce all'unione di ingrandire i riquadri della pagina.

4. Verifica il PDF salvato, oltre all'anteprima SVG

Confronto di PDF effettivamente renderizzati con segno sottostante nascosto, sovrapposizione traslucida visibile, pagina verticale ritagliata e pagina sorgente ruotata con emblema vettoriale centrato
Rendering Poppler effettivi dei PDF salvati. Gli sfondi originali nascondono il segno sottostante; la sovrapposizione superiore rimane visibile. Le ultime due viste verificano un'origine di ritaglio diversa da zero e una pagina originariamente ruotata.

Ho riaperto i PDF salvati ed esaminato flussi di contenuto, riquadri di pagina, rotazione, testo estratto e risorse immagine, poi ho renderizzato i riquadri di ritaglio visibili con Poppler 26.02 a 96dpi. L'ispezione finale ha superato 40 controlli. Un confronto nel browser ha verificato anche hash dei file salvati, rendering caricati e 12 sonde pixel alla risoluzione nativa.

Controllo su questo campioneRisultato osservato
Numero di pagine e testo originale notoTutte e tre le pagine e il testo sorgente atteso rimanevano in entrambi gli output
Geometria della sovrapposizioneDue tracciati riempiti, 18 vertici esaminati, alfa non di tratto 0,25 su ogni pagina
Risorse immagine aggiunteNessuna in queste pagine sorgente e di output originali
Posizionamento e riquadri di paginaCorrispondevano ai riquadri dichiarati e ai calcoli delle trasformazioni serializzate; rotazione di output zero
Sonde della cornice e del centro apertoI pixel della cornice superiore corrispondevano alla miscela attesa entro ±2 valori RGB; i centri aperti mantenevano lo sfondo
Segno sottostante su sfondo opacoI rendering del sorgente e della versione sottostante della prima pagina differivano in zero pixel a 96dpi

Il controllo del posizionamento inizialmente non ha superato un confronto rigoroso con coordinate ideali in virgola mobile a tolleranza 0,00001pt. ReportLab serializza le matrici di trasformazione con precisione finita. Ho conservato quel risultato fallito, calcolato indipendentemente le matrici serializzate attese e il loro limite di arrotondamento e confrontato i vertici salvati con quei valori. La massima differenza osservata dalle coordinate ideali era circa 0,000054pt, entro il limite derivato di 0,000086pt. Questo verifica un posizionamento visivamente centrato in questi file; non è una garanzia universale di precisione.

Per un rapido controllo visivo locale con Poppler installato:

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

Esamina ogni pagina a un ingrandimento utile. Seleziona o estrai anche il testo originale atteso, controlla le dimensioni della pagina e verifica se la filigrana sia rimasta a tracciati o abbia introdotto immagini. Uno screenshot nitido da solo non dimostra queste proprietà del file. La prova non ha misurato stampa, compatibilità dei visualizzatori, risparmio di dimensioni o resistenza alla rimozione della filigrana.

Correggi il problema nella fase giusta

SintomoControllo o modifica
Il segno scompareConfronta over=False e over=True; cerca sfondi originali opachi
Il segno è fuori posto su una pagina ritagliataUsa dimensioni e origine del riquadro di ritaglio, non una dimensione di pagina fissa
Le pagine ruotate danno al segno una direzione inattesaNormalizza la rotazione prima di calcolare le dimensioni visibili; esamina la vista risultante
Opacità o stile cambianoSemplifica l'SVG, esamina lo stato di disegno del PDF convertito e riprova quel file esatto
Resta una regione fotografica o pixelataCerca immagini incorporate nell'SVG; un involucro SVG non trasforma una bitmap in tracciati
Cambiano testo, moduli o comportamento del documentoFermati e convalida le funzionalità effettive invece di presumere che un'unione riuscita abbia preservato tutto

Il PDF originale rimane contenuto della pagina; lo script non rasterizza pagine intere. Questo non dimostra che ogni PDF di input sopravviva invariato a qualsiasi unione. Usa una copia e definisci contenuti e funzionalità che devono ancora funzionare prima di elaborare un gruppo più grande.

Prepara la grafica mancante prima dell'unione

Se un logo esistente è già un SVG pulito, conservalo. Se rimane soltanto un logo raster piatto adatto, il vettorializzatore di loghi di PerfectVector può aiutarti a preparare grafica modificabile prima di questa fase Python. Esamina sagoma, fori e piccoli dettagli recuperati, poi ripeti conversione SVG e controlli del PDF salvato. L'esempio qui usava tracciati nativi originali e non ha eseguito una conversione PerfectVector. La vettorializzazione non unisce pagine, non convalida firme e non garantisce la compatibilità con svglib.

Domande frequenti

pypdf può leggere direttamente una filigrana SVG? Questo esempio converte prima l'SVG tramite svglib e ReportLab in una sovrapposizione PDF. pypdf unisce poi contenuti di pagine PDF; in questa procedura non interpreta il sorgente SVG.

Perché una filigrana scompare dietro la mia pagina PDF? Un segno sottostante può essere coperto da uno sfondo opaco esistente. Usa over=True per una sovrapposizione superiore visibile e imposta separatamente l'opacità della grafica. Controlla il risultato effettivo su ogni pagina.

Preserva la grafica vettoriale e il testo originale? L'emblema di soli tracciati provato è rimasto formato da tracciati riempiti e il testo originale atteso è rimasto estraibile su tutte e tre le pagine. Funzionalità SVG e PDF esterne a questo campione richiedono una convalida separata.

Riscrivere il PDF conserva una firma digitale? Non usare questo esempio come flusso per documenti firmati. Riscrive il file e non verifica conservazione o validità della firma. Mantieni l'originale firmato e usa un flusso adeguato ai suoi requisiti di firma.

Fonti

  1. pypdf: Aggiungere un timbro o una filigrana — Ordine sopra/sotto, unione e indicazioni sulla rotazione.
  2. pypdf: API PageObject — Riquadri di pagina, trasferimento della rotazione e comportamento dell'unione trasformata.
  3. Documentazione del progetto svglib — Conversione SVG-ReportLab, unità e limitazioni delle funzionalità supportate.
  4. Guida alla grafica di ReportLab — Oggetti Drawing e rendering PDF nel sistema grafico ReportLab.
  5. pypdf 6.10.0: Versione dell'intestazione PDF — Dichiara una versione PDF che supporti le funzionalità dell'output.

Conserva la grafica nativa quando la hai. Quando rimane soltanto un logo raster piatto adatto, prepara un SVG modificabile con PerfectVector, esamina la grafica e verifica poi separatamente il PDF appena unito.

Altro dal blog

PerfectVector

Riparti da un SVG più pulito
e facile da modificare

Nessuna carta di credito