PerfectVector
Por Irene Kim

Agrega una marca de agua SVG a un PDF con Python

Combina una marca de agua SVG con un PDF existente usando svglib, ReportLab y pypdf. Revisa opacidad, recorte, rotación, texto conservado y orden de superposición.

13 min de lectura
En esta página

Para agregar una marca de agua SVG a un PDF existente en Python, convierte el SVG en un objeto de dibujo de ReportLab, renderízalo en un pequeño PDF de superposición y combina esa capa con cada página original mediante pypdf. Define su tamaño y posición a partir del cuadro de recorte visible de cada página y normaliza la rotación antes de combinar.

Usa over=True cuando la marca deba permanecer visible sobre un fondo de página opaco. Una marca translúcida también puede ir encima. Con over=False, la misma imagen queda debajo del contenido existente y puede desaparecer detrás de una página rellena.

El ejemplo completo siguiente crea su propio SVG y un PDF fuente de tres páginas. Incluye distintos tamaños de página, un cuadro de recorte cuyo origen no es cero y una página rotada. El resultado es un PDF nuevo; conserva el archivo original por separado.

Elige primero la operación sobre el documento

Punto de partidaFlujo útil
Un PDF existente necesita un emblema SVG en cada páginaRenderizar una capa SVG y combinarla con las páginas existentes, como abajo
Se compone un PDF nuevo desde ceroUsar directamente el flujo de dibujo SVG con ReportLab
Un SVG existente ya contiene la imagen correctaConservar esa fuente vectorial nativa; no hace falta trazarla
Un logo solo existe como imagen rasterizadaPreparar primero una imagen adecuada y probar la combinación PDF aparte

La guía de marcas de agua de pypdf distingue el contenido colocado encima y debajo de una página. Aquí, «marca de agua» es el emblema visible; over determina su orden real de apilamiento. No es una marca de seguridad, un método contra alteraciones ni un flujo para documentos firmados.

1. Empieza con un SVG pequeño y compatible

Esta prueba usa dos trazados rellenos: un marco triangular con el centro abierto y tres barras. Ambos trazados tienen fill-opacity="0.25". No hay texto, filtros, imágenes incorporadas ni recursos externos.

Ese alcance reducido importa. svglib admite un subconjunto útil de SVG, pero sus limitaciones documentadas incluyen soporte CSS limitado y funciones no compatibles como máscaras y foreignObject. Un SVG que se ve bien en el navegador aún necesita revisarse después de convertirlo. Si el archivo contiene imágenes rasterizadas, lee cómo encontrar imágenes incorporadas en SVG antes de tratar el resultado como totalmente vectorial.

Una versión inicial de esta figura de prueba contenía texto SVG además del marco. En la combinación de herramientas probada, el texto convertido se pintaba con opacidad total mientras que el trazado conservaba una opacidad de 0.25. Reemplacé ese texto con trazados originales y probé el reemplazo. El resultado final demuestra este caso de solo trazados; no demuestra una opacidad correcta para toda combinación de texto o estilos SVG.

2. Ejecuta el ejemplo completo

El entorno probado fue Python 3.12.14, svglib 2.2.0, ReportLab 4.4.9 y pypdf 6.10.0. La documentación actual de pypdf también describe versiones posteriores, así que distingue las versiones probadas de la versión de la documentación.

Instala esas versiones en un entorno separado:

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

Guarda lo siguiente como watermark_demo.py. Incluye todas las imágenes de entrada y crea los tres PDF, por lo que no faltan archivos de ejemplo PDF o SVG que debas descargar.

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)

Ejecútalo:

python watermark_demo.py

Abre watermark-output/source.pdf, overlay-top.pdf y underlay.pdf. La primera página debe mostrar el emblema en la salida con capa superior, pero ocultarlo en la salida inferior, porque su fondo original es opaco. Las otras dos páginas prueban el recorte y la rotación en vez de repetir la misma geometría de página.

Para tu propio PDF normal sin firma, conserva make_overlay() y add_artwork() y llama a:

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

El escritor conserva la versión del encabezado fuente con un mínimo de PDF 1.4, porque la capa usa transparencia. En pypdf 6.10.0, clonar el documento por sí solo puede dejar el encabezado predeterminado PDF 1.3 del escritor, incluso si la fuente declara una versión posterior. La asignación explícita evita esa reducción de versión; no es una validación de conformidad. La documentación del encabezado del escritor recomienda una versión que admita todas las funciones usadas en el archivo.

