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?

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