Render HTML to PDF in Django site

PythonHtmlDjangoPdfPdf Generation

Python Problem Overview


For my django powered site, I am looking for an easy solution to convert dynamic html pages to pdf.

Pages include HTML and charts from Google visualization API (which is javascript based, yet including those graphs is a must).

Python Solutions


Solution 1 - Python

Try the solution from [Reportlab][1].

Download it and install it as usual with python setup.py install

You will also need to install the following modules: xhtml2pdf, html5lib, pypdf with easy_install.

Here is an usage example:

First define this function:

import cStringIO as StringIO
from xhtml2pdf import pisa
from django.template.loader import get_template
from django.template import Context
from django.http import HttpResponse
from cgi import escape


def render_to_pdf(template_src, context_dict):
    template = get_template(template_src)
    context = Context(context_dict)
    html  = template.render(context)
    result = StringIO.StringIO()

    pdf = pisa.pisaDocument(StringIO.StringIO(html.encode("ISO-8859-1")), result)
    if not pdf.err:
        return HttpResponse(result.getvalue(), content_type='application/pdf')
    return HttpResponse('We had some errors<pre>%s</pre>' % escape(html))

Then you can use it like this:

def myview(request):
    #Retrieve data or whatever you need
    return render_to_pdf(
            'mytemplate.html',
            {
                'pagesize':'A4',
                'mylist': results,
            }
        )

The template:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <title>My Title</title>
        <style type="text/css">
            @page {
                size: {{ pagesize }};
                margin: 1cm;
                @frame footer {
                    -pdf-frame-content: footerContent;
                    bottom: 0cm;
                    margin-left: 9cm;
                    margin-right: 9cm;
                    height: 1cm;
                }
            }
        </style>
    </head>
    <body>
        <div>
            {% for item in mylist %}
                RENDER MY CONTENT
            {% endfor %}
        </div>
        <div id="footerContent">
            {%block page_foot%}
                Page <pdf:pagenumber>
            {%endblock%}
        </div>
    </body>
</html>

Hope it helps. [1]: https://www.reportlab.com/reportlabplus/installation/

Solution 2 - Python

Try wkhtmltopdf with either one of the following wrappers

django-wkhtmltopdf or python-pdfkit

This worked great for me,supports javascript and css or anything for that matter which a webkit browser supports.

For more detailed tutorial please see this blog post

Solution 3 - Python

https://github.com/nigma/django-easy-pdf

Template:

{% extends "easy_pdf/base.html" %}

{% block content %}
    <div id="content">
        <h1>Hi there!</h1>
    </div>
{% endblock %}

View:

from easy_pdf.views import PDFTemplateView

class HelloPDFView(PDFTemplateView):
    template_name = "hello.html"

If you want to use django-easy-pdf on Python 3 check the solution suggested here.

Solution 4 - Python

I just whipped this up for CBV. Not used in production but generates a PDF for me. Probably needs work for the error reporting side of things but does the trick so far.

import StringIO
from cgi import escape
from xhtml2pdf import pisa
from django.http import HttpResponse
from django.template.response import TemplateResponse
from django.views.generic import TemplateView

class PDFTemplateResponse(TemplateResponse):

    def generate_pdf(self, retval):

        html = self.content

        result = StringIO.StringIO()
        rendering = pisa.pisaDocument(StringIO.StringIO(html.encode("ISO-8859-1")), result)

        if rendering.err:
            return HttpResponse('We had some errors<pre>%s</pre>' % escape(html))
        else:
            self.content = result.getvalue()

    def __init__(self, *args, **kwargs):
        super(PDFTemplateResponse, self).__init__(*args, mimetype='application/pdf', **kwargs)
        self.add_post_render_callback(self.generate_pdf)


class PDFTemplateView(TemplateView):
    response_class = PDFTemplateResponse

Used like:

class MyPdfView(PDFTemplateView):
    template_name = 'things/pdf.html'

Solution 5 - Python

After trying to get this to work for too many hours, I finally found this: https://github.com/vierno/django-xhtml2pdf

It's a fork of https://github.com/chrisglass/django-xhtml2pdf that provides a mixin for a generic class-based view. I used it like this:

    # views.py
    from django_xhtml2pdf.views import PdfMixin
    class GroupPDFGenerate(PdfMixin, DetailView):
        model = PeerGroupSignIn
        template_name = 'groups/pdf.html'

    # templates/groups/pdf.html
    <html>
    <style>
    @page { your xhtml2pdf pisa PDF parameters }
    </style>
    </head>
    <body>
        <div id="header_content"> (this is defined in the style section)
            <h1>{{ peergroupsignin.this_group_title }}</h1>
            ...
            