Las comprobaciones rechazan entradas cifradas y campos de formulario detectados. No son una auditoría completa de compatibilidad o firmas. Los documentos firmados, formularios, anotaciones, cifrado y requisitos particulares necesitan un flujo y una validación aparte antes de reemplazar un archivo de trabajo.

3. Coloca la imagen en las coordenadas visibles de la página

Un punto PDF equivale a 1/72 de pulgada. En esta versión de svglib, las dimensiones SVG sin unidad de 240 × 80 producen un dibujo de 180 × 60 puntos: un píxel CSS corresponde a 0.75 puntos. El código lee drawing.width y drawing.height en vez de suponer que los píxeles SVG ya equivalen a puntos PDF.

El ejemplo ajusta el área de visualización SVG al 65% de la anchura visible de cada página, conserva su proporción y centra esa área. Centra el rectángulo de dibujo declarado, no la masa visual del emblema. El 65% es una elección de composición del ejemplo, no un estándar de marca de agua. Esta escala basada solo en la anchura sirve para el emblema ancho mostrado. Un reemplazo alto puede superar la altura visible; al adaptar la función, limita la escala por la anchura y altura disponibles e inspecciona cada página antes de usar tu propio PDF.

PáginaCondición de la fuenteCuadro de recorte visible de salida, en puntosAnchura nominal del área SVG
1Página horizontal de 420 × 300De 0, 0 a 420, 300273pt
2Página de 300 × 420, recortada hacia adentroDe 10, 20 a 290, 400182pt
3Página de 360 × 260 con rotación de 90°De 0, 0 a 260, 360 tras normalizar169pt

Llamar a transfer_rotation_to_content() transfiere la rotación al contenido y a los cuadros de página. No endereza las palabras originales respecto de quien mira. El texto original de la tercera página conserva su orientación en la vista de la fuente, mientras que el nuevo emblema se coloca en las coordenadas visibles normalizadas. La API de página de pypdf recomienda transferir la rotación antes de combinar páginas.

La capa empieza en su propio origen. Trasladarla por cropbox.left y cropbox.bottom la coloca dentro de la región visible de destino incluso cuando el cuadro de recorte empieza en un lugar distinto de cero. expand=False evita que la combinación amplíe los cuadros de página.

4. Revisa el PDF guardado, no solo la vista previa SVG

Comparación real de PDF renderizados con una capa inferior oculta, una capa superior translúcida visible, una página vertical recortada y una página fuente rotada con emblema vectorial centrado
Renderizados reales de Poppler de los PDF guardados. Los fondos originales ocultan la capa inferior; la superior sigue visible. Las últimas dos vistas revisan un origen de recorte distinto de cero y una página originalmente rotada.

Reabrí los PDF guardados e inspeccioné sus flujos de contenido, cuadros de página, rotación, texto extraído y recursos de imagen; después rendericé los cuadros de recorte visibles con Poppler 26.02 a 96dpi. La inspección final pasó 40 comprobaciones. Una comparación en navegador también verificó los hashes guardados, los renderizados cargados y 12 pruebas de píxeles a resolución nativa.

Revisión de esta figura de pruebaResultado observado
Número de páginas y texto original conocidoLas tres páginas y el texto esperado de la fuente permanecieron en ambas salidas
Geometría de la capaDos trazados rellenos, 18 vértices inspeccionados, alfa de relleno 0.25 en cada página
Recursos de imagen agregadosNinguno en estas páginas originales y de salida
Colocación y cuadros de páginaCoincidieron con los cuadros declarados y los cálculos de transformación serializados; la rotación de salida fue cero
Pruebas del marco y centro abiertoLos píxeles del marco superior coincidieron con la mezcla esperada dentro de ±2 valores RGB; los centros abiertos conservaron el fondo
Capa inferior sobre fondo opacoLos renderizados fuente e inferior de la primera página difirieron en cero píxeles a 96dpi

La auditoría de colocación falló inicialmente una comparación estricta con coordenadas ideales de punto flotante y tolerancia de 0.00001pt. ReportLab serializa las matrices de transformación con precisión finita. Conservé ese resultado fallido, calculé de forma independiente las matrices serializadas previstas y su límite de redondeo, y comparé los vértices guardados con esos valores. La mayor diferencia observada frente a la coordenada ideal fue de unos 0.000054pt, dentro del límite calculado de 0.000086pt. Esto comprueba la colocación visualmente centrada en estos archivos; no es una garantía universal de precisión.

