PerfectVector
Автор: Irene Kim

Как добавить водяной знак SVG в готовый PDF через Python

Добавьте водяной знак SVG в готовый PDF с svglib, ReportLab и pypdf. Проверьте прозрачность, обрезку, поворот, сохранение текста и порядок наложения.

11 мин чтения
На этой странице

Чтобы добавить водяной знак SVG в существующий PDF на Python, преобразуйте SVG в рисунок ReportLab, отрисуйте его в небольшой накладываемый PDF и объедините с каждой исходной страницей через pypdf. Размер и положение слоя рассчитывайте по видимой рамке обрезки страницы, а её поворот нормализуйте до объединения.

Используйте over=True, если знак должен оставаться видимым поверх непрозрачного фона страницы. Полупрозрачный знак тоже может находиться сверху. При over=False тот же рисунок помещается под существующим содержимым и может скрыться за залитой страницей.

Полный пример ниже создаёт собственные SVG и исходный трёхстраничный PDF. Он включает разные размеры страниц, рамку обрезки с ненулевым началом и повёрнутую страницу. Результат — новый PDF; храните оригинал отдельно.

Сначала выберите операцию над документом

Исходная ситуацияПодходящий процесс
Готовому PDF нужна эмблема SVG на каждой страницеСоздать слой PDF из SVG и объединить с существующими страницами, как ниже
Новый PDF создаётся с нуляНапрямую использовать процесс рисования SVG в ReportLab
Существующий SVG уже содержит нужный рисунокСохранить исходный вектор; трассировка не нужна
Логотип есть только в растреСначала подготовить подходящий рисунок, затем отдельно проверить объединение PDF

Руководство pypdf по водяным знакам различает содержимое над страницей и под ней. Здесь «водяной знак» означает видимую эмблему; over задаёт её реальный порядок наложения. Это не защитная метка, метод предотвращения изменений или процесс работы с подписанными документами.

1. Начните с небольшого поддерживаемого SVG

Тест использует два залитых контура: треугольную рамку с открытым центром и три полосы. Оба имеют fill-opacity="0.25". Текста, фильтров, встроенных изображений и внешних ресурсов нет.

Такое ограничение важно. svglib поддерживает полезную часть SVG, но его документированные ограничения включают ограниченную поддержку CSS и отсутствие масок и foreignObject. Правильный вид SVG в браузере ещё требует проверки после конвертации. Если файл содержит растр, прочитайте как найти встроенные изображения в SVG, прежде чем считать результат полностью векторным.

Начальная версия образца содержала текст SVG вместе с рамкой. В проверенном стеке преобразованный текст рисовался полностью непрозрачным, а контур сохранял 0,25. Я заменил текст собственными контурами и проверил замену. Итог подтверждает только случай с контурами, а не правильную прозрачность любой комбинации текста и стилей SVG.

2. Запустите полный пример

Проверенное окружение: Python 3.12.14, svglib 2.2.0, ReportLab 4.4.9 и pypdf 6.10.0. Актуальная документация pypdf также описывает более новые выпуски, поэтому различайте проверенные версии и версию документации.

В отдельном окружении установите эти версии:

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

Сохраните следующий код как watermark_demo.py. Он включает все исходные рисунки и создаёт три PDF, поэтому скачивать недостающий образец PDF или SVG не нужно.

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)

Запустите:

python watermark_demo.py

Откройте watermark-output/source.pdf, overlay-top.pdf и underlay.pdf. На первой странице эмблема должна быть видна в верхнем наложении и скрыта в нижнем из-за непрозрачного исходного фона. Другие две страницы проверяют обрезку и поворот, а не повторяют ту же геометрию.

Для своего обычного неподписанного PDF сохраните make_overlay() и add_artwork(), затем вызовите:

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

Записывающий объект сохраняет версию заголовка источника не ниже PDF 1.4, поскольку слой использует прозрачность. В pypdf 6.10.0 одно клонирование может оставить стандартный заголовок PDF 1.3, даже если источник объявляет более новую версию. Явное присваивание избегает этого понижения, но не проверяет соответствие стандарту. Документация заголовка writer рекомендует версию, поддерживающую все использованные возможности.

