概述
TTF是字体文件格式,里面存储的是矢量化的字体信息。TTF与图片之间的相互转换简单描述如下:
- 使用python中的PIL(pillow)图像库可以实现TTF转图片
- 使用potrace可以将图片转为矢量文件svg,再进一步使用fontforge可以将svg转为TTF文件
在windows中,我们可以拿系统字体做一下演示。系统字体的路径为:C:\Windows\Fonts
,这里我们用方正舒体作为示例,字体文件名是FZSTK.TTF
字体的浏览和修改需要使用专业的字体软件,这里使用免费的fontforge,界面略显简单粗糙,但是后续图片转TTF时候还要用,加之免费,所以选用该软件。
fontforge下载地址:https://fontforge.org/en-US/
ps:fontforge默认情况打开字体后,无论字体有没有实现,都会占一个格子,所以可能会出现大量空格子,给我们浏览字体带来不便(托滑动条都难找),因此可做如下设置,以紧凑的方式显示字体:菜单栏依次点击Encoding -> Compact (hide unused glyphs)
Unicode简介
unicode官网:https://home.unicode.org/
字体会关联一套编码系统,比如提到汉字编码我们常见的是GBK,但在做字体相关工作时候,更常用的是Unicode编码系统,Unicode更加通用,它为很多语言做了唯一编码,甚至还有一些表情。Unicode使用4个16进制的数字来作为字符的编码(索引),如4E00
表示汉字一
,0061
表示小写字母a
。
常用汉字的编码范围是:4E00(一)
- 9FA5(龥)
。
截止这篇博客写作时间(2024.09),unicode最新版本是16.0.0,包含了154,998个字符(数量相当多)。可以在这里查询Unicode最新版本情况:https://www.unicode.org/versions/latest/
ps:4位16进制最大可表示65536,所以为什么Unicode能编码15万+字符?暂且不深究了,对于本文来讲不关键。
TTF转图片
这部分我们需要使用两个关键性依赖库,pillow(PIL)和fonttools,其中pillow是必需的,fonttools可选但建议也装上,因为下面代码会依赖。
- fonttools主要用来获取字体的一些信息,主要指Unicode信息。
- pillow用来转图片
安装依赖:
pip install pillow fonttools
有个跟字体相关度较高的简单概念最好能提前了解一下,即字体的 cmap - Character to Glyph Index Mapping Table,它里面存储的是Unicode信息(已转为十进制数字)和对应的字符名称,在python中使用fonttools读取后以字典形式存放。字符名称是个字符串,一般只有西文字符(英文、北欧、拉丁等)会有有含义的名称,其他字符常用字符串形式的16进制Unicode码来作为名称。字符名称了解一下即可,不关键。关键的是Unicode数字,如下图中前面的数字才是关键的,后面字符串不用太在意。
在处理字体相关信息时,代码中会使用很多的try…except语句,并且except不指定错误类型,因为总有字体编码时候不那么规范,导致python里面的两个依赖库处理不了,错误类型有点不确定,所以就全接了,以避免程序崩掉。
代码如下:
示例使用的是TTF文件,实际上OTF文件也能用下面代码。
# -*- coding: utf-8 -*-
import os
from fontTools.ttLib import TTFont
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
def get_cmap(font_file):
"""
Get unicode cmap - Character to Glyph Index Mapping Table
font_file: path of font file
"""
try:
font = TTFont(font_file)
except:
return None
try:
cmap = font.getBestCmap()
except:
return None
font.close()
return cmap
def get_decimal_unicode(font_file):
"""
Get unicode (decimal mode - radix=10) of font.
"""
cmap = get_cmap(font_file)
if cmap is None:
return None
try:
decimal_unicode = list(cmap.keys())
except:
decimal_unicode = None
return decimal_unicode
def decimal_to_hex(decimal_unicode, prefix='uni'):
"""
Convert decimal unicode (radix=10) to hex unicode (radix=16, str type)
"""
def _regularize(single_decimal_unicode, prefix):
# result of hex() contains prefix '0x', such as '0x61',
# while font file usually use 'uni0061',
# so support changing prefix and filling to width 4 with 0
h = hex(single_decimal_unicode)
single_hex_unicode = prefix + h[2:].zfill(4)
return single_hex_unicode
is_single_code = False
if not isinstance(decimal_unicode, (list, tuple)):
decimal_unicode = [decimal_unicode]
is_single_code = True
hex_unicode = [_regularize(x, prefix) for x in decimal_unicode]
if is_single_code:
hex_unicode = hex_unicode[0]
return hex_unicode
def decimal_to_char(decimal_unicode):
"""
Convert decimal unicode (radix=10) to characters
"""
is_single_code = False
if not isinstance(decimal_unicode, (list, tuple)):
decimal_unicode = [decimal_unicode]
is_single_code = True
char = [chr(x) for x in decimal_unicode]
if is_single_code:
char = char[0]
return char
def get_bbox_offset(bbox, image_size):
"""
Get offset (x, y) for moving bbox to the center of image
bbox: bounding box of character, containing [xmin, ymin, xmax, ymax]
"""
if not isinstance(image_size, (list, tuple)):
image_size = (image_size, image_size)
center_x = image_size[0] // 2
center_y = image_size[1] // 2
xmin, ymin, xmax, ymax = bbox
bbox_xmid = (xmin + xmax) // 2
bbox_ymid = (ymin + ymax) // 2
offset_x = center_x - bbox_xmid
offset_y = center_y - bbox_ymid
return offset_x, offset_y
def char_to_image(char, font_pil, image_size, bg_color=255, fg_color=0):
"""
Generate an image containing single character in a font.
char: such as '中' , 'a' ...
font_pil: result of PIL.ImageFont
"""
try:
bbox = font_pil.getbbox(char)
except:
return None
if not isinstance(image_size, (list, tuple)):
image_size = (image_size, image_size)
offset_x, offset_y = get_bbox_offset(bbox, image_size)
offset = (offset_x, offset_y)
# convert ttf/otf to bitmap image using PIL
image = Image.new('L', image_size, bg_color)
draw = ImageDraw.Draw(image)
draw.text(offset, char, font=font_pil, fill=fg_color)
return image
def font2image(font_file,
font_size,
image_size,
out_folder=None,
decimal_unicode=None,
name_mode='char',
image_extension='jpg',
bg_color=255,
fg_color=0,
is_skip=True):
"""
Generate images from a font.
font_size: size of font when reading by PIL, type=float
image_size: image_size should normally be larger than font_size
decimal_unicode: if not None, only generate images of decimal_unicode
name_mode: if not 'char', then will be like 'uni0061'
is_skip: whether skip existed images
"""
if out_folder is None:
out_folder = os.path.splitext(font_file)[0]
os.makedirs(out_folder, exist_ok=True)
font_pil = ImageFont.truetype(font_file, font_size)
if not isinstance(image_size, (list, tuple)):
image_size = (image_size, image_size)
if decimal_unicode is None:
decimal_unicode = get_decimal_unicode(font_file)
for code in decimal_unicode:
char = chr(code)
# get output filename
if name_mode == 'char':
filename = char
else:
filename = decimal_to_hex(code)
filename = os.path.join(out_folder, f'{filename}.{image_extension}')
# skip existed images
if is_skip and os.path.exists(filename):
continue
image = char_to_image(char, font_pil, image_size, bg_color, fg_color)
if image is None:
continue
try:
image.save(filename)
except:
pass
if __name__ == '__main__':
font_file = r'FZSTK.TTF'
font2image(font_file, 112, 128)
生成的结果如图所示:
图片转TTF
需要安装fontforge和potrace,fontforge在概述中已经提到过了,这里再罗列一下。
- fontforge下载地址:https://fontforge.org/en-US/
- potrace下载地址:https://potrace.sourceforge.net/
fontforge需要安装,potrace实际上不需要安装,解压即可。
图片转TTF需要分两步:
- 图片转SVG
- SVG转TTF
图片转SVG
需用到potrace,解压potrace,把potrace.exe
的路径配置到下面python代码中。
然后改一改下面代码中的路径参数,执行即可。
生成的SVG可以用浏览器打开查看,如chrome。
代码如下:
# -*- coding: utf-8 -*-
import os
import subprocess
from PIL import Image
IMAGE_EXTENSIONS = ['jpg', 'png']
POTRACE_PATH = r'.\potrace-1.16.win64\potrace.exe'
def clamp(x, xmin, xmax):
return min(max(x, xmin), xmax)
def get_files(path, extensions):
files = []
for name in os.listdir(path):
fullname = os.path.join(path, name)
if os.path.isfile(fullname):
ext = os.path.splitext(name)[-1].lower()[1:]
if (ext == '') or (ext in extensions):
files.append(fullname)
return files
def get_folders(path):
children_paths = os.listdir(path)
folders = [os.path.join(path, x) for x in children_paths]
folders = [x for x in folders if os.path.isdir(x)]
return folders
def remove_file(filename):
if os.path.exists(filename):
os.remove(filename)
def image2pgm(image_file, dst_size, mode='L'):
"""
Convert ordinary image to resized pgm and return path of pgm_file
mode: image color model, can be 'L' (grayscale) or 'RGB'
"""
if not isinstance(dst_size, (tuple, list)):
dst_size = (dst_size, dst_size)
image = Image.open(image_file).convert(mode)
image = image.resize(dst_size)
pgm_file = os.path.splitext(image_file)[0] + '.pgm'
image.save(pgm_file)
return pgm_file
def image2svg(image_root_folder, out_folder, dst_size, alphamax=1):
"""
Convert image to svg using `potrace`
dst_size: resize image before calling potrace
alphamax: potrace parameter to control the roundness of curves.
larger value lead to round curve, while smaller value lead to straight curve.
"""
# pre-process parameters
if not isinstance(dst_size, (tuple, list)):
dst_size = (dst_size, dst_size)
alphamax = clamp(alphamax, 0., 1.)
folders = get_folders(image_root_folder)
if len(folders) == 0:
folders = [image_root_folder]
for i, folder in enumerate(folders):
print("processing %d / %d, %s" % (i + 1, len(folders), folder))
out_subfolder = folder.replace(image_root_folder, out_folder)
os.makedirs(out_subfolder, exist_ok=True)
image_files = get_files(folder, IMAGE_EXTENSIONS)
for image_file in image_files:
# convert ordinary image to pgm
pgm_file = image2pgm(image_file, dst_size)
svg_file = os.path.splitext(image_file)[0] + '.svg'
svg_file = svg_file.replace(image_root_folder, out_folder)
# convert pgm to svg using potrace
subprocess.run([POTRACE_PATH, pgm_file,
'--alphamax', str(alphamax),
'--svg',
'-o', svg_file]) # ignore_security_alert
remove_file(pgm_file)
if __name__ == '__main__':
image_root_folder = r'.\FZSTK'
out_folder = r'.\FZSTK_SVG'
dst_size = 512
alphamax = 1
image2svg(image_root_folder, out_folder, dst_size, alphamax)
生成的svg示例如下,svg边缘受图片质量的影响,相比原始字符多少有些毛糙。
图片质量包括分辨率较小,128*128,另外由jpeg压缩带来的画质损失。
SVG转TTF
# -*- coding: utf-8 -*-
"""
This script can NOT run directly, use the following command in cmd:
`fontforge.exe -script xxx.py param1 param2 ...`
"""
import os
import sys
import fontforge
def get_files(path, extensions):
files = []
for name in os.listdir(path):
fullname = os.path.join(path, name)
if os.path.isfile(fullname):
ext = os.path.splitext(name)[-1].lower()[1:]
if (ext == '') or (ext in extensions):
files.append(fullname)
return files
def svg2ttf(folder, out_file):
if not os.path.isdir(folder):
raise ValueError("%s is NOT a directory!" % folder)
svg_files = get_files(folder, ['svg'])
infos = []
for svg_file in svg_files:
char = os.path.splitext(svg_file)[0][-1]
decimal_unicode = ord(char)
hex_unicode = "uni" + hex(decimal_unicode)[2:].zfill(4)
infos.append([char, decimal_unicode, hex_unicode, svg_file])
# sort infos by decimal_unicode in ascend order
infos.sort(key=lambda x: (x[1]))
ff_font = fontforge.font()
ff_font.fontname = "fontname"
ff_font.fullname = "fullname"
ff_font.familyname = "familyname"
ff_font.encoding = "Unicode"
for info in infos:
char, dec_code, hex_code, svg_file = info
glyph = ff_font.createChar(dec_code)
glyph.width = 1000
glyph.importOutlines(svg_file)
# Make the glyph lay on the baseline.
ymin = glyph.boundingBox()[1]
glyph.transform([1, 0, 0, 1, 0, -ymin])
ff_font.generate(out_file)
ff_font.close()
if __name__ == '__main__':
# the sys.argv[0] is python scripts itself,
# sys.argv[1] is first param, sys.argv[2] is second param and so on
folder = sys.argv[1]
if len(sys.argv) == 3:
out_file = sys.argv[2]
else:
out_file = folder + '.ttf'
svg2ttf(folder, out_file)
给上述脚本保存个文件名为:svg_to_ttf.py
,需要给该脚本设置两个参数,第一个参数是SVG的文件夹路径,第二个参数是待输出的TTF的路径。
上述脚本在windows中不能直接执行,因为常规的python环境下 import fontforge
会报错,找不到module。需要让fontforge.exe
在脚本模式下去调用python脚本,才能使import fontforge正常生效,所以需要在cmd中使用如下命令:
.\FontForgeBuilds\bin\fontforge.exe -script svg_to_ttf.py FZSTK_SVG FZSTK_new.ttf
生成出来的字体如下,跟上面截图相比最大的区别在于字体的位置,在svg转ttf脚本中,我们把字体位置做了向下对齐,而非上下居中,这一点可以自行修改。
另外细节也会有损失,字体矢量化过程中矢量信息会增多,所以最终生成的字体文件也变大了不少。