Use the model name you defined in your view in all lowercase when populating the template fields. Because its a GCBV, you can just call it as '.as_view' in your urls.py:

    # urls.py (using url namespaces defined in the main urls.py file)
    url(
        regex=r"^(?P<pk>\d+)/generate_pdf/$",
        view=views.GroupPDFGenerate.as_view(),
        name="generate_pdf",
       ),

Solution 6 - Python

You can use iReport editor to define the layout, and publish the report in jasper reports server. After publish you can invoke the rest api to get the results.

Here is the test of the functionality:

from django.test import TestCase
from x_reports_jasper.models import JasperServerClient

"""
	to try integraction with jasper server through rest
"""
class TestJasperServerClient(TestCase):

	# define required objects for tests
	def setUp(self):

		# load the connection to remote server
		try:

			self.j_url = "http://127.0.0.1:8080/jasperserver"
			self.j_user = "jasperadmin"
			self.j_pass = "jasperadmin"

			self.client = JasperServerClient.create_client(self.j_url,self.j_user,self.j_pass)

		except Exception, e:
			# if errors could not execute test given prerrequisites
			raise
			
	# test exception when server data is invalid
	def test_login_to_invalid_address_should_raise(self):
		self.assertRaises(Exception,JasperServerClient.create_client, "http://127.0.0.1:9090/jasperserver",self.j_user,self.j_pass)

	# test execute existent report in server
	def test_get_report(self):
	
		r_resource_path = "/reports/<PathToPublishedReport>"
		r_format = "pdf"
		r_params = {'PARAM_TO_REPORT':"1",}

		#resource_meta = client.load_resource_metadata( rep_resource_path )

		[uuid,out_mime,out_data] = self.client.generate_report(r_resource_path,r_format,r_params)
		self.assertIsNotNone(uuid)

And here is an example of the invocation implementation:

from django.db import models
import requests
import sys
from xml.etree import ElementTree
import logging 

# module logger definition
logger = logging.getLogger(__name__)

