Skip to main content

A Django class-based view helper to generate PDF with WeasyPrint

Project description

1 django-weasypdf

A Django class-based view helper to generate PDF with WeasyPrint.

Requires: WeasyPrint

Optional requirements:

  • matplotlib (to render plots)

1.1 Installation

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

1.2 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:

path('weasypdf/', include('weasypdf.urls', namespace='weasypdf')),

then, visit:

http://127.0.0.1:8000/weasypdf/test/print/

1.3 Usage

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 .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

screenshots/pdf_sample.png

1.4 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:

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

1.5 Providing “extra_context” parameters

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)

1.6 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:

class ReportView(WeasypdfView):

    header_template_name = 'reports/header.html'
    footer_template_name = 'reports/footer.html'
    styles_template_name = 'reports/styles.css'

1.7 How to insert a page break

<p style="page-break-before: always" ></p>

1.8 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

  1. 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

2 Customizations examples

2.2 Embed images from media (ImageFields)

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.

3 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:

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 %}

3.1 Try it

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

screenshots/plots.png

4 History

4.1 v0.1.2

  • PdfView renamed as WeasypdfView

4.2 v0.1.1

  • Rename “pdf” module as “weasypdf”

  • Sample project added

4.3 v0.1.0

  • Publish on Pypi as “django-weasypdf”

  • Support for ‘line’, ‘bar’, ‘horizontalBar’, ‘pie’, ‘line’, ‘doughnut’ plots

4.4 v0.0.5

  • Support for plot added (only “line” chart type, at the moment; requires matplotlib)

  • Customization examples added to readme

4.5 v0.0.4

  • render_to_stream() renamed as render_as_pdf_to_stream()

  • render_as_html_to_string() method added

4.6 v0.0.3

  • render_to_stream() method added to produce a PDF from a background task

4.7 v0.0.2

  • Some cleanup

4.8 v0.0.1

  • Project start

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

django_weasypdf-0.1.2-py2.py3-none-any.whl (35.2 kB view hashes)

Uploaded Python 2 Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page