I am fairly new to Django but I've been stuck on a problem for several days. I have been trying to find answers online without any success.
I have a view called CreateReportView where I am trying to use weasyprint to generate a pdf that gets downloaded. In the future I want to be able to save the bytes array to a database, so a user can see the generated reports and then to download them. But a first step is just being able to download the generated pdf. When using the weasyprint write_pdf() method and saving to disc, e.g. to test.pdf, the pdf is ok when opening the file. However, when I try to generate the PDF to a variable and save it in either File- or HTTPresponse as an attachment, I only get text with characters in the browser that looks like this:
%PDF-1.7 %🖤 5 0 obj <> stream xڽXm 6 _ ? 9 IpZ [(]h n ܧ p r d ή ױg yy ( G 1 @ \ ? z
I have tried everything I can think of, such as changing between Http- and FileResponse, saving the file temporary to the disc, opening it and then passing it to the respons; using a buffer; etc.. I have also tried using the Reportlab library and I get the same problem. To clarify, when I tried using the HttpResponse, I used the following code:
response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="test.pdf"'
I saw that someone solved the problem using a buffer (Using Weasyprint to create file response), but it didn't work for me. The code for this is commented in the view below. I get the same problem
I found that someone else solved it by changing a 'traffic-cop' routine, but I cant seem to find the code that needs to be changed (Django - can't download file (showed as text instead)).
Also, when using the inspection mode in Chrome, I saw that the response includes the necessary headers such as content-disposition and and content-type.
Any help or guidance would be greatly appreciated. Let me know if you need further information.
Thank you in advance!
EDIT: The website also uses Alpine.js and HTMX. There is no HTMX or Alpine.js scripts in the template that gets rendered by the write_pdf method. In my base site, I have some HTMX-attributes as:
<body x-data="{'isModalOpen': false}" hx-boost='True' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
View file:
from django.core.files import File
from django.db.models import Avg, Count, Case, When, IntegerField, DecimalField
from django.http import HttpResponse, FileResponse
from django.shortcuts import render
from django.template.loader import render_to_string
from django.views import View
from weasyprint import HTML
from salary.models import Salary
class CreateReportView(View):
def get(self, request, *args, ** kwargs):
... # Omitted unrelevant code
context = context={'results': results, 'results_per_gender': results_per_gender, 'results_per_region': results_per_region, 'results_titles': results_titles}
rendered_html = render_to_string('report_template.html', context=context)
# buffer = io.BytesIO()
# pdf_file = HTML(string=rendered_html)
# pdf_file.write_pdf(buffer)
pdf_file = HTML(string=rendered_html).write_pdf()
# buffer.seek(0)
response = FileResponse(pdf_file, as_attachment=True, filename="test.pdf")
return response
weasyprint.HTML.write_pdf()
returns a bytes
instance when invoked without arguments but it returns None
when invoked with argument. If invoked with a file instance or an str
path to a file as target
, it writes the PDF bytes
data to the target
. There is nothing wrong with what you were doing all along on the Server.
buffer = BytesIO()
HTML(string=html).write_pdf(buffer)
buffer.seek(0)
return FileResponse(buffer, as_attachment=True, filename="test.pdf")
The problem was completely Client side. When you add HTMX attribute hx-boost="true"
to any HTML tag like so:
<div hx-boost="true">
What HTMX does is that it boosts normal anchor and Form
tags to use AJAX. This has a good effect which is that if the user does not have javascript enabled, the site will continue to work.
A GET request will push the request URL onto the Browser History Stack (if from anchor tags). The response body will be used to swap the innerHTML
of the body
tag of the page.
If the response body is valid HTML, Browsers can render it just fine. But since it isn’t, that was the problem. Browser’s cannot render PDF when the bytes
of the PDF is used to swap the innerHTML
of the body
tag of the page hence the result you got:
%PDF-1.7 %🖤 5 0 obj <> stream xڽXm 6 _ ? 9 IpZ [(]h n ܧ p r d ή ױg yy ( G 1 @ \ ? z
To complete the response and let the browser handle download on its own without attempting to swap the innerHTML
of the body
tag of the page with the response body, add hx-swap="none"
attribute to the section involved in making the request:
<div hx-boost="true" hx-swap="none">