C++
Linux
RaspberryPi
nanopineo
FreeType

freetype2 で文字列描画 (実践編)

はじめに

日本語描画が必要だったので、freetypeを使用し実践的なコード組んでみた。
OpenCV 3.2 でFreetype描画がサポートされたようだが、痒い所に手が届かなそう(CalcRectが無いとか)だったので、結局実装してみた。

_72D0559s.jpg

Freetype のAPI referenceなど

API reference
https://www.freetype.org/freetype2/docs/reference/ft2-index.html

Tutorial
https://www.freetype.org/freetype2/docs/tutorial/step1.html
https://www.freetype.org/freetype2/docs/tutorial/step2.html

Sample
https://www.freetype.org/freetype2/docs/tutorial/example1.c
https://www.freetype.org/freetype2/docs/tutorial/example2.cpp

作成コード

全体はgitで。
https://github.com/blue777/NanoPi-NEO/blob/master/common/img_font.h

クラス

クラス名:ImageFont

メソッド 概要
ImageFont( const char* filename, uint32_t height, bool isFitHeight=true ) コンストラクタ
CalcRect( int& left, int& top, int& right, int& bottom, const char* str ) 文字列の描画サイズ計算
DrawTextGRAY( int x, int y, const char* str, uint8_t color, uint8_t * image, int stride, int cx, int cy ) Grayバッファへの文字列描画
DrawTextBGRA( int x, int y, const char* str, uint32_t color, uint8_t * image, int stride, int cx, int cy ) BGRAバッファへの文字列描画(α値指定描画対応)

コンストラクタ

基本的な初期化を行う。
指定した高さからはみ出て描画(baseline管理になるので、gjpqyなど基準線より下の部分)されるので、はみ出ないように調整する機能も付けている。

メンバ変数
    FT_Library      m_piLibrary;
    FT_Face         m_piFace;
    int             m_nBaseline;
ImageFont()
    ImageFont( const char* filename, uint32_t height, bool isFitHeight=true )
    {
        FT_Error            error;
        FT_Size_RequestRec  tReqSize;

        // initialize member var.
        m_nBaseline     = height;
        m_piLibrary     = NULL;
        m_piFace        = NULL;

        error   = FT_Init_FreeType( &m_piLibrary );
        if( 0 != error )
        {
            throw   "ERROR: FT_Init_FreeType()";
        }

        error   = FT_New_Face( m_piLibrary, filename, 0, &m_piFace );
        if( 0 != error )
        {
            throw   "ERROR: FT_New_Face";
        }

        tReqSize.type           = FT_SIZE_REQUEST_TYPE_NOMINAL;
        tReqSize.width          = 0;
        tReqSize.height         = (height << 6);
        tReqSize.horiResolution = 0;
        tReqSize.vertResolution = 0;

        if( isFitHeight )
        {
            //  b. Scaled Global Metrics
            int     yMax    = m_piFace->bbox.yMax;
            int     yMin    = m_piFace->bbox.yMin;

            m_nBaseline     = height * yMax / (yMax - yMin);

            tReqSize.type   = FT_SIZE_REQUEST_TYPE_BBOX;
            tReqSize.height = (height << 6);
        }

        error   = FT_Request_Size( m_piFace, &tReqSize );
        if( 0 != error )
        {
            throw   "ERROR: FT_Request_Size()";
        }
    }

BGRA描画部

Freetypeに渡す際には、UNICODE32で1文字ずつ渡し描画する必要がある。

colorの上位8bitはα値で、文字列の半透明描画も出来るようにしてみている。
なお、BGRAは、OpenCVのCV_8UC4互換である。

