Upto: Table of Contents of full book "Programming the Raspberry Pi GPU"

Text processing in OpenVG on the Raspberry Pi

Displaying text is an important function of any application. Drawing the text should be a low-level operation that the application should not be concerned with. It has to be done explicitly in OpenVG and this chapter discusses various techniques of doing that.

Resources

Files

Files used are here

Drawing text

Text is an immensely difficult area. Even in English there are the following issues:

Once you switch to other languages the complexities multiply

Displaying text involves a number of issues

OpenVG has the capability of finding fonts and laying them out at locations on the rendering surface. The rules for layout are not part of OpenVG. The simplest rule is just to lay them out one after another, but this will often get them wrong or not visually satisfying.

In this chapter I will look at a variety of ways of drawing text using OpenVG, trading off ease of use versus complexity of programming and CPU usage on the RPi. Of necessity, this will involve use of other libraries: Cairo, Pango and the freetype libraries.

Drawing text using Cairo

Cairo is a popular 2D graphics library which includes support for 'toy' text and complex text processing. The 'toy' library is suitable for simple text layout rules, particularly for Latin languages.

Cairo draws 2D shapes onto surfaces of various kinds. A good tutorial is at Cairo graphics tutorial . Drawing involves creating a surface, drawing into it and then getting the data out of the surface to display it or otherwise process it. The link with OpenVG from Cairo is provided by a Cairo function and an OpenVG function

	
    unsigned char* data = cairo_image_surface_get_data(surface);

    vgWritePixels(data, tex_s,
		  VG_lBGRA_8888, left, 300, tex_w, tex_h );
	
      

where text parameters are given from Cairo

	
    int tex_w = cairo_image_surface_get_width(surface);
    int tex_h = cairo_image_surface_get_height(surface);
    int tex_s = cairo_image_surface_get_stride(surface);
    int left = 1920/2 - extents.width/2;
	
      

A program to display a set of lines progressively using the Cairo toy text interface is cairo.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

#include <assert.h>

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <VG/openvg.h>
#include <VG/vgu.h>

#include <bcm_host.h>

#include <gtk/gtk.h>

char *lines[] = {"hello", "world", "AWAKE"};
int num_lines = 3;

typedef struct
{
    EGLDisplay display;
    EGLSurface surface;
    EGLContext context;
    EGLConfig config;
} EGL_STATE_T;


EGL_STATE_T state, *p_state = &state;

void init_egl(EGL_STATE_T *state)
{
    EGLint num_configs;
    EGLBoolean result;

    //bcm_host_init();

    static const EGLint attribute_list[] =
	{
	    EGL_RED_SIZE, 8,
	    EGL_GREEN_SIZE, 8,
	    EGL_BLUE_SIZE, 8,
	    EGL_ALPHA_SIZE, 8,
	    EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
	    EGL_NONE
	};

    // get an EGL display connection
    state->display = eglGetDisplay(EGL_DEFAULT_DISPLAY);

    // initialize the EGL display connection
    result = eglInitialize(state->display, NULL, NULL);

    // get an appropriate EGL frame buffer configuration
    result = eglChooseConfig(state->display, attribute_list, &state->config, 1, &num_configs);
    assert(EGL_FALSE != result);

    // Choose the OpenVG API
    result = eglBindAPI(EGL_OPENVG_API);
    assert(EGL_FALSE != result);

    // create an EGL rendering context
    state->context = eglCreateContext(state->display, 
				      state->config, EGL_NO_CONTEXT, 
				      NULL);

    assert(state->context!=EGL_NO_CONTEXT);
}

void init_dispmanx(EGL_DISPMANX_WINDOW_T *nativewindow) {   
    int32_t success = 0;   
    uint32_t screen_width;
    uint32_t screen_height;

    DISPMANX_ELEMENT_HANDLE_T dispman_element;
    DISPMANX_DISPLAY_HANDLE_T dispman_display;
    DISPMANX_UPDATE_HANDLE_T dispman_update;
    VC_RECT_T dst_rect;
    VC_RECT_T src_rect;

    bcm_host_init();

    // create an EGL window surface
    success = graphics_get_display_size(0 /* LCD */, 
					&screen_width, 
					&screen_height);
    assert( success >= 0 );

    dst_rect.x = 0;
    dst_rect.y = 0;
    dst_rect.width = screen_width;
    dst_rect.height = screen_height;

    src_rect.x = 0;
    src_rect.y = 0;
    src_rect.width = screen_width << 16;
    src_rect.height = screen_height << 16;        

    dispman_display = vc_dispmanx_display_open( 0 /* LCD */);
    dispman_update = vc_dispmanx_update_start( 0 );

    dispman_element = 
	vc_dispmanx_element_add(dispman_update, dispman_display,
				0 /*layer*/, &dst_rect, 0 /*src*/,
				&src_rect, DISPMANX_PROTECTION_NONE, 
				0 /*alpha*/, 0 /*clamp*/, 0 /*transform*/);

    // Build an EGL_DISPMANX_WINDOW_T from the Dispmanx window
    nativewindow->element = dispman_element;
    nativewindow->width = screen_width;
    nativewindow->height = screen_height;
    vc_dispmanx_update_submit_sync(dispman_update);

    printf("Got a Dispmanx window\n");
}

