You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Django+WeasyPrint生成的PDF无法打开,求技术支持

Django + WeasyPrint生成PDF无法打开问题排查

问题背景

近期使用Django结合WeasyPrint生成PDF报告,应用已支持文件上传下载、CSV及XLSX生成下载功能,但生成的PDF文件下载后无法正常打开,后端无报错信息。环境配置正常:Docker容器已安装WeasyPrint,requirements.txt指定weasyprint==63.0,无依赖缺失提示。

相关代码实现

模型定义

import uuid
from django.db import models

class HelpdeskReport(models.Model):
    class Meta:
        app_label = "helpdesk"
        db_table = "helpdesk_report"
        verbose_name = "Helpdesk Report"
        verbose_name_plural = "Helpdesk Reports"
        default_permissions = ["view", "add", "delete"]
        permissions = [
            ("download_helpdeskreport", "Can download Heldpesk Report"),
        ]

    def _report_path(instance, filename) -> str:
        return f"helpdesk/reports/{instance.id}"

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=100, unique=True)
    file = models.FileField(upload_to=_report_path)

服务类代码

import uuid
from io import BytesIO
from django.core.files import File
from django.template.loader import render_to_string
from weasyprint import HTML

class HeldpeskReportService:
    def create_report(self) -> HelpdeskReport:
        report_name = f"helpdesk_report_{uuid.uuid4().hex}.pdf"
        html_source = render_to_string(template_name="reports/global_report.html")
        html_report = HTML(string=html_source)
        pdf_report = html_report.write_pdf()
        # print("check : ", pdf_report[:5])  # It gives proper content beginning (PDF header)
        pdf_bytes = BytesIO(pdf_report)
        pdf_file = File(file=pdf_file, name=report_name)  # 此处存在变量引用错误
        report = HelpdeskReport(name=report_name, file=pdf_file)
        report.full_clean()
        report.save()
        return report

模板代码

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html" charset="utf-8">
</head>
<body>
  <article>
    <h2>test report</h2>
  </article>
</body>
</html>

视图代码

from django.http import HttpResponse
from rest_framework.decorators import api_view
from rest_framework.request import Request

@api_view(http_method_names=["POST"])
def create_report(request: Request) -> HttpResponse:
    """
    Create a HelpdeskReport.
    """
    report = HelpdeskReportService().create_report()
    response = HttpResponse(content=report.file)
    response["Content-Type"] = "application/pdf"
    response["Content-Disposition"] = f"attachment; filename={report.name}"
    return response

qpdf检测结果

qpdf helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf -
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf: file is damaged
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (offset 2612): xref not found
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf: Attempting to reconstruct cross-reference table
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 6 0, offset 196): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 6 0, offset 66): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 6 0, offset 66): recovered stream length: 224
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 10 0, offset 1560): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 10 0, offset 373): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 10 0, offset 373): recovered stream length: 2194
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 11 0, offset 2894): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 11 0, offset 2636): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 11 0, offset 2636): recovered stream length: 455
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 16 0, offset 3889): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 16 0, offset 3187): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 16 0, offset 3187): recovered stream length: 1248
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 17 0, offset 4628): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 17 0, offset 4567): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 17 0, offset 4567): recovered stream length: 96
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (xref stream: object 17 0, offset 4628): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (xref stream: object 17 0, offset 4567): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (xref stream: object 17 0, offset 4567): recovered stream length: 96
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (offset 4567): error decoding stream data for object 17 0: stream inflate: inflate: data: incorrect header check
qpdf: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf: error decoding candidate xref stream while recovering damaged file

解决思路与方案

1. 修复服务类中的变量引用错误

服务类中创建File对象时,错误地引用了未定义的pdf_file变量,正确的应该是使用之前生成的pdf_bytes,同时需要重置BytesIO的指针到开头,确保文件内容被完整写入:

# 修正后的服务类代码片段
pdf_bytes = BytesIO(pdf_report)
pdf_bytes.seek(0)  # 重置指针到文件开头
pdf_file = File(file=pdf_bytes, name=report_name)

2. 修正视图中返回文件的方式

直接将report.file传给HttpResponse可能无法正确读取文件内容,需要打开文件并读取字节流:

# 修正后的视图代码
@api_view(http_method_names=["POST"])
def create_report(request: Request) -> HttpResponse:
    report = HelpdeskReportService().create_report()
    with report.file.open('rb') as f:
        response = HttpResponse(content=f.read(), content_type='application/pdf')
    response["Content-Disposition"] = f"attachment; filename={report.name}"
    return response

3. 优化模板的编码设置

模板中的meta标签写法不规范,修改为标准的UTF-8编码声明,避免HTML渲染时出现编码问题:

<meta charset="utf-8">
<!-- 或者 -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

4. 验证PDF生成的完整性

可以在服务类中临时保存生成的PDF到本地,验证WeasyPrint是否生成了完整的PDF文件:

# 在服务类中添加临时保存代码(用于调试)
with open('/tmp/test.pdf', 'wb') as f:
    f.write(pdf_report)

如果本地保存的PDF可以正常打开,说明问题出在Django的文件存储或返回环节,否则需要检查WeasyPrint的依赖是否完整(比如libpango、libcairo等系统库)。

内容的提问来源于stack exchange,提问作者lbris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 16:52:03