PerfectVector
Por Irene Kim

Adicione uma marca-d'água SVG a um PDF com Python

Mescle uma marca-d'água SVG a um PDF com svglib, ReportLab e pypdf. Confira opacidade, páginas recortadas, rotação, texto preservado e ordem da sobreposição.

13 min de leitura
Nesta página

Para adicionar uma marca-d'água SVG a um PDF existente em Python, converta o SVG em um objeto de desenho do ReportLab, renderize-o em um pequeno PDF de sobreposição e mescle essa camada a cada página original com pypdf. Defina tamanho e posição pelo quadro de recorte visível de cada página e normalize a rotação antes de mesclar.

Use over=True quando a marca precisar continuar visível acima de um fundo opaco de página. Uma marca translúcida também pode ficar por cima. Com over=False, a mesma arte vai para baixo do conteúdo existente e pode desaparecer atrás de uma página preenchida.

O exemplo completo abaixo cria seu próprio SVG e um PDF de origem com três páginas. Ele inclui tamanhos de página diferentes, um quadro de recorte com origem diferente de zero e uma página girada. O resultado é um novo PDF; preserve o original separadamente.

Escolha primeiro a operação no documento

Ponto de partidaFluxo útil
Um PDF existente precisa de um emblema SVG em cada páginaRenderizar uma camada SVG e mesclá-la às páginas existentes, como abaixo
Um novo PDF é composto do zeroUsar diretamente o fluxo de desenho SVG com ReportLab
Um SVG existente já contém a arte corretaPreservar essa origem vetorial nativa; o traçado é desnecessário
Um logo só existe como imagem rasterPreparar primeiro a arte adequada e testar a mesclagem PDF separadamente

O guia de marca-d'água do pypdf diferencia conteúdo colocado acima e abaixo de uma página. Aqui, “marca-d'água” significa o emblema visível; over determina sua ordem real de empilhamento. Não é uma marca de segurança, um método contra adulteração ou um fluxo para documentos assinados.

1. Comece com um SVG pequeno e suportado

Este teste usa dois caminhos preenchidos: uma moldura triangular com centro aberto e três barras. Os dois caminhos têm fill-opacity="0.25". Não há texto, filtros, imagens incorporadas ou recursos externos.

Essa entrada restrita importa. O svglib suporta um subconjunto útil de SVG, mas suas limitações documentadas incluem suporte limitado a CSS e recursos não suportados, como máscaras e foreignObject. Um SVG que parece correto no navegador ainda precisa ser verificado após a conversão. Se o arquivo contém raster, leia como encontrar imagens incorporadas em SVG antes de tratar o resultado como inteiramente vetorial.

Uma versão inicial desta amostra continha texto SVG além da moldura. No conjunto de ferramentas testado, o texto convertido era pintado com opacidade total, enquanto o caminho preservava opacidade 0,25. Substituí esse texto por arte original em caminhos e testei a substituição. O resultado final comprova este caso apenas com caminhos; não comprova opacidade correta para toda combinação de texto ou estilo SVG.

2. Execute o exemplo completo

O ambiente testado foi Python 3.12.14, svglib 2.2.0, ReportLab 4.4.9 e pypdf 6.10.0. A documentação atual do pypdf também descreve versões mais novas; portanto, diferencie as versões testadas da versão da documentação.

Em um ambiente separado, instale essas versões:

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

Salve o conteúdo abaixo como watermark_demo.py. Ele inclui toda a arte de entrada e cria os três PDFs, por isso não falta um PDF ou SVG de exemplo para baixar.

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)

Execute:

python watermark_demo.py

Abra watermark-output/source.pdf, overlay-top.pdf e underlay.pdf. A primeira página deve mostrar o emblema na saída com sobreposição superior, mas ocultá-lo na inferior, porque seu fundo original é opaco. As outras duas páginas exercitam recorte e rotação em vez de repetir a mesma geometria de página.

Para seu próprio PDF comum e não assinado, preserve make_overlay() e add_artwork() e chame:

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

O gravador preserva a versão do cabeçalho de origem com um mínimo de PDF 1.4, porque a sobreposição usa transparência. No pypdf 6.10.0, apenas clonar o documento pode deixar o cabeçalho padrão PDF 1.3 do gravador, mesmo quando a origem declara uma versão posterior. A atribuição explícita evita esse rebaixamento; não é uma validação de conformidade. A documentação do cabeçalho do gravador recomenda uma versão que suporte todos os recursos usados no arquivo.