void egl_from_dispmanx(EGL_STATE_T *state, 
		       EGL_DISPMANX_WINDOW_T *nativewindow) {
    EGLBoolean result;

    state->surface = eglCreateWindowSurface(state->display, 
					    state->config, 
					    nativewindow, NULL );
    assert(state->surface != EGL_NO_SURFACE);

    // connect the context to the surface
    result = eglMakeCurrent(state->display, state->surface, state->surface, state->context);
    assert(EGL_FALSE != result);
}

void cairo(char *text) {
    int width = 500;
    int height = 200;

    cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 
							   width, height);
    cairo_t *cr = cairo_create(surface);

    cairo_rectangle(cr, 0, 0, width, height);
    cairo_set_source_rgb(cr, 0.0, 0.0, 0.5);
    cairo_fill(cr);

    // draw some white text on top
    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    // this is a standard font for Cairo
    cairo_select_font_face (cr, "cairo:serif",
			    CAIRO_FONT_SLANT_NORMAL, 
			    CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size (cr, 36);
    cairo_move_to(cr, 10.0, 50.0);

    cairo_scale(cr, 1.0f, -1.0f);
    cairo_translate(cr, 0.0f, -50);

    cairo_text_extents_t extents;
    cairo_text_extents(cr, text, &extents);
   
    cairo_show_text (cr, text);

    int tex_w = cairo_image_surface_get_width(surface);
    int tex_h = cairo_image_surface_get_height(surface);
    int tex_s = cairo_image_surface_get_stride(surface);
    unsigned char* data = cairo_image_surface_get_data(surface);

    int left = 1920/2 - extents.width/2;
    vgWritePixels(data, tex_s,
		  VG_lBGRA_8888, left, 300, tex_w, tex_h );
}
 
void draw() {
    EGL_DISPMANX_WINDOW_T nativewindow;

    init_egl(p_state);
    init_dispmanx(&nativewindow);

    egl_from_dispmanx(p_state, &nativewindow);

    int n = 0;
    while (1) {
	cairo(lines[n++]);
	n %= num_lines;

	eglSwapBuffers(p_state->display, p_state->surface);
	sleep(3);	
    }
}

int main(int argc, char** argv) {

    draw();

    exit(0);
}

      

This draws each line of text in white on a blue background as in

Drawing text using Pango

While Cairo can draw any form of text, the functions such as cairo_show_text do not have much flexibility. To draw in, say, multiple colours will involve much work. Pango is a library for handling all aspects of text. There is a Pango Reference Manual . A good tutorial is at The Pango connection: Part 2 .

The simplest way of colouring text (and some other effects) is to create the text marked up with HTML such as

gchar *markup_text = "<span foreground=\"red\">hello </span><span foreground=\"black\">world</span>";
      

which has "hello" in red and "world" in black. This is then parsed into the text itself "ed black" and a set of attribute markups.

gchar *markup_text = "<span foreground=\"red\">hello </span><span foreground=\"black\">world</span>";
PangoAttrList *attrs;
gchar *text;

pango_parse_markup (markup_text, -1,0, &attrs, &text, NULL, NULL);
      

This can be rendered into a Cairo context by creating a PangoLayout from the Cairo context, laying out the text with its attributes in the Pango layout and then showing this layout in the Cairo context:

PangoLayout *layout;
PangoFontDescription *desc;
cairo_move_to(cr, 300.0, 50.0);
layout = pango_cairo_create_layout (cr);
pango_layout_set_text (layout, text, -1);
pango_layout_set_attributes(layout, attrs);
pango_cairo_update_layout (cr, layout);
pango_cairo_show_layout (cr, layout);
      

(Yes, there is a lot of jumping around between libraries in all of this!).

Once the Cairo surface has been drawn the data can be extracted as in the Cairo example and written into the OpenVG surface. The program is pango.c and is

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

#include <assert.h>

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <VG/openvg.h>
#include <VG/vgu.h>

#include <bcm_host.h>

#include <gtk/gtk.h>
//include <glib.h>

typedef struct
{
    EGLDisplay display;
    EGLSurface surface;
    EGLContext context;
    EGLConfig config;
} EGL_STATE_T;


EGL_STATE_T state, *p_state = &state;

