Kivy 展示柬埔寨语(Khmer) Unicode 错误解决方案

问题描述

假设项目中待显示的柬埔寨语为សួស្តី, 但在kivy中展示如下图的效果,明显是错误的。

1 场景

  • Python: 3.6.6
  • OS: Windows 10
  • Kivy: 1.11.1
  • Kivy installation method: pip install kivy

2 代码

代码的字体KhmerOSBattambang-Regular.ttf下载地址

from kivy.uix.label import Label
from kivy.app import App


class KhmerApp(App):
    def build(self):
        label = Label(text="សួស្តី")
        label.font_name = "./KhmerOSBattambang-Regular.ttf"
        label.font_size = 40
        return label


if __name__ == '__main__':
    KhmerApp().run()

3.问题分析

可能性1: 字体错误

由于字体缺失个别字符导致,所以立刻想到的办法是使用其它UI框架加载验证一下显示效果,下面是PyQt加载自定义字体KhmerOSBattambang-Regular.ttf效果:

# Load the font:
import sys

from PyQt5.QtGui import QFontDatabase, QFont
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton

if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = QPushButton()

    font_db = QFontDatabase()
    font_id = font_db.addApplicationFont(r'./KhmerOSBattambang-Regular.ttf')
    families = font_db.applicationFontFamilies(font_id)[0]
    print(families)
    f = QFont()
    f.setFamily(families)
    app.setFont(f)

    mainWindow.setText("សួស្តី")
    mainWindow.show()

    mainWindow.activateWindow()
    mainWindow.raise_()
    app.exec_()

从展示效果看明显不是字体的问题,就是与Kivy本身有关。

可能2 Kivy 有Bug

说Kivy有Bug这个事情还是很难验的, 先来看一下Kivy如何渲染文本的, 首先打开上面测试代码所用的Label控件的源码, 它在安装目录下源码kivy/uix/label.py 文件的Label类除了继承了Widget基类, 内部的self._label 成员是实际要展示的文本内容对像,可以是CoreMarkupLabelCoreLabel的实例。

from kivy.core.text import Label as CoreLabel, DEFAULT_FONT
from kivy.core.text.markup import MarkupLabel as CoreMarkupLabel

class Label(Widget):
     
    # kivy.uix.label.py 代码片段
    def _create_label(self):
        # create the core label class according to markup value
        if self._label is not None:
            cls = self._label.__class__
        else:
            cls = None
        markup = self.markup
        if (markup and cls is not CoreMarkupLabel) or \
           (not markup and cls is not CoreLabel):
            # markup have change, we need to change our rendering method.
            d = Label._font_properties
            dkw = dict(list(zip(d, [getattr(self, x) for x in d])))
            if markup:
                self._label = CoreMarkupLabel(**dkw)
            else:
                self._label = CoreLabel(**dkw)

再来看一下CoreMarkupLabelCoreLabelkivy/core/textmarkup.py代片段是怎么实现的:

from kivy.core.text import Label, LabelBase
from kivy.core.text.text_layout import layout_text, LayoutWord, LayoutLine
from copy import copy
from functools import partial

# We need to do this trick when documentation is generated
MarkupLabelBase = Label
if Label is None:
    MarkupLabelBase = LabelBase


class MarkupLabel(MarkupLabelBase):
    '''Markup text label.

    See module documentation for more informations.
    '''

可以看出CoreMarkupLabelCoreLabel本质都是kivy.core.text.Label的子类, 再来看一下kivy/core/text/__init__.py是怎么实现Label的:

# kivy/core/text/__init__.py  代码片段
# Load the appropriate provider
label_libs = []
if USE_PANGOFT2:
    label_libs += [('pango', 'text_pango', 'LabelPango')]

if USE_SDL2:
    label_libs += [('sdl2', 'text_sdl2', 'LabelSDL2')]
else:
    label_libs += [('pygame', 'text_pygame', 'LabelPygame')]
label_libs += [
    ('pil', 'text_pil', 'LabelPIL')]
Text = Label = core_select_lib('text', label_libs)