As verificações de proteção rejeitam entradas criptografadas e campos de formulário detectados. Não são uma auditoria completa de compatibilidade ou assinaturas. Documentos assinados, formulários, anotações, criptografia e requisitos específicos exigem outro fluxo e validação antes de substituir um arquivo em uso.

3. Posicione a arte nas coordenadas visíveis da página

Um ponto PDF equivale a 1/72 de polegada. Nesta versão do svglib, as dimensões SVG sem unidade de 240 × 80 produzem um desenho de 180 × 60 pontos: um pixel CSS corresponde a 0,75 ponto. O código lê drawing.width e drawing.height em vez de supor que pixels SVG já equivalem a pontos PDF.

O exemplo deixa o viewport SVG com 65% da largura visível de cada página, preserva sua proporção e centraliza esse viewport. Ele centraliza o retângulo de desenho declarado, não a massa visual do emblema. O valor de 65% é uma escolha de layout do exemplo, não um padrão de marca-d'água. Essa escala baseada apenas na largura serve para o emblema largo mostrado. Uma substituição alta pode ultrapassar a altura visível da página; ao adaptar a função, limite a escala tanto pela largura quanto pela altura disponíveis e inspecione cada página antes de usar seu próprio PDF.

PáginaCondição da origemQuadro de recorte visível da saída, em pontosLargura nominal do viewport SVG
1Página horizontal de 420 × 300De 0, 0 a 420, 300273pt
2Página de 300 × 420, recortada para dentroDe 10, 20 a 290, 400182pt
3Página de 360 × 260 com rotação de 90°De 0, 0 a 260, 360 após normalização169pt

Chamar transfer_rotation_to_content() transfere a rotação para o conteúdo e os quadros de página. Não endireita as palavras originais em relação ao leitor. O texto original da terceira página mantém a orientação exibida na origem, enquanto o novo emblema é colocado nas coordenadas visíveis normalizadas. A API de página do pypdf recomenda transferir a rotação antes de mesclar páginas.

A sobreposição começa em sua própria origem. Transladá-la por cropbox.left e cropbox.bottom a coloca na região visível de destino mesmo quando o quadro de recorte começa fora de zero. expand=False impede a mesclagem de ampliar os quadros de página.

4. Confira o PDF salvo, não apenas a prévia SVG

Comparação real de PDFs renderizados com camada inferior oculta, sobreposição superior translúcida visível, página vertical recortada e página de origem girada com emblema vetorial centralizado
Renderizações reais do Poppler dos PDFs salvos. Os fundos originais escondem a camada inferior; a sobreposição superior permanece visível. As últimas duas vistas conferem uma origem de recorte diferente de zero e uma página originalmente girada.

Reabri os PDFs salvos e inspecionei fluxos de conteúdo, quadros de página, rotação, texto extraído e recursos de imagem; depois renderizei os quadros de recorte visíveis com Poppler 26.02 a 96dpi. A inspeção final passou em 40 verificações. Uma comparação no navegador também verificou hashes dos arquivos salvos, renderizações carregadas e 12 amostras de pixels em resolução nativa.

Verificação nesta amostraResultado observado
Contagem de páginas e texto original conhecidoAs três páginas e o texto esperado da origem permaneceram nas duas saídas
Geometria da sobreposiçãoDois caminhos preenchidos, 18 vértices inspecionados e alfa de preenchimento 0,25 em cada página
Recursos de imagem adicionadosNenhum nestas páginas originais e de saída
Posição e quadros de páginaCorresponderam aos quadros declarados e cálculos de transformação serializados; a rotação de saída era zero
Amostras da moldura e do centro abertoPixels da moldura superior corresponderam à mistura esperada dentro de ±2 valores RGB; centros abertos preservaram o fundo
Camada inferior em fundo opacoAs renderizações de origem e inferior da primeira página diferiram em zero pixels a 96dpi

A auditoria de posicionamento inicialmente falhou em uma comparação rigorosa com coordenadas ideais de ponto flutuante, com tolerância de 0,00001pt. O ReportLab serializa matrizes de transformação com precisão finita. Preservei esse resultado de falha, calculei de forma independente as matrizes serializadas esperadas e seu limite de arredondamento e comparei os vértices salvos com esses valores. A maior diferença observada em relação às coordenadas ideais foi de cerca de 0,000054pt, dentro do limite derivado de 0,000086pt. Isso verifica o posicionamento visualmente centralizado nestes arquivos; não é uma garantia universal de precisão.