void init_egl(EGL_STATE_T *state)
{
    EGLint num_configs;
    EGLBoolean result;

    //bcm_host_init();

    static const EGLint attribute_list[] =
	{
	    EGL_RED_SIZE, 8,
	    EGL_GREEN_SIZE, 8,
	    EGL_BLUE_SIZE, 8,
	    EGL_ALPHA_SIZE, 8,
	    EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
	    EGL_NONE
	};

    // get an EGL display connection
    state->display = eglGetDisplay(EGL_DEFAULT_DISPLAY);

    // initialize the EGL display connection
    result = eglInitialize(state->display, NULL, NULL);

    // get an appropriate EGL frame buffer configuration
    result = eglChooseConfig(state->display, attribute_list, &state->config, 1, &num_configs);
    assert(EGL_FALSE != result);

    // Choose the OpenVG API
    result = eglBindAPI(EGL_OPENVG_API);
    assert(EGL_FALSE != result);

    // create an EGL rendering context
    state->context = eglCreateContext(state->display, 
				      state->config, EGL_NO_CONTEXT, 
				      NULL);

    assert(state->context!=EGL_NO_CONTEXT);
}

void init_dispmanx(EGL_DISPMANX_WINDOW_T *nativewindow) {   
    int32_t success = 0;   
    uint32_t screen_width;
    uint32_t screen_height;

    DISPMANX_ELEMENT_HANDLE_T dispman_element;
    DISPMANX_DISPLAY_HANDLE_T dispman_display;
    DISPMANX_UPDATE_HANDLE_T dispman_update;
    VC_RECT_T dst_rect;
    VC_RECT_T src_rect;

    bcm_host_init();

    // create an EGL window surface
    success = graphics_get_display_size(0 /* LCD */, 
					&screen_width, 
					&screen_height);
    assert( success >= 0 );

    dst_rect.x = 0;
    dst_rect.y = 0;
    dst_rect.width = screen_width;
    dst_rect.height = screen_height;

    src_rect.x = 0;
    src_rect.y = 0;
    src_rect.width = screen_width << 16;
    src_rect.height = screen_height << 16;        

    dispman_display = vc_dispmanx_display_open( 0 /* LCD */);
    dispman_update = vc_dispmanx_update_start( 0 );

    dispman_element = 
	vc_dispmanx_element_add(dispman_update, dispman_display,
				0/*layer*/, &dst_rect, 0/*src*/,
				&src_rect, DISPMANX_PROTECTION_NONE, 
				0 /*alpha*/, 0/*clamp*/, 0/*transform*/);

    // Build an EGL_DISPMANX_WINDOW_T from the Dispmanx window
    nativewindow->element = dispman_element;
    nativewindow->width = screen_width;
    nativewindow->height = screen_height;
    vc_dispmanx_update_submit_sync(dispman_update);

    printf("Got a Dispmanx window\n");
}

void egl_from_dispmanx(EGL_STATE_T *state, 
		       EGL_DISPMANX_WINDOW_T *nativewindow) {
    EGLBoolean result;

    state->surface = eglCreateWindowSurface(state->display, 
					    state->config, 
					    nativewindow, NULL );
    assert(state->surface != EGL_NO_SURFACE);

    // connect the context to the surface
    result = eglMakeCurrent(state->display, state->surface, state->surface, state->context);
    assert(EGL_FALSE != result);
}

// setfill sets the fill color
void setfill(float color[4]) {
    VGPaint fillPaint = vgCreatePaint();
    vgSetParameteri(fillPaint, VG_PAINT_TYPE, VG_PAINT_TYPE_COLOR);
    vgSetParameterfv(fillPaint, VG_PAINT_COLOR, 4, color);
    vgSetPaint(fillPaint, VG_FILL_PATH);
    vgDestroyPaint(fillPaint);
}
 
 
// setstroke sets the stroke color and width
void setstroke(float color[4], float width) {
    VGPaint strokePaint = vgCreatePaint();
    vgSetParameteri(strokePaint, VG_PAINT_TYPE, VG_PAINT_TYPE_COLOR);
    vgSetParameterfv(strokePaint, VG_PAINT_COLOR, 4, color);
    vgSetPaint(strokePaint, VG_STROKE_PATH);
    vgSetf(VG_STROKE_LINE_WIDTH, width);
    vgSeti(VG_STROKE_CAP_STYLE, VG_CAP_BUTT);
    vgSeti(VG_STROKE_JOIN_STYLE, VG_JOIN_MITER);
    vgDestroyPaint(strokePaint);
}

