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

# はじめに

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


![_72D0559s.jpg](https://qiita-image-store.s3.amazonaws.com/0/187531/a5aa2323-6637-20ae-55d4-23aa8cf196c4.jpeg)


# 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など基準線より下の部分)されるので、はみ出ないように調整する機能も付けている。

```C++:メンバ変数
	FT_Library		m_piLibrary;
	FT_Face			m_piFace;
	int				m_nBaseline;
```

```C++: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互換である。

```C++: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](http://qiita.com/blue-7/items/9cc34c9ecc6b2eb5447c) にて。

コンパイルコマンド例：

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

実装サンプル

```C: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;
}
```

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

<img width="320" alt="image.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/187531/66c0a298-ac0a-c9db-45c7-051c72795e17.png">

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