DrawTextBGRA()
    int DrawTextBGRA( int x, int y, const char* str, uint32_t color, uint8_t * image, int stride, int cx, int cy )
    {
        std::u32string  u32str  = std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t>().from_bytes(str);

        return  DrawTextBGRA( x, y, u32str, color, image, stride, cx, cy );
    }

    int DrawTextBGRA( int x, int y, const std::u32string& u32str, uint32_t color, uint8_t * image, int stride, int cx, int cy )
    {
        FT_Error        error;
        FT_GlyphSlot    slot    = m_piFace->glyph;
        FT_Matrix       matrix  = { 1 << 16, 0, 0, 1 << 16 };
        FT_Vector       pen     = { 0, 0 };
        uint32_t        alpha   = (color >> 24) + (color >> 31);

        for( size_t i = 0; i < u32str.size(); i++ )
        {
            switch( u32str[i] )
            {
            case '\r':
                break;

            case '\t':
                pen.x   += m_piFace->size->metrics.max_advance * 4;
                pen.x   -= pen.x % (m_piFace->size->metrics.max_advance * 4);
                break;

            case '\n':
                pen.x   = 0;
                pen.y   -= m_piFace->size->metrics.height;
                break;

            default:
                FT_Set_Transform( m_piFace, &matrix, &pen );

                error   = FT_Load_Char( m_piFace, u32str[i], FT_LOAD_RENDER );
                if( 0 == error )
                {
                    int bmp_cy  = slot->bitmap.rows;
                    int pos_y   = y + m_nBaseline - slot->bitmap_top;
                    int rs      = 0 <= pos_y ? 0 : -pos_y;
                    int re      = (pos_y + bmp_cy) <= cy ? bmp_cy : (cy - pos_y);

                    int bmp_cx  = slot->bitmap.width;
                    int pos_x   = x + slot->bitmap_left;
                    int cs      = 0 <= pos_x ? 0 : -pos_x;
                    int ce      = (pos_x + bmp_cx) <= cx ? bmp_cx : (cx - pos_x);

                    for( int r = rs; r < re; r++ )
                    {
                        uint8_t *   src_line    = &slot->bitmap.buffer[ bmp_cx * r ];
                        uint8_t *   dst_line    = &image[ stride * (pos_y + r) + (pos_x * 4) ];
                        int         c           = cs;

                        for( ; c < ce; c++ )
                        {
                            int32_t a   = src_line[c+0];

                            if( 0 < a )
                            {
                                uint8_t*    clr = (uint8_t*)&color;
                                int32_t     d0  = dst_line[c*4+0];
                                int32_t     d1  = dst_line[c*4+1];
                                int32_t     d2  = dst_line[c*4+2];
                                int32_t     d3  = dst_line[c*4+3];

                                a   += a >> 7;
                                a   *= alpha;

                                dst_line[c*4+0] = (uint8_t)( ((d0 << 16) + (clr[0] - d0) * a) >> 16 );
                                dst_line[c*4+1] = (uint8_t)( ((d1 << 16) + (clr[1] - d1) * a) >> 16 );
                                dst_line[c*4+2] = (uint8_t)( ((d2 << 16) + (clr[2] - d2) * a) >> 16 );
                                dst_line[c*4+3] = (uint8_t)( ((d3 << 16) + (clr[3] - d3) * a) >> 16 );
                            }
                        }
                    }

                    pen.x   += slot->advance.x;
                    pen.y   += slot->advance.y;
                }
                break;
            }
        }

        return  0;
    }

呼び出しサンプル

最初写真は、この実行結果になります。
UTF-8をUNICODE32に変換して、Freetypeでbitmap化して、αブレンドしながら描画し、OLEDに表示しています。

各種ディスプレイの接続などは、NanoPi-NEOと5種のLCDと2種のOLED にて。

コンパイルコマンド例:

g++ -O3 -std=c++11 Sample.cpp -o Sample.o `freetype-config --cflags` `freetype-config --libs`

実装サンプル

Sample
#include "common/img_font.h"
#include "common/display_ssd1306_i2c.h"
#include "common/display_st7735_spi.h"
#include "common/display_ili9225_spi.h"
#include "common/display_ili9328_spi.h"
#include "common/display_ili9341_spi.h"

#define FONT_PATH   "/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf"

int main()
{
    const int   buf_width   = 320;
    const int   buf_height  = 240;
    const int   buf_stride  = buf_width * 4;
    uint8_t     buf[buf_width*buf_height*4];

    std::vector<DisplayIF*> display;

//  display.push_back( new Display_ILI9225_spi(270) );
//  display.push_back( new Display_ILI9341_spi_TM24(270) );
//  display.push_back( new Display_ILI9341_spi_TM22(270) );
//  display.push_back( new Display_ILI9341_spi(270) );
//  display.push_back( new Display_ILI9328_spi_TM22(270) );
//  display.push_back( new Display_ST7735_spi(270) );
    display.push_back( new Display_SSD1306_i2c(180) );

    for( auto it : display )
    {
        it->Init();
        it->DispOn();
        it->DispClear();
    }

    ImageFont   iFont1( FONT_PATH, 32 );    
    ImageFont   iFont2( FONT_PATH, 64 );
    const char* pszText1    = u8"こんにちわ";
    const char* pszText2    = u8"世界";
    int         cx, cy;

    memset( buf, 0, sizeof(buf) );
    iFont1.DrawTextBGRA(  0, 0, pszText1, 0xFFFFFFFF, buf, buf_stride, buf_width, buf_height );
    iFont2.DrawTextBGRA( 16, 8, pszText2, 0xA000FF00, buf, buf_stride, buf_width, buf_height );

    for( auto it : display )
    {
        it->WriteImageBGRA( 0, 0, buf, buf_stride, buf_width, buf_height );
    }

    FILE*   fp  = fopen("image.bin", "wb");
    if( fp != NULL )
    {
        fwrite( buf, buf_stride, buf_height, fp );
        fclose( fp );
    }

    getchar();
    return  0;
}

ファイル保存した結果は、以下のようになる。

image.PNG

おわかりいただけただろうか?
世界がαブレンド描画されている。