使用无法正确解码名称的字体生成字符图像

ziy*_*ang 18 python truetype character-encoding pycairo pillow

我正在创建中国篆书的图像.我有三种真​​正的类型字体用于此任务(Jin_Wen_Da_Zhuan_Ti.7z,Zhong_Guo_Long_Jin_Shi_Zhuan.7z,Zhong_Yan_Yuan_Jin_Wen.7z,仅用于测试目的).以下是Microsoft Word中的外观

在Word中出现

中文字符"我"(我/我).这是我的Python脚本:

import numpy as np
from PIL import Image, ImageFont, ImageDraw, ImageChops
import itertools
import os


def grey2binary(grey, white_value=1):
    grey[np.where(grey <= 127)] = 0
    grey[np.where(grey > 127)] = white_value
    return grey


def create_testing_images(characters,
                          font_path,
                          save_to_folder,
                          sub_folder=None,
                          image_size=64):
    font_size = image_size * 2
    if sub_folder is None:
        sub_folder = os.path.split(font_path)[-1]
        sub_folder = os.path.splitext(sub_folder)[0]
    sub_folder_full = os.path.join(save_to_folder, sub_folder)
    if not os.path.exists(sub_folder_full):
        os.mkdir(sub_folder_full)
    font = ImageFont.truetype(font_path,font_size)
    bg = Image.new('L',(font_size,font_size),'white')

    for char in characters:
        img = Image.new('L',(font_size,font_size),'white')
        draw = ImageDraw.Draw(img)
        draw.text((0,0), text=char, font=font)
        diff = ImageChops.difference(img, bg)
        bbox = diff.getbbox()
        if bbox:
            img = img.crop(bbox)
            img = img.resize((image_size, image_size), resample=Image.BILINEAR)

            img_array = np.array(img)
            img_array = grey2binary(img_array, white_value=255)

            edge_top = img_array[0, range(image_size)]
            edge_left = img_array[range(image_size), 0]
            edge_bottom = img_array[image_size - 1, range(image_size)]
            edge_right = img_array[range(image_size), image_size - 1]

            criterion = sum(itertools.chain(edge_top, edge_left, 
                                           edge_bottom, edge_right))

            if criteria > 255 * image_size * 2:
                img = Image.fromarray(np.uint8(img_array))
                img.save(os.path.join(sub_folder_full, char) + '.gif')
Run Code Online (Sandbox Code Playgroud)

核心片段在哪里

        font = ImageFont.truetype(font_path,font_size)
        img = Image.new('L',(font_size,font_size),'white')
        draw = ImageDraw.Draw(img)
        draw.text((0,0), text=char, font=font)
Run Code Online (Sandbox Code Playgroud)

例如,如果您将这些字体放在文件夹中./fonts,并使用它来调用它

create_testing_images(['?'], 'fonts/?????.ttf', save_to_folder='test')
Run Code Online (Sandbox Code Playgroud)

该脚本将./test/?????/?.gif在您的文件系统中创建.

现在的问题是,虽然它与第一个字体金文大篆体.ttf(在Jin_Wen_Da_Zhuan_Ti.7z中)运行良好,但该脚本对其他两种字体不起作用,即使它们可以在Microsoft Word中正确呈现:for China龙金石篆的.ttf(在Zhong_Guo_Long_Jin_Shi_Zhuan.7z),它吸引没什么所以bboxNone; 对于中研院金文.ttf(在Zhong_Yan_Yuan_Jin_Wen.7z中),它会在图片中绘制一个没有字符的黑框.

在此输入图像描述

因此无法通过测试criterion,其目的是测试全黑输出.我使用FontForge检查字体的属性,发现第一个字体金文大篆体.ttf(在Jin_Wen_Da_Zhuan_Ti.7z中)使用UnicodeBmp

UnicodeBmp

而另外两个使用Big5hkscs

Big5hkscs_中国龙金石篆 中研院金文

这不是我系统的编码方案.这可能是我的系统中无法识别字体名称的原因:

字体查看器

实际上我也尝试通过尝试使用凌乱的字体名称来获取字体来解决这个问题.我pycairo在安装这些字体后尝试过:

import cairo

