PerfectVector
Autor: Irene Kim

Dodaj znak wodny SVG do istniejącego PDF w Pythonie

Połącz znak wodny SVG z PDF za pomocą svglib, ReportLab i pypdf. Sprawdź krycie, przycięte strony, obrót, zachowany tekst i kolejność nakładania.

11 min czytania
Na tej stronie

Aby dodać znak wodny SVG do istniejącego PDF w Pythonie, zamień SVG na rysunek ReportLab, wyrenderuj go do małej nakładki PDF, a następnie scal ją z każdą oryginalną stroną przez pypdf. Dobierz rozmiar i pozycję z widocznego pola przycięcia strony i znormalizuj obrót przed scalaniem.

Użyj over=True, gdy znak ma pozostać widoczny nad nieprzezroczystym tłem strony. Półprzezroczysty znak również może leżeć na wierzchu. Przy over=False ta sama grafika trafia pod istniejącą treść i może zniknąć za wypełnioną stroną.

Kompletny przykład poniżej tworzy własny SVG i trzystronicowy PDF źródłowy. Uwzględnia różne rozmiary stron, pole przycięcia z niezerowym początkiem i obróconą stronę. Wynikiem jest nowy PDF; zachowaj oryginał oddzielnie.

Najpierw wybierz operację dokumentu

Punkt wyjściaPrzydatny proces
Istniejący PDF potrzebuje emblematu SVG na każdej stronieWyrenderuj nakładkę SVG i scal z istniejącymi stronami, jak poniżej
Nowy PDF jest tworzony od podstawUżyj bezpośrednio procesu rysowania SVG w ReportLab
Istniejący SVG już ma właściwą grafikęZachowaj natywne źródło wektorowe; trasowanie jest zbędne
Logo istnieje wyłącznie jako rasterNajpierw przygotuj odpowiednią grafikę, potem oddzielnie przetestuj scalanie PDF

Poradnik znaków wodnych pypdf rozróżnia treść nad i pod stroną. Tutaj „znak wodny” oznacza widoczny emblemat; over określa rzeczywistą kolejność stosu. Nie jest to zabezpieczenie, metoda ochrony przed modyfikacją ani proces dokumentu podpisanego.

1. Zacznij od małego obsługiwanego SVG

Test używa dwóch wypełnionych ścieżek: trójkątnej ramki z otwartym środkiem i trzech pasków. Obie mają fill-opacity="0.25". Nie ma tekstu, filtrów, osadzonych obrazów ani zasobów zewnętrznych.

To wąskie wejście ma znaczenie. svglib obsługuje użyteczny podzbiór SVG, lecz jego udokumentowane ograniczenia obejmują ograniczony CSS oraz brak masek i foreignObject. SVG poprawny w przeglądarce nadal wymaga kontroli po konwersji. Jeśli zawiera raster, przeczytaj wyszukiwanie osadzonych obrazów w SVG, zanim uznasz wynik za całkowicie wektorowy.

Pierwsza wersja próbki miała tekst SVG obok ramki. W testowanym zestawie przekonwertowany tekst był malowany z pełnym kryciem, a ścieżka zachowała 0.25. Zastąpiłam tekst autorską grafiką ścieżek i przetestowałam zamiennik. Końcowy wynik potwierdza ten przypadek samych ścieżek, a nie poprawne krycie każdej kombinacji tekstu i stylów SVG.

2. Uruchom kompletny przykład

Testowane środowisko to Python 3.12.14, svglib 2.2.0, ReportLab 4.4.9 i pypdf 6.10.0. Aktualna dokumentacja pypdf opisuje też nowsze wydania, więc odróżniaj testowane wersje od wersji dokumentacji.

W oddzielnym środowisku zainstaluj te wersje:

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

Zapisz kod jako watermark_demo.py. Zawiera całą grafikę wejściową i tworzy trzy PDF, więc nie brakuje przykładowego PDF ani SVG do pobrania.

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)

Uruchom:

python watermark_demo.py

Otwórz watermark-output/source.pdf, overlay-top.pdf i underlay.pdf. Strona pierwsza powinna pokazywać emblemat w wersji nakładki górnej, lecz ukrywać go w dolnej, bo oryginalne tło jest nieprzezroczyste. Pozostałe strony sprawdzają przycięcie i obrót zamiast powtarzać tę samą geometrię.

Dla własnego zwykłego, niepodpisanego PDF zachowaj make_overlay() i add_artwork(), a potem wywołaj:

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

Writer zachowuje wersję nagłówka źródłowego z minimum PDF 1.4, ponieważ nakładka używa przezroczystości. W pypdf 6.10.0 samo klonowanie dokumentu może pozostawić domyślny nagłówek PDF 1.3 nawet przy nowszej wersji źródła. Jawne przypisanie unika obniżenia wersji; nie jest walidacją zgodności. Dokumentacja nagłówka writera zaleca wersję obsługującą wszystkie funkcje pliku.

