问题描述
假设项目中待显示的柬埔寨语为សួស្តី
, 但在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
成员是实际要展示的文本内容对像,可以是CoreMarkupLabel
和CoreLabel
的实例。
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)
再来看一下CoreMarkupLabel
和CoreLabel
在 kivy/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.
'''
可以看出CoreMarkupLabel
和CoreLabel
本质都是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:windows
或 C:windowssystem32
或python安装目录
再运行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
模块的Label
和Text
都指向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?