Para uma verificação visual local rápida com Poppler instalado:

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

Inspecione cada página com um zoom útil. Também selecione ou extraia o texto original esperado, confira as dimensões e inspecione se a marca-d'água continuou como caminhos ou introduziu imagens. Uma captura nítida sozinha não comprova essas propriedades. Este teste não mediu impressão, compatibilidade de visualizadores, economia de tamanho ou resistência à remoção da marca-d'água.

Corrija a falha na etapa certa

SintomaO que verificar ou mudar
A marca desapareceCompare over=False com over=True; procure fundos originais opacos
A marca fica fora de posição em uma página recortadaUse tamanho e origem do quadro de recorte, não um tamanho de página fixo
Páginas giradas orientam a marca de modo inesperadoNormalize a rotação antes de calcular as dimensões visíveis; inspecione a vista resultante
Opacidade ou estilo mudamSimplifique o SVG, inspecione o estado de pintura do PDF convertido e teste novamente esse arquivo exato
Permanece uma região fotográfica ou pixeladaInspecione o SVG em busca de imagens incorporadas; uma embalagem SVG não transforma bitmap em caminhos
Texto, formulários ou comportamento do documento mudamPare e valide os recursos reais do documento em vez de supor que uma mesclagem bem-sucedida preservou tudo

O PDF original continua sendo conteúdo de página; o script não rasteriza páginas inteiras. Isso não prova que todo PDF de entrada sobreviverá inalterado a uma mesclagem arbitrária. Use uma cópia e defina quais conteúdos e recursos precisam continuar funcionando antes de processar um lote maior.

Prepare a arte ausente antes de mesclar

Se um logo existente já é um SVG limpo, preserve-o. Se só resta um logo raster plano adequado, o vetorizador de logos do PerfectVector pode ajudar a preparar arte editável antes desta etapa em Python. Inspecione silhueta, furos e pequenos detalhes recuperados; depois repita a conversão SVG e as verificações do PDF salvo. Este exemplo usou caminhos nativos originais e não executou uma conversão com o PerfectVector. A vetorização não mescla páginas, não valida assinaturas nem garante compatibilidade com svglib.

Perguntas frequentes

O pypdf pode ler uma marca-d'água SVG diretamente? Este exemplo primeiro converte o SVG por svglib e ReportLab em uma sobreposição PDF. O pypdf então mescla conteúdo de páginas PDF; não interpreta o código-fonte SVG nesta sequência.

Por que uma marca-d'água desaparece atrás da página PDF? Uma camada inferior pode ser coberta por um fundo opaco existente. Use over=True para uma sobreposição superior visível e defina a opacidade da arte separadamente. Confira o resultado real em cada página.

Isso preserva a arte vetorial e o texto original? O emblema testado, composto apenas por caminhos, permaneceu como caminhos preenchidos, e o texto original esperado continuou extraível nas três páginas. Recursos SVG e PDF fora desta amostra precisam de validação separada.

Regravar o PDF preserva uma assinatura digital? Não use este exemplo como fluxo para documentos assinados. Ele regrava o arquivo e não testa a preservação ou validade de assinaturas. Preserve o original assinado e use um fluxo apropriado aos requisitos de assinatura.

Fontes

  1. pypdf: Adicionar um carimbo ou marca-d'água — Ordem superior e inferior, mesclagem e orientação de rotação.
  2. pypdf: API PageObject — Quadros de página, transferência de rotação e mesclagem transformada.
  3. Documentação do projeto svglib — Conversão SVG para ReportLab, unidades e limites dos recursos suportados.
  4. Guia de gráficos do ReportLab — Objetos de desenho e renderização PDF no sistema gráfico ReportLab.
  5. pypdf 6.10.0: Versão do cabeçalho PDF — Declara uma versão PDF que suporta os recursos usados na saída.

Preserve a arte nativa quando já tiver uma. Quando só restar um logo raster plano adequado, prepare um SVG editável com o PerfectVector, inspecione a arte e verifique separadamente o PDF recém-mesclado.

Mais do blog

PerfectVector

Recomece com um SVG mais limpo
e fácil de editar

Sem cartão de crédito