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

时间:2013-06-02 19:32:02

标签: python character-encoding true-type-fonts pycairo pillow

我正在创建中文seal script的图像。我有三种用于此任务的真实字体(Jin_Wen_Da_Zhuan_Ti.7zZhong_Guo_Long_Jin_Shi_Zhuan.7zZhong_Yan_Yuan_Jin_Wen.7z,仅用于测试目的)。以下是Microsoft Word中的外观

appearance in 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')

核心代码段

        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)

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

调用它
create_testing_images(['我'], 'fonts/金文大篆体.ttf', save_to_folder='test')

该脚本将在您的文件系统中创建./test/金文大篆体/我.gif

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

enter image description here

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

UnicodeBmp

而另外两个使用Big5hkscs

Big5hkscs_中國龍金石篆 中研院金文

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

font viewer

实际上我也尝试通过尝试使用凌乱的字体名称来获取字体来解决这个问题。我在安装这些字体后尝试了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')

这与金文大篆体.ttf(在Jin_Wen_Da_Zhuan_Ti.7z中)再次相得益彰:

enter image description here

但仍未与他人合作。例如: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)为例:

Mathematica

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

2 个答案:

答案 0 :(得分:7)

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

  

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

...让你半途而废,但如果你没有使用Unicode字体,你似乎还必须手动翻译Unicode字符。

对于使用“big5hkscs”编码的字体,我必须这样做......

>>> u = u'\u6211'      # Unicode for 我
>>> u.encode('big5hkscs')
'\xa7\xda'

...然后使用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)

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

此示例应该适用于ENCODING_MAP中声明的类型,可以根据需要进行扩展(请参阅FreeType encoding strings获取有效的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字符集正确值的唯一方法是通过利用unicode这一事实将Python unsigned long字符强制转换为正确的u'\ua7da'值。内部存储为整数0xa7da,可以是16位或32位,具体取决于编译Python的方式。

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)

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

假设所有这一切都是正确的,那么TTF没有任何问题 - 问题只是PIL使得访问没有Unicode映射的字体的字形有点尴尬,并且所需的字形为{{ 1}}字符代码大于255。

答案 1 :(得分:4)

问题是字体不严格符合TrueType规范。一个快速的解决方案是使用FontForge(您已经使用它),并让它清理字体。

  1. 打开字体文件
  2. 转到Encoding,然后选择Reencode
  3. 选择ISO 10646-1 (Unicode BMP)
  4. 转到File,然后转到Generate Fonts
  5. 另存为TTF
  6. 使用新生成的字体运行脚本
  7. 瞧!它以漂亮的字体打印我!