Проверки отвергают зашифрованные файлы и обнаруженные поля форм. Это не полный аудит совместимости или подписей. Подписанные документы, формы, аннотации, шифрование и особые требования требуют отдельного процесса и проверки до замены рабочего файла.

3. Разместите рисунок в координатах видимой страницы

Пункт PDF равен 1/72 дюйма. В этой версии svglib безразмерные 240 × 80 SVG дают рисунок 180 × 60 пунктов: один CSS-пиксель отображается в 0,75 пункта. Код читает drawing.width и drawing.height, а не считает пиксели SVG равными пунктам PDF.

Пример делает область SVG равной 65% ширины каждой видимой страницы, сохраняет пропорции и центрирует эту область. Центрируется объявленный прямоугольник рисунка, а не визуальная масса эмблемы. 65% — пример компоновки, а не стандарт водяных знаков. Масштабирование только по ширине подходит показанной широкой эмблеме. Высокая замена может выйти за высоту страницы; при адаптации ограничьте масштаб и шириной, и высотой и осмотрите каждую страницу до работы со своим PDF.

СтраницаИсходное состояниеВидимая рамка обрезки на выходе, в пунктахНоминальная ширина области SVG
1Альбомная страница 420 × 300От 0, 0 до 420, 300273pt
2Страница 300 × 420 с внутренней обрезкойОт 10, 20 до 290, 400182pt
3Страница 360 × 260 с поворотом 90°От 0, 0 до 260, 360 после нормализации169pt

Вызов transfer_rotation_to_content() переносит поворот в содержимое и рамки страницы. Он не выпрямляет исходные слова относительно зрителя. Текст третьей страницы сохраняет ориентацию отображаемого источника, а новая эмблема размещается в нормализованных видимых координатах. API страниц pypdf рекомендует переносить поворот до объединения.

Слой начинается в собственном начале координат. Перенос на cropbox.left и cropbox.bottom помещает его в видимую область цели даже при ненулевом начале обрезки. expand=False не позволяет объединению увеличить рамки страницы.

4. Проверьте сохранённый PDF, а не только предпросмотр SVG

Реальная отрисовка PDF: скрытый нижний слой, видимый полупрозрачный верхний слой, обрезанная книжная страница и повёрнутый источник с центрированной векторной эмблемой
Реальные отрисовки сохранённых PDF через Poppler. Исходные фоны скрывают нижний слой; верхний остаётся виден. Последние два вида проверяют ненулевое начало обрезки и исходно повёрнутую страницу.

Я заново открыл PDF, изучил потоки содержимого, рамки, поворот, извлечённый текст и ресурсы изображений, затем отрисовал видимые рамки обрезки Poppler 26.02 при 96dpi. Итоговая проверка файлов прошла 40 тестов. Сравнение в браузере также проверило хеши сохранённых файлов, загруженные отрисовки и 12 пиксельных точек в нативном разрешении.

Проверка образцаНаблюдаемый результат
Число страниц и известный исходный текстВсе три страницы и ожидаемый текст сохранились в обоих результатах
Геометрия слояДва залитых контура, 18 проверенных вершин, альфа заливки 0,25 на каждой странице
Добавленные ресурсы изображенийОтсутствуют в этих исходных и итоговых страницах
Положение и рамки страницСовпали с заявленными рамками обрезки и расчётами записанных преобразований; итоговый поворот нулевой
Точки рамки и открытого центраПиксели верхней рамки совпали с ожидаемым смешением с допуском ±2 RGB; открытые центры сохранили фон
Нижний слой за непрозрачным фономИсходная первая страница и нижний слой отличались на ноль пикселей при 96dpi

Проверка размещения сначала не прошла строгое сравнение с идеальными координатами при допуске 0,00001pt. ReportLab записывает матрицы с конечной точностью. Я сохранил неуспешный результат, независимо рассчитал ожидаемые записанные матрицы и границу округления, затем сравнил сохранённые вершины с ними. Максимальное отличие от идеальных координат было около 0,000054pt, внутри вычисленной границы 0,000086pt. Это проверяет визуально центрированное положение в этих файлах, но не даёт универсальной гарантии точности.