// Ellipse makes an ellipse at the specified location and dimensions, applying style
void Ellipse(float x, float y, float w, float h, float sw, float fill[4], float stroke[4]) {
    VGPath path = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_F, 1.0f, 0.0f, 0, 0, VG_PATH_CAPABILITY_ALL);
    vguEllipse(path, x, y, w, h);
    setfill(fill);
    setstroke(stroke, sw);
    vgDrawPath(path, VG_FILL_PATH | VG_STROKE_PATH);
    vgDestroyPath(path);
}

void pango() {
    int width = 500;
    int height = 200;

    cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 
							   width, height);
    cairo_t *cr = cairo_create(surface);

    cairo_rectangle(cr, 0, 0, width, height);
    cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
    cairo_fill(cr);


    // Pango marked up text, half red, half black
    gchar *markup_text = "<span foreground=\"red\" font='48'>hello</span>\
<span foreground=\"black\" font='48'>AWA㹴犬</span>";
    PangoAttrList *attrs;
    gchar *text;

    pango_parse_markup (markup_text, -1, 0, &attrs, &text, NULL, NULL);

    // draw Pango text
    PangoLayout *layout;
    PangoFontDescription *desc;
    
    cairo_move_to(cr, 30.0, 150.0);
    cairo_scale(cr, 1.0f, -1.0f);
    layout = pango_cairo_create_layout (cr);
    pango_layout_set_text (layout, text, -1);
    pango_layout_set_attributes(layout, attrs);
    pango_cairo_update_layout (cr, layout);
    pango_cairo_show_layout (cr, layout);

    int tex_w = cairo_image_surface_get_width(surface);
    int tex_h = cairo_image_surface_get_height(surface);
    int tex_s = cairo_image_surface_get_stride(surface);
    cairo_surface_flush(surface);
    cairo_format_t type =
	cairo_image_surface_get_format (surface);
    printf("Format is (0 is ARGB32) %d\n", type);
    unsigned char* data = cairo_image_surface_get_data(surface);

    PangoLayoutLine *layout_line =
	pango_layout_get_line(layout, 0);
    PangoRectangle ink_rect;
    PangoRectangle logical_rect;
    pango_layout_line_get_pixel_extents (layout_line,
					 &ink_rect,
					 &logical_rect);
    printf("Layout line rectangle is %d, %d, %d, %d\n",
	   ink_rect.x, ink_rect.y, ink_rect.width, ink_rect.height);

    int left = 1920/2 - ink_rect.width/2;
    vgWritePixels(data, tex_s,
		  //VG_lBGRA_8888, 
		  VG_sARGB_8888 ,
		  left, 300, tex_w, tex_h );    
}


void draw()
{ 
    EGL_DISPMANX_WINDOW_T nativewindow;

    init_egl(p_state);
    init_dispmanx(&nativewindow);

    egl_from_dispmanx(p_state, &nativewindow);
    //draw();
    pango();

    eglSwapBuffers(p_state->display, p_state->surface);

    sleep(30);
    eglTerminate(p_state->display);

    exit(0);
}

int main(int argc, char** argv) {

    draw();

    exit(0);
}

      

This draws some text in red and black onto a grey rectangle against a white backdrop as in

FreeType

The previous programs basically did all the rendering calculations using the RPi's CPU, and used the GPU only to render the resultant images. This is not using the GPU as we want to: to manage as much as possible of the rendering calculations.

It is not possible to do everything on the GPU: it doesn't know about Linux file systems for example. But more importantly it doesn't know how or where information about text fonts and glyphs is stored. This information has to be built from an external library and massaged into a form suitable for the RPi's GPU.

The most common library in use on Linux systems now seems to be the FreeType library. Here is the API reference . There is also a tutorial

Font faces

The highest level object in FreeType is a FT_Library. There may be more than one of these, say one for each thread in a multi-threaded application. We will only use one. An FT_Face represents a single typeface in a particular style, such as ‘Palatino Regular’ or ‘Palatino Italic’. An FT_Face is extracted from a font file which may contain many faces. Typically face zero is chosen. Once selected, a face may have characteristics such as pixel size set.

Typical code to set up a face with a particular size is

void ft_init() {
    int err;
    
    err = FT_Init_FreeType(&ft_library);
    assert( !err);

    err = FT_New_Face(ft_library,
		      "/usr/share/ghostscript/9.05/Resource/CIDFSubst/DroidSansFallback.ttf",
                     0,
                     &ft_face );
    assert( !err);

    int font_size = 128;
    err = FT_Set_Pixel_Sizes(ft_face, 0, font_size);
    assert( !err);
}
      

Where do we get the font files from? The filetype required is usually Truetype with extension .ttf, and many of these are found in the directory /usr/share/fonts/truetype/. These all contain the ASCII character set, and their appearance can be seen using gnome-font-viewer For example, the font /usr/share/fonts/truetype/freefont/FreeMonoBold.ttf looks like

