Renderuj HTML do PDF w witrynie Django

117

W przypadku mojej witryny opartej na django szukam prostego rozwiązania do konwersji dynamicznych stron HTML do formatu PDF.

Strony zawierają kod HTML i wykresy z interfejsu API wizualizacji Google (który jest oparty na języku JavaScript, ale uwzględnienie tych wykresów jest koniecznością).

Olli
źródło
Dokumentacja Django jest głęboka i wiele obejmuje. Czy masz jakieś problemy z proponowaną tam metodą? http://docs.djangoproject.com/en/dev/howto/outputting-pdf/
monkut
1
To właściwie nie odpowiada na pytanie. Ta dokumentacja dotyczy natywnego renderowania pliku PDF, a nie renderowanego kodu HTML.
Josh
Wydaje mi się, że właściwą rzeczą jest sprawienie, by przeglądarki tworzyły pliki PDF, ponieważ są jedynymi, które wykonują prawidłowe renderowanie html / css / js. zobacz to pytanie stackoverflow.com/q/25574082/39998
David Hofmann
To pytanie jest nie na temat w SO, ale na temat w softwarerecs.SE. Zobacz Jak mogę przekonwertować HTML za pomocą CSS na PDF? .
Martin Thoma
spróbuj użyć wkhtmltopdf learnbatta.com/blog/…
anjaneyulubatta505

Odpowiedzi:

207

Wypróbuj rozwiązanie z Reportlab .

Pobierz go i zainstaluj jak zwykle za pomocą pythona setup.py install

Będziesz także musiał zainstalować następujące moduły: xhtml2pdf, html5lib, pypdf z easy_install.

Oto przykład użycia:

Najpierw zdefiniuj tę funkcję:

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

Następnie możesz go użyć w ten sposób:

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

Szablon:

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

Mam nadzieję, że to pomoże.

Guillem Gelabert
źródło
9
+1 Używam tego rozwiązania od roku i jest świetne. PISA może nawet tworzyć kody kreskowe za pomocą prostego znacznika i nie tylko. I to jest łatwe .
arcanum
1
Człowieku, reportlab to pita do zainstalowania na Windows 7 64bit, Python2.7 64bit. Wciąż próbuję ...
Andriy Drozdyuk
5
Wygląda na to, że nie obsługuje JavaScript.
dfrankow
3
pisa jest teraz rozprowadzana jako xhtml2pdf
Pablo Albornoz
12
W pythonie3, poza konwersją cStringIO.StringIOdo io.StringIO, musimy zdefiniować resultas result = io.BytesIO()zamiast result = StringIO.
Sebastien
12

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

Szablon:

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

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

Widok:

from easy_pdf.views import PDFTemplateView

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

Jeśli chcesz używać django-easy-pdf w Pythonie 3, sprawdź sugerowane tutaj rozwiązanie .

laffuste
źródło
2
Jest to najłatwiejsza do zaimplementowania opcja, którą do tej pory wypróbowałem. Na moje potrzeby (generowanie raportu pdf z wersji html) to po prostu działa. Dzięki!
NetYeti
1
@alejoss Należy używać stylów wbudowanych zamiast CSS.
digz6666
To rozwiązanie może nie działać od razu w django 3.0, ponieważ django-utils-six jest usuwane, ale easy_pdf od tego zależy.
David
11

Właśnie podniosłem to dla CBV. Nie używany w produkcji, ale generuje dla mnie plik PDF. Prawdopodobnie wymaga pracy po stronie raportowania błędów, ale jak dotąd działa.

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

Używane jak:

class MyPdfView(PDFTemplateView):
    template_name = 'things/pdf.html'
Christian Jensen
źródło
1
To działało dla mnie prawie prosto do przodu. Jedyną rzeczą było zastąpienie html.encode("ISO-8859-1")przezhtml.decode("utf-8")
vinyll
Zmieniłem kod jak wspomniał @vinyll i dodatkowo musiałem dodać linię do klasy PDFTemplateView:content_type = "application/pdf"
normic
11

Wypróbuj wkhtmltopdf z jedną z następujących opakowań

django-wkhtmltopdf lub python-pdfkit

To działało świetnie dla mnie, obsługuje javascript i css lub cokolwiek w tym zakresie, które obsługuje przeglądarka webkit.

Aby uzyskać bardziej szczegółowy samouczek, zobacz ten wpis na blogu

jithin
źródło
A co z svg osadzonym w html, czy to też jest obsługiwane?
mehmet
Tylko uważaj, webkit nie obsługuje wszystkiego, co robi Chrome / Firefox: webkit.org/status
mehmet
1
django-wkhtmltopdf zrobił dla mnie cuda! pamiętaj również o wyłączeniu wszystkich animacji, które wykonuje twój silnik javascript / wykresów.
mehmet
@mehmet nie obsługuje mojego prostego wykresu słupkowego js. Mam dużo błędów. Czy możesz mi w tym pomóc?
Manish Ojha
3

Po zbyt wielu godzinach próbowania, aby to działało, w końcu znalazłem to: https://github.com/vierno/django-xhtml2pdf

Jest to rozwidlenie https://github.com/chrisglass/django-xhtml2pdf, które zapewnia połączenie dla ogólnego widoku opartego na klasach. Użyłem tego w ten sposób:

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

Podczas wypełniania pól szablonu użyj nazwy modelu zdefiniowanej w widoku, używając wszystkich małych liter. Ponieważ jest to GCBV, możesz po prostu nazwać go „.as_view” w swoim 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",
       ),
dziesiąty
źródło
2

Możesz użyć edytora iReport do zdefiniowania układu i opublikowania raportu na serwerze raportów Jasper. Po opublikowaniu możesz wywołać pozostałe API, aby uzyskać wyniki.

Oto test funkcjonalności:

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)

A oto przykład implementacji wywołania:

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
źródło
1

Otrzymuję kod do wygenerowania pliku PDF z szablonu 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'  
Zręczny pionek
źródło
0

Jeśli masz dane kontekstowe wraz z css i js w swoim szablonie HTML. Niż masz dobrą opcję korzystania z pdfjs .

W swoim kodzie możesz użyć w ten sposób.

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

W swoim kodzie HTML możesz połączyć zewnętrzne lub wewnętrzne css i js, wygeneruje to najlepszą jakość PDF.

Manoj Datt
źródło