通过 HttpResponse 和 AJAX 提供时,非 ASCII 字符无法在 PDF 中正确显示

Non-ASCII characters are not correctly displayed in PDF when served via HttpResponse and AJAX

我生成了一个 PDF 文件,其中包含带有 ReportLab 的西里尔字符(非 ASCII)。为此,我使用了支持此类字符的“Montserrat”字体。当我在 Django 的 media 文件夹中查看生成的 PDF 文件时,字符正确显示:

我在生成 PDF 的函数中使用以下代码嵌入了字体:

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

pdfmetrics.registerFont(TTFont('Montserrat', 'apps/Generic/static/Generic/tff/Montserrat-Regular.ttf'))
canvas_test = canvas.Canvas("media/"+filename, pagesize=A4)
canvas_test.setFont('Montserrat', 18)
canvas_test.drawString(10, 150, "Some text encoded in UTF-8")
canvas_test.drawString(10, 100, "как поживаешь")
canvas_test.save()

但是,当我尝试通过 HttpResponse 提供此 PDF 时,尽管西里尔字符以 Montserrat 字体显示,但并未正确显示:

提供 PDF 的代码如下:

# Return the pdf as a response
fs = FileSystemStorage()
if fs.exists(filename):
    with fs.open(filename) as pdf:
        response = HttpResponse(
            pdf, content_type='application/pdf; encoding=utf-8; charset=utf-8')
        response['Content-Disposition'] = 'inline; filename="'+filename+'"'
        return response

我几乎尝试了所有方法(使用 FileResponse、使用 with open(fs.location + "/" + filename, 'rb') as pdf 打开 PDF...)但都没有成功。其实,我不明白为什么,如果 ReportLab 正确嵌入了字体(media 文件夹中的本地文件),提供给浏览器的文件没有嵌入字体。

有趣的是,我通过 Chrome 或 Edge 使用 Foxit Reader 阅读 PDF。当我使用 Firefox 的默认 PDF 查看器时,会显示不同的错误字符。实际上在那种情况下字体似乎也是错误的:

编辑

感谢@Melvyn,我意识到错误不在直接从 Python 视图发送的响应中,而是在 AJAX 调用中的 success 代码中,我在后面留下:

$.ajax({
    method: "POST",
    url: window.location.href,
    data: { trigger: 'print_pdf', orientation: orientation, size: size},
    success: function (data) {
        if (data.error === undefined) {
            var blob = new Blob([data]);
            var link = document.createElement('a');
            link.href = window.URL.createObjectURL(blob);
            link.download = filename + '.pdf';
            link.click();
        }
    }
 });

这是以某种方式改变编码的代码部分。

根据评论中的想法解决

感谢我收到的所有评论,特别是来自@Melvyn 的评论,我终于想出了一个解决方案。我没有创建 Blob 对象,而是将 AJAX 的 responseType 设置为 Blob 类型。这是可能的,因为 JQuery 3:

$.ajax({
    method: "POST",
    url: window.location.href,
    xhrFields:{
        responseType: 'blob'
    },
    data: { trigger: 'print_pdf', orientation: orientation, size: size},
    success: function (data) {
        if (data.error === undefined) {
            var link = document.createElement('a');
            link.href = window.URL.createObjectURL(data);
            link.download = filename + '.pdf';
            link.click();
        }
    }
 });

在return响应

时处理错误

您可以 return 来自 Python 的错误(即捕获异常),如下所示:

except Exception as err:
    response = JsonResponse({'msg': "Error"})
    error = err.args[0]
    if error is not None:
        response.status_code = 403 # To announce that the user isn't allowed to publish
        if error==13:
            error = "Access denied to the PDF file."
        response.reason_phrase = error
        return response

然后,您只需使用 AJAX 中的原生错误处理(在 success 部分之后):

error: function(data){
    $("#message_rows2").text(data.statusText);
    $('#errorPrinting').modal();
}

this link 中查看更多详细信息。

我希望这 post 可以帮助那些在使用非 ASCII(西里尔)字符生成 PDF 时遇到同样问题的人。我花了好几天...

您正在做一些 encoding/recoding,因为如果您查看文件之间的差异,就会发现其中乱七八糟 unicode replacement characters:

% diff -ua Cyrillic_good.pdf Cyrillic_wrong.pdf > out.diff

% hexdump out.diff|grep 'ef bf bd'|wc -l
    2659

您说您在不设置编码和字符集的情况下进行了尝试,但我认为没有经过正确测试 - 很可能您看到了一个激进的 browser-cached 版本。

执行此操作的正确方法是使用 FileResponse,传入文件名并让 Django 确定正确的内容类型。

以下是工作情况的可重现测试:

首先,将 Cyrillic_good.pdf(不是 wrong.pdf)放入您的媒体根目录。

将以下内容添加到 urls.py:

#urls.py
from django.urls import path
from .views import pdf_serve

urlpatterns = [
    path("pdf/<str:filename>", pdf_serve),
]

与views.py在同一目录:

#views.py
from pathlib import Path

from django.conf import settings
from django.http import (
    HttpResponseNotFound, HttpResponseServerError, FileResponse
)

def pdf_serve(request, filename: str):
    pdf = Path(settings.MEDIA_ROOT) / filename
    if pdf.exists():
        response = FileResponse(open(pdf, "rb"), filename=filename)
        filesize = pdf.stat().st_size
        cl = int(response["Content-Length"])
        if cl != filesize:
            return HttpResponseServerError(
                f"Expected {filesize} bytes but response is {cl} bytes"
            )
        return response

    return HttpResponseNotFound(f"No such file: {filename}")


现在启动 runserver 并请求 http://localhost:8000/pdf/Cyrillic_good.pdf

如果这不能重现有效的 pdf,这是一个 本地问题 ,您应该查看中间件或您的 OS 或小绿人,但不是代码。我在本地处理你的文件,没有发生任何修改。

事实上,现在获得损坏的 pdf 的唯一方法是修改浏览器缓存或响应在 Django 发送它后,因为内容长度检查会阻止发送一个文件与磁盘上的大小不同。

JS 部分

我希望转换发生在 blob 构造函数中,因为可以将类型交给 blob。我不确定默认值是 binary-safe。 您的数据有一个错误 属性 并且您将整个数据传递给 blob,这也很奇怪,但我们看不到您对什么承诺做出反应。
success: function (data) {
    if (data.error === undefined) {
        console.log(data) // This will be informative
        var blob = new Blob([data]);
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = filename + '.pdf';
        link.click();
    }
}

对于那些在视图中进行表单验证的人,您需要在 js 文件中添加以下代码,因为 return type is expected as blob.

xhr: function() {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 2) {
            if (xhr.status == 200) {
                xhr.responseType = "blob";
            }
        }
    };
    return xhr;
},
success: function (response, textStatus, jqXHR) {
    var blob = new Blob([response])
    var link=document.createElement('a');
    link.href=window.URL.createObjectURL(blob);
    link.download="contract.pdf";
    link.click();
},
error: function (response, textStatus, jqXHR) {
    $('#my_form').click();
}