I want to be able to see Chinese characters as well as ASCII, and I know that with all the packages I have installed, Pango is able to find a font file containing them. Repolho shows at Find out fallback font used by fontconfig for a certain character how to find a font file for any particular character. For example, for the Chinese character '好' (good) the command is

FC_DEBUG=4 pango-view -t '好' 2>&1 |    
 grep -o 'family: "[^"]\+' | 
 cut -c 10- | tail -n 1
      

This will display the character in a small box, and when you quit ('q') the box, it will print the name of a font file. On my system, it printed Droid Sans Fallback. That's the name of the font, but not of the file. By using

locate DroidSansFallback
      

the file /usr/share/ghostscript/9.05/Resource/CIDFSubst/DroidSansFallback.ttf was identified. This need not be the only containing this character, but is the one used by Pango.

It may be that there is no such font file on your system. In that case, download the files zysong.ttf or Cyberbit.ttf.

Paths and glyphs

Once a face has been set up, you can hand characters to it and get an index for each character in the face using FT_Get_Char_Index(FT_Face face, FT_ULong charcode ). Although the function name says 'Char', with Unicode characters these won't be 8-bit C char's except for the ASCII subset. You will need the full 16- or 32-bit Unicode character, which is an FT_ULong to the FreeType system. So for example, the Chinese character '好' needs to be represented by its Unicode value of 0x597d. Unicode values can be found in many places, such as scarfboy.com .

Once an index is found, the character can be loaded into the face by FT_Load_Glyph. There are two things we can do with a loaded glyph: get it back as an outline or convert it to a bitmap. In each case there are different things that can done with OpenVG, which are discussed in the following sections.

Extracting the outline of a glyph from a face is easy:

 FT_Outline *outline = &ft_face->glyph->outline;
      

Getting a bitmap involves more steps: first you get the glyph as a FT_Glyph by FT_Get_Glyph. This is then converted to a bitmap by FT_Glyph_To_Bitmap . But a glyph has no field of a bitmap: it needs to be coerced into type FT_BitmapGlyph and then the bitmap can be found:

    FT_Glyph glyph;
    FT_Get_Glyph(ft_face->glyph, &glyph);
    FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, NULL, 1);
    FT_BitmapGlyph bit_glyph = (FT_BitmapGlyph) glyph;
    FT_Bitmap bitmap = bit_glyph->bitmap;
      

There are operations that can be performed on a glyph before extracting the bitmap: for example, using an FT_Stroker the corners can be smoothed out. The code to do that is

    FT_Glyph glyph;
    FT_Get_Glyph(ft_face->glyph, &glyph);

    // smooth the border
    FT_Stroker ft_stroker;
    FT_Stroker_New(ft_library, &ft_stroker);
    FT_Stroker_Set(ft_stroker,
		   200, // line_height_*border_thickness*64.0f,
		   FT_STROKER_LINECAP_ROUND,
		   FT_STROKER_LINEJOIN_ROUND,
		   0);
    FT_Glyph_StrokeBorder(&glyph, ft_stroker, 0, 1);

    // now get the smoothed bitmap
    FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, NULL, 1);
    FT_BitmapGlyph bit_glyph = (FT_BitmapGlyph) glyph;
    FT_Bitmap bitmap = bit_glyph->bitmap;
      

Drawing a FreeType outline

The FT_Outline type from FreeType contains information about the outline of a glyph, but this is not the same form as used in an OpenVG path. It has to be converted from FreeType to OpenVG.

Functions to do this are in /opt/vc/src/hello_pi/libs/vgfont/vgft.c

#define SEGMENTS_COUNT_MAX 256
#define COORDS_COUNT_MAX 1024

static VGuint segments_count;
static VGubyte segments[SEGMENTS_COUNT_MAX];
static VGuint coords_count;
static VGfloat coords[COORDS_COUNT_MAX];

static VGfloat float_from_26_6(FT_Pos x)
{
   return (VGfloat)x / 64.0f;
}

