Генерация PDF из Python: типичные ошибки и как их избежать
Битые шрифты, разъезжающиеся таблицы и молчаливые обрезки страниц — самые частые проблемы при генерации PDF из Python. Разбираем, почему они возникают в WeasyPrint, ReportLab и fpdf2, и как их не допустить.

Вы собрали отчёт, открыли PDF — а вместо кириллицы прямоугольники. Или таблица уехала за край и правый столбец обрезался без единого предупреждения. Или файл весит 40 МБ из-за одной фоновой картинки. Все три проблемы решаются, но по-разному в зависимости от библиотеки. Ниже — четыре библиотеки, их слабые места и конкретные приёмы, которыми эти места закрываются.
Речь о трёх популярных инструментах: ReportLab (низкоуровневая отрисовка), fpdf2 (форк старого PyFPDF, тоже императивный) и WeasyPrint (HTML/CSS в PDF). Версии на момент написания: ReportLab 4.x, fpdf2 2.7.x, WeasyPrint 62.x. Поведение шрифтов и таблиц между этими библиотеками отличается принципиально, поэтому ошибка «одна на всех» — это миф.
Ошибка 1. Кириллица превращается в квадраты
Самая частая жалоба. Причина одна: стандартные PDF-шрифты (Helvetica, Times, Courier) содержат только Latin-1. Кириллицы там физически нет, поэтому символы либо теряются, либо заменяются на прямоугольники. Лечение — подключить TrueType-шрифт с нужными глифами.
ReportLab: регистрируем шрифт вручную
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# Регистрируем TTF-файл под именем, которое будем использовать дальше
pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSans.ttf"))
c = canvas.Canvas("otchet.pdf")
c.setFont("DejaVu", 14) # без этой строки будет Helvetica и квадраты
c.drawString(72, 800, "Отчёт за квартал")
c.save()
Ключевой момент: путь к .ttf должен указывать на реальный файл. Шрифт DejaVu Sans свободный и покрывает кириллицу; системный Arial тоже подойдёт, но у него лицензия на распространение. Файл шрифта кладите в репозиторий рядом с кодом, а не полагайтесь на то, что он есть на сервере.
fpdf2: почти то же самое
from fpdf import FPDF
pdf = FPDF()
pdf.add_font("DejaVu", "", "DejaVuSans.ttf") # uni=True не нужен, fpdf2 всегда unicode
pdf.set_font("DejaVu", size=14)
pdf.add_page()
pdf.cell(0, 10, "Отчёт за квартал")
pdf.output("otchet.pdf")
В старом PyFPDF требовался параметр uni=True — в fpdf2 его выбросили, шрифты всегда в Unicode. Если вы копируете код из старых ответов на форумах, он сломается именно на этом.
WeasyPrint: шрифт задаётся через CSS
Здесь отдельной регистрации нет. WeasyPrint берёт шрифты через @font-face или из системы. Если в системе нет шрифта с кириллицей — будут те же квадраты. Явное подключение:
from weasyprint import HTML
html = """
<style>
@font-face {
font-family: 'DejaVu';
src: url('DejaVuSans.ttf');
}
body { font-family: 'DejaVu', sans-serif; }
</style>
<body>Отчёт за квартал</body>
"""
# base_url нужен, чтобы относительный путь к шрифту разрешился
HTML(string=html, base_url=".").write_pdf("otchet.pdf")
Забытый base_url — вторая по частоте причина, по которой в WeasyPrint не грузятся ни шрифты, ни картинки: относительные пути ему не от чего отсчитывать.
Ошибка 2. Таблица уезжает за край и данные обрезаются молча
Опасность здесь не в том, что верстка кривая, а в том, что ошибки нет. PDF генерируется, файл открывается, а правого столбца просто не видно. Для отчёта с суммами это хуже, чем явный краш.
Где именно ломается в каждой библиотеке
- ReportLab. Объект
TableвнутриPlatypusсам не переносит ячейки на новую страницу, если строка выше доступной высоты фрейма. Длинный текст в ячейке безParagraphне переносится по словам, а вылезает за границу. - fpdf2. Метод
cellс фиксированной шириной обрезает текст без переноса. Для переноса нуженmulti_cell— но он ломает автоматическое построение строки таблицы, если не следить за координатами. - WeasyPrint. Ведёт себя как браузер:
table-layout: fixedплюсword-wrap: break-wordобычно спасают, но широкая таблица безmax-widthчестно вылезет за поле страницы и обрежется по краю листа.
Рабочий приём для ReportLab: оборачивать текст в Paragraph
from reportlab.platypus import Table, Paragraph, SimpleDocTemplate
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
styles = getSampleStyleSheet()
cell = styles["BodyText"] # у этого стиля включён перенос по словам
# Длинный текст оборачиваем в Paragraph, иначе он вылезет за ячейку
data = [
["№", Paragraph("Наименование очень длинной позиции в накладной", cell)],
["1", Paragraph("Кабель UTP cat.6, 305 м, бухта", cell)],
]
# Явно задаём ширину столбцов — сумма не должна превышать ширину фрейма
table = Table(data, colWidths=[1 * cm, 15 * cm])
doc = SimpleDocTemplate("nakladnaya.pdf")
doc.build([table])
Голая строка вместо Paragraph — самая частая причина обрезки в ReportLab. Ширина строкового текста не считается и не переносится.
PDF, который открылся без ошибки, — это ещё не PDF с правильными данными. Обрезанный столбец не бросает исключение. Проверяйте результат глазами на самой длинной строке, а не на тестовой «строке 1».
Ошибка 3. Разрыв страницы посреди таблицы или заголовка
Заголовок раздела остаётся внизу страницы, а его содержимое переезжает на следующую. Или шапка таблицы печатается один раз, а на второй странице строки идут без заголовков столбцов.
- WeasyPrint: используйте CSS
break-inside: avoidдля блоков, которые нельзя рвать, и<thead>в таблице — тогда шапка повторяется на каждой странице автоматически. - ReportLab: оборачивайте заголовок и первый абзац в
KeepTogether, а для повтора шапки таблицы задавайтеrepeatRows=1в конструктореTable. - fpdf2: контролируйте разрыв вручную через проверку
pdf.get_y()перед добавлением блока, либо через контекстный менеджер таблиц — сверьтесь с актуальным разделом документации, API таблиц в fpdf2 в версиях 2.7.x менялся.
Ошибка 4. Файл весит десятки мегабайт
Причина почти всегда одна — растровые картинки в исходном разрешении. Фото со смартфона на 4000×3000 пикселей, вставленное как логотип шириной 3 см, тащит за собой все свои мегабайты. Ни одна из библиотек не пережимает изображение автоматически под целевой размер на странице.
Уменьшайте картинку до нужного разрешения до вставки. Для листа при печати 150–300 dpi достаточно; при ширине 3 см это порядка 350–700 пикселей, а не 4000. Пережимайте через Pillow перед подачей в генератор, а векторную графику (логотип, схемы) держите в SVG — WeasyPrint рисует его как вектор без потери качества и без веса растра.
Что выбрать под задачу
| Задача | Библиотека | Почему |
|---|---|---|
| Отчёт со сложной вёрсткой, стили как на сайте | WeasyPrint | HTML/CSS уже знаком, переносы и шапки таблиц из коробки |
| Точное попиксельное размещение, чертёж, бланк | ReportLab | прямой контроль координат на canvas |
| Простой документ, минимум зависимостей | fpdf2 | чистый Python, ставится без системных библиотек |
| Заполнение готовой PDF-формы | другой инструмент | генераторы выше не редактируют существующий PDF — это отдельная тема |
Отдельно про установку: WeasyPrint тянет системные зависимости (Pango, cairo) и на голом сервере без них не запустится — это его главный минус в проде. ReportLab и fpdf2 — чистый Python и ставятся без хлопот.
Если не получилось
- Квадраты вместо букв, шрифт зарегистрирован. Проверьте, что вызвали
setFont/set_fontименно тем именем, под которым регистрировали, и что глифы кириллицы вообще есть в этом TTF — не все шрифты их содержат. - WeasyPrint:
cannot load library 'libpango'. Не установлены системные зависимости. На Debian/Ubuntu ставятся отдельными пакетами — сверьтесь с разделом установки в документации WeasyPrint для вашей ОС. - fpdf2:
Not enough horizontal spaceв multi_cell. Ширина ячейки меньше одного слова с учётом отступов. Уменьшите шрифт или увеличьте ширину. - ReportLab:
LayoutError: too large on page. Элемент выше доступной высоты фрейма и не может быть перенесён. Разбейте таблицу или уменьшите содержимое ячейки.
Что дальше
Соберите тестовый набор из самых злых данных: длиннейшая строка, таблица на 200 строк, символы юникода за пределами кириллицы (тире, кавычки-ёлочки, значок рубля). Прогоняйте генерацию на нём в CI и открывайте результат глазами. Следующий шаг — вынести шаблоны в отдельные файлы: для WeasyPrint это HTML+CSS через Jinja2, для ReportLab — параметризованная функция сборки. Тему заполнения полей в готовых PDF и слияния файлов здесь не трогали — это отдельные библиотеки.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: WeasyPrint — официальная документация, ReportLab User Guide (PDF)
Частые вопросы
У меня другая версия fpdf2, код с add_font не работает
Почему в WeasyPrint не грузится картинка, хотя путь верный
Какой шрифт взять, чтобы точно была кириллица и не было проблем с лицензией
Можно ли этими библиотеками отредактировать уже готовый PDF
Что делать, если PDF генерируется в проде под нагрузкой и упирается в память
Как поймать обрезку столбца до того, как это увидит клиент
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.