A Django class-based view helper to generate PDF with WeasyPrint.
Requires: WeasyPrint
Optional requirements:
- matplotlib (to render plots)
Install the package by running:
pip install django-weasypdf
or:
pip install git+https://github.com/morlandi/django-weasypdf
You will probably build your own app, for example reports, to provide custom derived Views and templates:
python manage.py startapp reports
In your settings, add:
INSTALLED_APPS = [
...
'reports',
'weasypdf',
]
Note that reports is listed before weasypdf to make sure you can possibly override any template.
In your urls, add:
from django.urls import path, include
urlpatterns = [
...
path('reports/', include('reports.urls', namespace='reports')),
...
Optionally, you might want to copy the default templates from 'weasypdf/templates/weasypdf' to 'reports/templates/reports' for any required customization; see Customizing the templates below
A test view has already been included in the weasypdf app to render a sample document for demonstration purposes.
To use it, add the following to your urls:
path('weasypdf/', include('weasypdf.urls', namespace='weasypdf')),
then, visit:
http://127.0.0.1:8000/weasypdf/test/print/
You can copy the following to your reports app to have a sample view to start working with:
file reports/urls.py:
from django.urls import path
from . import views
app_name = 'weasypdf'
urlpatterns = [
path('test/print/', views.ReportTestView.as_view(), {'for_download': False, 'lines': 200, }, name="test-print"),
path('test/download/', views.ReportTestView.as_view(), {'for_download': True, 'lines': 200, }, name="test-download"),
]
file reports/views.py:
from weasypdf.views import WeasypdfView
class ReportView(WeasypdfView):
#my_custom_data = None
header_template_name = 'weasypdf/header.html'
footer_template_name = 'weasypdf/footer.html'
styles_template_name = 'weasypdf/styles.css'
def get_context_data(self, **kwargs):
context = super(ReportView, self).get_context_data(**kwargs)
#self.my_custom_data = context.pop('my_custom_data', None)
# context.update({
# 'footer_line_1': config.REPORT_FOOTER_LINE_1,
# 'footer_line_2': config.REPORT_FOOTER_LINE_2,
# })
return context
class ReportTestView(ReportView):
body_template_name = 'weasypdf/pages/test.html'
styles_template_name = 'weasypdf/pages/test.css'
# header_template_name = None
# footer_template_name = None
title = "Report Test"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add a plot
try:
from weasypdf.plot import build_plot_from_data
plot_image = build_plot_from_data(data=None, as_base64=True)
context.update({
'plot_image': plot_image,
})
except:
pass
# Add your stuff here ...
context.update({
...
})
return context
or replace `weasypdf/header.html` with `reports/header.html`, etc ... when using custom templates.
file reports/pages/test.html:
{% extends "weasypdf/base.html" %}
{% block content %}
<h1>Test PDF</h1>
{% if plot_image %}
<img class="plot" src="data:image/png;base64,{{plot_image}}">
{% endif %}
{% with lines=lines|default:100 %}
{% for i in "x"|rjust:lines %}
<div>line {{forloop.counter}} ...</div>
{% endfor %}
{% endwith %}
{% endblock content %}
You can now download the PDF document at:
http://127.0.0.1:8000/reports/test/download/
or open it with the browser at:
http://127.0.0.1:8000/reports/test/print/
You can inspect the HTML used for PDF rendering by appending ?format=html to the url:
http://127.0.0.1:8000/reports/test/print/?format=html
A WeasypdfView.render_as_pdf_to_stream(self, base_url, extra_context, output) method is supplied for this purpose:
def render_as_pdf_to_stream(self, base_url, extra_context, output):
"""
Build the PDF document and save in into "ouput" stream.
Automatically called when the view is invoked via HTTP (unless self.format == 'html'),
but you can also call it explicitly from a background task:
view = PdfTestView()
context = view.get_context_data()
with open(filepath, 'wb') as f:
view.render_as_pdf_to_stream('', context, f)
"""
A sample management command to build a PDF document outside the HTML request/response cycle is available here:
weasypdf/management/commands/build_test_pdf.py
Supply context parameters either in the urlpattern, or invoking get_context_data():
from urls.py:
urlpatterns = [
path('daily/print/', views.ReportDailyView.as_view(), {'exclude_inactives': False}, name="daily-print"),
]
from a background task:
from django.core.files.base import ContentFile
# Create a View to work with
from reports.views import ReportDailyView
view = ReportDailyView()
context = view.get_context_data(
exclude_inactives=task.exclude_inactives,
)
# Create empty file as result
filename = view.build_filename(extension="weasypdf")
task.result.save(filename, ContentFile(''))
# Open and write result
filepath = task.result.path
with open(filepath, 'wb') as f:
view.render_as_pdf_to_stream('', context, f)
These sample files:
weasypdf ├── static │ └── weasypdf │ └── images │ └── header_left.png └── templates └── weasypdf ├── base.html ├── base_nomargins.html ├── styles.css ├── footer.html ├── header.html └── pages ├── test.css └── test.html
can be copied into your app's local folder reports/templates/reports, and used for any required customization:
class ReportView(WeasypdfView):
header_template_name = 'reports/header.html'
footer_template_name = 'reports/footer.html'
styles_template_name = 'reports/styles.css'
<p style="page-break-before: always" ></p>
Add weasyprint to your requirements:
WeasyPrint==51
and optionally to your LOGGING setting:
LOGGING = { ... 'loggers': { ... 'weasyprint': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True, }, }, }
Deployment:
- Install Courier fonts for PDF rendering
# You can verify the available fonts as follows: # # fc-list - name: Install Courier font for PDF rendering become: true become_user: root copy: src: deployment/project/courier.ttf dest: /usr/share/fonts/truetype/courier/
The font file can be downloaded here:
- You might also need to install the following packages:
#weasyprint_packages: - libffi-dev # http://weasyprint.readthedocs.io/en/latest/install.html#linux - python-cffi # http://weasyprint.readthedocs.io/en/latest/install.html#linux - python-dev # http://weasyprint.readthedocs.io/en/latest/install.html#linux - python-pip # http://weasyprint.readthedocs.io/en/latest/install.html#linux - python-lxml # http://weasyprint.readthedocs.io/en/latest/install.html#linux - libcairo2 # http://weasyprint.readthedocs.io/en/latest/install.html#linux - libpango1.0-0 # http://weasyprint.readthedocs.io/en/latest/install.html#linux - libgdk-pixbuf2.0-0 # http://weasyprint.readthedocs.io/en/latest/install.html#linux - shared-mime-info # http://weasyprint.readthedocs.io/en/latest/install.html#linux - libxml2-dev # http://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu#6504860 - libxslt1-dev # http://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu#6504860
For an updated list, check here:
https://weasyprint.readthedocs.io/en/latest/install.html#linux
For example you can save a custom bitmap with django-constance:
CONSTANCE_ADDITIONAL_FIELDS = {
'image_field': ['django.forms.ImageField', {}]
}
CONSTANCE_CONFIG = {
...
'PDF_RECORD_LOGO': ('', 'Image for PDF logo', 'image_field'),
}
then in your header.html template:
<body>
<div class="pageHeader">
<img class="pageLogo" title="{{ PDF_RECORD_LOGO }}" src="media://{{ PDF_RECORD_LOGO }}">
<div class="pageTitle">{{print_date|date:'d/m/Y H:i:s'}} - {{title}}</div>
</div>
</body>
If Image is a Model to keep the images you want to embed, use a templatetag like this:
@register.filter
def local_image_url(image_slug):
"""
Example:
"/backend/images/signature_mo.png"
"""
url = ''
try:
image = Image.objects.get(slug=image_slug)
if bool(image.image):
url = image.image.url.lstrip(settings.MEDIA_URL)
except Image.DoesNotExist as e:
pass
if len(url):
url = 'media://' + url
else:
url = 'static://reports/images/placeholder.png'
return url
then, in your templates:
<img class="pageLogoMiddle" src="{{'report-header-middle'|local_image_url}}">
where 'report-header-middle' is the slug used to select the image.
In the frontend, you have many javascript libraries available to plot data and draw fancy charts.
This doesn't help you in embedding a plot in a PDF documents built offline, however; in this case, you need to build an image server side.
An helper function has been included in this app for that purpose; to use it, matplotlib must be installed.
At the moment, it is more a POC then a complete solution; you can either use it from the package, or copy the source file weasypdf/plot.py in your project and use build_plot_from_data() as a starting point:
def build_plot_from_data(data, chart_type='line', as_base64=False, dpi=300, ylabel=''):
"""
Build a plot from given "data";
Returns: a bitmap of the plot
Requires:
matplotlib
Keyword arguments:
data -- see sample_line_plot_data() for an example; if None, uses sample_line_plot_data()
chart_type -- 'line', 'bar', 'horizontalBar', 'pie', 'line', 'doughnut',
as_base64 -- if True, returns the base64 encoding of the bitmap
dpi -- bitmap resolution
ylabel -- optional label for Y axis
Data layout
===========
Similar to django-jchart:
- either (shared values for x)
{
"labels": ["A", "B", ...],
"x" [x1, x2, ...],
"columns": [
[ay1, ay2, ...],
[by1, by2, ...],
],
"colors": [
"rgba(64, 113, 191, 0.2)",
"rgba(191, 64, 64, 0.0)",
"rgba(26, 179, 148, 0.0)"
]
}
- or
{
"labels": ["A", "B", ..., ],
"columns": [
[
{"x": ax1, "y": ay1 },
{"x": ax2, "y": ay2 },
{"x": ax3, "y": ay3 },
], [
{"x": bx1, "y": by1 },
{"x": bx2, "y": by2 },
], ...
],
"colors": ["transparent", "rgba(121, 0, 0, 0.2)", "rgba(101, 0, 200, 0.2)", ]
}
"""
then, in the view, add the resulting bitmap to context:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
from .plot import build_plot_from_data
plot_image = build_plot_from_data(data=None, chart_type='line', as_base64=True)
context.update({
'plot_image': plot_image,
})
except:
pass
return context
In the template, render it as an embedded image:
<style>
.plot {
border: 1px solid #ccc;
width: 18cm;
height: 6cm;
margin: 1.0cm 0;
}
</style>
{% if plot_image %}
<img class="plot" src="data:image/png;base64,{{plot_image}}">
{% endif %}
The management command build_test_pdf can be used with the "--plot_data" switch to test the resulting image:
python manage.py build_test_pdf test.png -o -p '{"labels": ["sin", "cos"], "x": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5], "columns": [[0.0, 9.09, -7.57, -2.79, 9.89, -5.44, -5.37, 9.91, -2.88, -7.51], [20.0, -13.07, -2.91, 16.88, -19.15, 8.16, 8.48, -19.25, 16.68, -2.56]]}' --plot_font Tahoma