Skip to content

Python如何解析TTF?

Author: [麟十一]

Link: [https://mp.weixin.qq.com/s/Kay6WSVMkQ8VpNANUhshfg]

这篇文使用Python来解析TTF文件,全文共3节,第一节介绍需要用到的两个Python库 ,第二节解析TTF文件结构并查看字体表中的内容 ,第三节通过解析'glyf'表,从中提取字形数据并绘制成PNG和SVG两种格式的图片 。 本文涉及到矢量图形绘制命令、SVG文件、TTF文件、XML解析 和少量字符编码 的内容,建议大家可以先阅读一下之前的文章对这些内容做个简单的了解,避免阅读过程中感到困惑:

必读篇:

  1. TTF简介和结构 TTF里都有什么

  2. SVG简介和绘制命令介绍 SVG里都有什么

选读篇:

  1. XML解析方法 SVG笔记(一):Python如何解析SVG?第一部分

  2. 字符编码 Unicode和它的朋友们(上)

  3. Unicode和它的朋友们(下) 本文使用的环境是 ,不同Python库要求的版本在后文会有说明。

1. 需要用到的两个库

不同语言解析TTF或者OTF文件有不同方法,例如 可以使用 ,C语言 可以使用 , 的话可以使用 。

今天要使用的是 中的 库(_https://fonttools.readthedocs.io/en/latest/_) 官网上对于这个库的定义很简洁:

fontTools is a family of libraries and utilities for manipulating fonts in Python.

fontTools是一个操作字体的库 ,它可以处理TrueType、OpenType等多种字体,还可以将TTF文件和XML文件进行转换。本文主要使用了fontTools中的 和 模块,ttLib模块负责处理TTF文件pens模块用来提取和构建字形轮廓 。fontTools要求Python版本> =3.6

在第三节绘制字形轮廓的时候我们还会用到 库,这是一个非常强大的绘图库,官网定义如下:

Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python.

本文主要使用matplotlib库中的 和 模块来绘制字形轮廓, matplotlib要求Python版本 >=3.5,官网地址 _https://matplotlib.org/_

2. 解析TTF文件并查看结构

这一节主要解析TTF文件的结构并查看字体表中的内容,一共介绍两种方法。第一种方法是使用fontTools库中的ttLib模块 ,第二种方法是先将TTF文件转换成XML文件 ,再通过解析XML的方式来解析TTF文件 。fontTools方法获得的信息会更多一些,但XML方法更直观,两种方法获得的结果相同,下文中会混合使用。

2.1 查看所有字体表

之前介绍TTF文件的时候提到过,TrueType字体文件中共有9张必须表 ,先复习一下(有关TrueType字体的所有表介绍见苹果官网 _https://developer.apple.com/fonts/TrueType-Reference-Manual/_

TrueType字体的9张必须表

我们以标准楷体文件(simkai.ttf) 为例,先用fontTools方法查看该文件中有哪些字体表:

python
#1. fontTools方法查看所有表
from fontTools.ttLib import TTFont
#1.1 加载TTF文件
font = TTFont("simkai.ttf")
#1.2 获取所有表名
print(font.keys())
#['GlyphOrder', 'head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'cmap', 'fpgm', 'prep', 'cvt ', 'loca', 'glyf', 'name', 'post', 'gasp', 'GSUB', 'vhea', 'vmtx', 'DSIG']

函数可以得到包含所有字体表名称的列表 ,#1.2的结果说明标准楷体文件中一共有 张字体表。

下面使用XML方法,我们先将TTF文件转换成XML文件,Python解析XML一共有3种方法,本文使用 方法来解析XML文件:

python
#2. XML方法查看所有表
import xml.dom.minidom
#2.1 TTF转换成XML文件
font.saveXML("simkai.xml")
#2.2 DOM方法解析XML
DOMTree = xml.dom.minidom.parse('simkai.xml')
#2.2.1 提取根节点
root = DOMTree.documentElement
#2.2.2 提取所有子节点
childnodes = root.childNodes
#2.2.3 处理多余的换行符节点
tables = []
for i in childnodes:
    if str(i) == '<DOM Text node "\'\\n\\n  \'">' or str(i) == '<DOM Text node "\'\\n\\n\'">':
        continue
    else:
        tables.append(i)
  #2.3 查看所有表
print(tables)

由于#2.2.2中的 函数会将XML文件中的换行符也当做节点返回 ,所以#2.2.3中对结果做了一个处理。#2.3展示了TTF文件中的所有字体表,可以看到,文件中的每一张表都被转换成了一个节点 ,一共 个节点。

2.2 GlyphOrder

在标准楷体文件的19张表中,包括9张必须表,9张可选表 ,还有一张名为**’GlyphOrder'** 的表。 The 'id' attribute is only for humans; it is ignored when parsed. 上面是fontTools对'GlyphOrder'表的注释,它存储了字符名称和字形索引的映射,目的是为了方便人们查找字形数据 ,'GlyphOrder'并不是TrueType字体文件中的字体表,计算机在解析TTF文件时也会忽略这张表。 我们可以使用fontTools中的 获取所有字符名称的列表,除去特殊字符,字符名称都是以Unicode十六进制编码 命名的。列表中字符名称的顺序就是TTF文件中字形数据的存储顺序 ,我们查找一下“马”是第几个字形:

python
#3.'GlyphOrder'表
#3.1 fontTools方法
#3.1.1 获取'GlyphOrder'表中的字符名称列表
Glyphorder_table = font.getGlyphOrder()
#3.1.2 查看前10项
print(glyphorder_table[0:10])
#['.notdef', 'glyph00001', 'glyph00002', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand']
#3.1.3 查找"马"是第几个字形数据
print("马的Unicode十六进制编码是 {}".format(hex(ord("马"))[2:]))
#马的Unicode十六进制编码是 9a6c
for i in range(len(glyphorder_table)):
    if glyphorder_table[i] == 'uni9A6C':
        print("马是第 {} 个字形".format(i))
    else:
        continue
#马是第 20642 个字形

可以看到“马”在标准楷体文件中是第 个字形,我们使用XML方法也可以验证一下:

python
#3.2 XML方法
#3.2.1 从根节点提取'GlyphOrder'节点
glyphorder_node = root.getElementsByTagName('GlyphOrder')
#3.2.2 获取GlyphOrder节点下的所有子节点
glyphorder_node_list = glyphorder_node[0].getElementsByTagName('GlyphID')
#3.2.3 查找字符"马"的节点
ma_node = glyphorder_node_list[20642]
#3.2.4 查看属性值
print(ma.getAttribute('id'))
#20642
print(ma.getAttribute('name'))
#uni9A6C

在XML文件中,节点GlyphOrder的每一个子节点都代表一个字符,每个字符包含了 和 两个属性,画张图来看'GlyphOrder'表的结构:

'GlyphOrder'表的结构--XML版

2.3 获取表中的信息

不同的字体表中存储着不同的数据,这一节我们就来看看表中存储的信息,由于表比较多,在这里只简单举几个例子:

python
#4. 查看表中的数据
#4.1 fontTools方法
#4.1.1 所有字形的边界框
xMin, xMax, yMin, yMax = font['head'].xMin, font['head'].xMax, font['head'].yMin, font['head'].yMax
print("所有字形的边界框: xMin = {}, xMax = {}, yMin = {}, yMax = {}".format(xMin, xMax, yMin, yMax))
#4.1.2 所有字形的最大上坡度和下坡度
ascent, descent = font['hhea'].ascent, font['hhea'].descent
print("所有字形的最大上坡度为 {}, 最大下坡度为 {}".format(ascent, descent))
#4.1.3 "马"的步进宽度和左侧轴承
width, lsb = font['hmtx']['uni9A6C']
print("'马'的步进宽度为 {}, 左侧轴承为 {}".format(width, lsb))
#4.1.4 TTF文件中存储了多少个字形数据
numGLyphs = font['maxp'].numGlyphs
print("标准楷体文件中共有 {} 个字符的字形数据".format(numGLyphs))
#4.1.5 TTF文件的编码方式
encoding_format = font['cmap'].tables[0].getEncoding()
print("标准楷体文件在Windows系统下的字符编码方式为 {} ".format(encoding_format))

#4.1.1 从**'head'** 表中提取了所有字形的边界框坐标,#4.1.2从**'hhea'** 表中提取了所有字形的最大上坡度ascent和最大下坡度descent,#4.1.3从**'hmtx'** 表中提取了“马”的步进宽度和左侧轴承,#4.1.4从**'maxp'** 表中查看了标准楷体文件中字形数据的个数,#4.1.5在**'cmap'** 表中查看了标准楷体文件的编码方式,结果如下:

python
#4.1 fontTools方法
#4.1.1 所有字形的边界框
所有字形的边界框: xMin = -12, xMax = 264, yMin = -47, yMax = 220
#4.1.2 所有字形的最大上坡度和下坡度
所有字形的最大上坡度为 220, 最大下坡度为 -36
#4.1.3 "马"的步进宽度和左侧轴承
'马'的步进宽度为 256, 左侧轴承为 23
#4.1.4 TTF文件中存储了多少个字形数据
标准楷体文件中共有 28562 个字符的字形数据
#4.1.5 TTF文件的编码方式
标准楷体文件在Windows系统下的字符编码方式为 utf_16_be

从结果可以看到标准楷体文件 中共有 个字符,而且字符编码方式为UTF-16大端序 。从'head'表和'hhea'表中我们还提取出了适用于所有字形的字形度量,从'hmtx'表中提取了具体字符“马”的步进宽度和左侧轴承,这些字形度量的概念在TTF里都有什么一文中有详细介绍。 下面用XML方法,在这里简单看一下'head','hhea'和'hmtx'三个表中的数据提取过程:

Details
python
#4.2 XML方法
#4.2.1 所有字形的边界框
#提取head节点
head = root.getElementsByTagName('head')
#分别提取边界框
xMin = head[0].getElementsByTagName('xMin')[0].getAttribute('value')
xMax = head[0].getElementsByTagName('xMax')[0].getAttribute('value')
yMin = head[0].getElementsByTagName('yMin')[0].getAttribute('value')
yMax = head[0].getElementsByTagName('yMax')[0].getAttribute('value')
print("所有字形的边界框: xMin = {}, xMax = {}, yMin = {}, yMax = {}".format(xMin, xMax, yMin, yMax))
#所有字形的边界框: xMin = -12, xMax = 264, yMin = -47, yMax = 220
#4.2.2 所有字形的最大上坡度和下坡度
#提取hhea节点
hhea = root.getElementsByTagName('hhea')
#分别提取最大上坡度和最大下坡度
ascent = hhea[0].getElementsByTagName('ascent')[0].getAttribute('value')
descent = hhea[0].getElementsByTagName('descent')[0].getAttribute('value')
print("所有字形的最大上坡度为 {}, 最大下坡度为 {}".format(ascent, descent))
#所有字形的最大上坡度为 220, 最大下坡度为 -36
#4.2.3 "马"的步进宽度和左侧轴承
#提取hmtx节点
hmtx = root.getElementsByTagName('hmtx')
#分别提取"马"的编码,步进宽度和左侧轴承
name = hmtx[0].getElementsByTagName('mtx')[27016].getAttribute('name')
width = hmtx[0].getElementsByTagName('mtx')[27016].getAttribute('width')
lsb = hmtx[0].getElementsByTagName('mtx')[27016].getAttribute('lsb')
print("'马'的Unicode编码为 {}, 步进宽度为 {}, 左侧轴承为 {}".format(name, width, lsb))
#'马'的Unicode编码为 'uni9A6C', 步进宽度为 256, 左侧轴承为 23

两种方法的结果是一样的,我们可以发现XML方法语法更复杂一些,所以如果只是想看看TTF文件的结构,推荐直接打开XML文件查看 没有必要再用Python解析了。

2.4 字形映射

在2.2节中我们通过'GlyphOrder'表发现“马”在标准楷体文件中是第 个字形,但在#4.2.3中我们又发现“马”在'hmtx'表中是第 个字形,这其实与字形的排列顺序有关:

python
#5. 字符和字形数据的映射
print("马的Unicode十六进制编码是 {}".format(hex(ord("马"))[2:]))
#马的Unicode十六进制编码是 9a6c
#5.1 通过字符名称获取字形ID
font.getGlyphID("uni9A6C") #20642
#5.2 根据字形ID获取字符名称
font.getGlyphName(20642) #'uni9A6C'
#5.3 根据字形ID获取字符名称
font.getGlyphOrder()[20642]  #'uni9A6C'
#5.4 根据字形ID获取字符名称
font.glyphOrder[20642] #'uni9A6C'
#5.5 按字母顺序表提取字形数据
glyphnames = font.getGlyphNames()
for i in range(len(glyphnames)):
    if glyphnames[i] == 'uni9A6C':
        print("按照字母顺序表排列,'马'的字形数据是第 {} 个".format(i))
#按照字母顺序表排列,'马'的字形数据是第 27016 个

刚刚的5种方法都可以查找TTF文件中的字符和字形映射,不过结果略有不同。#5.5使用 获取的字符名称列表是按照字母顺序表排列 的,XML文件中的'hmtx'节点和'glyf'节点中的字形数据都是按这个顺序进行排列的,而TTF文件中真正的字形ID并非如此排列。

简单来说,使用fontTools方法在TTF文件中查询字形,需要使用字形ID, 在这里就是20642;而使用XML方法查询字形,需要用到按字母顺序表排列的字形ID ,即27016。

在“TTF有什么”那一篇文中提到过,'cmap','loca'和'glyf'三张表之间存在着映射关系。'cmap'表包含了字符名称和对应的字形ID'loca'表存储了各个字符相对于'glyf'表头的偏移量 ,也就是具体字形数据的位置,'glyf'表里有字符名称和字形数据

一般的字符映射过程是:根据字符名称在'cmap'表中查找字形ID ,然后通过'loca'表定位到字形数据在'glyf'表中的位置 ,最后从'glyf'表中取出数据 。先复习一下这个流程:

字符映射流程--"马"

用Python代码查看一下:

Details
python
#6. "马"的字形映射
#6.1 fontTools方法
#6.1.1 'cmap'表 - 查看字形ID
font['cmap'].tables[0].ttFont.getGlyphID("uni9A6C")
#20642
#6.1.2 'loca'表 - 查看字形相对于'glyf'头部的偏移量
#查看'loca'表格式 - 0 for short offsets,1 for long
  print('loca表格式 {}'.format(font['head'].indexToLocFormat))
#loca表格式 1
print('偏移量 {}'.format(font['loca'].__getitem__(20642)))
#偏移量 7944804
print("'马'字形长度 {}".format(font['loca'].__getitem__(20642) - font['loca'].__getitem__(20641)))
#"马"字形长度 812
#6.1.3 'glyf'表 -  查看字形数据
font['glyf']["uni9A6C"].xMin #23
font['glyf']["uni9A6C"].xMax #223
font['glyf']["uni9A6C"].yMin #-24
font['glyf']["uni9A6C"].yMax #194
font['glyf']["uni9A6C"].coordinates
#GlyphCoordinates([(182, 102),(194, 109),(211, 100),(223, 91),(217, 81),(213, 69),(208, 20),(198, -7),(173, -24),(171, -5),(147, 19),(179, 5),(190, 15),(198,1),(197, 87),(192, 95),(165, 94),(111, 87),(87, 82),(77, 73),(66, 91),(72, 95),(78, 114),(80, 143),(73, 159),(90, 153),(99, 143),(93, 135),(84, 94),(85, 91),00, 93),(142, 98),(137, 109),(142, 121),(148, 147),(151, 176),(144, 179),(101, 171),(79, 166),(62, 178),(70, 178),(79, 178),(107, 180),(136, 186),(151, 194),63, 188),(176, 179),(164, 168),(157, 127),(150, 99),(133, 58),(152, 64),(167, 58),(174, 48),(138, 47),(68, 38),(43, 32),(23, 46),(51, 45),(92, 51)])
font['glyf']['uni9A6C'].flags
#array('B', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])

#6.1.1我们从'cmap'表中通过字符名称'uni9A6C'查找到“马”的字形ID为20642#6.1.2使用字形ID在'loca'表中查找出“马”的字形数据相对于'glyf'表头的偏移量为7944804字节长度为812字节#6.1.3中查询了'glyf'表中“马”的字形数据,包括边界框坐标,轮廓点坐标和flags (判断轮廓点是否在曲线上)

多说一句#6.1.2中的 方法,这个方法是用于查看TTF文件中**'loca'表的格式(format)** ,这个格式决定了偏移量的大小: The size of the offset depends upon the format used. This is specified in the Font Header ('head') table. In the short format, an offset of 100 would represent 200 bytes. In the long format, an offset of 100 would represent 100 bytes.

这是苹果官网中对于short format和long format的解释,在上面的代码中我们看到indexToLocFormat返回的值为 ,说明该TTF文件中的'loca'表为长格式,即1偏移量等于1字节 。 下面我们用XML方法看一下字符和字形的映射,不过在TTF转换成XML文件时,'loca'表中的数据没有被转换 ,对此fontTools给出了注释: The 'loca' table will be calculated by the compiler.

所以下面我们只查看'cmap'表和'glyf'表的结构和内容,先看'cmap'表的结构示意图:

'cmap'表的结构--XML版

'cmap'表中包含了多张子表,具体使用哪一张子表会根据解析平台来确定 。比如上图的子表中属性platformID为3 ,说明我们使用了Windows系统 进行解析,属性(Windows Platform-specific Encoding Identifiers)为1 ,说明字符编码支持UnicodeBMP-only (UCS-2) ,属性language为0 代表着解析平台非Macintosh系统 。 除去这三个属性,我们还能看到绿色的子表名称为cmap_format_4 ,这代表着'cmap'中根据解析平台选择的子表格式为格式4 ,这是一种两字节编码格式 。'cmap'子表一共有9种格式,在这里不一一介绍了,具体内容都在之前贴过的苹果官网链接中。 上图中的绿色子表中包含了许多红色子表 ,一个红色字表代表了一个字符,每个子表都包含了属性 和 ,即字符编码字符名称

接下来看一下'glyf'表的结构:

'glyf'表的结构--XML版

绿色框代表着字形数据 ,包含字符名称name和字形边界框的坐标,红色框代表轮廓线数据 ,每一个pt标签中都是一个轮廓点的坐标,这里的on就是flags,即判断轮廓点是否在曲线上的参数。

最后我们使用XML方法从'glyf'表中提取“马”的字形轮廓坐标,本小节最开始的时候提到过,XML文件中'glyf'表的字形ID是按照字母顺序表来排列的,所以**“马”在XML文件的'glyf'节点中字形ID是27016** :

python
#6.2 XML方法
#6.2.1 'glyf'表 - 查看字形数据
#获取glyf节点
glyf = root.getElementsByTagName('glyf')
#获取字形列表
ttglyph = glyf[0].getElementsByTagName('TTGlyph')
#获取"马"的字符名称
ttglyph[27016].getAttribute('name')#'uni9A6C'
#获取"马"的边界框坐标
ttglyph[27016].getAttribute('xMin') #23
ttglyph[27016].getAttribute('xMax') #223
ttglyph[27016].getAttribute('yMin') #-24
ttglyph[27016].getAttribute('yMax') #194
#获取"马"的字形轮廓节点
contour = ttglyph[27016].getElementsByTagName('contour')
print(contour)
#[<DOM Element: contour at 0x192de792cc0>, <DOM Element: contour at 0x192de7aac28>]
#获取"马"的第一条轮廓中第一个轮廓点的信息
contour[0].getElementsByTagName('pt')[0].getAttribute('x') #182
contour[0].getElementsByTagName('pt')[0].getAttribute('y') #102
contour[0].getElementsByTagName('pt')[0].getAttribute('on') #0

上面的结果我都注释在了代码中,这里不做过多解释了。 fontTools和XML两种方法各有优劣,个人感觉XML方法 适合直接使用记事本查看内容或者结构,解析的话这种方法耗费时间稍长 ,而且语法更麻烦fontTools方法 可以直接操作TTF文件,语法更简单,速度也更快 ,不过需要读者提前对于各个表中的属性有所了解 ,否则会出现提取不出数据的错误。

3. 解析'glyf'表并提取字形

最后一节主要针对'glyf'表中的字形数据进行提取和复现,3.1节 生成(Portable Network Graphics)格式的位图图片,3.2节 生成(Scalable Vector Graphics)格式的矢量图片。

3.1 生成PNG图片

绘制PNG图片的思路是:从TTF文件中提取出轮廓点坐标和绘制指令,之后将绘制命令转化为matplotlib可以识别的指令,最后使用matplotlib进行绘图

这部分内容在网上资料很少,所以我造了一个小轮子来生成图片,第一步是提取绘制语句

python
#7. 提取"马"的字形并复现
from fontTools.ttLib.ttFont import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.path import Path
import matplotlib._color_data as mcd
%matplotlib inline
#加载字体
font = TTFont('simkai.ttf')
#7.1 生成PNG图片
#7.1.1 第一步提取绘制命令语句
#获取包含字形名称和字形对象的--字形集对象glyphset
glyphset = font.getGlyphSet()
#获取pen的基类
pen = SVGPathPen(glyphset)
#查找"马"的字形对象
glyph = glyphset['uni9A6C']
#绘制"马"的字形对象
glyph.draw(pen)
#提取"马"的绘制语句
commands = pen._commands
print(commands)
#['M84 94', 'Q85 91 92.5 92.0', 'Q100 93 142 98', 'Q137 109 139.5 115.0', 'Q142 121 145.0 134.0', 'Q148 147 149.5 161.5', 'Q151 176 147.5 177.5', 'Q144 179 122.5 175.0', 'Q101 171 90.0 168.5', 'Q79 166 70.5 172.0', 'Q62 178 70 178', 'Q79 178 93.0 179.0', 'Q107 180 121.5 183.0', 'Q136 186 143.5 190.0', 'Q151 194 157.0 191.0', 'Q163 188 169.5 183.5', 'Q176 179 170.0 173.5', 'Q164 168 160.5 147.5', 'Q157 127 150 99', 'Q182 102 188.0 105.5', 'Q194 109 202.5 104.5', 'Q211 100 217.0 95.5', 'Q223 91 220.0 86.0', 'Q217 81 215.0 75.0', 'Q213 69 210.5 44.5', 'Q208 20 203.0 6.5', 'Q198 -7 185.5 -15.5', 'Q173 -24 172.0 -14.5', 'Q171 -5 159.0 7.0', 'Q147 19 163.0 12.0', 'Q179 5 184.5 10.0', 'Q190 15 194.0 38.0', 'Q198 61 197.5 74.0', 'Q197 87 194.5 91.0', 'Q192 95 178.5 94.5', 'Q165 94 138.0 90.5', 'Q111 87 99.0 84.5', 'Q87 82 82.0 77.5', 'Q77 73 71.5 82.0', 'Q66 91 69.0 93.0', 'Q72 95 75.0 104.5', 'Q78 114 79.0 128.5', 'Q80 143 76.5 151.0', 'Q73 159 81.5 156.0', 'Q90 153 94.5 148.0', 'Q99 143 96.0 139.0', 'Q93 135 84 94', 'Z', 'M92 51', 'Q133 58 142.5 61.0', 'Q152 64 159.5 61.0', 'Q167 58 170.5 53.0', 'Q174 48 156.0 47.5', 'Q138 47 103.0 42.5', 'Q68 38 55.5 35.0', 'Q43 32 33.0 39.0', 'Q23 46 37.0 45.5', 'Q51 45 92 51', 'Z']

代码中的 是一个列表,每个元素都是“指令+坐标 ”的形式,M代表绘制起点,Q代表二次贝塞尔曲线,Z代表闭合路径。“马”字的轮廓中没有出现其他的指令,但是TrueType字体中的绘图指令共有10种 ,在其他类型的TTF文件中也会涉及到其他的命令,具体的命令解释见SVG里都有什么的最后一节。

由于我想要把不同的轮廓线用不同的颜色显示出来,所以对commands列表做了一点修改,修改后列表的每一个子列表代表一条轮廓线

python
#将绘制命令按照轮廓线划分
total_commands = []
command = []
for i in commands:
    #每一个命令语句
    if  i == 'Z':
        #以闭合路径指令Z区分不同轮廓线
        command.append(i)
        total_commands.append(command)
        command = []
    else:
        command.append(i)

为了让字形正确地显示在图片正中心,我们从'head'表中提取所有字形的边界框

python
#从'head'表中提取所有字形的边界框
xMin = font['head'].xMin
yMin = font['head'].yMin
xMax = font['head'].xMax
yMax = font['head'].yMax
print("所有字形的边界框: xMin = {}, xMax = {}, yMin = {}, yMax = {}".format(xMin, xMax, yMin, yMax))
#所有字形的边界框: xMin = -12, xMax = 264, yMin = -47, yMax = 220

之后我们进行 第二步将TTF中的绘制命令转换成matplotlib可以看懂的命令语句 (预警,这一部分,比较长)

Details
python
#7.1.2 将TTF中的绘制命令转换成matplotlib可以看懂的命令语句
#笔的当前位置
preX = 0.0
preY = 0.0
#笔的起始位置
startX = 0.0
startY = 0.0
#所有轮廓点
total_verts = []
#所有指令
total_codes = []
#转换命令
for i in total_commands:
    #每一条轮廓线
    verts = []
    codes = []
    for command in i:
        #每一条轮廓线中的每一个命令
        code = command[0] #第一个字符是指令
        vert = command[1:].split(' ') #其余字符是坐标点,以空格分隔
        # M = 路径起始 - 参数 - 起始点坐标 (x y)+
        if code == 'M':
            codes.append(Path.MOVETO)  #转换指令
            verts.append((float(vert[0]), float(vert[1])))  #提取x和y坐标
            #保存笔的起始位置
            startX = float(vert[0])
            startY = float(vert[1])
            #保存笔的当前位置(由于是起笔,所以当前位置就是起始位置)
            preX = float(vert[0])
            preY = float(vert[1])
        # Q = 绘制二次贝塞尔曲线 - 参数 - 曲线控制点和终点坐标(x1 y1 x y)+
        elif code == 'Q':
            codes.append(Path.CURVE3)  #转换指令
            verts.append((float(vert[0]), float(vert[1]))) #提取曲线控制点坐标
            codes.append(Path.CURVE3) #转换指令
            verts.append((float(vert[2]), float(vert[3]))) #提取曲线终点坐标
            #保存笔的当前位置--曲线终点坐标x和y
            preX = float(vert[2])
            preY = float(vert[3])
        # C = 绘制三次贝塞尔曲线 - 参数 - 曲线控制点1,控制点2和终点坐标(x1 y1 x2 y2 x y)+
        elif code == 'C':
            codes.append(Path.CURVE4)  #转换指令
            verts.append((float(vert[0]), float(vert[1]))) #提取曲线控制点1坐标
            codes.append(Path.CURVE4) #转换指令
            verts.append((float(vert[2]), float(vert[3]))) #提取曲线控制点2坐标
            codes.append(Path.CURVE4) #转换指令
            verts.append((float(vert[4]), float(vert[5]))) #提取曲线终点坐标
            #保存笔的当前位置--曲线终点坐标x和y
            preX = float(vert[4])
            preY = float(vert[5])
        # L = 绘制直线 - 参数 - 直线终点(x, y)+
        elif code == 'L':
            codes.append(Path.LINETO)  #转换指令
            verts.append((float(vert[0]), float(vert[1]))) #提取直线终点坐标
            #保存笔的当前位置--直线终点坐标x和y
            preX = float(vert[0])
            preY = float(vert[1])
        # V = 绘制垂直线 - 参数 - 直线y坐标 (y)+
        elif code == 'V':
            #由于是垂直线,x坐标不变,提取y坐标
            x = preX
            y = float(vert[0])
            codes.append(Path.LINETO)  #转换指令
            verts.append((x, y)) #提取直线终点坐标
            #保存笔的当前位置--直线终点坐标x和y
            preX = x
            preY = y
        # H = 绘制水平线 - 参数 - 直线x坐标 (x)+
        elif code == 'H':
            #由于是水平线,y坐标不变,提取x坐标
            x = float(vert[0])
            y = preY
            codes.append(Path.LINETO)  #转换指令
            verts.append((x, y)) #提取直线终点坐标
            #保存笔的当前位置--直线终点坐标x和y
            preX = x
            preY = y
        # Z = 路径结束,无参数
        elif code == 'Z':
            codes.append(Path.CLOSEPOLY)  #转换指令
            verts.append((startX, startY)) #终点坐标就是路径起点坐标
            #保存笔的当前位置--起点坐标x和y
            preX = startX
            preY = startY
        #有一些语句指令为空,当作直线处理
        else:
            codes.append(Path.LINETO)  #转换指令
            verts.append((float(vert[0]), float(vert[1]))) #提取直线终点坐标
            #保存笔的当前位置--直线终点坐标x和y
            preX = float(vert[0])
            preY = float(vert[1])
    #整合所有指令和坐标
    total_verts.append(verts)
    total_codes.append(codes)

最后一步,我们来绘制这个图像并保存成PNG图片

Details
python
#7.1.3 绘制PNG图片
#获取matplotklib中的颜色列表
color_list = list(mcd.CSS4_COLORS)
#获取所有的轮廓坐标点
total_x = []
total_y = []
for contour in total_verts:
    #每一条轮廓曲线
    x = []
    y = []
    for i in contour:
        #轮廓线上每一个点的坐标(x,y)
        x.append(i[0])
        y.append(i[1])
    total_x.append(x)
    total_y.append(y)
#创建画布窗口
fig, ax = plt.subplots()
#按照'head'表中所有字形的边界框设定x和y轴上下限
ax.set_xlim(xMin, xMax)
ax.set_ylim(yMin, yMax)
#设置画布1:1显示
ax.set_aspect(1)
#添加网格线
ax.grid(alpha=0.8,linestyle='--')
#画图
for i in range(len(total_codes)):
    #(1)绘制轮廓线
    #定义路径
    path = Path(total_verts[i], total_codes[i])
    #创建形状,无填充,边缘线颜色为color_list中的颜色,边缘线宽度为2
    patch = patches.PathPatch(path, facecolor = 'none', edgecolor = color_list[i+10], lw=2)
    #将形状添到图中
    ax.add_patch(patch)
    #(2)绘制轮廓点--黑色,点大小为10
    ax.scatter(total_x[i], total_y[i], color='black',s=10)
#保存图片
plt.savefig("simkai-马.png")

同样的方法,我还使用标准黑体(simhei.ttf) 画了一个“马”,我们看一下两个字形的结果:

标准楷体 vs 标准黑体的"马"

全篇都是在画“马”难免有些无聊,我们下来画一张复杂的“ ”看一下,由于点太多,所以这里只显示轮廓线,“麟”一共有 条轮廓线: 标准楷体 - 配色过丑 - “麟”

3.2 生成SVG图片

刚刚我们生成的是PNG文件,我们还可以生成矢量图SVG,不过这里需要注意,SVG的坐标系和我们常见的x,y轴坐标系是不同的。SVG坐标系中的零点(0,0)位于左上角,X轴向右为正,Y轴向下为正 ,所以如果我们根据TTF中的命令来绘制图像的话,图像会是反的:

常见坐标系 vs SVG坐标系中的"马"

所以我们需要用到SVG中的 属性对图形进行翻转,有关

transform属性更多的内容见 _https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform_ 。下面提供了两种方法,生成的结果只是 的大小不一样而已,视觉上是一样的:

Details
python
#7.2 绘制SVG图片
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
#加载字体
font = TTFont("simkai.ttf")
#与7.1.1相同--提取绘制命令语句
glyphset = font.getGlyphSet()
glyph = glyphset['uni9A6C']
pen = SVGPathPen(glyphset)
glyph.draw(pen)
#方法1
#获取所有字形的边界框坐标
xMin, xMax, yMin, yMax = font['head'].xMin, font['head'].xMax, font['head'].yMin, font['head'].yMax
#viewbox的宽和高
height1 = yMax - yMin
width1 = xMax - xMin
#SVG语句
svg1 = f"""<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="{xMin} 0 {width1} {height1}">
<g transform="matrix(1 0 0 -1 0 {yMax})">
<path stroke = "black" fill = "none" d="{pen.getCommands()}"/>
</g>
</svg>"""
#写入SVG文件
with open("test1.svg", "w") as f:
    f.write(svg1)
#方法2
#获取"马"的步进宽度和左侧轴承
width2, lsb = font['hmtx']['uni9A6C']
#获取所有字形的上坡度和下坡度
ascent, descent = font['hhea'].ascent, font['hhea'].descent
height2 = ascent - descent
#SVG语句
svg2 = f"""<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="{lsb} 0 {width2} {height2}">
<g transform="matrix(1 0 0 -1 0 {ascent})">
<path stroke = "black" fill = "none" d="{pen.getCommands()}" />
</g>
</svg>"""
#写入SVG文件
with open("test2.svg", "w") as f:
    f.write(svg2)

这里的transform属性使用matrix矩阵 的方式旋转字符。我画了一张示意图说明翻转前和翻转后的"马"字:

翻转前 vs 翻转后的"马"

翻转前 vs 翻转后的"马"

以上就是Python解析TTF文件的所有内容 这一篇内容涉及到了我之前写的好几篇文章,有一种前期铺支线,这一篇终于收尾了的感觉

下一篇应该是读写系列的第四篇:有关XML的解析 ,之前还想着应该不会再写第四篇了,没想到最近就在写解析XML的接口 等到下一篇写完,读写系列也就结束了,再之后估计会是2-3篇的OCR识别,1篇opencv的安装,然后一直到年底估计就全都是小程序开发系列的文了。

以上算是今年的写作计划,最近有点忙也有点懒,但是希望自己能一直坚持写,至少先写满一年再说别的 哦对,最后一个小小的愿望,但愿我能在7月初把“无锡游记”修完然后发出来

~END~