跳转至

字体与编码

本章介绍字体、编码以及亚洲语言支持功能。 如果您只需要为西欧语言生成 PDF,可以先阅读下面的"Unicode 是默认编码"一节, 然后在第一次阅读时跳过其余部分。 我们期望本节内容随着时间推移而大幅扩展。我们希望开源能让我们比其他工具 更好地支持世界上更多的语言,并欢迎在这一领域提供反馈和帮助。

3.1 Unicode 和 UTF8 是默认的输入编码

从 ReportLab 2.0 版本(2006 年 5 月)开始,所有提供给我们的 API 的文本输入 都应该使用 UTF8 编码或 Python Unicode 对象。这适用于 canvas.drawString 及相关 API 的参数、 表格单元格内容、绘图对象参数以及段落源文本。

我们曾考虑让输入编码可配置甚至依赖于区域设置, 但最终决定"显式优于隐式"。

这简化了我们以前在处理希腊字母、符号等方面所做的许多工作。 要显示任何字符,只需找到其 Unicode 码位, 并确保您使用的字体能够显示该字符。

如果您正在适配 ReportLab 1.x 应用程序,或者从包含单字节数据 (例如 latin-1 或 WinAnsi)的其他来源读取数据, 您需要将其转换为 Unicode。Python codecs 包现在包含了 所有常见编码的转换器,包括亚洲语言编码。

如果您的数据没有使用 UTF8 编码,一旦输入非 ASCII 字符, 就会得到一个 UnicodeDecodeError。例如,下面的代码片段 尝试读取并打印一系列名字,包括一个带有法语重音的名字: Marc-André Lemburg。标准错误信息非常有用, 会告诉您它不喜欢哪个字符:

>>> from reportlab.pdfgen.canvas import Canvas
>>> c = Canvas('temp.pdf')
>>> y = 700
>>> for line in file('latin_python_gurus.txt','r'):
...     c.drawString(100, y, line.strip())
...
Traceback (most recent call last):
...
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 9-11: invalid data
-->é L<--emburg
>>>

最简单的修复方法是将您的数据转换为 Unicode, 指明其原始编码,如下所示:

>>> for line in file('latin_input.txt','r'):
...     uniLine = unicode(line, 'latin-1')
...     c.drawString(100, y, uniLine.strip())
>>>
>>> c.save()

3.2 自动输出字体替换

代码中仍有一些地方会引用编码,包括 rl_config 的 defaultEncoding 参数,以及传递给各种 Font 构造函数的参数。 在过去,当人们需要使用 PDF 查看设备支持的 Symbol 和 ZapfDingbats 字体中的字形时, 这些编码非常有用。

默认情况下,标准字体(Helvetica、Courier、Times Roman) 将提供 Latin-1 中可用的字形。但是,如果我们的引擎检测到 字体中没有的字符,它会尝试切换到 Symbol 或 ZapfDingbats 来显示这些字符。 例如,如果您在调用 drawString 时包含一把右向剪刀的 Unicode 字符 ✂,您应该能看到它(在 test_pdfgen_general.py/pdf 中有一个示例)。 在您的代码中无需切换字体。

3.3 使用非标准 Type 1 字体

如上一章所述,每份 Acrobat Reader 都内置了 14 种标准字体。 因此,ReportLab PDF 库只需通过名称引用这些字体。 如果您想使用其他字体,它们必须对您的代码可用, 并且将被嵌入到 PDF 文档中。

您可以使用下面描述的机制在文档中包含任意字体。 我们有一个名为 DarkGardenMK 的开源字体, 可用于测试和/或文档编写目的(您也可以使用它)。 它随 ReportLab 发行版一起提供,位于 reportlab/fonts 目录中。

目前字体嵌入依赖于 Adobe AFM('Adobe Font Metrics')和 PFB('Printer Font Binary') 格式的字体描述文件。前者是 ASCII 文件,包含字体中字符('字形')的信息, 如高度、宽度、边界框信息和其他'度量数据',而后者是描述字体形状的二进制文件。 reportlab/fonts 目录包含 'DarkGardenMK.afm''DarkGardenMK.pfb' 文件,它们被用作示例字体。

在下面的示例中,找到包含测试字体的文件夹, 并将其注册到 pdfmetrics 模块中以供将来使用, 之后我们就可以像使用任何其他标准字体一样使用它。