Warunki ochronne odrzucają szyfrowane wejścia i wykryte pola formularzy. Nie są pełnym audytem zgodności ani podpisów. Dokumenty podpisane, formularze, adnotacje, szyfrowanie i szczególne wymagania dokumentu potrzebują oddzielnego procesu i walidacji przed zastąpieniem roboczego pliku.

3. Umieść grafikę we współrzędnych widocznej strony

Punkt PDF ma 1/72 cala. W tej wersji svglib surowe wymiary SVG 240 × 80 dają rysunek 180 × 60 punktów: jeden piksel CSS mapuje się na 0.75 punktu. Kod odczytuje drawing.width i drawing.height, zamiast zakładać równość pikseli SVG i punktów PDF.

Przykład nadaje viewportowi SVG 65% szerokości widocznej strony, zachowuje proporcje i centruje viewport. Centruje zadeklarowany prostokąt rysunku, a nie wizualny ciężar emblematu. 65% jest przykładową decyzją układu, a nie standardem znaku wodnego. Skala tylko według szerokości pasuje do pokazanego szerokiego emblematu. Wysoki zamiennik może wyjść poza wysokość strony; dostosowując funkcję, ogranicz skalę dostępną szerokością i wysokością oraz sprawdź każdą stronę przed użyciem własnego PDF.

StronaWarunek źródłowyWidoczne pole przycięcia wyniku w punktachNominalna szerokość viewportu SVG
1Pozioma strona 420 × 3000, 0 do 420, 300273pt
2Strona 300 × 420 przycięta do wewnątrz10, 20 do 290, 400182pt
3Strona 360 × 260 z obrotem 90°0, 0 do 260, 360 po normalizacji169pt

transfer_rotation_to_content() przenosi obrót do treści i pól strony. Nie prostuje oryginalnych słów względem oglądającego. Tekst strony trzeciej zachowuje orientację widoku źródła, a nowy emblemat trafia do znormalizowanych współrzędnych widocznego obszaru. API stron pypdf zaleca przenoszenie obrotu przed scalaniem.

Nakładka zaczyna we własnym początku. Przesunięcie o cropbox.left i cropbox.bottom umieszcza ją wewnątrz widocznego obszaru celu nawet przy początku innym niż zero. expand=False zapobiega powiększeniu pól strony przy scalaniu.

4. Sprawdź zapisany PDF, a nie tylko podgląd SVG

Rzeczywiste porównanie renderów PDF z ukrytą dolną nakładką, widoczną półprzezroczystą górną, przyciętą stroną pionową i obróconą stroną źródłową z wyśrodkowanym emblematem wektorowym
Rzeczywiste rendery Poppler zapisanych PDF. Oryginalne tła stron ukrywają dolną nakładkę; górna pozostaje widoczna. Ostatnie dwa widoki sprawdzają niezerowy początek przycięcia i pierwotnie obróconą stronę.

Ponownie otworzyłam PDF i sprawdziłam strumienie treści, pola stron, obrót, wyodrębniony tekst i zasoby obrazów, a potem wyrenderowałam widoczne pola przycięcia Poppler 26.02 w 96dpi. Końcowa kontrola pliku przeszła 40 testów. Porównanie w przeglądarce potwierdziło też hasze plików, wczytane rendery i 12 próbek pikseli w natywnej rozdzielczości.

Kontrola tego przykładuZaobserwowany wynik
Liczba stron i znany oryginalny tekstTrzy strony i oczekiwany tekst źródłowy pozostały w obu wynikach
Geometria nakładkiDwie wypełnione ścieżki, 18 sprawdzonych wierzchołków i alfa wypełnienia 0.25 na każdej stronie
Dodane zasoby obrazówBrak w tych oryginalnych stronach źródłowych i wynikowych
Położenie i pola stronZgodne z zadeklarowanym przycięciem i obliczeniami zapisanych transformacji; obrót wyjścia wynosił zero
Próbki ramki i otwartego środkaPiksele górnej ramki zgodne z mieszaniem w granicach ±2 RGB; otwarte środki zachowały tło strony
Dolna nakładka pod nieprzezroczystym tłemRendery źródła i dolnej nakładki strony pierwszej różniły się w zerze pikseli przy 96dpi

Audyt położenia początkowo nie przeszedł ścisłego porównania z idealnymi współrzędnymi zmiennoprzecinkowymi przy tolerancji 0.00001pt. ReportLab zapisuje macierze transformacji ze skończoną precyzją. Zachowałam błędny wynik, niezależnie obliczyłam oczekiwane zapisane macierze i granicę zaokrągleń oraz porównałam wierzchołki z tymi wartościami. Największa różnica od idealnej współrzędnej wyniosła około 0.000054pt, w wyliczonej granicy 0.000086pt. Kontroluje to wizualne wyśrodkowanie w tych plikach, a nie uniwersalną gwarancję precyzji.

