Найти в Дзене
Алексей Семенов

Django + PDF - обзор методов и инструментов

Оглавление

Сегодня я хотел бы показать вам инструменты и методы, доступные для создания файлов PDF из фреймворка Django . Документация самого фреймворка в этом разделе крайне скудна и очень кратко показывает только один метод, использующий инструмент reportlab.

Давайте подумаем, что нам действительно нужно , чтобы создавать PDF-документы на уровне Django ?

  • во-первых - подходящий инструмент, который позволит нам создавать PDF-документы на языке Python . Это может быть внешняя программа или модуль / библиотека Python.
  • второй - элемент (надстройка), который установит связь между кодом нашего проекта Django и этим инструментом.

Инструменты для создания PDF

Ниже приведен список самых популярных инструментов, связанных с созданием PDF-документов из Python и Django . Все перечисленные здесь инструменты и библиотеки можно использовать с Python 3.x и Django 2.x.

ReportLab

ReportLab - это бесплатный движок с открытым исходным кодом для создания динамических PDF-документов и векторной графики, написанных на Python. Более обширная платная версия PLUS также доступна на сайте производителя . Версия PLUS имеет множество дополнительных функций, в том числе он реализует собственный язык шаблонов RML (язык разметки отчетов).

Механизм ReportLab состоит из трех компонентов (слоев):

  • низкоуровневым API, который позволяет «графическое рисование», представляющем страницу PDF
  • библиотекой виджетов и диаграмм для создания графики
  • высокоуровневой библиотеки PLATYPUS (Page Layout And TYPography Using Scripts), которая позволяет программно создавать PDF-документы из таких элементов, как заголовки, абзацы, шрифты, таблицы и векторная графика.

Установка движка ReportLab сводится к команде:

$ pip install reportlab

xhtml2pdf

Xhtml2pdf - это библиотека Python, которая позволяет создавать документы PDF из содержимого HTML.

Здесь у нас есть возможность управлять потоком контента - автоматическим или ручным разделением на страницы.

Эта библиотека действует как модуль Python, который можно импортировать (например, Django). Кроме того, существует отдельная программа, которую можно запустить из командной строки.

Установить библиотеку можно следующим образом:

$ pip install xhtml2pdf

wkhtmltopdf

Еще один инструмент, который мы можем использовать для создания PDF-документов, - это внешняя программа wkhtmltopdf.

Как и xhtml2pdf, он позволяет создавать документы PDF на основе содержимого HTML. Чтобы использовать его, у нас должна быть двоичная копия программы, запущенной в операционной системе, в которой работает наш проект - это сильно ограничивает ее использование, например, в облачных средах (например, Heroku).

Установка так же проста, как загрузка скомпилированной двоичной версии программы или ее самостоятельное создание из исходного кода.

WeasyPrint

Последний инструмент, который я хотел бы здесь представить, - это WeasyPrint. Это бесплатный механизм рендеринга с открытым исходным кодом для содержимого HTML и CSS, который позволяет экспортировать в документы PDF и предназначен для соответствия стандартам печати в Интернете.

Как и xhtml2pdf, это и модуль Python, и независимая исполняемая программа.

WeasyPrint устанавливается следующим образом:

$ pip install WeasyPrint

Может оказаться, что для установки нам понадобится набор дополнительных пакетов (в случае дистрибутивов Linux, macOs) или компонентов Python (Windows).

Способы создания PDF-файлов

ReportLab

Для инструмента ReportLab пример кода представления выглядит следующим образом (низкоуровневый API):

import io
from django.http import FileResponse, HttpResponse
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics, ttfonts

def pdf_view(request):
buffer
= io.BytesIO()

pdfmetrics
.registerFont(ttfonts.TTFont('Arial', 'arial.ttf'))

pdf
= canvas.Canvas(buffer, pagesize='A4')
pdf
.setFont("Arial", 15)
pdf
.drawString(40, 500, "Пример текста")
pdf
.showPage()
pdf
.save()

response
= HttpResponse(buffer.getvalue(), content_type="application/pdf")
response
['Content-Disposition'] = 'attachment; filename="document.pdf"'
return response

Создание PDF-файлов с помощью PLATYPUS:

import tempfile
import os

from django.http import FileResponse

from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.pdfbase import pdfmetrics, ttfonts


def pdf_view(request):
path
= os.path.join(tempfile.mkdtemp(), 'document.pdf')

