python解析ttf转svg

技术 · 2023-03-16 · 53 人浏览

python解析ttf转svg

解析TTF文件并查看结构

  • 使用fontTools库中的ttLib模块

TrueType字体的所有表

TagTable主要内容
cmapcharacter code mapping字符到字形的映射
glyfglyph outline字形轮廓数据
headfont header字体全局信息
hheahorizontal header水平布局字体的通用信息
hmtxhorizontal metrics每个字符的水平布局度量
locaglyph location各自行在glyf表中的偏移量
maxpmaximum profile字体所需内存信息
namename字体版权、名称等信息
postglyph name and PostScript compatibility打印时所需的信息

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

#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']

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

在标准楷体文件的19张表中,包括9张必须表,9张可选表,还有一张名为GlyphOrder的表。

它存储了字符名称和字形索引的映射,目的是为了方便人们查找字形数据GlyphOrder并不是TrueType字体文件中的字体表,计算机在解析TTF文件时也会忽略这张表。

我们可以使用fontTools中的getGlyphOrder()获取所有字符名称的列表,除去特殊字符,字符名称都是以Unicode十六进制编码命名的。

解析glyf表并提取字形

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

生成PNG图片

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

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')
#生成PNG图片
# 第一步提取绘制命令语句
#获取包含字形名称和字形对象的--字形集对象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']

代码中的commands是一个列表,每个元素都是“指令+坐标”的形式,M代表绘制起点,Q代表二次贝塞尔曲线,Z代表闭合路径。

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

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

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

#从'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可以看懂的命令语句

#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图片

#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")

生成SVG图片

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

所以需要用到SVG中的transform属性对图形进行翻转。下面提供了两种方法,生成的结果只是viewBox的大小不一样而已,视觉上是一样的:

from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
#加载字体
def text_svg(text,lg): # text文本 lg语言
    font = TTFont("./font/"+lg+".ttf")
    #与7.1.1相同--提取绘制命令语句
    glyphset = font.getGlyphSet()
    font1=format(hex(ord(text))[2:])
    glyph = glyphset['uni'+font1.upper()]
    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}" style="width:100px; height:100px">
    <g transform="matrix(1 0 0 -1 0 {yMax})">
    <path stroke = "black" fill = "none" d="{pen.getCommands()}"/>
    </g>
    </svg>"""
    #写入SVG文件
    with open("./image/"+lg+".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("./image/test2.svg", "w") as f:
        f.write(svg2)
python ttf svg
Theme Jasmine by Kent Liao