【问题标题】:Non-ASCII characters are not correctly displayed in PDF when served via HttpResponse and AJAX通过 HttpResponse 和 AJAX 提供时,非 ASCII 字符无法正确显示在 PDF 中
【发布时间】:2021-02-17 03:05:18
【问题描述】:

我生成了一个 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 时,尽管以蒙特塞拉特字体显示,但西里尔字符未正确显示:

提供 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();
        }
    }
 });

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

用 cmets 的想法解决

感谢我收到的所有 cmets,特别是来自 @Melvyn 的所有 cmets,我终于想出了一个解决方案。我没有创建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();
        }
    }
 });

返回响应时处理错误

您可以从 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 中查看更多详细信息。

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

【问题讨论】:

  • 确保字体嵌入在 PDF 中,而不是仅仅假设客户端将拥有该字体。请显示生成 PDF 的代码。
  • 嗨@AntoinePinsard。我添加了与 reportlab 一起使用的行来嵌入字体。我想这就是你的意思,对吧?问题出在 httpresponse 中,在媒体内生成的文件中,一切都很好......
  • 我已经在没有字体的计算机中检查了媒体中的 PDF 文件,它也正确显示。
  • @Sunil,我通过以下链接stackoverflow.com/questions/377644/… 解决了这个问题。基本上,您必须在 Python 中引发错误,然后使用 AJAX 原生错误处理。
  • 它仍然失败,因为返回类型应为 blob。我通过在下面添加而不是仅仅返回类型来解决它。 xhr: function() { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 2) { if (xhr.status == 200) { xhr.responseType = "blob"; } } }; return xhr; }

标签: javascript python django utf-8 reportlab


【解决方案1】:

您正在进行一些编码/重新编码,因为如果您查看文件之间的差异,就会发现它到处都是unicode replacement characters

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

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

您说您尝试过不设置编码和字符集,但我认为测试不正确 - 很可能您看到了一个激进的浏览器缓存版本。

正确的方法是使用 FileResponse,传入文件名,让 Django 找出正确的内容类型。

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

首先,将Cyrillic_good.pdf(没有错.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,这是一个本地问题,您应该查看中间件或您的操作系统或小绿人,而不是代码。我在本地使用您的文件进行此操作,并且没有发生任何修改。

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

JS部分

我希望转换发生在 blob 构造函数中,因为可以将 blob 传递给类型。我不确定默认是二进制安全的。 你的数据有一个错误属性也很奇怪,你将整个事情传递给 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();
    }
}

【讨论】:

  • 以这种方式使用FileResponse会导致以下错误ValueError: read of closed file。显然,FileReponse 不能与上下文管理器一起使用(请参阅code.djangoproject.com/ticket/29278)。无论如何,我已经清除了所有缓存,并且直接使用了return FileResponse(open(filename))。这会在lib/encodings/cp1252.py 的第 23 行出现以下错误:charmap code can't decode byte 0x8d in position 561: character map to &lt;underfined&gt;。所以它似乎无法正确进行编码......
  • 我也尝试过这个FileResponse(open(filename, encoding="utf-8"),这会导致以下错误UnicodeDecodeError: 'utf-8' codec can't decode byte 0x93 in position 10: invalid start byte。无论如何,我认为这个编码命令不应该在像 PDF 这样的二进制文件中工作。
  • 顺便说一句,您可能是正确的,浏览器正在使用缓存,因为在我使用 FileReponse 下载文件之前。
  • 你需要编码。我不确定它为什么要尝试传递文件名=文件名,所以 mimetypes.guess_type 正确设置了内容。请设置 content_type 或 encoding,以便它尝试读取为文本。那就是问题所在。这些应该是二进制响应,使用二进制文件输入。除非您有一个默认以文本形式打开的自定义 FileSystemStorage(),否则我不明白它为什么会这样做。
  • 表示 jquery 不尊重标头但假定文本响应,无论服务器发送的 mime 类型如何。我想知道 Axios 是否做得更好。为了您的理解:PDF file 是一个二进制 blob 容器,即使它的 document 内容可以是 UTF-8 编码的文本,并且容器具有 ASCII 格式的 pdf-reader 指令,它与嵌入的图像和字体交织在一起。
【解决方案2】:

对于那些在视图中进行表单验证的人,您需要在 js 文件中添加以下代码,因为返回类型应为 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();
}  

【讨论】:

  • 嗨@Sunil。处理错误的好方法。另一种方法是直接从 Python 返回错误(请参阅我对问题的最后编辑)。
  • @DavidDuran,我遇到了 JsonResponse 的问题。与错误函数一样,预期数据的数据类型是 blob,因为我们将其初始化为 blob。
猜你喜欢
  • 1970-01-01
  • 2019-05-21
  • 1970-01-01
  • 2023-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多