0

I generate a PDF file using PyFPDF library and would like to return this in-memory buffer without writing it to local disk.

@app.post("/pdf")
def generate_report(id_worker: int = Form(...),  start_date: date = Form(...), end_date: date = Form(...)):
    user = [id_worker]
    start_date = datetime(start_date.year, start_date.month, start_date.day)
    end_date = datetime(end_date.year, end_date.month, end_date.day)
    attendance = filter_by_date(zk, user, start_date, end_date)
    user_history = attendance_to_dict(attendance)
    dates, data, days, errors, updated_history = data_to_july(user_history, start_date, end_date)
    pdf = create_user_pdf(updated_history, start_date, end_date, days, errors)
    pdf_temp = "attendance.pdf"
    pdf.output(pdf_temp)
    name = "report.pdf"

    return FileResponse(pdf_temp, media_type="application/pdf", filename=name)

I've tried the following; however, without success:

name = "report.pdf"
pdf_buff = io.BytesIO()
pdf.output(pdf_buff)
pdf_buff.seek(0)
return FileResponse(pdf_buff.getvalue(), media_type="application/pdf", filename=name)

Is there a way to do this?

Chris
  • 18,724
  • 6
  • 46
  • 80
Ukhu
  • 19
  • 3
  • Does this answer your question? [How to return a PDF file from in-memory buffer using FastAPI?](https://stackoverflow.com/questions/73585779/how-to-return-a-pdf-file-from-in-memory-buffer-using-fastapi) – Chris May 07 '23 at 19:27
  • @Chris, sadly not. I tried using it and I get an empty pdf file. – Ukhu May 07 '23 at 20:30
  • They use a different method for the pdf, in that answer the pdf is already in memory but I don't know which kind, is it string or iobyte. I tried that one and I'm getting an oupuit of 0kb on my pdf file – Ukhu May 08 '23 at 00:50

2 Answers2

1

I wouldn't suggest using PyFPDF, as it is outdated and no longer being maintained. Instead, you could use fpdf2 (see the documentation as well), which is a fork and the successor of PyFPDF, as well as has very similar syntax. You could install it as follows:

pip install fpdf2

As described in this tutorial, when calling FPDF.output() without providing any filepath parameter, the function returns the PDF bytearray buffer, which you could convert to a bytes object using the bytes() function and pass it to a custom Response (if you need to return it as part of a Jinja2 template, see here). As described in this answer, to have the PDF file viewed in the browser, you could set the Content-Disposition response header as follows:

headers = {'Content-Disposition': 'inline; filename="out.pdf"'}

Alternatively, to have the PDF file downloaded rather than viewed in the borwser, use:

headers = {'Content-Disposition': 'attachment; filename="out.pdf"'}

Here are two options on how to generate and return a PDF file from a FastAPI endpoint. The first option uses an endpoint defined with normal def, while the second option uses an async def endpoint. Please have a look at this answer to understand the difference between the two, as well as how FastAPI handles requests for def endpoints compared to async def endpoints.

Option 1 - Using def endpoint

from fastapi import FastAPI, Response
from fpdf import FPDF

app = FastAPI()


def create_PDF(text):
    pdf = FPDF()
    pdf.add_page()
    pdf.set_font('helvetica', 'B', 16)
    pdf.cell(10, 30, text)
    return pdf.output()

    
@app.get('/')
def get_pdf():
    out = create_PDF('Hello World!')
    headers = {'Content-Disposition': 'inline; filename="out.pdf"'}
    return Response(bytes(out), headers=headers, media_type='application/pdf')

Option 2 - Using async def endpoint

from fastapi import FastAPI, Response
from fpdf import FPDF
import asyncio
import concurrent.futures

app = FastAPI()


def create_PDF(text):
    pdf = FPDF()
    pdf.add_page()
    pdf.set_font('helvetica', 'B', 16)
    pdf.cell(10, 30, text)
    return pdf.output()
    

@app.get('/')
async def get_pdf():
    loop = asyncio.get_running_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        out = await loop.run_in_executor(pool, create_PDF, 'Hello World!')
        
    headers = {'Content-Disposition': 'inline; filename="out.pdf"'}
    return Response(bytes(out), headers=headers, media_type='application/pdf')
Chris
  • 18,724
  • 6
  • 46
  • 80
0

I made it work with this.

@app.post("/pdf")
def generate_report(id_worker: int = Form(...),  start_date: date = Form(...), end_date: date = Form(...)):
    user = [id_worker]
    start_date = datetime(start_date.year, start_date.month, start_date.day)
    end_date = datetime(end_date.year, end_date.month, end_date.day)
    attendance = filter_by_date(zk, user, start_date, end_date)
    user_history = attendance_to_dict(attendance)
    dates, data, days, errors, updated_history = data_to_july(user_history, start_date, end_date)
    pdf = create_user_pdf(updated_history, start_date, end_date, days, errors)
    pdf_string = pdf.output(dest='S').encode('latin-1')
    pdf_buff = io.BytesIO(pdf_string)
    pdf_bytes = pdf_buff.getvalue()
    headers = {'Content-Disposition': 'attachment; filename="out.pdf"'}
    return Response(pdf_bytes, headers=headers, media_type='application/pdf')

I used @Chris' suggestion fastapi from buffer, fpdf to bytes and encoding fpdf to make everything work

Ukhu
  • 19
  • 3