Django 사이트에서 HTML을 PDF로 렌더링


117

내 장고 기반 사이트의 경우 동적 html 페이지를 pdf로 변환하는 쉬운 솔루션을 찾고 있습니다.

페이지에는 Google 시각화 API (JavaScript 기반이지만 해당 그래프를 포함해야 함)의 차트와 HTML이 포함됩니다.


Django 문서는 깊고 많이 다루고 있습니다. 거기에서 제안한 방법에 문제가 있습니까? http://docs.djangoproject.com/en/dev/howto/outputting-pdf/
monkut

1
이것은 실제로 질문에 대한 답이 아닙니다. 이 문서는 렌더링 된 HTML이 아닌 기본적으로 PDF를 렌더링하는 방법에 관한 것입니다.
Josh

적절한 html / css / js 렌더링을 수행하는 유일한 브라우저이기 때문에 브라우저가 pdf를 생성하도록하는 것이 옳은 일이라고 생각합니다. 이 질문 참조 stackoverflow.com/q/25574082/39998
데이비드 호프만

이 질문은 SO에서는 주제에서 벗어 났지만 softwarerecs.SE에서는 주제에 관한 것입니다. CSS가있는 HTML을 PDF로 어떻게 변환 할 수 있습니까?를 참조하십시오 . .
Martin Thoma

wkhtmltopdf를 사용해보십시오 learnbatta.com/blog/...을
anjaneyulubatta505

답변:


207

Reportlab 의 솔루션을 사용해보십시오 .

그것을 다운로드하고 python setup.py install로 평소와 같이 설치하십시오.

또한 easy_install을 사용하여 xhtml2pdf, html5lib, pypdf 모듈을 설치해야합니다.

다음은 사용 예입니다.

먼저이 함수를 정의하십시오.

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

그런 다음 다음과 같이 사용할 수 있습니다.

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

템플릿 :

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

도움이 되었기를 바랍니다.


9
+1 저는이 솔루션을 1 년 동안 사용해 왔으며 훌륭합니다. PISA는 간단한 태그 외에도 훨씬 더 많은 바코드를 생성 할 수 있습니다. 그리고 그것의 쉽습니다 .
arcanum

1
야, reportlab은 Windows 7 64bit, python2.7 64bit에 설치하는 피타입니다. 아직 노력 중입니다 ...
Andriy Drozdyuk 2011-07-14

5
Javascript를 실행하지 않는 것 같습니다.
dfrankow

3
피사 지금 xhtml2pdf로 배포됩니다
파블로 알 보르 노즈에게

12
python3에서, 변환 제외 cStringIO.StringIOio.StringIO, 우리는 정의해야합니다 result으로 result = io.BytesIO()대신 result = StringIO.
Sebastien

12

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

주형:

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

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

전망:

from easy_pdf.views import PDFTemplateView

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

Python 3에서 django-easy-pdf를 사용하려면 여기에 제안 된 솔루션을 확인 하십시오 .


2
지금까지 시도한 옵션을 구현하는 가장 쉬운 방법입니다. 내 필요에 따라 (html 버전에서 pdf 보고서 생성) 이것은 단순히 작동합니다. 감사!
NetYeti

1
@alejoss CSS 대신 인라인 스타일을 사용해야합니다.
digz6666

이 솔루션은 django-utils-six가 제거 되었기 때문에 django 3.0에서 바로 작동하지 않을 수 있지만 easy_pdf는 그것에 의존합니다.
David

11

나는 CBV를 위해 이것을 채찍질했다. 프로덕션에는 사용되지 않지만 나를 위해 PDF를 생성합니다. 아마도 오류보고 측면에 대한 작업이 필요하지만 지금까지 트릭을 수행합니다.

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

다음과 같이 사용 :

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

1
이것은 나를 위해 거의 직선적으로 작동했습니다. 유일한 것은 다음으로 교체 html.encode("ISO-8859-1")하는 것이 었습니다.html.decode("utf-8")
vinyll

@vinyll이 언급 한대로 코드를 변경했으며 추가로 PDFTemplateView 클래스에 한 줄을 추가해야했습니다.content_type = "application/pdf"
normic

11

다음 래퍼 중 하나를 사용하여 wkhtmltopdf 를 시도하십시오.

django-wkhtmltopdf 또는 python-pdfkit

이것은 나를 위해 훌륭하게 작동했으며 javascript 및 css 또는 웹킷 브라우저가 지원하는 문제에 대한 모든 것을 지원합니다.

자세한 자습서는이 블로그 게시물을 참조하십시오.


html에 포함 된 svg도 지원됩니까?
메 흐멧


그냥 웹킷는 않는, 조심하지 지원 모든 크롬 / 파이어 폭스을 수행합니다 webkit.org/status
메 흐멧

1
django-wkhtmltopdf는 나를 위해 놀라운 일을했습니다! 또한 자바 스크립트 / 차팅 엔진이 수행하는 모든 애니메이션을 꺼야합니다.
메 흐멧

@mehmet 그것은 내 간단한 막대 차트 js를 지원하지 않았습니다. 많은 오류가 발생했습니다. 도와 주실 수 있나요 ??
Manish Ojha 2011

3

이것을 너무 많은 시간 동안 작동 시키려고 시도한 후에 마침내 이것을 발견했습니다 : https://github.com/vierno/django-xhtml2pdf

일반 클래스 기반보기를위한 믹스 인을 제공하는 https://github.com/chrisglass/django-xhtml2pdf 의 포크입니다 . 나는 이것을 다음과 같이 사용했습니다.

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

템플릿 필드를 채울 때 뷰에서 정의한 모델 이름을 모두 소문자로 사용하십시오. GCBV이기 때문에 urls.py에서 '.as_view'라고 부를 수 있습니다.

    # 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",
       ),

2

iReport 편집기를 사용하여 레이아웃을 정의하고 jasper 보고서 서버에 보고서를 게시 할 수 있습니다. 게시 후 나머지 API를 호출하여 결과를 얻을 수 있습니다.

다음은 기능 테스트입니다.

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)

다음은 호출 구현의 예입니다.

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

1

html 템플릿에서 PDF를 생성하는 코드를 얻습니다.

    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'  

0

html 템플릿에 css 및 js와 함께 컨텍스트 데이터가있는 경우. pdfjs 를 사용하는 좋은 옵션이 있습니다 . .

코드에서 다음과 같이 사용할 수 있습니다.

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

HTML에서 외부 또는 내부 css 및 js를 연결할 수 있으며 최상의 pdf 품질을 생성합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.