import os
import reportlab
from reportlab.pdfgen import canvas
folder = os.path.dirname(reportlab.__file__) + os.sep + 'fonts'
afmFile = os.path.join(folder, 'DarkGardenMK.afm')
pfbFile = os.path.join(folder, 'DarkGardenMK.pfb')

from reportlab.pdfbase import pdfmetrics
justFace = pdfmetrics.EmbeddedType1Face(afmFile, pfbFile)
faceName = 'DarkGardenMK' # pulled from AFM file
pdfmetrics.registerTypeFace(justFace)
justFont = pdfmetrics.Font('DarkGardenMK', faceName, 'WinAnsiEncoding')
pdfmetrics.registerFont(justFont)

# Create a canvas to draw on
output_file = "output.pdf"
c = canvas.Canvas(output_file)

# Use the font and write text
c.setFont('DarkGardenMK', 32)
c.drawString(10, 150, 'This should be in')
c.drawString(10, 100, 'DarkGardenMK')

# Save the canvas
c.save()

请注意,参数 "WinAnsiEncoding" 与输入无关; 它是指定字体文件中哪组字符将被激活和可用。

字体的 face name 来自 AFM 文件的 FontName 字段。 在上面的示例中,我们提前知道了名称,但通常字体描述文件的名称 相当晦涩,这时您可能希望从 AFM 文件中自动获取名称。 如果没有更复杂的方法,您可以使用如下简单代码:

class FontNameNotFoundError(Exception):
    pass


def findFontName(path):
    "Extract a font name from an AFM file."

    f = open(path)

    found = 0
    while not found:
        line = f.readline()[:-1]
        if not found and line[:16] == 'StartCharMetrics':
            raise FontNameNotFoundError, path
        if line[:8] == 'FontName':
            fontName = line[9:]
            found = 1

    return fontName

DarkGardenMK 示例中,我们显式指定了要加载的字体描述文件的位置。 通常,您更希望将字体存储在某个规范的位置, 并让嵌入机制知道这些位置。使用我们在本节开头已经看到的相同配置机制, 我们可以指定 Type-1 字体的默认搜索路径。

遗憾的是,目前还没有针对此类位置的可靠标准 (即使在同一平台上也没有),因此您可能需要编辑 reportlab_settings.py~/.reportlab_settings 文件, 以修改 T1SearchPath 标识符的值, 使其包含额外的目录。我们自己的建议是在开发中使用 reportlab/fonts 文件夹;在任何类型的服务器部署中,将所需的字体作为应用程序的打包部分。 这可以避免其他软件或系统管理员安装和卸载字体对您造成影响。

关于缺失字形的警告

如果您指定了编码,通常假定字体设计者已经提供了所有需要的字形。 然而,事实并非总是如此。在我们的示例字体中, 字母表中的字母是存在的,但许多符号和重音符号缺失。 默认行为是当传入无法绘制的字符时,字体打印一个 'notdef' 字符—— 通常是一个色块、点或空格。但是,您可以要求库改为发出警告; 下面的代码(在加载字体之前执行)将在注册字体时 为任何不在字体中的字形生成警告。

import reportlab.rl_config
reportlab.rl_config.warnOnMissingFontGlyphs = 0

3.4 标准单字节字体编码

本节向您展示常见编码中可用的字形。

下面的代码表显示了 WinAnsiEncoding 中的字符。 这是 Windows 和美洲及西欧许多 Unix 系统上的标准编码。 它也被称为代码页 1252(Code Page 1252),并且实际上与 ISO-Latin-1 相同(它包含一两个额外字符)。这是 ReportLab PDF 库使用的默认编码。 它是从 reportlab/lib 中的标准例程 codecharts.py 生成的, 该例程可用于显示字体的内容。边缘的索引编号为十六进制。

下面的代码表显示了 MacRomanEncoding 中的字符。 顾名思义,这是美洲和西欧 Macintosh 计算机上的标准编码。 与非 Unicode 编码一样,前 128 个码位(在本例中为顶部 4 行) 是 ASCII 标准,与上面的 WinAnsi 代码表一致;但底部 4 行不同。

这两种编码可用于标准字体(Helvetica、Times-Roman、Courier 及其变体), 并且可用于大多数商业字体,包括 Adobe 的字体。但是,某些字体包含非文本字形, 这个概念并不真正适用。例如,ZapfDingbats 和 Symbol 可以各自被视为 拥有自己的编码。

3.5 TrueType 字体支持