Do szybkiej kontroli wizualnej z zainstalowanym 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

Obejrzyj każdą stronę w użytecznym powiększeniu. Zaznacz lub wyodrębnij też oczekiwany tekst, sprawdź wymiary i ustal, czy znak wodny pozostał ścieżkami, czy wprowadził obrazy. Sam ostry zrzut nie potwierdza tych właściwości. Test nie mierzył druku, zgodności przeglądarek PDF, oszczędności rozmiaru ani odporności na usunięcie znaku.

Napraw błąd na właściwym etapie

ObjawCo sprawdzić lub zmienić
Znak znikaPorównaj over=False z over=True; szukaj nieprzezroczystych oryginalnych teł
Znak leży źle na przyciętej stronieUżyj rozmiaru i początku pola przycięcia zamiast stałego rozmiaru strony
Obrócone strony ustawiają znak niespodziewanieZnormalizuj obrót przed obliczeniem widocznych wymiarów; sprawdź wynik
Krycie lub styl się zmieniaUprość SVG, sprawdź stan malowania przekonwertowanego PDF i ponownie przetestuj ten plik
Pozostaje fotograficzny lub pikselowy obszarSprawdź osadzone obrazy SVG; opakowanie SVG nie zamienia bitmapy w ścieżki
Tekst, formularze lub zachowanie dokumentu się zmieniająZatrzymaj proces i zweryfikuj rzeczywiste cechy dokumentu zamiast zakładać, że udane scalanie zachowuje wszystko

Oryginalny PDF pozostaje treścią stron; skrypt nie rasteruje całych stron. Nie dowodzi to zachowania każdego wejściowego PDF przy dowolnym scalaniu. Używaj kopii i określ wymagane działanie treści oraz funkcji dokumentu przed większą partią.

Przygotuj brakującą grafikę przed scalaniem

Jeśli istniejące logo jest czystym SVG, zachowaj je. Jeśli pozostał tylko odpowiedni płaski raster, wektoryzator logo PerfectVector może pomóc przygotować edytowalną grafikę przed etapem Pythona. Sprawdź odzyskaną sylwetkę, otwory i drobne detale, a potem powtórz konwersję SVG i kontrole zapisanego PDF. Przykład używał autorskich natywnych ścieżek bez konwersji PerfectVector. Wektoryzacja nie scala stron, nie sprawdza podpisów i nie gwarantuje zgodności svglib.

FAQ

Czy pypdf odczyta znak wodny SVG bezpośrednio? Przykład najpierw konwertuje SVG przez svglib i ReportLab do nakładki PDF. pypdf scala potem treść stron PDF; nie interpretuje tutaj źródła SVG.

Dlaczego znak wodny znika za stroną PDF? Dolną nakładkę może zakrywać istniejące nieprzezroczyste tło. Użyj over=True dla widocznej górnej nakładki i ustaw krycie grafiki oddzielnie. Sprawdź rzeczywisty wynik na każdej stronie.

Czy zachowuje to wektory i oryginalny tekst? Testowany emblemat samych ścieżek pozostał wypełnionymi ścieżkami, a oczekiwany tekst dawał się wyodrębnić na wszystkich trzech stronach. Inne funkcje SVG i PDF wymagają oddzielnej walidacji.

Czy przepisanie PDF zachowa podpis cyfrowy? Nie używaj przykładu jako procesu dokumentów podpisanych. Przepisuje plik i nie testuje zachowania ani ważności podpisu. Zachowaj podpisany oryginał i stosuj proces odpowiedni do wymagań podpisu.

Źródła

  1. pypdf: dodawanie stempla lub znaku wodnego — kolejność nakładek, scalanie i wskazówki obrotu.
  2. pypdf: API PageObject — pola stron, przenoszenie obrotu i scalanie z transformacją.
  3. Dokumentacja projektu svglib — konwersja SVG do ReportLab, jednostki i ograniczenia funkcji.
  4. Poradnik grafiki ReportLab — obiekty rysunkowe i renderowanie PDF w systemie grafiki ReportLab.
  5. pypdf 6.10.0: wersja nagłówka PDF — deklarowanie wersji obsługującej funkcje wyniku.

Zachowaj natywną grafikę, jeśli ją masz. Gdy pozostało tylko odpowiednie rastrowe logo, przygotuj edytowalny SVG za pomocą PerfectVector, sprawdź grafikę, a następnie oddzielnie zweryfikuj nowy scalony PDF.

Więcej z bloga

PerfectVector

Zacznij od czystszego SVG,
który łatwiej edytować

Bez karty kredytowej