Rendu HTML en PDF sur le site Django

117

Pour mon site alimenté par django, je recherche une solution simple pour convertir des pages html dynamiques en pdf.

Les pages incluent du HTML et des graphiques de l'API de visualisation de Google (qui est basée sur JavaScript, mais il est indispensable d'inclure ces graphiques).

Olli
la source
La documentation de Django est complète et couvre beaucoup de choses. Avez-vous eu des problèmes avec la méthode suggérée ici? http://docs.djangoproject.com/en/dev/howto/outputting-pdf/
monkut
1
Cela ne répond pas vraiment à la question. Cette documentation explique comment rendre un PDF de manière native, et non à partir du rendu HTML.
Josh
Je pense que la bonne chose à faire est de faire en sorte que les navigateurs produisent le pdf car ils sont les seuls à faire un rendu html / css / js correct. voir cette question stackoverflow.com/q/25574082/39998
David Hofmann
Cette question est hors sujet chez SO, mais sur le sujet dans softwarerecs.SE. Voir Comment puis-je convertir du HTML avec CSS en PDF? .
Martin Thoma
essayez d'utiliser wkhtmltopdf learnbatta.com/blog/…
anjaneyulubatta505

Réponses:

207

Essayez la solution de Reportlab .

Téléchargez-le et installez-le comme d'habitude avec l'installation de python setup.py

Vous devrez également installer les modules suivants: xhtml2pdf, html5lib, pypdf avec easy_install.

Voici un exemple d'utilisation:

Définissez d'abord cette fonction:

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

Ensuite, vous pouvez l'utiliser comme ceci:

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

Le gabarit:

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

J'espère que ça aide.

Guillem Gelabert
la source
9
+1 J'utilise cette solution depuis un an et c'est génial. PISA peut même créer des codes-barres avec une simple balise, et bien plus encore. Et c'est facile .
arcanum
1
Homme, reportlab est pita à installer sur Windows 7 64bit, python2.7 64bit. Toujours en train d'essayer ...
Andriy Drozdyuk
5
Ne semble pas exécuter Javascript.
dfrankow
3
pisa est maintenant distribué en tant que xhtml2pdf
Pablo Albornoz
12
En python3, à l'exception de la conversion cStringIO.StringIOen io.StringIO, nous devons définir resultcomme result = io.BytesIO()au lieu de result = StringIO.
Sébastien
12

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

Modèle:

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

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

Vue:

from easy_pdf.views import PDFTemplateView

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

Si vous souhaitez utiliser django-easy-pdf sur Python 3, consultez la solution suggérée ici .

laffuste
la source
2
C'est la plus simple à mettre en œuvre des options que j'ai essayées jusqu'à présent. Pour mes besoins (générer un rapport pdf à partir d'une version html) cela fonctionne simplement. Merci!
The NetYeti
1
@alejoss Vous devriez utiliser des styles en ligne au lieu de CSS.
digz6666
Cette solution peut ne pas fonctionner tout de suite pour django 3.0 car django-utils-six est supprimé mais le fichier easy_pdf en dépend.
David le
11

Je viens de préparer ça pour CBV. Non utilisé en production mais génère un PDF pour moi. A probablement besoin de travail pour le côté des rapports d'erreurs, mais fait l'affaire jusqu'à présent.

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

Utilisé comme:

class MyPdfView(PDFTemplateView):
    template_name = 'things/pdf.html'
Christian Jensen
la source
1
Cela a fonctionné presque directement pour moi. La seule chose était de remplacer html.encode("ISO-8859-1")parhtml.decode("utf-8")
vinyll
J'ai changé le code comme @vinyll mentionné et j'ai également dû ajouter une ligne à la classe PDFTemplateView:content_type = "application/pdf"
normique
11

Essayez wkhtmltopdf avec l'un des wrappers suivants

django-wkhtmltopdf ou python-pdfkit

Cela a très bien fonctionné pour moi, prend en charge javascript et css ou tout autre élément pris en charge par un navigateur Webkit.

Pour un didacticiel plus détaillé, veuillez consulter cet article de blog

jithin
la source
Qu'en est-il du svg intégré au HTML, est-ce également pris en charge?
mehmet
@mmatt Oui, il prend en charge svg. Voir ce stackoverflow.com/questions/12395541/… et ce github.com/wkhtmltopdf/wkhtmltopdf/issues/1964
jithin
Il suffit de faire attention, WebKit fait tout pas de support chrome / firefox fait: webkit.org/status
mehmet
1
django-wkhtmltopdf a fait des merveilles pour moi! assurez-vous également de désactiver toutes les animations de votre moteur javascript / charting.
mehmet
@mehmet, il n'a pas pris en charge mes simples js. J'ai eu beaucoup d'erreurs. Pouvez-vous m'aider?
Manish Ojha
3

Après avoir essayé de faire fonctionner cela pendant trop d'heures, j'ai finalement trouvé ceci: https://github.com/vierno/django-xhtml2pdf

C'est un fork de https://github.com/chrisglass/django-xhtml2pdf qui fournit un mixin pour une vue générique basée sur les classes. Je l'ai utilisé comme ceci:

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

Utilisez le nom du modèle que vous avez défini dans votre vue en minuscules lorsque vous remplissez les champs du modèle. Parce que c'est un GCBV, vous pouvez simplement l'appeler comme ".as_view" dans votre 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",
       ),
tthayer
la source
2

Vous pouvez utiliser l'éditeur iReport pour définir la mise en page et publier le rapport sur le serveur de rapports jasper. Après la publication, vous pouvez appeler le reste de l'API pour obtenir les résultats.

Voici le test de la fonctionnalité:

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)

Et voici un exemple de l'implémentation de l'appel:

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
andhdo
la source
1

Je reçois le code pour générer le PDF à partir du modèle html:

    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'  
Deft-pawN
la source
0

Si vous avez des données de contexte avec css et js dans votre modèle html. Que vous avez une bonne option pour utiliser pdfjs .

Dans votre code, vous pouvez utiliser comme ceci.

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

Dans votre HTML, vous pouvez lier des css et js externes ou internes, cela générera la meilleure qualité de pdf.

Manoj Datt
la source