Marius Gedminas([email protected])在 Viktorija Zaksiene([email protected]) 的帮助下贡献了对嵌入式 TrueType 字体的支持。TrueType 字体以 Unicode/UTF8 方式工作, 不受 256 个字符的限制。

我们使用 reportlab.pdfbase.ttfonts.TTFont 创建 TrueType 字体对象,并使用 reportlab.pdfbase.pdfmetrics.registerFont 进行注册。 在 pdfgen 中直接绘制到画布时,我们可以这样做

import os
import reportlab
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

pdfmetrics.registerFont(TTFont('Vera', 'Vera.ttf'))
pdfmetrics.registerFont(TTFont('VeraBd', 'VeraBd.ttf'))
pdfmetrics.registerFont(TTFont('VeraIt', 'VeraIt.ttf'))
pdfmetrics.registerFont(TTFont('VeraBI', 'VeraBI.ttf'))

# we know some glyphs are missing, suppress warnings
import reportlab.rl_config
reportlab.rl_config.warnOnMissingFontGlyphs = 0

# Create a canvas to draw on
output_file = "output.pdf"
c = canvas.Canvas(output_file)

c.setFont('Vera', 32)
c.drawString(10, 150, "Some text encoded in UTF-8")
c.drawString(10, 100, "In the Vera TT Font!")

# Save the canvas
c.save()

在上面的示例中,TrueType 字体对象是使用以下方式创建的

    TTFont(name,filename)

其中第一个参数指定 ReportLab 内部名称,第二个参数是表示字体 TTF 文件的 字符串(或类文件对象)。在 Marius 的原始补丁中,文件名应该完全正确, 但我们做了修改,使得如果文件名是相对路径, 则会在当前目录以及 reportlab.rl_config.TTFSearchpath 指定的目录中 搜索对应的文件!

在 Platypus 中使用 TT 字体之前,我们应该添加从字体系列名称到 各个字体名称的映射,这些名称描述了在 <b><i> 属性下的行为。

from reportlab.pdfbase.pdfmetrics import registerFontFamily
registerFontFamily('Vera',normal='Vera',bold='VeraBd',italic='VeraIt',boldItalic='VeraBI')

如果我们只有 Vera 常规字体,没有粗体或斜体,那么我们必须将所有字体 映射到相同的内部字体名称。 标签现在可以安全使用, 但没有效果。按照上述方式注册和映射 Vera 字体后, 我们可以使用如下段落文本

<font name="Times-Roman" size="14">This is in Times-Roman</font>
<font name="Vera" color="magenta" size="14">and this is in magenta <b>Vera!</b></font>

TrueType 字体回退(实验性功能)

这是一项实验性功能。当 TrueType 字体不包含某个字符的字形时, ReportLab 可以自动回退到替代字体。 这对于混合文本文档(如拉丁文 + 中日韩文字)非常有用。

该功能默认禁用。设置环境变量 REPORTLAB_FONT_FALLBACK=1 以启用它。

REPORTLAB_FONT_FALLBACK=1 python your_script.py

通过设置 TTFont 上的 substitutionFonts 属性来配置回退字体:

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

font = TTFont('NotoSans', 'NotoSans-Regular.ttf')
fallback = TTFont('NotoSansCJK', 'NotoSansCJK-Regular.ttf')
pdfmetrics.registerFont(font)
pdfmetrics.registerFont(fallback)
font.substitutionFonts = [fallback]
# Now mixed-script text will automatically use the fallback font
c.setFont('NotoSans', 12)
c.drawString(100, 700, 'Hello 你好 World')

还提供了一个便捷函数 registerFontWithFallback

font = pdfmetrics.registerFontWithFallback(
    'NotoSans', 'NotoSans-Regular.ttf',
    fallbackFonts=[TTFont('NotoSansCJK', 'NotoSansCJK-Regular.ttf')]
)

您可以使用 hasGlyph() 检查字体是否包含特定字形:

font.hasGlyph('A')        # True
font.hasGlyph(0x4F60)     # False if font lacks CJK glyphs

3.6 亚洲字体支持

ReportLab PDF 库旨在为亚洲字体提供完整支持。 PDF 是第一个真正可移植的亚洲文本处理解决方案。主要有两种方法: Adobe 的亚洲语言包,或 TrueType 字体。

亚洲语言包

这种方法提供最佳性能,因为不需要在 PDF 文件中嵌入任何内容; 与标准字体一样,一切都在阅读器端。