static void convert_contour(const FT_Vector *points, 
			    const char *tags, short points_count)
{
   int first_coords = coords_count;

   int first = 1;
   char last_tag = 0;
   int c = 0;

   for (; points_count != 0; ++points, ++tags, --points_count) {
      ++c;

      char tag = *tags;
      if (first) {
         assert(tag & 0x1);
         assert(c==1); c=0;
         segments[segments_count++] = VG_MOVE_TO;
         first = 0;
      } else if (tag & 0x1) {
         /* on curve */

         if (last_tag & 0x1) {
            /* last point was also on -- line */
            assert(c==1); c=0;
            segments[segments_count++] = VG_LINE_TO;
         } else {
            /* last point was off -- quad or cubic */
            if (last_tag & 0x2) {
               /* cubic */
               assert(c==3); c=0;
               segments[segments_count++] = VG_CUBIC_TO;
            } else {
               /* quad */
               assert(c==2); c=0;
               segments[segments_count++] = VG_QUAD_TO;
            }
         }
      } else {
         /* off curve */

         if (tag & 0x2) {
            /* cubic */

            assert((last_tag & 0x1) || (last_tag & 0x2)); /* last either on or off and cubic */
         } else {
            /* quad */

            if (!(last_tag & 0x1)) {
               /* last was also off curve */

               assert(!(last_tag & 0x2)); /* must be quad */

               /* add on point half-way between */
               assert(c==2); c=1;
               segments[segments_count++] = VG_QUAD_TO;
               VGfloat x = (coords[coords_count - 2] + float_from_26_6(points->x)) * 0.5f;
               VGfloat y = (coords[coords_count - 1] + float_from_26_6(points->y)) * 0.5f;
               coords[coords_count++] = x;
               coords[coords_count++] = y;
            }
         }
      }
      last_tag = tag;

      coords[coords_count++] = float_from_26_6(points->x);
      coords[coords_count++] = float_from_26_6(points->y);
   }

   if (last_tag & 0x1) {
      /* last point was also on -- line (implicit with close path) */
      assert(c==0);
   } else {
      ++c;

      /* last point was off -- quad or cubic */
      if (last_tag & 0x2) {
         /* cubic */
         assert(c==3); c=0;
         segments[segments_count++] = VG_CUBIC_TO;
      } else {
         /* quad */
         assert(c==2); c=0;
         segments[segments_count++] = VG_QUAD_TO;
      }
      coords[coords_count++] = coords[first_coords + 0];
      coords[coords_count++] = coords[first_coords + 1];
   }

   segments[segments_count++] = VG_CLOSE_PATH;
}

static void convert_outline(const FT_Vector *points, 
			    const char *tags, const short *contours, 
			    short contours_count, short points_count)
{
   segments_count = 0;
   coords_count = 0;

   short last_contour = 0;
   for (; contours_count != 0; ++contours, --contours_count) {
      short contour = *contours + 1;
      convert_contour(points + last_contour, tags + last_contour, contour - last_contour);
      last_contour = contour;
   }
   assert(last_contour == points_count);

   assert(segments_count <= SEGMENTS_COUNT_MAX); /* oops... we overwrote some me
mory */
   assert(coords_count <= COORDS_COUNT_MAX);
}
      

These functions are used to set segments and co-ordinates suitable for adding to an OpenVG path created using vgCreatePath by vgAppendPath. The resultant path can then be drawn by vgDrawpath:

      FT_Outline *outline = &ft_face->glyph->outline;
      if (outline->n_contours != 0) {
         vg_path = vgCreatePath(VG_PATH_FORMAT_STANDARD, 
				VG_PATH_DATATYPE_F, 1.0f, 
				0.0f, 0, 0, VG_PATH_CAPABILITY_ALL);
         assert(vg_path != VG_INVALID_HANDLE);

         convert_outline(outline->points, outline->tags, 
			 outline->contours, outline->n_contours, 
			 outline->n_points);
         vgAppendPathData(vg_path, segments_count, segments, coords);
      } else {
         vg_path = VG_INVALID_HANDLE;
      }

      VGfloat strokeColor[4] = {0.4, 0.1, 1.0, 1.0}; // purple
      VGfloat fillColor[4] = {1.0, 1.0, 0.0, 1.0}; // yellow

      setfill(fillColor);
      setstroke(strokeColor, 5);

      vgDrawPath(vg_path, VG_FILL_PATH | VG_STROKE_PATH);  
      

It looks like

The complete program is text-outline.c

Drawing a FreeType bitmap

In an earlier section we derived an FT_Bitmap for a glyph. The bitmap contains fields for stride (which it calls pitch), width and height, plus a buffer of data. The format is grey-scale with 256 levels. What this becomes in OpenVG I'm not sure: VG_sL_8 seems to work okay for me, while omxplayer uses VG_A_8.

The only real wrinkle is that the bitmap image is upside down in OpenVG co-ordinates. So you can't just use vgCreateImage as that draws the bitmap upside down. Instead, you have to add the data to a VGImage using vgImageSubData. The program omxplayer uses a neat trick to insert the data upside down: while vgImageSubData expects to load data from bottom to top, increasing the stride by one each time, we hand the data in from the top, decreasing the stride each time.

