Pango text provider: Initial commit

This commit is contained in:
Terje Skjaeveland 2017-08-06 15:38:16 +02:00 committed by Mathieu Virbel
parent 8841927f64
commit f1835deacf
7 changed files with 612 additions and 0 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ examples/*/bin
examples/*/.buildozer
kivy/*.c
kivy/*.pyd
kivy/core/text/_text_pango.c
kivy/core/text/text_layout.c
kivy/core/text/text_layout.pyd
kivy/core/window/window_info.c

View File

@ -778,6 +778,9 @@ class LabelBase(object):
# Load the appropriate provider
label_libs = []
if platform in ('linux', ):
label_libs += [('pango', 'text_pango', 'LabelPango')]
if USE_SDL2:
label_libs += [('sdl2', 'text_sdl2', 'LabelSDL2')]
else:

View File

@ -0,0 +1,341 @@
# NOTE: We could probably use a single global FcConfig, but it's not obvious
# how since adding a font doesn't give you back something that can be directly
# used to render with. Apparently you must look it up via a font description.
# Where do you get that from? I don't know... maybe FcConfigGetFonts then
# pango_fc_font_description_from_pattern(), somehow?
#
# The current implementation uses pango_fc_font_map_set_config() to force the
# loaded TTF to be used, seems to work. As far as I can tell alternative is:
#
# cdef FcConfig *oldconfig = FcConfigGetCurrent()
# FcConfigSetCurrent(context.fc_config)
# <-- render -->
# FcConfigSetCurrent(oldconfig)
#
cimport cython
from libc.stdint cimport uint32_t
from libc.string cimport memset
from cpython.mem cimport PyMem_Malloc, PyMem_Free
include "../../lib/pangoft2.pxi"
from kivy.logger import Logger
from kivy.core.image import ImageData
# Cached contexts; dict key + list value is font filename ("font_name_r")
# Dict values are ContextContainer instances, one for each font.
cdef dict kivy_pango_cache = {}
cdef list kivy_pango_cache_order = []
# Map text direction to pango constant - neutral is considered auto, anything
# else will call pango_layout_set_auto_dir(the_layout, FALSE)
cdef dict kivy_pango_text_direction = {
'ltr': PANGO_DIRECTION_LTR,
'rtl': PANGO_DIRECTION_RTL,
'weak_ltr': PANGO_DIRECTION_WEAK_LTR,
'weak_rtl': PANGO_DIRECTION_WEAK_RTL,
'neutral': PANGO_DIRECTION_NEUTRAL,
'auto': PANGO_DIRECTION_NEUTRAL}
# Fontconfig and pango structures (one per loaded font). Can't use
# ctypedef struct here, because that won't fit in the cache dict
cdef class ContextContainer:
cdef PangoContext *context
cdef PangoFontMap *fontmap
cdef PangoFontDescription *fontdesc
cdef PangoLayout *layout
cdef FcConfig *fc_config
# Called explicitly on malloc fail to release as fast as possible
# Note: calling a method from __dealloc__ can lead to revived object
def __dealloc__(self):
if self.fontdesc:
pango_font_description_free(self.fontdesc)
if self.layout:
g_object_unref(self.layout)
if self.context:
g_object_unref(self.context)
if self.fontmap:
g_object_unref(self.fontmap)
if self.fc_config:
FcConfigDestroy(self.fc_config)
# Add a contextcontainer to cache + keep max 64 open fonts
cdef inline _add_pango_cache(unicode fontid, ContextContainer cc):
global kivy_pango_cache, kivy_pango_cache_order
cdef unicode popid
while len(kivy_pango_cache_order) >= 64:
popid = kivy_pango_cache_order.pop(0)
del kivy_pango_cache[popid]
kivy_pango_cache[fontid] = cc
kivy_pango_cache_order.append(fontid)
# NOTE: for future, this applies to font selection, irrelevant with one font
#cdef _configure_pattern_destroy_data(gpointer data):
# print("_configure_pattern_destroy_data()!")
#cdef _configure_pattern_callback(FcPattern *pattern, gpointer data):
# cdef unsigned int flags = GPOINTER_TO_UINT(data)
# print("_configure_pattern_callback()!")
# FcPatternDel(pattern, FC_HINTING)
# FcPatternAddBool(pattern, FC_HINTING, ...)
# FcPatternDel(pattern, FC_AUTOHINT)
# FcPatternAddBool(pattern, FC_AUTOHINT, ...)
# FcPatternDel(pattern, FC_HINT_STYLE)
# FcPatternAddInteger(pattern, FC_HINT_STYLE, ...)
# FcPatternDel(pattern, FC_ANTIALIAS)
# FcPatternAddBool(pattern, FC_ANTIALIAS, ...)
# Creates a ContextContainer for the font_name_r of the label
cdef _get_context_container(kivylabel):
global kivy_pango_text_direction
cdef dict options = kivylabel.options
cdef unicode fontid = <unicode>options['font_name_r']
if fontid in kivy_pango_cache:
return kivy_pango_cache.get(fontid)
# Creat a new context
cdef ContextContainer cc = ContextContainer()
# Create blank FcConfig (fontconfig), and load the TTF file
cc.fc_config = FcConfigCreate()
cdef bytes filename = options['font_name_r'].encode('UTF-8')
if FcConfigAppFontAddFile(cc.fc_config, <FcChar8 *>filename) == FcFalse:
Logger.warn("_text_pango: Error loadinging font '{}'".format(filename))
cc.__dealloc__()
return
# Create a blank font map and assign the config from above (one TTF file)
cc.fontmap = pango_ft2_font_map_new()
if not cc.fontmap:
Logger.warn("_text_pango: Could not create new font map")
cc.__dealloc__()
return
pango_fc_font_map_set_config(PANGO_FC_FONT_MAP(cc.fontmap), cc.fc_config)
# FIXME: should we configure this?
#pango_ft2_font_map_set_resolution(cc.fontmap, n, n)
# FIXME: This may become relevant, leaving for now
#cc.callback_data_ptr = GUINT_TO_POINTER(flags)
#pango_ft2_font_map_set_default_substitute(
# PANGO_FT2_FONT_MAP(cc.fontmap),
# &_configure_pattern_callback,
# cc.callback_data_ptr,
# &_configure_pattern_destroy_data)
# Finally create our pango context from the fontmap
cc.context = pango_font_map_create_context(cc.fontmap)
if not cc.context:
Logger.warn("_text_pango: Could not create pango context")
cc.__dealloc__()
return
# Configure the context's base direction. If user specified something
# explicit, force it. Otherwise (auto/neutral), let pango decide.
# FIXME: add text_direction externally so it's available.
cdef PangoDirection text_dir = kivy_pango_text_direction.get(
options.get('text_direction', 'neutral'), PANGO_DIRECTION_NEUTRAL)
pango_context_set_base_dir(cc.context, text_dir)
# Create layout from context
cc.layout = pango_layout_new(cc.context)
if not cc.layout:
Logger.warn("_text_pango: Could not create pango layout")
cc.__dealloc__()
return
# If autodir is false, the context's base direction is used (set above)
if text_dir == PANGO_DIRECTION_NEUTRAL:
pango_layout_set_auto_dir(cc.layout, TRUE)
else:
pango_layout_set_auto_dir(cc.layout, FALSE)
# The actual font size is specified in pango markup, and the
# actual font is whatever TTF is loaded in this context. This
# may not be needed at all.
cc.fontdesc = pango_font_description_from_string("Arial")
pango_layout_set_font_description(cc.layout, cc.fontdesc)
# FIXME: does this need to change w/label settings?
pango_layout_set_alignment(cc.layout, PANGO_ALIGN_LEFT)
#pango_layout_set_spacing(cc.layout, n)
_add_pango_cache(fontid, cc)
return cc
# Renders the pango layout to a grayscale bitmap, and blits RGBA at x, y
cdef _render_context(ContextContainer cc, unsigned char *dstbuf,
int x, int y, int final_w, int final_h,
unsigned char textcolor[]):
if not dstbuf or final_w == 0 or final_h == 0 or x > final_w or y > final_h:
Logger.warn('_text_pango: Invalid blit: final={}x{} x={} y={}'
.format(final_w, final_h, x, y))
return
# Note, w/h refers to the current subimage size, final_w/h is end result
cdef int w, h
pango_layout_get_pixel_size(cc.layout, &w, &h)
if w == 0 or h == 0 or x + w > final_w or y + h > final_h:
Logger.warn('_text_pango: Invalid blit: final={}x{} x={} y={} w={} h={}'
.format(final_w, final_h, x, y, w, h))
return
cdef FT_Bitmap bitmap
cdef int xi, yi
cdef unsigned char graysrc
cdef unsigned long grayidx
cdef unsigned long yi_w
cdef unsigned long offset
cdef unsigned long offset_fixed = x + (y * final_w)
cdef unsigned long offset_yi = final_w - w
cdef unsigned long maxpos = final_w * final_h
with nogil:
# Prepare ft2 bitmap for pango's grayscale data
FT_Bitmap_Init(&bitmap)
bitmap.width = w
bitmap.rows = h
bitmap.pitch = w # 1-byte grayscale
bitmap.pixel_mode = FT_PIXEL_MODE_GRAY # no BGRA in pango (ft2 has it)
bitmap.num_grays = 256
bitmap.buffer = <unsigned char *>g_malloc0(w * h)
if not bitmap.buffer:
with gil:
Logger.warn('_text_pango: Could not malloc FT_Bitmap.buffer')
return
# Render the layout as 1 byte per pixel grayscale bitmap
# FIXME: does render_layout_subpixel() do us any good?
pango_ft2_render_layout(&bitmap, cc.layout, 0, 0)
# Blit the bitmap as RGBA at x, y in dstbuf (w/h is the ft2 bitmap)
for yi in range(0, h):
offset = offset_fixed + (yi * offset_yi)
yi_w = yi * w
if offset + yi_w + w - 1 > maxpos:
with gil:
Logger.warn('_text_pango: OOB blit: yi={} final={}x{} '
'x={} y={} w={} h={} maxpos={}'.format(
yi, final_w, final_h, x, y, w, h, maxpos))
break
# FIXME: Handle big endian - either use variable shifts here, or
# return as abgr + handle elsewhere
for xi in range(0, w):
grayidx = yi_w + xi
graysrc = (bitmap.buffer)[grayidx]
(<uint32_t *>dstbuf)[offset + grayidx] = (
<int>(((textcolor[0] * graysrc) / 255)) |
<int>(((textcolor[1] * graysrc) / 255) << 8) |
<int>(((textcolor[2] * graysrc) / 255) << 16) |
<int>(((textcolor[3] * graysrc) / 255) << 24) )
g_free(bitmap.buffer)
# /nogil blit
cdef class KivyPangoRenderer:
# w, h is the final bitmap size, drawn by 1+ render() calls in *pixels
cdef int w, h, canary
cdef unsigned char *pixels
def __cinit__(self, int w, int h):
self.canary = 1
self.w = w
self.h = h
self.pixels = <unsigned char *>PyMem_Malloc(w * h * 4)
if self.pixels:
memset(self.pixels, 0, w * h * 4)
# Kivy's markup system breaks labels down to smaller chunks and render
# separately. In that case, we get several calls to render() with misc
# options and x/y positions. End result is stored in self.pixels.
def render(self, kivylabel, text, x, y):
if not self.pixels:
Logger.warn('_text_pango: render() called, but self.pixels is NULL')
return
cdef ContextContainer cc = _get_context_container(kivylabel)
if not cc:
Logger.warn('_text_pango: Could not get context container, aborting')
return
# Set markup, this could use kivylabel.options + attrs
markup = <bytes>text.encode('UTF-8')
pango_layout_set_markup(cc.layout, markup, len(markup))
# Kivy normalized text color -> 0-255 rgba
cdef unsigned char textcolor[4]
textcolor[:] = [ min(255, int(c * 255)) for c in kivylabel.options['color'] ]
# Finally render the layout and blit it to self.pixels
_render_context(cc, self.pixels, x, y, self.w, self.h, textcolor)
self.canary = 0
# Return ImageData instance with the rendered pixels
def get_ImageData(self):
if not self.pixels:
Logger.warn('_text_pango: get_ImageData() self.pixels == NULL')
return
if self.canary:
Logger.warn("_text_pango: Dead canary in get_ImageData()")
return
self.canary = 1
try:
b_pixels = <bytes>self.pixels[:self.w * self.h * 4]
return ImageData(self.w, self.h, 'rgba', b_pixels)
finally:
PyMem_Free(self.pixels)
def kpango_get_extents(kivylabel, text):
cdef ContextContainer cc = _get_context_container(kivylabel)
cdef int w, h
if not cc:
Logger.warn('_text_pango: Could not get container for extents: {}'
.format(kivylabel.options['font_name_r']))
return 0, 0
markup = <bytes>text.encode('UTF-8')
pango_layout_set_markup(cc.layout, markup, len(markup))
pango_layout_get_pixel_size(cc.layout, &w, &h)
return w, h
def kpango_get_ascent(kivylabel):
cdef ContextContainer cc = _get_context_container(kivylabel)
if not cc:
Logger.warn('_text_pango: Could not get container for ascent: {}'
.format(kivylabel.options['font_name_r']))
return 0
# FIXME: pango_language_from_string(kivylabel.text_language)
cdef PangoFontMetrics *fm = pango_context_get_metrics(
cc.context, cc.fontdesc, pango_language_get_default())
try:
return <double>(pango_font_metrics_get_ascent(fm) / PANGO_SCALE)
finally:
pango_font_metrics_unref(fm)
def kpango_get_descent(kivylabel):
cdef ContextContainer cc = _get_context_container(kivylabel)
if not cc:
Logger.warn('_text_pango: Could not get container for decent: {}'
.format(kivylabel.options['font_name_r']))
return 0
# FIXME: pango_language_from_string(kivylabel.text_language)
cdef PangoFontMetrics *fm = pango_context_get_metrics(
cc.context, cc.fontdesc, pango_language_get_default())
try:
return <double>(pango_font_metrics_get_descent(fm) / PANGO_SCALE)
finally:
pango_font_metrics_unref(fm)

View File

@ -0,0 +1,63 @@
'''
Pango text provider
===================
'''
__all__ = ('LabelPango', )
from kivy.compat import PY2
from kivy.core.text import LabelBase
from kivy.core.text._text_pango import (KivyPangoRenderer, kpango_get_extents,
kpango_get_ascent, kpango_get_descent)
class LabelPango(LabelBase):
# This is a hack to avoid dealing with glib attrs to configure layout,
# we just create markup out of the options and let pango set attrs
def _pango_markup(self, text):
markup = text.replace('<', '&lt;').replace('>', '&gt;')
options = self.options
tags = []
if options['bold']:
markup = '<b>{}</b>'.format(markup)
if options['underline']:
markup = '<u>{}</u>'.format(markup)
if options['strikethrough']:
markup = '<s>{}</s>'.format(markup)
if options['font_hinting'] == 'mono':
markup = '<tt>{}</tt>'.format(markup)
# FIXME: does this do the right thing? .. don't see much w/roboto
weight_attr = ''
if options['font_hinting'] in ('light', 'normal'):
weight_attr = ' weight="{}"'.format(options['font_hinting'])
return b'<span font="{}"{}>{}</span>'.format(
int(self.options['font_size']),
weight_attr,
markup)
def get_extents(self, text):
if not text:
return (0, 0)
w, h = kpango_get_extents(self, self._pango_markup(text))
return (w, h)
def get_ascent(self):
return kpango_get_ascent(self)
def get_descent(self):
return kpango_get_descent(self)
def _render_begin(self):
self._rdr = KivyPangoRenderer(self._size[0], self._size[1])
def _render_text(self, text, x, y):
self._rdr.render(self, self._pango_markup(text), x, y)
def _render_end(self):
imgdata = self._rdr.get_ImageData()
del self._rdr
return imgdata

10
kivy/lib/pangoft2.h Normal file
View File

@ -0,0 +1,10 @@
#ifndef KIVY_PANGOFT2_HEADER
#define KIVY_PANGOFT2_HEADER
// FreeType2 headers must be included via macros in ft2build.h
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_BITMAP_H
#endif

186
kivy/lib/pangoft2.pxi Normal file
View File

@ -0,0 +1,186 @@
cdef extern from "glib.h" nogil:
ctypedef void *gpointer
ctypedef int gint
ctypedef unsigned int guint
ctypedef unsigned long gsize
ctypedef gint gboolean
gboolean TRUE
gboolean FALSE
void *g_malloc(gsize n_bytes)
void *g_malloc0(gsize n_bytes)
void g_free(gpointer mem)
void g_object_unref(gpointer obj)
# gpointer GUINT_TO_POINTER(guint u)
# guint GPOINTER_TO_UINT(gpointer p)
# https://www.freetype.org/freetype2/docs/reference/ft2-index.html
cdef extern from "../../lib/pangoft2.h" nogil:
ctypedef struct FT_Library:
pass
ctypedef enum FT_Pixel_Mode:
FT_PIXEL_MODE_NONE = 0
FT_PIXEL_MODE_MONO
FT_PIXEL_MODE_GRAY
FT_PIXEL_MODE_GRAY2
FT_PIXEL_MODE_GRAY4
FT_PIXEL_MODE_LCD
FT_PIXEL_MODE_LCD_V
FT_PIXEL_MODE_BGRA
FT_PIXEL_MODE_MAX
ctypedef struct FT_Bitmap:
unsigned int rows
unsigned int width
int pitch
unsigned char* buffer
unsigned short num_grays
unsigned char pixel_mode
unsigned char palette_mode
void* palette
void FT_Bitmap_Init(FT_Bitmap *bitmap)
void FT_Bitmap_Done(FT_Library library, FT_Bitmap *bitmap)
# https://www.freedesktop.org/software/fontconfig/fontconfig-devel/t1.html
cdef extern from "fontconfig/fontconfig.h" nogil:
ctypedef struct FcConfig:
pass
ctypedef struct FcPattern:
pass
# ctypedef struct FcFontSet:
# int nfont
# int sfont
# FcPattern **fonts
ctypedef bint FcBool
ctypedef unsigned char FcChar8
bint FcTrue
bint FcFalse
FcConfig *FcConfigCreate()
void FcConfigDestroy(FcConfig *config)
FcConfig *FcConfigGetCurrent()
FcBool FcConfigSetCurrent(FcConfig *config)
FcBool FcConfigAppFontAddFile(FcConfig *config, const FcChar8 *file)
# FcPattern *FcPatternCreate()
# void FcPatternDestroy(FcPattern *p)
# FcBool FcPatternDel(FcPattern *p, const char *object)
# FcBool FcPatternAddInteger (FcPattern *p, const char *object, int i)
# FcBool FcPatternAddDouble (FcPattern *p, const char *object, double d)
# FcBool FcPatternAddString (FcPattern *p, const char *object, const FcChar8 *s)
# FcBool FcPatternAddMatrix (FcPattern *p, const char *object, const FcMatrix *m)
# FcBool FcPatternAddCharSet (FcPattern *p, const char *object, const FcCharSet *c)
# FcBool FcPatternAddBool (FcPattern *p, const char *object, FcBool b)
# FcBool FcPatternAddFTFace (FcPattern *p, const char *object, const FT_Facef)
# FcBool FcPatternAddLangSet (FcPattern *p, const char *object, const FcLangSet *l)
# FcBool FcPatternAddRange (FcPattern *p, const char *object, const FcRange *r)
# https://developer.gnome.org/pango/stable/pango-Glyph-Storage.html
cdef extern from "pango/pango-types.h" nogil:
# ctypedef struct PangoRectangle:
# int x
# int y
# int width
# int height
unsigned int PANGO_SCALE
# https://developer.gnome.org/pango/stable/pango-Scripts-and-Languages.html
cdef extern from "pango/pango-language.h" nogil:
ctypedef struct PangoLanguage:
pass
PangoLanguage *pango_language_get_default()
PangoLanguage *pango_language_from_string(const char *language)
# https://developer.gnome.org/pango/stable/pango-FreeType-Fonts-and-Rendering.html
cdef extern from "pango/pangoft2.h" nogil:
ctypedef struct PangoFT2FontMap:
pass
ctypedef void *PangoFT2SubstituteFunc
ctypedef void *GDestroyNotify
PangoFT2FontMap *PANGO_FT2_FONT_MAP(PangoFontMap *fontmap)
void pango_ft2_render_layout(FT_Bitmap *bitmap, PangoLayout *layout, int x, int y)
void pango_ft2_render_layout_subpixel(FT_Bitmap *bitmap, PangoLayout *layout, int x, int y)
void pango_ft2_font_map_set_default_substitute(PangoFT2FontMap *fontmap, PangoFT2SubstituteFunc func, gpointer data, GDestroyNotify notify)
# https://developer.gnome.org/pango/stable/pango-Text-Processing.html
cdef extern from "pango/pango-context.h" nogil:
ctypedef struct PangoContext:
pass
void pango_context_set_base_dir(PangoContext *context, PangoDirection direction)
PangoFontMetrics *pango_context_get_metrics(PangoContext *context, const PangoFontDescription *desc, PangoLanguage *language)
# https://developer.gnome.org/pango/stable/pango-Bidirectional-Text.html
cdef extern from "pango/pango-bidi-type.h" nogil:
ctypedef enum PangoDirection:
PANGO_DIRECTION_LTR
PANGO_DIRECTION_RTL
PANGO_DIRECTION_TTB_LTR # deprecated
PANGO_DIRECTION_TTB_RTL # deprecated
PANGO_DIRECTION_WEAK_LTR
PANGO_DIRECTION_WEAK_RTL
PANGO_DIRECTION_NEUTRAL
# https://developer.gnome.org/pango/stable/pango-Fonts.html
cdef extern from "pango/pango-font.h" nogil:
ctypedef struct PangoFontMap:
pass
ctypedef struct PangoFontDescription:
pass
ctypedef struct PangoFontMetrics:
pass
PangoFontDescription* pango_font_description_from_string(const char *string)
void pango_font_description_free(PangoFontDescription *desc)
# void pango_font_description_set_size(PangoFontDescription *desc, gint size)
PangoContext *pango_font_map_create_context(PangoFontMap *fontmap)
PangoFontMap *pango_ft2_font_map_new()
int pango_font_metrics_get_ascent(PangoFontMetrics *metrics)
int pango_font_metrics_get_descent(PangoFontMetrics *metrics)
void pango_font_metrics_unref(PangoFontMetrics *metrics)
# https://developer.gnome.org/pango/stable/PangoFcFontMap.html
cdef extern from "pango/pangofc-fontmap.h" nogil:
ctypedef struct PangoFcFontMap:
pass
PangoFcFontMap *PANGO_FC_FONT_MAP(PangoFontMap *fontmap)
void pango_fc_font_map_set_config(PangoFcFontMap *fontmap, FcConfig *config)
# https://developer.gnome.org/pango/stable/pango-Layout-Objects.html
cdef extern from "pango/pango-layout.h" nogil:
ctypedef struct PangoLayout:
pass
ctypedef enum PangoAlignment:
PANGO_ALIGN_LEFT
PANGO_ALIGN_CENTER
PANGO_ALIGN_RIGHT
PangoLayout *pango_layout_new(PangoContext *context)
void pango_layout_get_pixel_size(PangoLayout *layout, int *width, int *height)
void pango_layout_get_size(PangoLayout *layout, int *width, int *height)
void pango_layout_set_alignment(PangoLayout *layout, PangoAlignment alignment)
void pango_layout_set_auto_dir(PangoLayout *layout, gboolean auto_dir)
void pango_layout_set_markup(PangoLayout *layout, const char *markup, int length)
void pango_layout_set_font_description(PangoLayout *layout, const PangoFontDescription *desc)
void pango_layout_set_text(PangoLayout *layout, const char *text, int length)
void pango_layout_set_width(PangoLayout *layout, int width)
void pango_layout_set_height(PangoLayout *layout, int height)
void pango_layout_set_spacing(PangoLayout *layout, int spacing)

View File

@ -811,11 +811,19 @@ if c_options['use_sdl2'] and sdl2_flags:
for source_file in ('core/window/_window_sdl2.pyx',
'core/image/_img_sdl2.pyx',
'core/text/_text_sdl2.pyx',
'core/text/_text_pango.pyx',
'core/audio/audio_sdl2.pyx',
'core/clipboard/_clipboard_sdl2.pyx'):
sources[source_file] = merge(
base_flags, sdl2_flags, sdl2_depends)
pango_flags = pkgconfig('pangoft2')
if pango_flags:
pango_depends = {'depends': ['lib/pangoft2.pxi',
'lib/pangoft2.h']}
sources['core/text/_text_pango.pyx'] = merge(
base_flags, pango_flags, pango_depends)
if platform in ('darwin', 'ios'):
# activate ImageIO provider for our core image
if platform == 'ios':