問題描述
假設項目中待顯示的柬埔寨語爲សួស្តី
, 但在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-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:\windows
或 C:\windows\system32
或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?