The image is drawn using vgSetPixels to set the location on the screen. The relevant code is

    FT_Glyph glyph;    
    FT_Get_Glyph(ft_face->glyph, &glyph);

    FT_Stroker ft_stroker;
    FT_Stroker_New(ft_library, &ft_stroker);
    FT_Stroker_Set(ft_stroker,
		   200, // line_height_*border_thickness*64.0f,
		   FT_STROKER_LINECAP_ROUND,
		   FT_STROKER_LINEJOIN_ROUND,
		   0);
    FT_Glyph_StrokeBorder(&glyph, ft_stroker, 0, 1);
    
    FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, NULL, 1);
    FT_BitmapGlyph bit_glyph = (FT_BitmapGlyph) glyph;
    FT_Bitmap bitmap = bit_glyph->bitmap;
    printf("Bitmap mode is %d (2 is gray) with %d levels\n", 
	   bitmap.pixel_mode, bitmap.num_grays);

    int tex_s = bitmap.pitch;
    int tex_w = bitmap.width;
    int tex_h = bitmap.rows;
  
    VGImage image = vgCreateImage(VG_sL_8, tex_w, tex_h, 
				  VG_IMAGE_QUALITY_NONANTIALIASED);

    // invert bitmap image
    vgImageSubData(image,
		   bitmap.buffer + tex_s*(tex_h-1),
		   -tex_s,
		   VG_sL_8, //VG_A_8, VG_sL_8 seem to be okay too
		   0, 0, tex_w, tex_h);

    vgSetPixels(600, 600, image, 0, 0, tex_w, tex_h);
      

The complete program is text-bitmap.c and it looks like

There is no border, nor colour control.

VGFonts and multiple characters

If you want to draw multiple characters you will need to get multiple outline paths or bitmaps from FreeType and convert them. If characters are repeated or drawn multiple times then this can lead to redundant processing. OpenVG has a type VGFont which can essentially act as a cache for the paths and bitmaps, so that you load each one into the font and then can draw them multiple times without having to repeat the loading process.

A font is created using vgCreateFont which takes a parameter for expected size or zero. Paths are added to a font by vgSetGlyphToPath while bitmaps are added using vgSetGlyphToImage. These take as parameters the font, the index used for the glyph, the path or image and two other parameters describing the geometry of the path/image. These are discussed below.

VGDrawGlyph

There is then a bonus function: instead of extracting the path or image from the font and then drawing it, the function vgDrawGlyph will perform the drawing for you, stroking or filling the path as required, as in

    vgDrawGlyph(font, ch,
		VG_STROKE_PATH | VG_FILL_PATH,
		VG_FALSE);
      

Where it draws the path/image depends on the value of the parameter VG_GLYPH_ORIGIN. This is initially set to an (x, y) location as in

    VGfloat origin[2] = {300.0f, 300.0f};
    vgSetfv(VG_GLYPH_ORIGIN, 2, origin);
     

Each time a glyph is drawn, this location is updated by the width and height of the glyph, so that successive glyphs are drawn next to each other.

A pseudo-code algorithm for drawing a string is then

for each ch in the string
    if ch is not in the font
    then
        add the glyph to the font, using ch as index
    draw the glyph using ch as index
      

There is only one glitch in this: there is no way of telling if a glyph is or is not in the font! If you are just adding a small set of glyphs such as the ASCII characters this is not a problem: just loop through them all adding each one. But if it is a large set, such as the 120,000+ Unicode characters then you won't want to do that. Instead, you would like to add them on an as-needed basis, say as you loop through a piece of text.

To avoid repeated additions of a character you need to keep a list outside of OpenVG. I used the hash table from GLib: add it to the hash table when first seen and add it the font, but not add it if already there. This gives code to add an outline glyph as

void add_glyph(int *ch) {
    if (g_hash_table_lookup(glyphs_seen, GINT_TO_POINTER(ch)) != NULL) {
	printf("Glyph %d already seen\n", *ch);
	return;
    }
    g_hash_table_insert(glyphs_seen, 
			GINT_TO_POINTER(ch), 
			GINT_TO_POINTER(ch));
    printf("Inserting %d\n", *ch);

    int glyph_index = FT_Get_Char_Index(ft_face, *ch);
    if (glyph_index == 0) printf("No glyph found\n");
    FT_Load_Glyph(ft_face, glyph_index, 
		  FT_LOAD_NO_HINTING| FT_LOAD_LINEAR_DESIGN);
    
    VGPath vg_path;
    
    FT_Outline *outline = &ft_face->glyph->outline;
    if (outline->n_contours != 0) {
	vg_path = vgCreatePath(VG_PATH_FORMAT_STANDARD, 
			       VG_PATH_DATATYPE_F, 1.0f, 
			       0.0f, 0, 0, VG_PATH_CAPABILITY_ALL);
	assert(vg_path != VG_INVALID_HANDLE);
	
	convert_outline(outline->points, outline->tags, 
			outline->contours, outline->n_contours, 
			outline->n_points);
	vgAppendPathData(vg_path, segments_count, segments, coords);
    } else {
	vg_path = VG_INVALID_HANDLE;
    }

    VGfloat glyphOrigin[2] = {0.0f, 0.0f};
    printf("Width %d, height %d advance %d\n", 
	   ft_face->glyph->metrics.width,
	   ft_face->glyph->metrics.height,
	   ft_face->glyph->linearHoriAdvance);
    VGfloat escapement[2] = {ft_face->glyph->linearHoriAdvance,
			     0.0f};

    vgSetGlyphToPath(font, *ch,
		     vg_path, 0,
		     glyphOrigin,
		     escapement);
}
      

