Как добавить водяной знак SVG в готовый PDF через Python
Добавьте водяной знак SVG в готовый PDF с svglib, ReportLab и pypdf. Проверьте прозрачность, обрезку, поворот, сохранение текста и порядок наложения.
На этой странице
- Сначала выберите операцию над документом
- 1. Начните с небольшого поддерживаемого SVG
- 2. Запустите полный пример
- 3. Разместите рисунок в координатах видимой страницы
- 4. Проверьте сохранённый PDF, а не только предпросмотр SVG
- Исправьте ошибку на нужном этапе
- Подготовьте отсутствующий рисунок до объединения
- Частые вопросы
- Источники
Чтобы добавить водяной знак 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, 300 | 273pt |
| 2 | Страница 300 × 420 с внутренней обрезкой | От 10, 20 до 290, 400 | 182pt |
| 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, изучил потоки содержимого, рамки, поворот, извлечённый текст и ресурсы изображений, затем отрисовал видимые рамки обрезки 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 цифровую подпись? Не используйте этот пример как процесс подписанных документов. Он переписывает файл и не проверяет сохранение или действительность подписи. Сохраните подписанный оригинал и используйте подходящий его требованиям процесс.
Источники
- pypdf: Добавление штампа или водяного знака — Порядок верхнего и нижнего слоёв, объединение и поворот.
- pypdf: PageObject API — Рамки страниц, перенос поворота и объединение с преобразованием.
- Документация проекта svglib — Преобразование SVG в ReportLab, единицы и ограничения поддержки.
- Руководство ReportLab по графике — Объекты рисунка и отрисовка PDF в системе графики ReportLab.
- pypdf 6.10.0: Версия заголовка PDF — Объявление версии PDF, поддерживающей использованные возможности результата.
Сохраняйте исходный вектор, если он есть. Если остался только подходящий растровый логотип, подготовьте редактируемый SVG в PerfectVector, проверьте рисунок, а затем отдельно проверьте вновь объединённый PDF.
Ещё из блога

SVG в fpdf2: сохраните контуры в PDF на Python
Поместите SVG-логотип в PDF на Python через fpdf2, задайте размер, проверьте сохранённые контуры и найдите причину пропавшего текста или фигур перед переходом к растру.

SVG в PDFKit: векторный логотип без растеризации
Вставьте полный SVG-логотип в документ PDFKit, задайте размер в пунктах и проверьте сохранённый PDF: контуры, отверстия, пропавшие свойства и встроенные изображения.