# Create your models here.
class JasperServerClient(models.Manager):

	def __handle_exception(self, exception_root, exception_id, exec_info ):
		type, value, traceback = exec_info
		raise JasperServerClientError(exception_root, exception_id), None, traceback

	# 01: REPORT-METADATA 
	# 	get resource description to generate the report
	def __handle_report_metadata(self, rep_resourcepath):

		l_path_base_resource = "/rest/resource"
		l_path = self.j_url + l_path_base_resource
		logger.info( "metadata (begin) [path=%s%s]"  %( l_path ,rep_resourcepath) )

		resource_response = None
		try:
			resource_response = requests.get( "%s%s" %( l_path ,rep_resourcepath) , cookies = self.login_response.cookies)

		except Exception, e:
			self.__handle_exception(e, "REPORT_METADATA:CALL_ERROR", sys.exc_info())

		resource_response_dom = None
		try:
			# parse to dom and set parameters
			logger.debug( " - response [data=%s]"  %( resource_response.text) )
			resource_response_dom = ElementTree.fromstring(resource_response.text)

			datum = "" 
			for node in resource_response_dom.getiterator():
				datum = "%s<br />%s - %s" % (datum, node.tag, node.text)
			logger.debug( " - response [xml=%s]"  %( datum ) )

			#
			self.resource_response_payload= resource_response.text
			logger.info( "metadata (end) ")
		except Exception, e:
			logger.error( "metadata (error) [%s]" % (e))
			self.__handle_exception(e, "REPORT_METADATA:PARSE_ERROR", sys.exc_info())


	# 02: REPORT-PARAMS 
	def __add_report_params(self, metadata_text, params ):
		if(type(params) != dict):
			raise TypeError("Invalid parameters to report")
		else:
			logger.info( "add-params (begin) []" )
			#copy parameters
			l_params = {}
			for k,v in params.items():
				l_params[k]=v
			# get the payload metadata
			metadata_dom = ElementTree.fromstring(metadata_text)
			# add attributes to payload metadata
			root = metadata_dom #('report'):

			for k,v in l_params.items():
				param_dom_element = ElementTree.Element('parameter')
				param_dom_element.attrib["name"] = k
				param_dom_element.text = v
				root.append(param_dom_element)
			
			#
			metadata_modified_text =ElementTree.tostring(metadata_dom, encoding='utf8', method='xml')
			logger.info( "add-params (end) [payload-xml=%s]" %( metadata_modified_text )  )
			return metadata_modified_text



	# 03: REPORT-REQUEST-CALL 
	#   call to generate the report
	def __handle_report_request(self, rep_resourcepath, rep_format, rep_params):

		# add parameters
		self.resource_response_payload = self.__add_report_params(self.resource_response_payload,rep_params)

		# send report request

		l_path_base_genreport = "/rest/report"
		l_path = self.j_url + l_path_base_genreport
		logger.info( "report-request (begin) [path=%s%s]"  %( l_path ,rep_resourcepath) )

		genreport_response = None
		try:
			genreport_response = requests.put( "%s%s?RUN_OUTPUT_FORMAT=%s" %(l_path,rep_resourcepath,rep_format),data=self.resource_response_payload, cookies = self.login_response.cookies )
			logger.info( " - send-operation-result [value=%s]"  %( genreport_response.text) )
		except Exception,e:
			self.__handle_exception(e, "REPORT_REQUEST:CALL_ERROR", sys.exc_info())


		# parse the uuid of the requested report
		genreport_response_dom = None

		try:
			genreport_response_dom = ElementTree.fromstring(genreport_response.text)

			for node in genreport_response_dom.findall("uuid"):
				datum = "%s" % (node.text)

			genreport_uuid = datum		

			for node in genreport_response_dom.findall("file/[@type]"):
				datum = "%s" % (node.text)
			genreport_mime = datum

			logger.info( "report-request (end) [uuid=%s,mime=%s]"  %( genreport_uuid, genreport_mime) )

			return [genreport_uuid,genreport_mime]
		except Exception,e:
			self.__handle_exception(e, "REPORT_REQUEST:PARSE_ERROR", sys.exc_info())

	# 04: REPORT-RETRIEVE RESULTS 
	def __handle_report_reply(self, genreport_uuid ):

		
		l_path_base_getresult = "/rest/report"
		l_path = self.j_url + l_path_base_getresult 
		logger.info( "report-reply (begin) [uuid=%s,path=%s]"  %( genreport_uuid,l_path) )

		getresult_response = requests.get( "%s%s/%s?file=report" %(self.j_url,l_path_base_getresult,genreport_uuid),data=self.resource_response_payload, cookies = self.login_response.cookies )
		l_result_header_mime =getresult_response.headers['Content-Type']

		logger.info( "report-reply (end) [uuid=%s,mime=%s]"  %( genreport_uuid, l_result_header_mime) )
		return [l_result_header_mime, getresult_response.content]

	# public methods ---------------------------------------	

	# tries the authentication with jasperserver throug rest
	def login(self, j_url, j_user,j_pass):
		self.j_url= j_url

		l_path_base_auth = "/rest/login"
		l_path = self.j_url + l_path_base_auth

		logger.info( "login (begin) [path=%s]"  %( l_path) )

		try:
			self.login_response = requests.post(l_path , params = {
					'j_username':j_user,
					'j_password':j_pass
				})					

			if( requests.codes.ok != self.login_response.status_code ):
				self.login_response.raise_for_status()

			logger.info( "login (end)" )
			return True
			# see http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/

		except Exception, e:
			logger.error("login (error) [e=%s]" % e )
			self.__handle_exception(e, "LOGIN:CALL_ERROR",sys.exc_info())
			#raise

	def generate_report(self, rep_resourcepath,rep_format,rep_params):
		self.__handle_report_metadata(rep_resourcepath)
		[uuid,mime] = self.__handle_report_request(rep_resourcepath, rep_format,rep_params)
		# TODO: how to handle async?
		[out_mime,out_data] = self.__handle_report_reply(uuid)
		return [uuid,out_mime,out_data]

	@staticmethod
	def create_client(j_url, j_user, j_pass):
		client = JasperServerClient()
		login_res = client.login( j_url, j_user, j_pass )
		return client


class JasperServerClientError(Exception):

	def __init__(self,exception_root,reason_id,reason_message=None):
		super(JasperServerClientError, self).__init__(str(reason_message))
		self.code = reason_id 
		self.description = str(exception_root) + " " + str(reason_message)
	def __str__(self):
		return self.code + " " + self.description

