Rendern Sie HTML in PDF auf der Django-Site

117

Für meine Django-Site suche ich nach einer einfachen Lösung, um dynamische HTML-Seiten in PDF zu konvertieren.

Die Seiten enthalten HTML und Diagramme aus der Google-Visualisierungs-API (die auf Javascript basiert, diese Grafiken jedoch ein Muss ist).

Olli
quelle
Die Django-Dokumentation ist tiefgreifend und deckt viel ab. Hatten Sie Probleme mit der dort vorgeschlagenen Methode? http://docs.djangoproject.com/de/dev/howto/outputting-pdf/
monkut
1
Dies beantwortet die Frage nicht wirklich. In dieser Dokumentation wird beschrieben, wie ein PDF nativ gerendert wird, nicht aus gerendertem HTML.
Josh
Ich vermute, dass das Richtige darin besteht, Browser dazu zu bringen, das PDF zu erstellen, da sie die einzigen sind, die das richtige Rendern von HTML / CSS / JS ausführen. siehe diese Frage stackoverflow.com/q/25574082/39998
David Hofmann
Diese Frage ist bei SO nicht thematisch, in softwarerecs.SE jedoch themenbezogen. Siehe Wie kann ich HTML mit CSS in PDF konvertieren? .
Martin Thoma
versuchen Sie es mit wkhtmltopdf learnbatta.com/blog/…
anjaneyulubatta505

Antworten:

206

Probieren Sie die Lösung von Reportlab aus .

Laden Sie es herunter und installieren Sie es wie gewohnt mit python setup.py install

Sie müssen auch die folgenden Module installieren: xhtml2pdf, html5lib, pypdf mit easy_install.

Hier ist ein Anwendungsbeispiel:

Definieren Sie zuerst diese Funktion:

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

Dann können Sie es so verwenden:

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

Die Vorlage:

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

Ich hoffe es hilft.

Guillem Gelabert
quelle
9
+1 Ich benutze diese Lösung seit einem Jahr und sie ist großartig. PISA kann sogar Barcodes mit einem einfachen Tag und vielem mehr erstellen. Und es ist einfach .
Arcanum
1
Mann, reportlab ist pita unter Windows 7 64bit, python2.7 64bit zu installieren.
Ich
5
Scheint kein Javascript auszuführen.
dfrankow
3
Pisa ist jetzt als xhtml2pdf
Pablo Albornoz
12
In Python3 müssen wir mit Ausnahme der Konvertierung cStringIO.StringIOin als anstelle von io.StringIOdefinieren . resultresult = io.BytesIO()result = StringIO
Sebastien
12

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

Vorlage:

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

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

Aussicht:

from easy_pdf.views import PDFTemplateView

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

Wenn Sie django-easy-pdf für Python 3 verwenden möchten, überprüfen Sie die hier vorgeschlagene Lösung .

laffuste
quelle
2
Dies ist die am einfachsten zu implementierende Option, die ich bisher ausprobiert habe. Für meine Bedürfnisse (Generieren eines PDF-Berichts aus einer HTML-Version) funktioniert dies einfach. Vielen Dank!
Die NetYeti
1
@alejoss Sie sollten Inline-Stile anstelle von CSS verwenden.
digz6666
Diese Lösung funktioniert möglicherweise nicht sofort für django 3.0, da django-utils-six entfernt wird, aber das easy_pdf hängt davon ab.
David
11

Ich habe das gerade für CBV ausgepeitscht. Wird nicht in der Produktion verwendet, generiert aber ein PDF für mich. Benötigt wahrscheinlich Arbeit für die Fehlerberichterstattung, macht aber den Trick bis jetzt.

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

Verwendet wie:

class MyPdfView(PDFTemplateView):
    template_name = 'things/pdf.html'
Christian Jensen
quelle
1
Das hat bei mir fast einfach geklappt. Das einzige , was war zu ersetzen html.encode("ISO-8859-1")durchhtml.decode("utf-8")
vinyll
Ich habe den Code wie @vinyll erwähnt geändert und musste zusätzlich eine Zeile zur Klasse PDFTemplateView hinzufügen:content_type = "application/pdf"
normal
11

Versuchen Sie wkhtmltopdf mit einem der folgenden Wrapper

django-wkhtmltopdf oder python-pdfkit

Dies hat bei mir hervorragend funktioniert, unterstützt Javascript und CSS oder alles, was ein Webkit-Browser unterstützt.

Weitere Informationen finden Sie in diesem Blogbeitrag

Jithin
quelle
Wie wäre es mit svg eingebettet in HTML, wird das auch unterstützt?
Mehmet
@mmatt Ja, es unterstützt svg. Siehe diesen stackoverflow.com/questions/12395541/… und diesen github.com/wkhtmltopdf/wkhtmltopdf/issues/1964
jithin
Seien Sie vorsichtig, das Webkit unterstützt nicht alles, was Chrome / Firefox tut: webkit.org/status
mehmet
1
django-wkhtmltopdf hat Wunder für mich getan! Stellen Sie außerdem sicher, dass alle Animationen deaktiviert sind, die Ihre Javascript / Charting-Engine ausführt.
Mehmet
@mehmet es hat meine einfachen Balkendiagramme js nicht unterstützt. Ich habe viele Fehler bekommen. Kannst du mir dabei helfen??
Manish Ojha
3

Nachdem ich versucht hatte, dies zu viele Stunden lang zum Laufen zu bringen, fand ich schließlich Folgendes: https://github.com/vierno/django-xhtml2pdf

Es ist eine Abzweigung von https://github.com/chrisglass/django-xhtml2pdf , die ein Mixin für eine generische klassenbasierte Ansicht bietet. Ich habe es so benutzt:

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

Verwenden Sie beim Ausfüllen der Vorlagenfelder den in Ihrer Ansicht definierten Modellnamen in Kleinbuchstaben. Da es sich um ein GCBV handelt, können Sie es in Ihrer urls.py einfach als ".as_view" bezeichnen:

    # 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
quelle
2

Mit dem iReport-Editor können Sie das Layout definieren und den Bericht auf dem Jasper Reports-Server veröffentlichen. Nach der Veröffentlichung können Sie die restliche API aufrufen, um die Ergebnisse zu erhalten.

Hier ist der Test der Funktionalität:

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)

Und hier ist ein Beispiel für die Aufrufimplementierung:

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

Ich erhalte den Code zum Generieren des PDF aus einer HTML-Vorlage:

    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'  
Geschickte Pfote
quelle
0

Wenn Sie Kontextdaten zusammen mit CSS und JS in Ihrer HTML-Vorlage haben. Dann haben Sie eine gute Möglichkeit, pdfjs zu verwenden .

In Ihrem Code können Sie so verwenden.

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 Ihrem HTML können Sie extranale oder interne CSS und JS verknüpfen, um die beste PDF-Qualität zu erzielen.

Manoj Datt
quelle