通过 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();
}
我生成了一个 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();
}