We can then draw a set of glyphs by

void draw() { 
    EGL_DISPMANX_WINDOW_T nativewindow;
    glyphs_seen = g_hash_table_new(g_int_hash, g_int_equal);

    init_egl(p_state);
    init_dispmanx(&nativewindow);

    egl_from_dispmanx(p_state, &nativewindow);
 
    vgClear(0, 0, 1920, 1080);

    ft_init();

    font = vgCreateFont(0);
    //font_border = vgCreateFont(0);
    
    VGfloat strokeColor[4] = {0.4, 0.1, 1.0, 1.0}; // purple
    VGfloat fillColor[4] = {1.0, 1.0, 0.0, 1.0}; // yellowish

    setfill(fillColor);
    setstroke(strokeColor, 5);

    VGfloat origin[2] = {300.0f, 300.0f};
    vgSetfv(VG_GLYPH_ORIGIN, 2, origin);

    int ch;

    ch = 0x597d;
    add_glyph(&ch);
    vgDrawGlyph(font, ch,
		VG_STROKE_PATH,
		VG_FALSE);

    ch = 'A';
    add_glyph(&ch);
    vgDrawGlyph(font, ch,
		VG_STROKE_PATH,
		VG_FALSE);

    ch = 'b';
    add_glyph(&ch);
    vgDrawGlyph(font, ch,
		VG_STROKE_PATH | VG_FILL_PATH,
		VG_FALSE);
    ch = 'g';
    add_glyph(&ch);
    vgDrawGlyph(font, ch,
		VG_STROKE_PATH | VG_FILL_PATH,
		VG_FALSE);
    ch = 'b';    
    add_glyph(&ch);
    vgDrawGlyph(font, ch,
		VG_STROKE_PATH,
		VG_FALSE);

    vgFlush();

    eglSwapBuffers(p_state->display, p_state->surface);
}
      

with appearance

The complete program is text-font.c

vgDrawGlyphs

In the previous example we drew some glyphs filled and some in outline only. We could have made other changes too, such as different colour fills. However, if all the characters are to be drawn using the same parameters, then the convenience function vgDrawGlyphs could be used.

Glyph metrics

The geometry of a glyph is actually very complex, and we have ignored that so far. For FreeType, they are discussed in the tutorial Managing Glyphs . The metrics are accessible from the structure face->glyph->metrics and are:

width
This is the width of the glyph image's bounding box. It is independent of the layout direction.
height
This is the height of the glyph image's bounding box. It is independent of the layout direction. Be careful not to confuse it with the ‘height’ field in the FT_Size_Metrics structure.
horiBearingX
For horizontal text layouts, this is the horizontal distance from the current cursor position to the leftmost border of the glyph image's bounding box.
horiBearingY
For horizontal text layouts, this is the vertical distance from the current cursor position (on the baseline) to the topmost border of the glyph image's bounding box.
horiAdvance
For horizontal text layouts, this is the horizontal distance to increment the pen position when the glyph is drawn as part of a string of text.
vertBearingX
For vertical text layouts, this is the horizontal distance from the current cursor position to the leftmost border of the glyph image's bounding box.
vertBearingY
For vertical text layouts, this is the vertical distance from the current cursor position (on the baseline) to the topmost border of the glyph image's bounding box.
vertAdvance
For vertical text layouts, this is the vertical distance used to increment the pen position when the glyph is drawn as part of a string of text.

where for horizontal layout,

OpenVG does not seem to go to so much detail. The only diagram that is shown is

When adding a glyph to a font, only the glyph origin and the escapement are set, each as an (x, y) vector. The best values seem to be

    VGfloat glyphOrigin[2] = {0.0f, 0.0f};
    VGfloat escapement[2] = {ft_face->glyph->linearHoriAdvance,
			     0.0f}; 
      

Conclusion

This chapter has looked at a number of different ways of drawing text using OpenVG. There are tradeoffs between ease of use and utilising the GPU.


      

Copyright © Jan Newmarch, jan@newmarch.name
Creative Commons License
" Programming AudioVideo on the Raspberry Pi GPU " by Jan Newmarch is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License .
Based on a work at https://jan.newmarch.name/RPi/ .

If you like this book, please contribute using PayPal

Or Flattr me:
Flattr this book