可以看现Label 是一系列的工厂类提供者, 有基于pango、sdl2、pygame(低层也是sdl2)、pillow, 再看一下安装时候配置site-packages\kivy\setupconfig.py

# Autogenerated file for Kivy configuration
PY3 = 1
CYTHON_MIN = '0.24'
CYTHON_MAX = '0.29.10'
CYTHON_BAD = '0.27, 0.27.2'
USE_RPI = 0
USE_EGL = 0
USE_OPENGL_ES2 = 0
USE_OPENGL_MOCK = 0
USE_SDL2 = 1
USE_PANGOFT2 = 0
USE_IOS = 0
USE_ANDROID = 0
USE_MESAGL = 0
USE_X11 = 0
USE_WAYLAND = 0
USE_GSTREAMER = 1
USE_AVFOUNDATION = 0
USE_OSX_FRAMEWORKS = 0
DEBUG_GL = 0
DEBUG = False
PLATFORM = "win32"

从上面看默认使用了LabelSDL2 和 LabelPIL, 所以在kivy/core/text/init.py 的 Text = Label = core_select_lib('text', label_libs) 打个断点调试看Label是哪个类:

Label 是LabelSDL2类名,所以之后所有的控件的文本都是交给LabelSDL2来渲染的, 而LabelSDL2是在kivy/core/text/text_sd2.py中定义的

class LabelSDL2(LabelBase):
    # 代码片断
    def _render_begin(self):
        self._surface = _SurfaceContainer(self._size[0], self._size[1])

这里是在字体渲染时候生了_SurfaceContainer的对象,它是C代码编译了成pyd文件,在pycharm里自动生成的C:\Users\admin\AppData\Local\JetBrains\PyCharm2020.1\python_stubs\498501734\kivy\core\text_text_sdl2.py文件,只供接口查看,隐藏了内部实现细节

# encoding: utf-8
# module kivy.core.text._text_sdl2
# from C:\Users\admin\Envs\wumart32\lib\site-packages\kivy\core\text\_text_sdl2.cp36-win32.pyd
# by generator 1.147
"""
TODO:
    - ensure that we correctly check allocation
    - remove compat sdl usage (like SDL_SetAlpha must be replaced with sdl 1.3
      call, not 1.2)
"""

# imports
import builtins as __builtins__ # <module 'builtins' (built-in)>

class _SurfaceContainer(object):
    """ _SurfaceContainer(w, h) """
    def get_data(self): # real signature unknown; restored from __doc__
        """ _SurfaceContainer.get_data(self) """
        pass

然而_text_sdl2.cp36-win32.pyd内部的C代码怎么实现,需要上github代码仓里看,且也没有修改调试试, 到这SDL2的text渲染暂时先放放,接下来换一个渲染库验证一下。

4.使用PIL text渲染库

从上面的分析中Kivy是支持Pillow字体渲染的,把安装目录中的kivy/core/text/inti.py 暂时修改一下, 然后pillow渲染效果:

# 代码片段 kivy/core/text/__inti__.py 
# Load the appropriate provider
label_libs = []
if USE_PANGOFT2:
    label_libs += [('pango', 'text_pango', 'LabelPango')]

if USE_SDL2:
    label_libs += [('sdl2', 'text_sdl2', 'LabelSDL2')]
else:
    label_libs += [('pygame', 'text_pygame', 'LabelPygame')]

# 这里直接使kivy 封装的Pillow类
label_libs = [
    ('pil', 'text_pil', 'LabelPIL')]
Text = Label = core_select_lib('text', label_libs)

得到一样的效果:

上Pillow的代码仓里打找一下关于高棉语的issue描述, 2.5.0 之前的版本确实有bug。 但发我本地安装的版本是5.4.0应该肯定支付高棉语言。

那么为上面的显示的是错的呢?有两种可能:

  • pillow 对windows 不对支持高棉语
  • pillow 依赖库缺失
# pillow 依赖库测试代码
from PIL import features
print(features.check("raqm"))

False

果然缺失libraqm库, 下载一个windows版本的, 解压对应的版本到C:\windowsC:\windows\system32python安装目录
再运行pillow 依赖库测试代码, 输出Ture说明库安装对了,接下来运行kivy demo

显示对了