Для быстрой локальной визуальной проверки с установленным 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

Осмотрите каждую страницу при полезном увеличении. Также выделите или извлеките ожидаемый исходный текст, проверьте размеры и выясните, остался ли знак контурами или добавил изображения. Одного чёткого снимка недостаточно для подтверждения этих свойств. Тест не измерял печать, совместимость просмотрщиков, сокращение размера файла или устойчивость к удалению знака.

Исправьте ошибку на нужном этапе

СимптомЧто проверить или изменить
Знак исчезаетСравнить over=False и over=True; искать непрозрачный исходный фон
На обрезанной странице знак смещёнИспользовать размер и начало рамки обрезки вместо жёстко заданного размера страницы
На повёрнутой странице знак направлен неожиданноНормализовать поворот до расчёта видимых размеров и осмотреть результат
Меняются прозрачность или стильУпростить SVG, проверить состояние окрашивания в преобразованном PDF и повторно испытать именно этот файл
Остаётся фотографическая или пиксельная областьПроверить SVG на встроенные изображения; оболочка SVG не превращает растр в контуры
Меняются текст, формы или поведение документаОстановиться и проверить настоящие возможности документа вместо предположения, что успешное объединение сохранило всё

Исходный PDF остаётся содержимым страницы; скрипт не растеризует страницы целиком. Это не доказывает, что любой входной PDF переживёт произвольное объединение без изменений. Работайте с копией и до большой партии определите содержимое и функции, которые должны сохраниться.

Подготовьте отсутствующий рисунок до объединения

Если существующий логотип уже чистый SVG, сохраните его. Если остался только подходящий плоский растр, векторизатор логотипов PerfectVector поможет подготовить редактируемый рисунок до Python-этапа. Осмотрите силуэт, отверстия и мелкие детали, затем повторите преобразование SVG и проверки сохранённого PDF. Пример использовал собственные исходные контуры без конвертации PerfectVector. Векторизация не объединяет страницы, не проверяет подписи и не гарантирует совместимость svglib.

Частые вопросы

Может ли pypdf напрямую прочитать водяной знак SVG? Сначала пример преобразует SVG через svglib и ReportLab в слой PDF. Затем pypdf объединяет содержимое страниц PDF; исходный SVG в этом процессе он не интерпретирует.

Почему водяной знак исчезает за страницей PDF? Нижний слой может закрываться существующим непрозрачным фоном. Используйте over=True для видимого верхнего слоя, а прозрачность рисунка задавайте отдельно. Проверяйте результат на каждой странице.

Сохраняются ли векторный рисунок и исходный текст? Проверенная эмблема только из контуров осталась залитыми контурами, а ожидаемый текст извлекался на всех трёх страницах. Возможности SVG и PDF за рамками образца требуют отдельной проверки.

Сохранит ли перезапись PDF цифровую подпись? Не используйте этот пример как процесс подписанных документов. Он переписывает файл и не проверяет сохранение или действительность подписи. Сохраните подписанный оригинал и используйте подходящий его требованиям процесс.

Источники

  1. pypdf: Добавление штампа или водяного знака — Порядок верхнего и нижнего слоёв, объединение и поворот.
  2. pypdf: PageObject API — Рамки страниц, перенос поворота и объединение с преобразованием.
  3. Документация проекта svglib — Преобразование SVG в ReportLab, единицы и ограничения поддержки.
  4. Руководство ReportLab по графике — Объекты рисунка и отрисовка PDF в системе графики ReportLab.
  5. pypdf 6.10.0: Версия заголовка PDF — Объявление версии PDF, поддерживающей использованные возможности результата.

Сохраняйте исходный вектор, если он есть. Если остался только подходящий растровый логотип, подготовьте редактируемый SVG в PerfectVector, проверьте рисунок, а затем отдельно проверьте вновь объединённый PDF.

Ещё из блога

PerfectVector

Начните с более чистого SVG,
который проще редактировать

Без банковской карты