Solution 7 - Python

I get the code to generate the PDF from html template :

    import os
    
    from weasyprint import HTML
    
    from django.template import Template, Context
    from django.http import HttpResponse 


    def generate_pdf(self, report_id):

            # Render HTML into memory and get the template firstly
            template_file_loc = os.path.join(os.path.dirname(__file__), os.pardir, 'templates', 'the_template_pdf_generator.html')
            template_contents = read_all_as_str(template_file_loc)
            render_template = Template(template_contents)

            #rendering_map is the dict for params in the template 
            render_definition = Context(rendering_map)
            render_output = render_template.render(render_definition)
    
            # Using Rendered HTML to generate PDF
            response = HttpResponse(content_type='application/pdf')
            response['Content-Disposition'] = 'attachment; filename=%s-%s-%s.pdf' % \
                                              ('topic-test','topic-test', '2018-05-04')
            # Generate PDF
            pdf_doc = HTML(string=render_output).render()
            pdf_doc.pages[0].height = pdf_doc.pages[0]._page_box.children[0].children[
                0].height  # Make PDF file as single page file 
            pdf_doc.write_pdf(response)
            return response

    def read_all_as_str(self, file_loc, read_method='r'):
        if file_exists(file_loc):
            handler = open(file_loc, read_method)
            contents = handler.read()
            handler.close()
            return contents
        else:
            return 'file not exist'  

Solution 8 - Python

If you have context data along with css and js in your html template. Than you have good option to use pdfjs.

In your code you can use like this.

from django.template.loader import get_template
import pdfkit
from django.conf import settings

context={....}
template = get_template('reports/products.html')
html_string = template.render(context)
pdfkit.from_string(html_string, os.path.join(settings.BASE_DIR, "media", 'products_report-%s.pdf'%(id)))

In your HTML you can link extranal or internal css and js, it will generate best quality of pdf.

Solution 9 - Python

  • This is for Django >=3
  • This code converts HTML template to pdf file for any page. For example: post/1/new1, post/2/new2
  • pdf file name is last part in url. For example for post/2/new2, file name is new2

First install xhtml2pdf

pip install xhtml2pdf

urls.py

from .views import generatePdf as GeneratePdf
from django.urls import re_path
urlpatterns = [
#...
re_path(r'^pdf/(?P<cid>[0-9]+)/(?P<value>[a-zA-Z0-9 :._-]+)/$', GeneratePdf, name='pdf'),
#...
]

views.py

from django.template.loader import get_template
from .utils import render_to_pdf
# pdf
def generatePdf(request,cid,value):
    print(cid,value)
    pdf = render_to_pdf('myappname/pdf/your.html',cid)
    return HttpResponse(pdf, content_type='application/pdf')

utils.py

from io import BytesIO #A stream implementation using an in-memory bytes buffer
                       # It inherits BufferIOBase

from django.http import HttpResponse
from django.template.loader import get_template

#pisa is a html2pdf converter using the ReportLab Toolkit,
#the HTML5lib and pyPdf.

from xhtml2pdf import pisa  
#difine render_to_pdf() function
from .models import myappname
from django.shortcuts import get_object_or_404


def render_to_pdf(template_src,cid, context_dict={}):
    template = get_template(template_src)
    node = get_object_or_404(myappname, id =cid)
    context = {'node':node}
    context_dict=context
    html  = template.render(context_dict)
    result = BytesIO()

    #This part will create the pdf.
    pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
    if not pdf.err:
        return HttpResponse(result.getvalue(), content_type='application/pdf')
    return None

Structure:

myappname/
      |___views.py
      |___urls.py
      |___utils.py
      |___templates/myappname/your.html

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioncribView Question on Stackoverflow
Solution 1 - PythonGuillem GelabertView Answer on Stackoverflow
Solution 2 - PythonjithinView Answer on Stackoverflow
Solution 3 - PythonlaffusteView Answer on Stackoverflow
Solution 4 - PythonChristian JensenView Answer on Stackoverflow
Solution 5 - PythontthayerView Answer on Stackoverflow
Solution 6 - PythonandhdoView Answer on Stackoverflow
Solution 7 - PythonDeft-pawNView Answer on Stackoverflow
Solution 8 - PythonManoj DattView Answer on Stackoverflow
Solution 9 - PythonnamjooView Answer on Stackoverflow