TTF与图片之间的相互转换,使用python,potrace,fontforge

概述

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需要安装,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脚本中,我们把字体位置做了向下对齐,而非上下居中,这一点可以自行修改。
另外细节也会有损失,字体矢量化过程中矢量信息会增多,所以最终生成的字体文件也变大了不少。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值