Adobe 为每种主要语言提供附加组件。在 Adobe Reader 6.0 和 7.0 中, 当您尝试打开使用这些字体的文档时,系统会提示您下载并安装它们。 在早期版本中,打开亚洲文档时会看到错误消息, 需要知道该怎么做。

日语、繁体中文(台湾/香港)、简体中文(中国大陆) 和韩语都受支持,我们的软件了解以下字体:

  • chs = 简体中文(大陆):'STSong-Light'

  • cht = 繁体中文(台湾):'MSung-Light'、'MHei-Medium'

  • kor = 韩语:'HYSMyeongJoStd-Medium'、'HYGothic-Medium'

  • jpn = 日语:'HeiseiMin-W3'、'HeiseiKakuGo-W5'

由于许多用户没有安装字体包,我们包含了一张 日语字符的位图(分辨率较低)。我们将在下面讨论生成它们所需的内容。

此处应该显示一张图片。

在 2.0 版本之前,注册 CID 字体时必须指定众多本地编码之一。 在 2.0 版本中,您应该使用新的 UnicodeCIDFont 类。

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfgen import canvas
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
pdfmetrics.registerFont(UnicodeCIDFont('HeiseiMin-W3'))

# Create a canvas to draw on
output_file = "output.pdf"
c = canvas.Canvas(output_file)

c.setFont('HeiseiMin-W3', 16)

# the two unicode characters below are "Tokyo"
msg = u'\u6771\u4EAC : Unicode font, unicode input'
c.drawString(100, 675, msg)

带有显式编码的旧编码风格仍然有效,但现在仅在需要构建 垂直文本时才相关。我们计划在未来为 UnicodeCIDFont 构造函数添加 更易读的水平和垂直文本选项。以下四个测试脚本生成相应语言的示例:

tests/test_multibyte_jpn.py
tests/test_multibyte_kor.py
tests/test_multibyte_chs.py
tests/test_multibyte_cht.py

在早期版本的 ReportLab PDF 库中,我们必须使用 Adobe 的 CMap 文件 (位于 Acrobat Reader 附近,如果安装了亚洲语言包的话)。 现在我们只需要处理一种编码,字符宽度数据已嵌入到包中, 生成时不再需要 CMap 文件。rl_config.py 中的 CMap 搜索路径 现在已弃用,如果您仅使用 UnicodeCIDFont,它不会产生任何效果。

包含亚洲字符的 TrueType 字体

这是最简单的方法。使用亚洲 TrueType 字体完全不需要特殊处理。 例如,在"控制面板"中安装了日语选项的 Windows 用户, 将拥有一个 "msmincho.ttf" 字体可以使用。但请注意, 解析字体需要时间,而且可能需要在 PDF 中嵌入相当大的子集。 我们现在还可以解析以 .ttc 结尾的文件,它是 .ttf 的一个小变体。

待办事项

我们预计将在一段时间内持续开发此包的这一领域。 以下是主要优先事项的概要。我们欢迎您的帮助!

  • 确保我们在水平和垂直书写中拥有所有编码的准确字符度量数据。

  • UnicodeCIDFont 添加选项,以允许在字体支持的情况下使用垂直和比例变体。

  • 改进段落中的自动换行代码,并允许垂直书写。

3.7 RenderPM 测试

这里也许是提及 reportlab/graphics/renderPM.py 测试函数的最佳位置, 它可以被认为是测试 renderPM(即"像素映射渲染器", 与 renderPDF、renderPS 或 renderSVG 相对)的规范位置。

如果您从命令行运行它,应该会看到大量如下输出。

C:\code\reportlab\graphics>renderPM.py
wrote pmout\renderPM0.gif
wrote pmout\renderPM0.tif
wrote pmout\renderPM0.png
wrote pmout\renderPM0.jpg
wrote pmout\renderPM0.pct
...
wrote pmout\renderPM12.gif
wrote pmout\renderPM12.tif
wrote pmout\renderPM12.png
wrote pmout\renderPM12.jpg
wrote pmout\renderPM12.pct
wrote pmout\index.html

它运行一系列测试,从"Hello World"测试开始, 依次测试各种线条;不同大小、字体、颜色和对齐方式的文本字符串; 基本形状;平移和旋转的组;缩放坐标;旋转字符串;嵌套组; 锚定和非标准字体。

它创建一个名为 pmout 的子目录,将图像文件写入其中, 并写入一个 index.html 页面,方便查看所有结果。

您可能想查看的字体相关测试是测试 #11('非标准字体中的文本字符串') 和测试 #12('测试各种字体')。