zoukankan      html  css  js  c++  java
  • 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-packageskivysetupconfig.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:UsersadminAppDataLocalJetBrainsPyCharm2020.1python_stubs498501734kivycore ext_text_sdl2.py文件,只供接口查看,隐藏了内部实现细节

    # encoding: utf-8
    # module kivy.core.text._text_sdl2
    # from C:UsersadminEnvswumart32libsite-packageskivycore	ext\_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:windowssystem32python安装目录
    再运行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?

  • 相关阅读:
    [公告]博客园新服务器已下订单
    清除SearchNet.exe
    [新闻]微软将在2007年发布的Office产品阵容
    卸载Internet Explorer 7 Beta 2 Preview引起原来的IE无法正常使用
    博客园准备购买新服务器
    [微软活动公告]微软最有价值专家(MVP)四月份在线申请开始了
    [公告]今晚数据库迁移至新服务器
    请wz不要把别人的文章当作自己的文章发表
    SQL Server 2000 To SQL Server 2005
    [公告]博客园数据库已成功迁移至新服务器
  • 原文地址:https://www.cnblogs.com/onsunsl/p/kivy_show_unicode_of_khmer_error.html
Copyright © 2011-2022 走看看