4.kivy text_pil.py 渲染bug

上面的修改字休是能正常显示了,但是换一个背景时有阴影

!!! Pillow版本需要6.1.0以上,否显示Unicode变成方框乱码

再看一下text_pil.py 的源码片段


__all__ = ('LabelPIL', )

from PIL import Image, ImageFont, ImageDraw


from kivy.compat import text_type
from kivy.core.text import LabelBase
from kivy.core.image import ImageData

# used for fetching extends before creature image surface
default_font = ImageFont.load_default()


class LabelPIL(LabelBase):
    _cache = {}

    def _select_font(self):
        fontsize = int(self.options['font_size'])
        fontname = self.options['font_name_r']
        try:
            id = '%s.%s' % (text_type(fontname), text_type(fontsize))
        except UnicodeDecodeError:
            id = '%s.%s' % (fontname, fontsize)

        if id not in self._cache:
            font = ImageFont.truetype(fontname, fontsize)
            self._cache[id] = font

        return self._cache[id]

    def get_extents(self, text):
        font = self._select_font()
        w, h = font.getsize(text)
        return w, h

    def get_cached_extents(self):
        return self._select_font().getsize

    def _render_begin(self):
        # create a surface, context, font...
        # 改前
        # self._pil_im = Image.new('RGBA', self._size)       
        # 改后         
        self._pil_im = Image.new('RGBA', self._size, (255, 255, 255, 0))
        self._pil_draw = ImageDraw.Draw(self._pil_im)

    def _render_text(self, text, x, y):
        color = tuple([int(c * 255) for c in self.options['color']])
        self._pil_draw.text((int(x), int(y)),
                            text, font=self._select_font(), fill=color)

    def _render_end(self):
        data = ImageData(self._size[0], self._size[1],
                         self._pil_im.mode.lower(), self._pil_im.tobytes())

        del self._pil_im
        del self._pil_draw

        return data

_render_begin()方法在开始渲染时候创建了一个Image对象, 但是没有设置背景色导致了阴影

5.修复Bug

上面的修改方法是可以正运行了,但是只不能团队跑这个工程的所有同事都修改源码吧,而且打包也不方便,修改方法如下:

  • 创建一个渲染text的类LabelPillow存于pil_label.py, 继承于LabelPIL,然后在这新类里修复这个BUG

import os
import shutil
import logging


def local_path() -> str:
    _self_path = __file__.split(".py")[0]
    _local_path = os.path.abspath(os.path.join(_self_path, ".."))
    return _local_path


os.environ["path"] += ";" + local_path()


from PIL import Image
from PIL import features
from PIL import ImageDraw
from kivy.core.text.text_pil import LabelPIL


class LabelPillow(LabelPIL):
    """pillow label config"""

    lib_path = "C:/windows"
    req_lib = ("fribidi-0.dll", "libraqm.dll")

    def _render_begin(self):
        # create a surface, context, font...
        self._pil_im = Image.new('RGBA', self._size, (255, 255, 255, 0))
        self._pil_draw = ImageDraw.Draw(self._pil_im)

    @classmethod
    def check_lib(cls):
        local_file, sys_file = "", ""
        for name in cls.req_lib:
            try:
                sys_file = os.path.join(cls.lib_path, name)
                if os.path.isfile(sys_file):
                    continue
                local_file = os.path.join(local_path(), name)
                shutil.copy(local_file, sys_file)
            except Exception as err:
                msg = "Copy lib file {} to {} error:{}"
                logging.error(msg.format(local_file, sys_file, err))

    @classmethod
    def check(cls):
        cls.check_lib()
        return features.check("raqm")
  • 在应用导入kivy前把, 把kivy/core/text/__init__.py模块的LabelText都指向LabelPillow新类:

from kivy.core import text
from ui.kv.base.pil_label import LabelPillow

LabelPillow.check()
text.Text = text.Label = LabelPillow

到这类于高棉语言的Unicode错误显示得于解决, kivy还支持pango字体渲染,但是它依赖于glib,在windows下还没有试过。

参考:
Pillow Khmer issue
How to install pre-built Pillow wheel with libraqm DLLs on Windows?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章