pdfmetrics
.registerFont(ttfonts.TTFont('Arial', 'arial.ttf'))

doc
= SimpleDocTemplate(path, pagesize=A4)
styles
= getSampleStyleSheet()
story
= [Spacer(1, 1*cm)]
style
= styles["Normal"]
style
.fontName = 'Arial'

p
= Paragraph("Пример текста", style)
for i in range(5):
story
.append(p)
story
.append(Spacer(1, 1*cm))

doc
.build(story)

pdf
= open(path, "rb")
response
= FileResponse(pdf, as_attachment=True,
filename
='doсument.pdf')
return response

xhtml2pdf

Сначала мы подготовим HTML-шаблон django, который мы будем использовать в следующих примерах представлений:

# dokument.html
<html>
<head>
<meta charset="UTF-8" />
<title>{{ title }}</title>
<style>
@page {
size: a4 portrait;
margin: 2cm;
}
@font-face {
font-family: 'Lato';
src: url(https://github.com/google/fonts/blob/master/ofl/lato/Lato-Regular.ttf?raw=True);
}
* {
font-family: 'Lato';
}
body {
background-color: #f0f0f0;
}
</style>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</body>
</html>

Воспользуемся «чистой» библиотекой xhtml2pdf , чтобы потом можно было сравнить коды представлений:

import os
from django.http import HttpResponse
from django.template import Context
from django.template.loader import get_template
from xhtml2pdf import pisa

def pdf_view(request):
response
= HttpResponse(content_type='application/pdf')
response
['Content-Disposition'] = 'attachment; filename="document.pdf"'

context
= {'title': 'xhtml2pdf', 'content': 'Пример текста'}

template
= get_template('document.html')
html
= template.render(context=context)

pdf
= pisa.CreatePDF(html, dest=response)
if pdf.err:
return HttpResponse("We had some errors!")

return response

Пример кода просмотра с использованием надстройки django-xhtml2pdf -on :

# app/views.py
from django_xhtml2pdf.utils import generate_pdf

def pdf_view(request):
response
= HttpResponse(content_type='application/pdf')
response
['Content-Disposition'] = 'attachment; filename="document.pdf"'

context
= {'title': 'django-xhtml2pdf', 'content': 'Пример текста'}

result
= generate_pdf('document.html', file_object=response, context=context)
return result

В дополнение к django-xhtml2pdf у нас уже есть функция обратного вызова, реализованная по умолчанию, и нам не нужно беспокоиться об обработке и извлечении ссылок (URL-адресов), которые будут отображаться в шаблоне HTML (локальном и удаленном).

В том случае, когда мы используем «чистую» библиотеку xhtml2pdf , мы должны написать свою функцию и передать ее методу CreatePDF .

Давайте посмотрим, как выглядит код представления класса:

from django.views.generic import TemplateView
from django_xhtml2pdf.views import PdfMixin

class PdfView(PdfMixin, TemplateView):
template_name
= "document.html"

def get_context_data(self):
return {'title': 'django-xhtml2pdf class based view', 'content': 'Пример текста'}

Django-xhtml2pdf добавить дает нам возможность использовать pdf_decorator декоратор , благодаря которому мы можем легко превратить существующий вид в вид , возвращающий PDF документ

from django.shortcuts import render
from django_xhtml2pdf.utils import pdf_decorator

@pdf_decorator(pdfname='document.pdf')
def pdf_view(request):
context
= {'title': 'django-xhtml2pdf decorated view', 'content': 'Пример текста'}
return render(request, template_name='document.html', context=context)

wkhtmltopdf

from wkhtmltopdf.views import PDFTemplateView

class PDFView(PDFTemplateView):
template_name
= 'document.html'
filename
= 'document.pdf'
cmd_options
= {
'page-size'
: 'A4',
'orientation'
: 'Portrait',
'margin-top'
: 20,
'margin-bottom'
: 20,
'margin-left'
: 20,
'margin-right'
: 20
}

def get_context_data(self):
return {'title': 'django-wkhtmltopdf', 'content': 'Пример текста'}

Как упоминалось ранее, плагин django-wkhtmltopdf использует внешнюю программу wkhtmltopdf , которая должна быть доступна для запуска из проекта django. Сначала на основе шаблона создается временный HTML-файл, затем Popen. Программа называется wkhtmltopdf , стандартный вывод которой фиксируется и возвращается в представление.

WeasyPrint

django_weasyprint предоставляет нам класс представления WeasyTempalteView, который может предоставить нам документы PDF. У нас также есть вспомогательный класс ( Mixin ) WeasyTemplateResponseMixin , который позволяет нам легко расширять существующие представления для создания документов PDF.

from django_weasyprint import WeasyTemplateResponseMixin

class MyView(TemplateView):
template_name
= "document.html"

def get_context_data(self):
return {
'title'
: 'django-weasyprint class based view',
'content'
: 'Пример текста'
}

class WeasyPrintView(WeasyTemplateResponseMixin, MyView):
pdf_filename
= 'document.pdf'

У нас есть написано представление, которое возвращает HTML-код MyView . Затем мы добавляем представление WeasyPrintView , которое расширяет существующее представление и добавляет соответствующий миксин, чтобы получить в качестве ответа документ PDF.

Сохранить PDF в файл

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

До сих пор создание PDF-документов выполнялось «на лету». На основе запроса была вызвана функция просмотра, чтобы инициировать создание документа PDF, которая сразу же вернула созданный документ в качестве ответа на запрос пользователя.

Все делалось «в памяти» без использования дискового пространства (кроме: wkhtmltopdf - для этой программы готовится временный HTML-файл, и ReportLab в режиме PLATYPUS - здесь мы создаем временный PDF-файл для SimpleDocTemplate на диске).

Возьмем для примера конкретный вариант использования. Нам нужен функционал для создания электронных счетов для наших клиентов.

У нас есть подходящие модели для представления клиентов, услуг, цен и т. Д. Мы хотим создавать счета в формате PDF один раз в месяц и автоматически отправлять их клиентам по электронной почте. Кроме того, каждый клиент должен иметь доступ к своим счетам в формате PDF на уровне своей клиентской панели.

Важный вопрос, который возникает в этом случае, - это расположение сгенерированных файлов PDF.

Если бы он просто отправлялся по электронной почте в виде вложения, мы могли бы не размещать PDF-файл на диске, а только передать содержимое документа методу отправки писем, аналогично объекту response . Мы также могли бы использовать механизм временных файлов Python .

Сгенерированные файлы PDF необходимо физически где-то хранить. Вопрос в том, подходит ли расположение MEDIA_ROOT в django? В этом случае точно НЕТ !

Любой, кто знает расположение и имя файла PDF, сможет загрузить его с нашего сервера, независимо от того, какую схему именования и расположения этих файлов мы примем.

Одна из возможностей, которую мы можем здесь использовать, - создать расположение вне MEDIA_ROOT и STATIC_ROOT (если мы решим хранить файлы на дисковом пространстве сервера) и разместить там сгенерированные файлы PDF.

Кроме того, мы должны создать механизм «обслуживания» этих файлов пользователям, оснащенный механизмами авторизации. Здесь могут быть полезны класс FileSystemStorage и поля типа db.models.FileField .

Посмотрим, как выглядит фрагмент кода, позволяющий сохранить PDF-документ в файл с помощью WeasyPrint :

from weasyprint import HTML

def save_pdf_file(pdf_path, html_string):
html
= HTML(string=html_string)
html
.write_pdf(target=pdf_path)

и аналогично скачивание такого файла:

from os import path
from django.core.files.storage import FileSystemStorage

def download_pdf_file(pdf_path, response):
fs
= FileSystemStorage(path.dirname(pdf_path))
with fs.open(path.basename(pdf_path)) as pdf:
response
= HttpResponse(pdf, content_type='application/pdf')
response
['Content-Disposition'] = f'attachment; filename="{path.basename(pdf_path)}"'
return response
return response

Эффективность

Создание PDF-документов непосредственно в представлении может потребовать дополнительных затрат времени, что с точки зрения пользователя браузера иногда может быть неприемлемым.

В таком случае следует использовать методы асинхронной генерации документов (например, с Celera).

Среднее время ответа на запрос в случае вышеупомянутых методов и представленного простого HTML-шаблона колеблется в пределах от 1000 до 1700 мс (то есть от 1 до примерно 2 секунд).

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

---

Подписывайтесь на канал, впереди еще больше полезных статей по Python https://zen.yandex.ru/id/5f56875a664c2c1f0b8dc68a

Статья была полезной - поставьте палец вверх :-)