Para una revisión visual local rápida con 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

Inspecciona cada página con un zoom útil. También selecciona o extrae el texto original esperado, revisa las dimensiones e inspecciona si la marca siguió siendo trazados o introdujo imágenes. Una captura nítida por sí sola no demuestra esas propiedades. Esta prueba no midió impresión, compatibilidad de visores, ahorro de tamaño ni resistencia a la eliminación de la marca de agua.

Corrige la falla en la etapa adecuada

SíntomaQué revisar o cambiar
La marca desapareceCompara over=False con over=True; busca fondos originales opacos
La marca queda mal colocada en una página recortadaUsa el tamaño y origen del cuadro de recorte, no un tamaño de página fijo
Las páginas rotadas orientan la marca de forma inesperadaNormaliza la rotación antes de calcular las dimensiones visibles e inspecciona la vista resultante
Cambian la opacidad o el estiloSimplifica el SVG, inspecciona el estado de pintura del PDF convertido y prueba otra vez ese archivo exacto
Queda una región fotográfica o pixeladaInspecciona el SVG en busca de imágenes incorporadas; una envoltura SVG no convierte un mapa de bits en trazados
Cambian el texto, los formularios o el comportamiento del documentoDetente y valida las funciones reales del documento en vez de suponer que combinar con éxito conservó todo

El PDF original sigue siendo contenido de página; el script no rasteriza páginas completas. Eso no demuestra que todo PDF de entrada sobreviva intacto a una combinación arbitraria. Usa una copia y define qué contenido y funciones deben seguir funcionando antes de procesar un lote mayor.

Prepara la imagen faltante antes de combinar

Si un logo existente ya es un SVG limpio, consérvalo. Si solo queda un logo rasterizado plano adecuado, el vectorizador de logos de PerfectVector puede ayudar a preparar una imagen editable antes de este paso de Python. Inspecciona la silueta, los huecos y los detalles pequeños recuperados; luego repite la conversión SVG y las revisiones del PDF guardado. Este ejemplo usó trazados nativos originales y no ejecutó una conversión PerfectVector. La vectorización no combina páginas, no valida firmas ni garantiza compatibilidad con svglib.

Preguntas frecuentes

¿pypdf puede leer una marca de agua SVG directamente? Este ejemplo convierte primero el SVG mediante svglib y ReportLab en una capa PDF. Después, pypdf combina contenido de páginas PDF; no interpreta la fuente SVG en este proceso.

¿Por qué una marca de agua desaparece detrás de mi página PDF? Un fondo opaco existente puede cubrir una capa inferior. Usa over=True para una capa superior visible y configura aparte la opacidad de la imagen. Revisa el resultado real en cada página.

¿Esto conserva la imagen vectorial y el texto original? El emblema probado, hecho solo de trazados, se mantuvo como trazados rellenos y el texto original esperado siguió siendo extraíble en las tres páginas. Las funciones SVG y PDF fuera de esta figura de prueba necesitan validación aparte.

¿Reescribir el PDF conserva una firma digital? No uses este ejemplo como flujo para documentos firmados. Reescribe el archivo y no prueba la conservación ni validez de firmas. Mantén el original firmado y usa un flujo apropiado para sus requisitos de firma.

Fuentes

  1. pypdf: Agregar un sello o marca de agua — Orden de capas superiores e inferiores, combinación y rotación.
  2. pypdf: API PageObject — Cuadros de página, transferencia de rotación y combinación transformada.
  3. Documentación del proyecto svglib — Conversión SVG a ReportLab, unidades y límites de funciones compatibles.
  4. Guía de gráficos de ReportLab — Objetos de dibujo y renderizado PDF en el sistema gráfico ReportLab.
  5. pypdf 6.10.0: Versión del encabezado PDF — Declara una versión PDF compatible con las funciones usadas en la salida.

Conserva la imagen nativa cuando la tengas. Si solo queda un logo rasterizado plano adecuado, prepara un SVG editable con PerfectVector, inspecciónalo y verifica el PDF recién combinado por separado.

Más del blog

PerfectVector

Empieza con un SVG más limpio
y fácil de editar

Sin tarjeta de crédito