Render HTML ke PDF di situs Django

117

Untuk situs saya yang didukung django, saya mencari solusi mudah untuk mengubah halaman html dinamis ke pdf.

Halaman menyertakan HTML dan grafik dari Google visualization API (yang berbasis javascript, namun menyertakan grafik tersebut adalah suatu keharusan).

Olli
sumber
Dokumentasi Django sangat dalam dan mencakup banyak hal. Apakah Anda mengalami masalah dengan metode yang disarankan di sana? http://docs.djangoproject.com/en/dev/howto/outputting-pdf/
monkut
1
Ini sebenarnya tidak menjawab pertanyaan itu. Dokumentasi itu tentang cara merender PDF secara asli, bukan dari HTML yang dirender.
Josh
Saya berpendapat bahwa hal yang benar untuk dilakukan adalah membuat browser menghasilkan pdf karena hanya mereka yang melakukan rendering html / css / js dengan benar. lihat pertanyaan ini stackoverflow.com/q/25574082/39998
David Hofmann
Pertanyaan ini di luar topik di SO, tetapi di luar topik di software.SE. Lihat Bagaimana cara mengonversi HTML dengan CSS ke PDF? .
Martin Thoma
coba gunakan wkhtmltopdf learnbatta.com/blog/…
anjaneyulubatta505

Jawaban:

207

Coba solusi dari Reportlab .

Unduh dan instal seperti biasa dengan python setup.py install

Anda juga perlu menginstal modul berikut: xhtml2pdf, html5lib, pypdf dengan easy_install.

Berikut adalah contoh penggunaan:

Pertama tentukan fungsi ini:

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

Kemudian Anda bisa menggunakannya seperti ini:

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

Templatenya:

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

Semoga membantu.

Guillem Gelabert
sumber
9
+1 Saya telah menggunakan solusi ini selama setahun dan itu luar biasa. PISA bahkan dapat membuat kode batang dengan tag sederhana, dan banyak lagi. Dan itu mudah .
arcanum
1
Sobat, reportlab adalah pita untuk diinstal di windows 7 64bit, python2.7 64bit. Masih mencoba ...
Andriy Drozdyuk
5
Sepertinya tidak menjalankan Javascript.
dfrankow
3
pisa sekarang didistribusikan sebagai xhtml2pdf
Pablo Albornoz
12
Di python3, kecuali konversi cStringIO.StringIOke io.StringIO, kita harus mendefinisikan resultsebagai, result = io.BytesIO()bukan result = StringIO.
Sebastien
12

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

Template:

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

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

Melihat:

from easy_pdf.views import PDFTemplateView

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

Jika Anda ingin menggunakan django-easy-pdf pada Python 3 periksa solusi yang disarankan di sini .

laffuste
sumber
2
Ini adalah cara termudah untuk menerapkan opsi yang telah saya coba sejauh ini. Untuk kebutuhan saya (menghasilkan laporan pdf dari versi html) ini hanya berfungsi. Terima kasih!
The NetYeti
1
@alejoss Anda harus menggunakan gaya sebaris sebagai ganti CSS.
digz6666
Solusi ini mungkin tidak langsung bekerja untuk django 3.0 karena django-utils-six dihapus tetapi easy_pdf bergantung padanya.
David
11

Saya baru saja menyiapkan ini untuk CBV. Tidak digunakan dalam produksi tetapi menghasilkan PDF untuk saya. Mungkin perlu bekerja untuk sisi pelaporan kesalahan tetapi sejauh ini berhasil.

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

Digunakan seperti:

class MyPdfView(PDFTemplateView):
    template_name = 'things/pdf.html'
Christian Jensen
sumber
1
Ini bekerja hampir langsung bagi saya. Satu-satunya hal yang harus diganti html.encode("ISO-8859-1")denganhtml.decode("utf-8")
vinyll
Saya telah mengubah kode sebagai @vinyll disebutkan dan sebagai tambahan harus menambahkan baris ke kelas PDFTemplateView:content_type = "application/pdf"
normic
11

Coba wkhtmltopdf dengan salah satu pembungkus berikut

django-wkhtmltopdf atau python-pdfkit

Ini bekerja dengan baik untuk saya, mendukung javascript dan css atau apa pun yang didukung browser webkit.

Untuk tutorial lebih detail silahkan lihat postingan blog ini

jithin
sumber
Bagaimana dengan svg yang disematkan dalam html, apakah itu juga didukung?
mehmet
Berhati-hatilah, webkit tidak mendukung semua yang dilakukan chrome / firefox: webkit.org/status
mehmet
1
django-wkhtmltopdf memberikan keajaiban bagi saya! juga pastikan untuk mematikan semua animasi yang dilakukan mesin javascript / charting Anda.
mehmet
@mehmet itu tidak mendukung diagram batang sederhana saya js. Saya mendapat banyak kesalahan. Bisakah Anda membantu saya dengan itu ??
Manish Ojha
3

Setelah mencoba membuat ini bekerja selama berjam-jam, akhirnya saya menemukan ini: https://github.com/vierno/django-xhtml2pdf

Ini adalah cabang dari https://github.com/chrisglass/django-xhtml2pdf yang menyediakan mixin untuk tampilan berbasis kelas yang umum. Saya menggunakannya seperti ini:

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

Gunakan nama model yang Anda tentukan dalam tampilan Anda dalam huruf kecil semua saat mengisi bidang template. Karena ini adalah GCBV, Anda dapat menyebutnya sebagai '.as_view' di urls.py Anda:

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

Anda dapat menggunakan editor iReport untuk menentukan tata letak, dan menerbitkan laporan di server laporan jasper. Setelah mempublikasikan, Anda dapat memanggil api lainnya untuk mendapatkan hasilnya.

Berikut adalah pengujian fungsionalitasnya:

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)

Dan berikut adalah contoh implementasi pemanggilan:

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

Saya mendapatkan kode untuk menghasilkan PDF dari template 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'  
Cekatan-pawN
sumber
0

Jika Anda memiliki data konteks bersama dengan css dan js di template html Anda. Daripada Anda memiliki opsi yang baik untuk menggunakan pdfjs .

Dalam kode Anda, Anda dapat menggunakan seperti ini.

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

Di HTML Anda, Anda dapat menautkan css dan js eksternal atau internal, ini akan menghasilkan kualitas pdf terbaik.

Manoj Datt
sumber