android文本布局引擎
android的文本渲染框架其实也是一个基于FreeType2的一个框架。所以,我们可以先看一下,FreeType2 API的一些用法,以及使用FreeType2 直接来做文本渲染的方法。(说明一下,下面的例子都是用Python写的,用的FreeType2 Python binding 为freetype-py-0.4.1,用的GUI框架为gtk)
用FreeType2绘制单个字符
先是看一下,用FreeType2的API画单个字符的方法,如下面的code:
#!/usr/bin/python
'''
Author: Wolf-CS
Website: http://my.oschina.net/wolfcs/blog
Last edited: May 2013
'''
import gtk, gtk.gdk
import freetype
class MainWindow(gtk.Window):
def __init__(self):
super(self.__class__, self).__init__()
self.init_ui()
self.create_pixbuf()
def init_ui(self):
self.darea = gtk.DrawingArea()
self.darea.connect("expose_event", self.expose)
self.add(self.darea)
self.set_title("Draw Single Character")
self.resize(480, 320)
self.set_position(gtk.WIN_POS_CENTER)
self.connect("delete-event", gtk.main_quit)
self.show_all()
def create_pixbuf(self):
width = 480
height = 320
self.datapb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
self.clear_pixbuf(self.datapb, 0, 128, 255, 255)
def expose(self, widget, event):
self.context = widget.window.cairo_create()
self.on_draw(700, self.context)
def on_draw(self, wdith, cr):
character = 'A'
face = freetype.Face("./Arial.ttf")
text_size = 128
face.set_char_size(text_size * 64)
self.draw_char(self.datapb, 180, 100, character, face)
gtk.gdk.CairoContext.set_source_pixbuf(cr, self.datapb, 0, 0)
cr.paint()
def draw_ft_bitmap(self, pixbuf, bitmap, pen):
x_pos = pen.x >> 6
y_pos = pen.y >> 6
width = bitmap.width
rows = bitmap.rows
pixbuf_width = pixbuf.get_width()
pixbuf_height = pixbuf.get_height()
# print "y_pos = %d, pixbuf_height = %d" % (y_pos, pixbuf_height)
assert ((y_pos > 0) and (y_pos + rows < pixbuf_height))
assert ((x_pos > 0) and (x_pos + width < pixbuf_width))
glyph_pixels = bitmap.buffer
for line in range(rows):
for column in range(width):
if glyph_pixels[line * width + column] != 0:
self.put_pixel(pixbuf, y_pos + line, x_pos + column,
glyph_pixels[line * width + column],
glyph_pixels[line * width + column],
glyph_pixels[line * width + column],
255)
def draw_char(self, pixbuf, x_pos, y_pos, char, face):
face.load_char(char)
slot = face.glyph
bitmap = slot.bitmap
pen = freetype.Vector()
pen.x = x_pos << 6
pen.y = y_pos << 6
self.draw_ft_bitmap(pixbuf, bitmap, pen)
def put_pixel(self, pixbuf, y_pos, x_pos, red, green, blue, alpha):
n_channels = pixbuf.get_n_channels()
width = pixbuf.get_width()
height = pixbuf.get_height()
assert (n_channels == 4)
assert (y_pos >= 0 and y_pos < height)
assert (x_pos >= 0 and x_pos < width)
pixels = pixbuf.get_pixels_array()
# print "pixels = " + str (pixels)
pixels[y_pos][x_pos][0] = red
pixels[y_pos][x_pos][1] = green
pixels[y_pos][x_pos][2] = blue
pixels[y_pos][x_pos][3] = alpha
def clear_pixbuf(self, pixbuf, red, green, blue, alpha):
n_channels = pixbuf.get_n_channels()
assert (n_channels == 4)
width = pixbuf.get_width()
height = pixbuf.get_height()
pixels = pixbuf.get_pixels_array()
for row in range(height):
for column in range(width):
pixels[row][column][0] = red
pixels[row][column][1] = green
pixels[row][column][2] = blue
pixels[row][column][3] = alpha
def main():
window = MainWindow()
gtk.main()
if __name__ == "__main__":
main()
上面的那段code,draw_char()方法,用于向GTK的pixbuf中画入一个字符,可以看到,它完成的动作为,将一个字符的bitmap加载进Face对象的glyph slot中,然后再由glyph slot提取到glyph的bitmap,最后则调用draw_ft_bitmap()函数来将glyph的bitmap绘制到pixbuf中。
在上面的那段code中,除了draw_char()和draw_ft_bitmap()这两个函数外,其他的函数都用于和GTK GUI框架来打交道。由上面的那段code,对于pixbuf中存储像素数据的格式,及FreeType2的bitmap中存储像素的格式,想必还是可以找到一点感觉的。具体GTK窗口系统以及GTK pixbuf更详细的用法说明,则可以再借由网络来寻找到更多的信息。
上面那段code执行的结果如上图。
用FreeType绘制一小段文本
只能画一个字符岂不是太弱了,画字串才是我们最常遇到的情况。闭上眼睛想一想,基于绘制单个字符的方法,我们想要绘制一个文本串,会遇到什么特别的问题呢?没错,正是依据当前字符的位置,确定下一个字符的位置的问题。想想要手动的确定每一个字符的水平位置坐标就让人头大,还是依据一些规则来算让人感觉舒服。可以从下面的这段code中具体看一下,到底要如何来做:
#!/usr/bin/python
'''
Author: Wolf-CS
Website: http://my.oschina.net/wolfcs/blog
Last edited: June 2013
Draw Simple Text String.
'''
import gtk, gtk.gdk
import freetype
class MainWindow(gtk.Window):
def __init__(self):
super(self.__class__, self).__init__()
self.init_ui()
self.create_pixbuf()
def init_ui(self):
self.darea = gtk.DrawingArea()
self.darea.connect("expose_event", self.expose)
self.add(self.darea)
self.set_title("Draw Simple Text String")
self.resize(700, 200)
self.set_position(gtk.WIN_POS_CENTER)
self.connect("delete-event", gtk.main_quit)
self.show_all()
def create_pixbuf(self):
width = 700
height = 200
self.datapb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
self.clear_pixbuf(self.datapb, 0, 128, 255, 255)
def expose(self, widget, event):
self.context = widget.window.cairo_create()
self.on_draw(700, self.context)
def on_draw(self, wdith, cr):
text = "A Quick Brown Fox Jumps Over The Lazy Dog"
face = freetype.Face("./Arial.ttf")
text_size = 32
face.set_char_size(text_size * 64)
self.draw_string(self.datapb, 5, 100, text, face)
gtk.gdk.CairoContext.set_source_pixbuf(cr, self.datapb, 0, 0)
cr.paint()
def draw_ft_bitmap(self, pixbuf, bitmap, pen):
x_pos = pen.x >> 6
y_pos = pen.y >> 6
width = bitmap.width
rows = bitmap.rows
pixbuf_width = pixbuf.get_width()
pixbuf_height = pixbuf.get_height()
# print "y_pos = %d, pixbuf_height = %d" % (y_pos, pixbuf_height)
assert ((y_pos > 0) and (y_pos + rows < pixbuf_height))
assert ((x_pos > 0) and (x_pos + width < pixbuf_width))
glyph_pixels = bitmap.buffer
for line in range(rows):
for column in range(width):
if glyph_pixels[line * width + column] != 0:
self.put_pixel(pixbuf, y_pos + line, x_pos + column,
glyph_pixels[line * width + column],
glyph_pixels[line * width + column],
glyph_pixels[line * width + column],
255)
def draw_string(self, pixbuf, x_pos, y_pos, textstr, face):
prev_char = 0;
pen = freetype.Vector()
pen.x = x_pos << 6
pen.y = y_pos << 6
hscale = 1.0
matrix = freetype.Matrix(int((hscale) * 0x10000L), int((0.2) * 0x10000L),
int((0.0) * 0x10000L), int((1.1) * 0x10000L))
cur_pen = freetype.Vector()
pen_translate = freetype.Vector()
for cur_char in textstr:
face.set_transform(matrix, pen_translate)
face.load_char(cur_char)
kerning = face.get_kerning(prev_char, cur_char)
pen.x += kerning.x
slot = face.glyph
bitmap = slot.bitmap
cur_pen.x = pen.x
cur_pen.y = pen.y - slot.bitmap_top * 64
self.draw_ft_bitmap(pixbuf, bitmap, cur_pen)
pen.x += slot.advance.x
prev_char = cur_char
def put_pixel(self, pixbuf, y_pos, x_pos, red, green, blue, alpha):
n_channels = pixbuf.get_n_channels()
width = pixbuf.get_width()
height = pixbuf.get_height()
assert (n_channels == 4)
assert (y_pos >= 0 and y_pos < height)
assert (x_pos >= 0 and x_pos < width)
pixels = pixbuf.get_pixels_array()
pixels[y_pos][x_pos][0] = red
pixels[y_pos][x_pos][1] = green
pixels[y_pos][x_pos][2] = blue
pixels[y_pos][x_pos][3] = alpha
def clear_pixbuf(self, pixbuf, red, green, blue, alpha):
n_channels = pixbuf.get_n_channels()
assert (n_channels == 4)
width = pixbuf.get_width()
height = pixbuf.get_height()
pixels = pixbuf.get_pixels_array()
for row in range(height):
for column in range(width):
pixels[row][column][0] = red
pixels[row][column][1] = green
pixels[row][column][2] = blue
pixels[row][column][3] = alpha
def main():
window = MainWindow()
gtk.main()
if __name__ == "__main__":
main()
由上面的draw_string()函数,可以看到,在使用Face对象来加载字符的glyph时,其实是同时会加载很多相关的信息,比如glyph的宽度啦(称为为advance)之类的。依据当前字符的位置,来确定下一个字符的位置,正是依据于这个advance。比如当前字符的位置为x1,当前字符的glyph的advance为advance1,那么下一个字符的位置就应该是x1 + advance1。
对各个字符的位置有影响的,还有另外一个因素,称为kerning。其含义大体为,在一些上下文中,一些字符的位置,需要在通过advance调整之后,再做微小的调整,以使得显示的效果更好。比如“V”和“A”这两个字符放在一起显示,“A”字符的位置,不会仅仅是“V”的位置加上“V”的advance,而是会将“A”的位置往前再做一点点的调整这样。使用kerning 信息来对字符位置做调整,对应于上面的code中,get_kerning()相关的那一部分。
上面那段code执行的结果
用Freetype绘制居中文字
前面的两个例子中,我们总是手动的指定一个字符或者一个字串的起始位置。这种硬编码的东西总是让人感觉不舒服。要是能够算出一个字串的宽度,然后动态的依据一些规则来确定字串的位置就好了。当然,这是可以实现的,就比如下面这个例子所演示的那样:
#!/usr/bin/python
'''
Author: Wolf-CS
Website: http://my.oschina.net/wolfcs/blog
Last edited: June 2013
Draw Simple Text String.
'''
import gtk, gtk.gdk
import freetype
class Glyph:
def __init__(self, glyphID, advance, x_position, y_position):
self.glyphId = glyphID
self.advance = advance
self.x_position = x_position
self.y_position = y_position
class TextLayoutValue:
def __init__(self, glyphs, total_advance):
self.glyphs = glyphs
self.total_advance = total_advance
class MainWindow(gtk.Window):
def __init__(self):
super(self.__class__, self).__init__()
self.init_ui()
self.create_pixbuf()
def init_ui(self):
self.darea = gtk.DrawingArea()
self.darea.connect("expose_event", self.expose)
self.add(self.darea)
self.set_title("Draw Single Character")
self.resize(700, 360)
self.set_position(gtk.WIN_POS_CENTER)
self.connect("delete-event", gtk.main_quit)
self.show_all()
def create_pixbuf(self):
width = 700
height = 360
self.datapb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
self.clear_pixbuf(self.datapb, 0, 128, 255, 255)
def expose(self, widget, event):
self.context = widget.window.cairo_create()
self.on_draw(700, self.context)
def on_draw(self, wdith, cr):
textstr = "A Quick Brown Fox Jumps Over The Lazy Dog"
face = freetype.Face("./Arial.ttf")
text_size = 32
face.set_char_size(text_size * 64)
text_str_layout_value = self.compute_text_string_layout_value(textstr, face)
# for glyph in text_str_layout_value.glyphs:
# print "glyph.glyphId = " + str(glyph.glyphId) \
# + "glyph.advance = " + str(glyph.advance) \
# + "glyph.x_position = " + str(glyph.x_position) \
# + "glyph.y_position = " + str(glyph.y_position)
# print "text_str_layout_value.total_advance = " + str(text_str_layout_value.total_advance)
metrics = face.size
self.ascender = metrics.ascender/64.0
self.descender = metrics.descender/64.0
self.height = metrics.height/64.0
self.linegap = self.height - self.ascender + self.descender
x_position = (700 - text_str_layout_value.total_advance / 64) / 2
ypos = int(self.ascender)
color_map_index = 0
while ypos + int(self.height) < 360:
self.draw_pos_glyph_text(self.datapb, x_position, int(ypos), text_str_layout_value.glyphs, face)
color_map_index += 1
ypos += int(self.ascender - self.descender)
gtk.gdk.CairoContext.set_source_pixbuf(cr, self.datapb, 0, 0)
cr.paint()
def draw_pos_glyph_text(self, pixbuf, x_pos, y_pos, glyphs, face):
x_position = x_pos * 64
y_position = y_pos * 64
cur_pen = freetype.Vector()
for glyph in glyphs:
glyph_index = glyph.glyphId
cur_pen.x = glyph.x_position + x_position
cur_pen.y = glyph.y_position + y_position
face.load_glyph(glyph_index)
slot = face.glyph
bitmap = slot.bitmap
self.draw_ft_bitmap(pixbuf, bitmap, cur_pen)
def compute_text_string_layout_value(self, textstr, face):
prev_char = 0
pen = freetype.Vector()
pen.x = 0 << 6
pen.y = 0 << 6
hscale = 1.0
matrix = freetype.Matrix(int((hscale) * 0x10000L), int((0.2) * 0x10000L),
int((0.0) * 0x10000L), int((1.1) * 0x10000L))
glyphs = []
cur_pen = freetype.Vector()
pen_translate = freetype.Vector()
for cur_char in textstr:
face.set_transform(matrix, pen_translate)
glyph_index = face.get_char_index(cur_char)
face.load_glyph(glyph_index)
kerning = face.get_kerning(prev_char, cur_char)
pen.x += kerning.x
slot = face.glyph
cur_pen.x = pen.x
cur_pen.y = pen.y - slot.bitmap_top * 64
glyph = Glyph(glyph_index, slot.advance.x, cur_pen.x, cur_pen.y)
glyphs.append(glyph)
pen.x += slot.advance.x
prev_char = cur_char
text_str_layout_value = TextLayoutValue(glyphs, pen.x)
return text_str_layout_value
def draw_ft_bitmap(self, pixbuf, bitmap, pen):
x_pos = pen.x >> 6
y_pos = pen.y >> 6
width = bitmap.width
rows = bitmap.rows
pixbuf_width = pixbuf.get_width()
pixbuf_height = pixbuf.get_height()
# print "y_pos = %d, pixbuf_height = %d" % (y_pos, pixbuf_height)
assert ((y_pos > 0) and (y_pos + rows < pixbuf_height))
assert ((x_pos > 0) and (x_pos + width < pixbuf_width))
glyph_pixels = bitmap.buffer
for line in range(rows):
for column in range(width):
if glyph_pixels[line * width + column] != 0:
self.put_pixel(pixbuf, y_pos + line, x_pos + column,
glyph_pixels[line * width + column],
glyph_pixels[line * width + column],
glyph_pixels[line * width + column],
255)
def put_pixel(self, pixbuf, y_pos, x_pos, red, green, blue, alpha):
n_channels = pixbuf.get_n_channels()
width = pixbuf.get_width()
height = pixbuf.get_height()
assert (n_channels == 4)
assert (y_pos >= 0 and y_pos < height)
assert (x_pos >= 0 and x_pos < width)
pixels = pixbuf.get_pixels_array()
pixels[y_pos][x_pos][0] = red
pixels[y_pos][x_pos][1] = green
pixels[y_pos][x_pos][2] = blue
pixels[y_pos][x_pos][3] = alpha
def clear_pixbuf(self, pixbuf, red, green, blue, alpha):
n_channels = pixbuf.get_n_channels()
assert (n_channels == 4)
width = pixbuf.get_width()
height = pixbuf.get_height()
pixels = pixbuf.get_pixels_array()
for row in range(height):
for column in range(width):
pixels[row][column][0] = red
pixels[row][column][1] = green
pixels[row][column][2] = blue
pixels[row][column][3] = alpha
def main():
window = MainWindow()
gtk.main()
if __name__ == "__main__":
main()
可以看到上面那段code中绘制文本的方法,它是先调用compute_text_string_layout_value()来将字串的各个glyph的相关信息,比如advance,glyph index,x_position,y_position等收集进一个数组里面,然后再算出整个字串的宽度,随后依据字串的宽度确定字串的位置,最后才将一个个的glyph绘制出来。
看一下上面那段code执行的结果:
上面的那个绘制文字的流程,与android中绘制文字的流程,其实已经有很多的相似之处了,只不过上面的那段code中所演示的各个部分,比如text layout value的计算,或者glyph的绘制,glyph的管理等,不是那么简简单单的通过一个函数,就接到了FreeType里去了,而是会使用整个模块或者多个模块来共同完成。接下来,看一下,android中对应于上面的那个compute_text_string_layout_value()函数的模块的一部分,即android的文本布局引擎的实现。
android文本布局引擎的TextLayoutValue
首先来看一下TextLayoutValue这个结构的定义(在文件frameworks/base/core/jni/android/graphics/TextLayoutCache.h中):
class TextLayoutValue : public RefBase {
public:
TextLayoutValue(size_t contextCount);
void setElapsedTime(uint32_t time);
uint32_t getElapsedTime();
inline const jfloat* getAdvances() const { return mAdvances.array(); }
inline size_t getAdvancesCount() const { return mAdvances.size(); }
inline jfloat getTotalAdvance() const { return mTotalAdvance; }
inline const jchar* getGlyphs() const { return mGlyphs.array(); }
inline size_t getGlyphsCount() const { return mGlyphs.size(); }
inline const jfloat* getPos() const { return mPos.array(); }
inline size_t getPosCount() const { return mPos.size(); }
/**
* Advances vector
*/
Vector<jfloat> mAdvances;
/**
* Total number of advances
*/
jfloat mTotalAdvance;
/**
* Glyphs vector
*/
Vector<jchar> mGlyphs;
/**
* Pos vector (2 * i is x pos, 2 * i + 1 is y pos, same as drawPosText)
*/
Vector<jfloat> mPos;
/**
* Get the size of the Cache entry
*/
size_t getSize() const;
private:
/**
* Time for computing the values (in milliseconds)
*/
uint32_t mElapsedTime;
}; // TextLayoutCacheValue
可以看到,这个结构的定义,与我们前面的那个例子中定义的TextLayoutValue是何等的相似啊。好吧,我们前面的那个例子中的定义正是仿照android中的这段code来写的。仅有的一点区别是,这个结构中,存储各个glyph的information是通过多个数组来完成的。看一下这个结构里面的几个成员,想必都不用多做解释了吧,mAdvances用来保存各个glyph的advance;mGlyphs用于保存各个glyph 的ID,注意,不是glyph 的index哦;mPos则用于保存各个glyph的位置信息,当然这些位置信息都基于首个glyph的位置在(0,0)这个假设。
android文本布局引擎的TextLayoutShaper
看了前面的那个 TextLayoutValue ,脑袋里不自觉的就会冒出一个大大的问号,这个 TextLayoutValue在android里,到底 是如何算出来的呢?想要找到答案,还请看TextLayoutShaper。在android里主要由这个class来算出TextLayoutValue。
computeValues()函数
TextLayoutShaper的入口在computeValues()函数(在文件frameworks/base/core/jni/android/graphics/TextLayoutCache.h中),下面是这个函数的声明:
void computeValues(TextLayoutValue* value, const SkPaint* paint, const UChar* chars,
size_t start, size_t count, size_t contextCount, int dirFlags);
在这个函数的实现中,我们可以看到,它只是将参数转换了一下,然后就调用了同名的私有函数来完成实际的动作了。
在TextLayoutShaper的computeValues()私有函数的实现中,抛开那一堆的错误处理,可以看到它的主要执行过程为:
- 将传入的Bidi flag做一个转换
- 调用ubidi_open()函数创建一个UBiDi对象
- 调用ubidi_setPara(bidi, chars, contextCount, bidiReq, NULL, &status)函数来对字串做Bidi分析。
- 调用ubidi_getParaLevel(bidi)获取到整个子串paragraph的方向,以及调用ubidi_countRuns(bidi, &status)来获取整个字串中所包含的Bidi run的个数。
- 通过一个循环来,来计算每一个Bidi run的text layout value。在循环中,它会调用ubidi_getVisualRun(bidi, i, &startRun, &lengthRun)函数来获取到一个Bidi run的方向,起始位置,长度等信息,然后再调用computeRunValues(paint, chars + startRun, lengthRun, isRTL, outAdvances, outTotalAdvance, outGlyphs, outPos)来具体的完成计算一个Bidi run text layout value的工作。
总结一下,在这个函数中,主要处理与Bidirectional 有关的东西,即,对字串做Bidi,将字串分割成小的Bidi run,然后调用computeRunValues()函数来计算Bidi run text layout value。
关于Bidi的含义,以及Bidi的算法等更详细的信息,可以参考Unicode的文档,http://unicode.org/reports/tr9/,以及互联网上的其他的信息。
computeRunValues()函数
然后是computeRunValues()函数的实现。如前所述,这个函数用于计算一个Bidi run text的text layout value。由它的code,我们可以看到,它主要做了这么几件事情:
- 针对于字符串中出现的UBLOCK_COMBINING_DIACRITICAL_MARKS类型的字符做normalization。具体什么样的字符算是UBLOCK_COMBINING_DIACRITICAL_MARKS字符,可以参考Unicode官方提供的一份文档,在http://ishare.iask.sina.com.cn/f/10124684.html可以下载。大体上这都是一些重音符号之类的上下标。而normalization的过程,则大致是会根据上下文,将一些上下标的字符与它旁边的一些字符整体替换为另外的一个字符这样。
- 针对于RTL的Bidi Run做mirror。大体上就是某些成对的符号,比如小括号,大括号这些,在RTL子串中要反过来,即原来左小括号的Unicode,要被替换为右小括号的Unicode这样。
- 用一个循环,将一个Bidi Run的字串,切分成多个小的script run,然后计算每一个小的script run的text layout value并输出。
接下来,再来看一下计算一个小的script run的text layout value并输出的过程:
- 通过hb_utf16_script_run_prev()或者hb_utf16_script_run_next()获取到一个script run在Bidi run中的位置,长度,及script值。
- 调用shapeFontRun()来对一个script run做shape。在shapeFontRun()中,实际上最终会使用harfbuzz来做shape。
- 输出advances。advance的语义可以参考"Android Text Layout 框架"中的那个图。由于有些复杂语系,并不是每一个字符对应一个glyph,所以此处需要再做一些处理。
- 输出glyph ID。
- 计算并输出各个glyph的position的信息。harfbuzz实际上只会返回各个glyph的advance和offsets这样的一些信息,所以此处需要再做计算来确定各个glyph的位置。
computeRunValues()函数的执行大体上就如上面所述的那样。
harfbuzz的用法
说白了TextLayoutShaper就只不过是一个harfbuzz的客户端而已,而它的最主要的computeValues()和computeRunValues()函数,所做的事情也就是在一步步的为调用harfbuzz的接口构造适当的参数而已。
那harfbuzz的接口,都需要传给它哪些参数呢?又对参数都有些什么要求呢?
1、harfbuzz的入口为:
HB_Bool HB_ShapeItem(HB_ShaperItem *shaperItem);
这个函数只接收一个参数shaperItem,harfbuzz所需要的所有的东西,都要由这个参数来带入。实际传给harfbuzz的HB_ShaperItem为TextLayoutShaper的成员变量mShaperItem。
2、harfbuzz一次能shape的字串,应该是一个script的。因为,一般情况下,字库的设计者都会保证同属于一个script的字符的glyph会同时出现在相同的字库文件里。关于这一点,可以看到TextLayoutShaper中有如下的两段code:
// Set the string properties
mShaperItem.string = useNormalizedString ? mNormalizedString.getTerminatedBuffer() : chars;
mShaperItem.stringLength = count;
while ((isRTL) ?
hb_utf16_script_run_prev(&numCodePoints, &mShaperItem.item, mShaperItem.string,
mShaperItem.stringLength, &indexFontRun):
hb_utf16_script_run_next(&numCodePoints, &mShaperItem.item, mShaperItem.string,
mShaperItem.stringLength, &indexFontRun)) {
hb_utf16_script_run_next()和 hb_utf16_script_run_prev()函数,会初始化 mShaperItem.item为一个script的相关信息,什么起始位置,长度,script值之类的。同时,还有带script的direction信息给harfbuzz,通过mShaperItem->item.bidiLevel。
3、harfbuzz shape时需要一个HB_Face对象,通过shaperItem->face传入。harfbuzz会通过HB_Face对象来获取到字库文件中一些table的内容,比如“GSUB”,“GPOS”之类的。在android中这个参数有由getCachedHBFace()函数创建:
HB_Face TextLayoutShaper::getCachedHBFace(SkTypeface* typeface) {
SkFontID fontId = typeface->uniqueID();
ssize_t index = mCachedHBFaces.indexOfKey(fontId);
if (index >= 0) {
return mCachedHBFaces.valueAt(index);
}
HB_Face face = HB_NewFace(typeface, harfbuzzSkiaGetTable);
if (face) {
#if DEBUG_GLYPHS
ALOGD("Created HB_NewFace %p from paint typeface = %p", face, typeface);
#endif
mCachedHBFaces.add(fontId, face);
}
return face;
}
HB_Face对象通过一个user_data和一个callback来创建,user_data应该是一个我们可以从中读取到字库文件信息的对象,而callback则通过这个user_data来真正的从字库文件里面读取到信息。HB_Face在需要读取字库文件的table时,就会调用callback,并将user_data传进去。在android里,user_data为一个SkTypeface对象。TextLayoutShaper额外做的一件事情是,会将HB_Face对象缓存起来,其key为SkTypeface的ID。callback为harfbuzzSkiaGetTable,这个函数在frameworks/base/core/jni/android/graphics/HarfbuzzSkia.cpp中定义,它的实现基本上就是通过skia提供的接口,来在SkTypeface对象中读取table。
4、Harfbuzz shape时,需要一个HB_FontRec对象,通过shaperItem->font传入。harfbuzz所需要的许多信息都将来源于这个对象。客户端需要提供一组callback,及相应的user_data,让这个对象带给harfbuzz,harfbuzz通过这组callback来将unicode的字串转换成glyph id,获取到一个glyph 的metrics等。harfbuzz不管理glyph,在android里,通过skia来进行glyph的管理,所以会需要这组callback,以方便的将获取glyph id这样的一些功能接到skia里去。
TextLayoutShaper::TextLayoutShaper() : mShaperItemGlyphArraySize(0) {
init();
mFontRec.klass = &harfbuzzSkiaClass;
mFontRec.userData = 0;
// Note that the scaling values (x_ and y_ppem, x_ and y_scale) will be set
// below, when the paint transform and em unit of the actual shaping font
// are known.
memset(&mShaperItem, 0, sizeof(mShaperItem));
mShaperItem.font = &mFontRec;
mShaperItem.font->userData = &mShapingPaint;
}
void TextLayoutShaper::init() {
mDefaultTypeface = SkFontHost::CreateTypeface(NULL, NULL, NULL, 0, SkTypeface::kNormal);
}
前面提到的那组callback为harfbuzzSkiaClass,它的实现一样是在frameworks/base/core/jni/android/graphics/HarfbuzzSkia.cpp中。而相应的user_data为,根据传入的SkPaint而创建出来的mShapingPaint。
// Define shaping paint properties
mShapingPaint.setTextSize(paint->getTextSize());
float skewX = paint->getTextSkewX();
mShapingPaint.setTextSkewX(skewX);
mShapingPaint.setTextScaleX(paint->getTextScaleX());
mShapingPaint.setFlags(paint->getFlags());
mShapingPaint.setHinting(paint->getHinting());
mShapingPaint.setFontVariant(paint->getFontVariant());
mShapingPaint.setLanguage(paint->getLanguage());
可以看一下mShapingPaint将会继承的属性都有哪些。
客户端还需要通过HB_FontRec对象将字体的大小这样的一些信息待给harfbuzz,x_ppem,y_ppem,x_scale,y_scale等。
int textSize = paint->getTextSize();
float scaleX = paint->getTextScaleX();
mFontRec.x_ppem = floor(scaleX * textSize + 0.5);
mFontRec.y_ppem = textSize;
uint32_t unitsPerEm = SkFontHost::GetUnitsPerEm(typeface->uniqueID());
// x_ and y_scale are the conversion factors from font design space
// (unitsPerEm) to 1/64th of device pixels in 16.16 format.
const int kDevicePixelFraction = 64;
const int kMultiplyFor16Dot16 = 1 << 16;
float emScale = kDevicePixelFraction * kMultiplyFor16Dot16 / (float)unitsPerEm;
mFontRec.x_scale = emScale * scaleX * textSize;
mFontRec.y_scale = emScale * textSize;
5、客户端需要自行分配缓冲区,以用于harfbuzz存储shape的结果。
bool TextLayoutShaper::doShaping(size_t size) {
if (size > mShaperItemGlyphArraySize) {
deleteShaperItemGlyphArrays();
createShaperItemGlyphArrays(size);
}
mShaperItem.num_glyphs = mShaperItemGlyphArraySize;
memset(mShaperItem.offsets, 0, mShaperItem.num_glyphs * sizeof(HB_FixedPoint));
return HB_ShapeItem(&mShaperItem);
}
void TextLayoutShaper::createShaperItemGlyphArrays(size_t size) {
#if DEBUG_GLYPHS
ALOGD("Creating Glyph Arrays with size = %d", size);
#endif
mShaperItemGlyphArraySize = size;
// These arrays are all indexed by glyph.
mShaperItem.glyphs = new HB_Glyph[size];
mShaperItem.attributes = new HB_GlyphAttributes[size];
mShaperItem.advances = new HB_Fixed[size];
mShaperItem.offsets = new HB_FixedPoint[size];
// Although the log_clusters array is indexed by character, Harfbuzz expects that
// it is big enough to hold one element per glyph. So we allocate log_clusters along
// with the other glyph arrays above.
mShaperItem.log_clusters = new unsigned short[size];
}
给harfbuzz分配的这个缓冲区不需要保证能够完全的容纳下所产生的所有的glyph information。在缓冲区大小不足的情况下,HB_ShapeItem()会返回false,然后就需要客户端重新分配更大的缓冲区再做shape。
6、对于复杂语系所需要做的特殊的处理
前面我们说,harfbuzz一次能够shape的字串应该要隶属于相同的script才对。但由于有了callback这种东西,也不是完全要求一定要这样。但如果是复杂语系,那么harfbuzz需要能够做OpenType的处理,则对于这一点有硬性的要求。
同时创建HB_Face时所用的SkTypeface对象,则也一定要求要是特定的复杂语系所对应的那个字库文件所创建的SkTypeface。
我们前面提到,harfbuzz通过callback来获取到glyph ID,callback会借由SkPaint来做到这一点。对于复杂语系,则会要求那个SkPaint的SkTypeface就是特定的复杂语系所对应的那个字库文件所创建的SkTypeface。只有这样,通过SkPaint获取的glyph ID才会是字库内的glyph index,而实际上OpenType处理主要也是基于glyph index的。
harfbuzz返回的glyph id也将是glyph index,在android里,它是会先获取到包含有特定复杂语系glyph的字库文件它的首个glyph的glyph ID,即baseGlyphCount,最终再将glyph index加上这个值以算出真正的glyph ID。
size_t TextLayoutShaper::shapeFontRun(const SkPaint* paint, bool isRTL) {
// Reset kerning
mShaperItem.kerning_applied = false;
// Update Harfbuzz Shaper
mShaperItem.item.bidiLevel = isRTL;
SkTypeface* typeface = paint->getTypeface();
// Get the glyphs base count for offsetting the glyphIDs returned by Harfbuzz
// This is needed as the Typeface used for shaping can be not the default one
// when we are shaping any script that needs to use a fallback Font.
// If we are a "common" script we dont need to shift
size_t baseGlyphCount = 0;
SkUnichar firstUnichar = 0;
if (isComplexScript(mShaperItem.item.script)) {
const uint16_t* text16 = (const uint16_t*) (mShaperItem.string + mShaperItem.item.pos);
const uint16_t* text16End = text16 + mShaperItem.item.length;
firstUnichar = SkUTF16_NextUnichar(&text16);
while (firstUnichar == ' ' && text16 < text16End) {
firstUnichar = SkUTF16_NextUnichar(&text16);
}
baseGlyphCount = paint->getBaseGlyphCount(firstUnichar);
}
if (baseGlyphCount != 0) {
typeface = typefaceForScript(paint, typeface, mShaperItem.item.script);
if (!typeface) {
typeface = mDefaultTypeface;
SkSafeRef(typeface);
#if DEBUG_GLYPHS
ALOGD("Using Default Typeface");
#endif
}
} else {
if (!typeface) {
typeface = mDefaultTypeface;
#if DEBUG_GLYPHS
ALOGD("Using Default Typeface");
#endif
}
SkSafeRef(typeface);
}
mShapingPaint.setTypeface(typeface);
mShaperItem.face = getCachedHBFace(typeface);
harfbuzz API的用法大体上就如上面所述的那样。
android文本布局引擎的TextLayoutEngine及
TextLayoutCache
android文本布局引擎,所做的另外的事情即是,会将字串shape的结果缓存起来。这个部分对性能的影响非常大。缓存命中和不命中,获取text layout value所需消耗的时间相差少则10几倍,多则几十倍。
android文本布局引擎,即TextLayoutEngine class,在缓存打开的情况下,它只会通过TextLayoutCache来得到TextLayoutValue。而在TextLayoutCache中,若缓存没有命中,则它才会通过TextLayoutShaper来compute,否则就直接返回缓存的结果。
sp<TextLayoutValue> TextLayoutEngine::getValue(const SkPaint* paint, const jchar* text,
jint start, jint count, jint contextCount, jint dirFlags) {
sp<TextLayoutValue> value;
#if USE_TEXT_LAYOUT_CACHE
value = mTextLayoutCache->getValue(paint, text, start, count,
contextCount, dirFlags);
if (value == NULL) {
ALOGE("Cannot get TextLayoutCache value for text = '%s'",
String8(text + start, count).string());
}
#else
value = new TextLayoutValue(count);
mShaper->computeValues(value.get(), paint,
reinterpret_cast<const UChar*>(text), start, count, contextCount, dirFlags);
#endif
return value;
}
缓存主要借助于GenerationCache来实现。
android framework画字的流程
可以看一下frameworks/base/core/jni/android/graphics/Canvas.cpp中drawText__StringIIFFIPaint()这个函数的实现:
static void drawText__StringIIFFIPaint(JNIEnv* env, jobject,
SkCanvas* canvas, jstring text,
int start, int end,
jfloat x, jfloat y, int flags, SkPaint* paint) {
const jchar* textArray = env->GetStringChars(text, NULL);
drawTextWithGlyphs(canvas, textArray, start, end, x, y, flags, paint);
env->ReleaseStringChars(text, textArray);
}
static void drawTextWithGlyphs(SkCanvas* canvas, const jchar* textArray,
int start, int end,
jfloat x, jfloat y, int flags, SkPaint* paint) {
jint count = end - start;
drawTextWithGlyphs(canvas, textArray + start, 0, count, count, x, y, flags, paint);
}
static void drawTextWithGlyphs(SkCanvas* canvas, const jchar* textArray,
int start, int count, int contextCount,
jfloat x, jfloat y, int flags, SkPaint* paint) {
sp<TextLayoutValue> value = TextLayoutEngine::getInstance().getValue(paint,
textArray, start, count, contextCount, flags);
if (value == NULL) {
return;
}
SkPaint::Align align = paint->getTextAlign();
if (align == SkPaint::kCenter_Align) {
x -= 0.5 * value->getTotalAdvance();
} else if (align == SkPaint::kRight_Align) {
x -= value->getTotalAdvance();
}
paint->setTextAlign(SkPaint::kLeft_Align);
doDrawGlyphsPos(canvas, value->getGlyphs(), value->getPos(), 0, value->getGlyphsCount(), x, y, flags, paint);
doDrawTextDecorations(canvas, x, y, value->getTotalAdvance(), paint);
paint->setTextAlign(align);
}
// Same values used by Skia
#define kStdStrikeThru_Offset (-6.0f / 21.0f)
#define kStdUnderline_Offset (1.0f / 9.0f)
#define kStdUnderline_Thickness (1.0f / 18.0f)
static void doDrawTextDecorations(SkCanvas* canvas, jfloat x, jfloat y, jfloat length, SkPaint* paint) {
uint32_t flags;
SkDrawFilter* drawFilter = canvas->getDrawFilter();
if (drawFilter) {
SkPaint paintCopy(*paint);
drawFilter->filter(&paintCopy, SkDrawFilter::kText_Type);
flags = paintCopy.getFlags();
} else {
flags = paint->getFlags();
}
if (flags & (SkPaint::kUnderlineText_Flag | SkPaint::kStrikeThruText_Flag)) {
SkScalar left = SkFloatToScalar(x);
SkScalar right = SkFloatToScalar(x + length);
float textSize = paint->getTextSize();
float strokeWidth = fmax(textSize * kStdUnderline_Thickness, 1.0f);
if (flags & SkPaint::kUnderlineText_Flag) {
SkScalar top = SkFloatToScalar(y + textSize * kStdUnderline_Offset
- 0.5f * strokeWidth);
SkScalar bottom = SkFloatToScalar(y + textSize * kStdUnderline_Offset
+ 0.5f * strokeWidth);
canvas->drawRectCoords(left, top, right, bottom, *paint);
}
if (flags & SkPaint::kStrikeThruText_Flag) {
SkScalar top = SkFloatToScalar(y + textSize * kStdStrikeThru_Offset
- 0.5f * strokeWidth);
SkScalar bottom = SkFloatToScalar(y + textSize * kStdStrikeThru_Offset
+ 0.5f * strokeWidth);
canvas->drawRectCoords(left, top, right, bottom, *paint);
}
}
}
static void doDrawGlyphs(SkCanvas* canvas, const jchar* glyphArray, int index, int count,
jfloat x, jfloat y, int flags, SkPaint* paint) {
// Beware: this needs Glyph encoding (already done on the Paint constructor)
canvas->drawText(glyphArray + index * 2, count * 2, x, y, *paint);
}
static void doDrawGlyphsPos(SkCanvas* canvas, const jchar* glyphArray, const jfloat* posArray,
int index, int count, jfloat x, jfloat y, int flags, SkPaint* paint) {
SkPoint* posPtr = new SkPoint[count];
for (int indx = 0; indx < count; indx++) {
posPtr[indx].fX = SkFloatToScalar(x + posArray[indx * 2]);
posPtr[indx].fY = SkFloatToScalar(y + posArray[indx * 2 + 1]);
}
canvas->drawPosText(glyphArray, count << 1, posPtr, *paint);
delete[] posPtr;
}
drawText__StringIIFFIPaint()是一个JNI方法,上层会call到这个方法来画字。可以看到,它所做的事情,总结起来为:
- 通过TextLayoutEngine来获取到TextLayoutValue。
- 依据对齐方式,调整子串的x position,居中时调整为x -= 0.5 * value->getTotalAdvance(),而右对齐时调整为x -= value->getTotalAdvance()。
- 设置对齐方式为左对齐。
- 计算各个字符的position,然后通过canvas->drawPosText(glyphArray, count << 1, posPtr, *paint)来把glyph都画出来。
- 如果有需要则绘制下划线。
- 如果有需要,则绘制删除线。
- 恢复对齐方式。
基本上即是这样。
Done