py-pdf / fpdf2

Simple PDF generation for Python
https://py-pdf.github.io/fpdf2/
GNU Lesser General Public License v3.0
1.12k stars 254 forks source link

pdf.output() no delivering application/pdf (Django) #116

Closed ivanleoncz closed 3 years ago

ivanleoncz commented 3 years ago

I have the following code while trying to deliver a PDF file via Django:

from django.http import HttpResponse
from fpdf import FPDF

def certificate_entrepreneur(request):
    """
    Provides Entrepreneur Certificate as PDF.
    TODO: define permission (is authenticated)
    TODO: protect view (only if user has completed incubation program)
    """
    pdf = FPDF(orientation='L', unit='mm', format='A4')
    pdf.add_page()
    pdf.set_draw_color(39, 147, 217)
    pdf.line(15, 15, 15, 195)    # left bar
    pdf.line(15, 15, 280, 15)    # upper bar
    pdf.line(280, 15, 280, 195)  # right bar
    pdf.line(15, 195, 280, 195)  # bottom bar
    pdf.image("bfb_certificates/images/image.png", x=165, y=35, w=100)
    response = HttpResponse(pdf.output(), content_type='application/pdf')
    response['Content-Disposition'] = 'filename="file.pdf"'
    return response

After a request, this view provides a new tab where the browser tries to load the PDF file, but in fact, it presents the download option. When I download the file, I check its MIME type, and it turns out that it is a text/plain, comparison to other PDF files that I have from different sources:

~/Downloads $ file --mime-type hello.pdf 
hello.pdf: application/pdf
~/Downloads $ file --mime-type file.pdf 
file.pdf: text/plain

While looking for an answer on Stackoverflow, I found this Thread for Reportlab, which gave me the perception that maybe, I should set the buffer position to the beginning, but the FPDF() class has no method or variable where I can set the buffer position.

I tried to implement a custom buffer by using io.BytesIO() from Python standard library, trying to adapt Reportlab example to fpdf2, but after generating a request to Django, I got the following exception:

    ....
    File "/code/bfb_certificates/views.py", line 38, in certificate_entrepreneur
    response = HttpResponse(pdf.output(), content_type='application/pdf')
    File "/usr/local/lib/python3.9/site-packages/fpdf/fpdf.py", line 1772, in output
    self.close()
    File "/usr/local/lib/python3.9/site-packages/fpdf/fpdf.py", line 452, in close
    self._enddoc()  # close document
    File "/usr/local/lib/python3.9/site-packages/fpdf/fpdf.py", line 2421, in _enddoc
    with self._trace_size("header"):
    File "/usr/local/lib/python3.9/contextlib.py", line 117, in __enter__
    return next(self.gen)
    File "/usr/local/lib/python3.9/site-packages/fpdf/fpdf.py", line 2661, in _trace_size
    prev_size = len(self.buffer)
    TypeError: object of type '_io.BytesIO' has no len()

If buffer position is the issue here, how could I adapt this, so I generate PDF files on the fly and deliver them via Django?

Disclaimer:

Lucas-C commented 3 years ago

Thank you for reporting this problem.

I started with this test:

#!/usr/bin/env python3
from fpdf import FPDF

pdf = FPDF(orientation='L', unit='mm', format='A4')
pdf.add_page()
pdf.set_draw_color(39, 147, 217)
pdf.line(15, 15, 15, 195)    # left bar
pdf.line(15, 15, 280, 15)    # upper bar
pdf.line(280, 15, 280, 195)  # right bar
pdf.line(15, 195, 280, 195)  # bottom bar
pdf.image("docs/fpdf2-logo.png", x=165, y=35, w=100)
with open("issue_116.pdf", "wb") as out_file:
    out_file.write(pdf.output())

The MIME type of the generated file is correct:

$ file --mime-type issue_116.pdf
issue_116.pdf: application/pdf

Hence, I'm starting to wonder if the issue could not come from how Django handles HttpResponse...

Could you please provide a runnable minimal test? Maybe a really minimal Django app with a single ressource returning a PDF, similar to your initial code sample?

ivanleoncz commented 3 years ago

Thank you. I have a Django app here. I just have to do some modifications, and I'll put it here :+1:

alexp1917 commented 3 years ago

you probably want to make sure you are setting the correct value for Content-Disposition as well as making sure that django respects your content_type header

Lucas-C commented 3 years ago

I found an articled dedicated to this: https://python.plainenglish.io/generate-and-serve-pdf-files-with-django-e3efd9fde7bc

Closing this now

senzlord commented 2 years ago

I found an articled dedicated to this: https://python.plainenglish.io/generate-and-serve-pdf-files-with-django-e3efd9fde7bc

Closing this now

I'm sorry, on that articled the PDF is saved to folder first is there anyway to not saved it first and send directly to server?

Lucas-C commented 2 years ago

The information in this thread should be enough for you to implement this, knowing that FPDF.output() returns a bytearray buffer when the method is not passed any argument. Documentation: https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.output

Here is some non-tested pseudo-code based on the aforementioned article:

return FileResponse(pdf.output(), as_attachment=True, content_type='application/pdf')

(edit) Tested solution:

return HttpResponse(bytes(pdf.output()), content_type='application/pdf')
senzlord commented 2 years ago

The information in this thread should be enough for you to implement this, knowing that FPDF.output() returns a bytearray buffer when the method is not passed any argument. Documentation: https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.output

Here is some non-tested pseudo-code based on the aforementioned article:

pdf_as_bytes = pdf.output()
return FileResponse(pdf_as_bytes, as_attachment=True, content_type='application/pdf')

Ty but It show error like this in the browser: image

When I remove content type, it show the byte... image

Lucas-C commented 2 years ago

Without some minimal (but fully functional) code reproducing your problem, it will be very difficult to help... https://stackoverflow.com/help/minimal-reproducible-example

Also, this issue is closed. If you'd like some help from the fpdf2 community, could you please open a new discussion with details about the code you are using?

Lucas-C commented 2 years ago

For reference, a solution was found in https://github.com/PyFPDF/fpdf2/discussions/374:

from fpdf import FPDF
from django.http import HttpResponse
from django.shortcuts import render

def report(request):
    ...
    return HttpResponse(bytes(pdf.output()), content_type='application/pdf')

The original code just missed the conversion from bytearray to bytes