# adapted from
# http://heuristically.wordpress.com/2011/01/31/pycairo-hello-world/

# setup a place to draw
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 100, 100)
ctx = cairo.Context (surface)

# paint background
ctx.set_source_rgb(1, 1, 1)
ctx.rectangle(0, 0, 100, 100)
ctx.fill()

# draw text
ctx.select_font_face('?????')
ctx.set_font_size(80)
ctx.move_to(12,80)
ctx.set_source_rgb(0, 0, 0)
ctx.show_text('?')

# finish up
ctx.stroke() # commit to surface
surface.write_to_png('?.gif')
Run Code Online (Sandbox Code Playgroud)

金文大篆体.ttf(在Jin_Wen_Da_Zhuan_Ti.7z中)再次运作良好:

在此输入图像描述

但仍然不和他人在一起.例如:既不ctx.select_font_face('??????')(哪些报告_cairo_win32_scaled_font_ucs4_to_index:GetGlyphIndicesW)也不ctx.select_font_face('¤¤°êÀsª÷¥Û½f')(使用默认字体绘制)有效.(后一个名称是字体查看器中显示的凌乱代码,如上所示,由Mathematica代码行获得,ToCharacterCode["??????", "CP950"] // FromCharacterCode其中CP950是Big5的代码页.)

所以我认为我已经尽力解决这个问题,但仍然无法解决.我还提出了其他方法,比如使用FontForge重命名字体名称或将系统编码更改为Big5,但我仍然更喜欢仅涉及Python的解决方案,因此用户需要更少的额外操作.任何提示将不胜感激.谢谢.

对于stackoverflow的主持人:这个问题乍一看似乎"过于本地化",但它可能发生在其他语言/其他编码/其他字体中,并且解决方案可以推广到其他情况,所以请不要关闭它有这个原因.谢谢.

更新:很奇怪Mathematica可以识别CP936中的字体名称(GBK,可以认为是我的系统编码).以中国龙金石篆.ttf(在Zhong_Guo_Long_Jin_Shi_Zhuan.7z)为例:

数学

但设置ctx.select_font_face('ÖÐøý½ðʯ*­')也不起作用,这将使用默认字体创建字符图像.

Aya*_*Aya 7

西尔维娅对OP的评论......

您可能需要考虑指定encoding参数 ImageFont.truetype(font_path,font_size,encoding="big5")

...让你到达那里,但如果你没有使用Unicode字体,你似乎还必须手动翻译Unicode字符.

对于使用"big5hkscs"编码的字体,我不得不这样做......

>>> u = u'\u6211'      # Unicode for ?
>>> u.encode('big5hkscs')
'\xa7\xda'
Run Code Online (Sandbox Code Playgroud)

...然后u'\ua7da'用来获得正确的字形,这有点奇怪,但它看起来是将多字节字符传递给PIL的唯一方法.

以下代码适用于Python 2.7.4和Python 3.3.1,使用PIL 1.1.7 ...

from PIL import Image, ImageDraw, ImageFont


# Declare font files and encodings
FONT1 = ('Jin_Wen_Da_Zhuan_Ti.ttf',          'unicode')
FONT2 = ('Zhong_Guo_Long_Jin_Shi_Zhuan.ttf', 'big5hkscs')
FONT3 = ('Zhong_Yan_Yuan_Jin_Wen.ttf',       'big5hkscs')


# Declare a mapping from encodings used by str.encode() to encodings used by
# the FreeType library
ENCODING_MAP = {'unicode':   'unic',
                'big5':      'big5',
                'big5hkscs': 'big5',
                'shift-jis': 'sjis'}


# The glyphs we want to draw
GLYPHS = ((FONT1, u'\u6211'),
          (FONT2, u'\u6211'),
          (FONT3, u'\u6211'),
          (FONT3, u'\u66ce'),
          (FONT2, u'\u4e36'))


# Returns PIL Image object
def draw_glyph(font_file, font_encoding, unicode_char, glyph_size=128):

    # Translate unicode string if necessary
    if font_encoding != 'unicode':
        mb_string = unicode_char.encode(font_encoding)
        try:
            # Try using Python 2.x's unichr
            unicode_char = unichr(ord(mb_string[0]) << 8 | ord(mb_string[1]))
        except NameError:
            # Use Python 3.x-compatible code
            unicode_char = chr(mb_string[0] << 8 | mb_string[1])

    # Load font using mapped encoding
    font = ImageFont.truetype(font_file, glyph_size, encoding=ENCODING_MAP[font_encoding])

    # Now draw the glyph
    img = Image.new('L', (glyph_size, glyph_size), 'white')
    draw = ImageDraw.Draw(img)
    draw.text((0, 0), text=unicode_char, font=font)
    return img


# Save an image for each glyph we want to draw
for (font_file, font_encoding), unicode_char in GLYPHS:
    img = draw_glyph(font_file, font_encoding, unicode_char)
    filename = '%s-%s.png' % (font_file, hex(ord(unicode_char)))
    img.save(filename)
Run Code Online (Sandbox Code Playgroud)

请注意,我将字体文件重命名为与7zip文件相同的名称.我尽量避免在代码示例中使用非ASCII字符,因为它们有时会在复制/粘贴时被搞砸.

此示例应该适用于声明的类型ENCODING_MAP,如果需要可以扩展(请参阅FreeType编码字符串以获取有效的FreeType编码),但是在Python str.encode()不生成的情况下,您需要更改一些代码长度为2的多字节字符串.


更新

如果问题出在ttf文件中,你怎么能在PIL和FreeType源代码中找到答案?上面,你似乎在说PIL是罪魁祸首,但是当你想要unicode_char时,为什么必须传递unicode_char.encode(...).decode(...)?

据我所知,TrueType字体格式是在Unicode被广泛采用之前开发的,所以如果你想创建一个中文字体,那么你必须使用当时正在使用的编码之一,而中国自20世纪80年代中期以来,大多数人一直在使用Big5.

因此,有理由认为必须有一种方法可以使用Big5字符编码从Big5编码的TTF中检索字形.

使用PIL渲染字符串的C代码从font_render()函数开始,并最终调用FT_Get_Char_Index()以找到正确的字形,给定字符代码为unsigned long.

但是,PIL的font_getchar()函数产生的unsigned long只接受Python stringunicode类型,并且由于它似乎没有对字符编码本身进行任何转换,因此似乎获取Big5字符集的正确值的唯一方法是强制通过利用内部存储为整数的事实将Python unicode字符转换为正确的unsigned long值,可以是16位还是32位,具体取决于您编译Python的方式.u'\ua7da'0xa7da

TBH,有猜测相当数量的参与,因为我没有刻意去调查究竟效果怎样ImageFont.truetype()encoding参数,但它的外观,它不应该做的字符编码的任何翻译,而是允许单个TTF文件支持相同字形的多个字符编码,使用该FT_Select_Charmap()函数在它们之间切换.

所以,据我所知,FreeType库与TTF文件的交互是这样的......

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class TTF(object):
    glyphs = {}
    encoding_maps = {}

    def __init__(self, encoding='unic'):
        self.set_encoding(encoding)

    def set_encoding(self, encoding):
        self.current_encoding = encoding

    def get_glyph(self, charcode):
        try:
            return self.glyphs[self.encoding_maps[self.current_encoding][charcode]]
        except KeyError:
            return ' '


class MyTTF(TTF):
    glyphs = {1: '?',
              2: '?'}
    encoding_maps = {'unic': {0x6211: 1, 0x66ce: 2},
                     'big5': {0xa7da: 1, 0x93be: 2}}


font = MyTTF()
print 'Get via Unicode map: %s' % font.get_glyph(0x6211)
font.set_encoding('big5')
print 'Get via Big5 map: %s' % font.get_glyph(0xa7da)
Run Code Online (Sandbox Code Playgroud)

...但是由每个TTF来提供encoding_maps变量,并且不需要TTF为Unicode提供一个.实际上,在采用Unicode之前创建的字体不太可能.

假设所有这些都是正确的,那么TTF没有任何问题 - 问题只是PIL使得访问没有Unicode映射的字体的字形有点尴尬,并且所需字形的unsigned long字符代码大于255.

  • 这是一个绝妙的答案! (2认同)