django-weasypdf
===============
A Django class-based view helper to generate PDF with WeasyPrint.
Requires: `WeasyPrint <https://github.com/Kozea/WeasyPrint>`_
Optional requirements:
- matplotlib (to render plots)
.. contents::
.. sectnum::
Installation
------------
Install the package by running:
.. code:: bash
pip install django-weasypdf
or:
.. code:: bash
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:
.. code:: bash
python manage.py startapp reports
In your settings, add:
.. code:: python
INSTALLED_APPS = [
...
'reports',
'weasypdf',
]
Note that `reports` is listed before `weasypdf` to make sure you can possibly
override any template.
In your urls, add:
.. code:: python
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 sample report
---------------
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:
.. code:: python
path('weasypdf/', include('weasypdf.urls', namespace='weasypdf')),
then, visit:
http://127.0.0.1:8000/weasypdf/test/print/
Usage
-----
You can copy the following to your `reports` app to have a sample view
to start working with:
file `reports/urls.py`:
.. code:: python
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`:
.. code:: python
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 .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`:
.. code:: 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
.. image:: screenshots/pdf_sample.png
Building a PDF document from a background process
-------------------------------------------------
A `WeasypdfView.render_as_pdf_to_stream(self, base_url, extra_context, output)` method is supplied for this purpose:
.. code:: python
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 <./weasypdf/management/commands/build_test_pdf.py>`_
Providing "extra_context" parameters
------------------------------------
Supply context parameters either in the urlpattern, or invoking get_context_data():
from `urls.py`:
.. code:: python
urlpatterns = [
path('daily/print/', views.ReportDailyView.as_view(), {'exclude_inactives': False}, name="daily-print"),
]
from a background task:
.. code:: python
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)
Customizing the templates
-------------------------
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:
.. code:: python
class ReportView(WeasypdfView):
header_template_name = 'reports/header.html'
footer_template_name = 'reports/footer.html'
styles_template_name = 'reports/styles.css'
How to insert a page break
--------------------------
.. code:: html
<p style="page-break-before: always" ></p>
Adding Weasyprint to your project
---------------------------------
Add `weasyprint` to your requirements::
WeasyPrint==51
and optionally to your LOGGING setting::
LOGGING = {
...
'loggers': {
...
'weasyprint': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
}
Deployment:
1) 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:
`courier.ttf <resources/fonts/courier.ttf>`_
2) 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
Customizations examples
=======================
Add a custom logo
-----------------
For example you can save a custom bitmap with django-constance:
.. code :: python
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:
.. code:: html
<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>
Embed images from media (ImageFields)
-------------------------------------
If Image is a Model to keep the images you want to embed, use a templatetag like this:
.. code:: python
@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:
.. code:: html
<img class="pageLogoMiddle" src="{{'report-header-middle'|local_image_url}}">
where `'report-header-middle'` is the slug used to select the image.
Adding a plot to the PDF documents
==================================
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:
.. code:: python
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:
.. code:: python
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:
.. code:: html
<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 %}
Try it
------
The management command `build_test_pdf` can be used with the "--plot_data" switch to test the resulting image:
.. code:: bash
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
|
.. image:: screenshots/plots.png
History
=======
v0.1.2
------
* PdfView renamed as WeasypdfView
v0.1.1
------
* Rename "pdf" module as "weasypdf"
* Sample project added
v0.1.0
------
* Publish on Pypi as "django-weasypdf"
* Support for 'line', 'bar', 'horizontalBar', 'pie', 'line', 'doughnut' plots
v0.0.5
------
* Support for plot added (only "line" chart type, at the moment; requires matplotlib)
* Customization examples added to readme
v0.0.4
------
* render_to_stream() renamed as render_as_pdf_to_stream()
* render_as_html_to_string() method added
v0.0.3
------
* render_to_stream() method added to produce a PDF from a background task
v0.0.2
------
* Some cleanup
v0.0.1
------
* Project start