FreeType を使って文字を描画する

こんにちは、masm11 です。

テキストエディタで1~2行めに以下のように書きます。

あいうえおかきくけこさしすせとたちつてと
abcdefghijklmnopqrstuvwxyzabcdefghijklmn

お使いのエディタにコピペしてみてください。 文字幅がきっちり 全角:半角=2:1 になっていれば、右端が揃うはずです。

最近、私の環境では揃わないんです… 何故なんでしょう… 今回はそんな疑問を調査してみました。

FreeType を使って描画する

Linux のアプリでは文字の描画に FreeType を使っていることが多いです。 さっそく FreeType を使ってみます。

こういったものはとっつきにくいので、チュートリアルを探しました。 以下にありました。

https://freetype.org/freetype2/docs/tutorial/step1.html

これを参考に作ったものが以下になります。 まずはコード全体をお見せした後、説明していきます。

#include <ft2build.h>
#include FT_FREETYPE_H

static unsigned int text[] = {
    0x00003042, 0x00003044, 0x00003046, 0x00003048,
    0x0000304a, 0x0000304b, 0x0000304d, 0x0000304f,
    0x00003051, 0x00003053, 0x00003055, 0x00003057,
    0x00003059, 0x0000305b, 0x0000305d, 0x0000305f,
    0x00003061, 0x00003064, 0x00003066, 0x00003068,
    0x0000000a,
    0x00000061, 0x00000062, 0x00000063, 0x00000064,
    0x00000065, 0x00000066, 0x00000067, 0x00000068,
    0x00000069, 0x0000006a, 0x0000006b, 0x0000006c,
    0x0000006d, 0x0000006e, 0x0000006f, 0x00000070,
    0x00000071, 0x00000072, 0x00000073, 0x00000074,
    0x00000075, 0x00000076, 0x00000077, 0x00000078,
    0x00000079, 0x0000007a,
    0x00000061, 0x00000062, 0x00000063, 0x00000064,
    0x00000065, 0x00000066, 0x00000067, 0x00000068,
    0x00000069, 0x0000006a, 0x0000006b, 0x0000006c,
    0x0000006d, 0x0000006e,
    0x0000000a,
};
#define LEN (sizeof text / sizeof text[0])

#define WIDTH  1024
#define HEIGHT 256
#define LEFT 16
#define TOP 128
static unsigned char paper[HEIGHT][WIDTH];


static void draw(FT_Bitmap *bitmap, int x, int y)
{
    for (int j = 0; j < bitmap->rows; j++) {
        for (int i = 0; i < bitmap->width; i++) {
            unsigned char c = bitmap->buffer[j * bitmap->pitch + i];
            if (c) {
                if (y + j >= 0 && y + j < HEIGHT) {
                    if (x + i >= 0 && x + i < WIDTH)
                        paper[y + j][x + i] = 255;
                }
            }
        }
    }
}

int main(void)
{
    FT_Library library;
    int error;

    error = FT_Init_FreeType(&library);
    if (error) {
        fprintf(stderr, "ft init error\n");
        exit(1);
    }

    FT_Face face;
    error = FT_New_Face(
            library,
            "/usr/share/fonts/TTF/NasuM-Regular-20141215.ttf",
            0,
            &face);
    if (error) {
        fprintf(stderr, "new face error\n");
        exit(1);
    }

    error = FT_Set_Char_Size(
            face,
            0,
            8 * 64,
            300,
            300);
    if (error) {
        fprintf(stderr, "set size error\n");
        exit(1);
    }
    
#define GETA(x) ((x) << 6)
#define UGETA(x) ((x) >> 6)
    FT_Vector pen;
    pen.x = GETA(LEFT);
    pen.y = GETA(TOP);
    unsigned int idces[LEN];
    for (int i = 0; i < LEN; i++) {
        if (text[i] == '\n') {
            fprintf(stderr, "%ld\n", pen.x);
            pen.x = GETA(LEFT);
            pen.y += GETA(32);
            continue;
        }
        
        idces[i] = FT_Get_Char_Index(face, text[i]);
        
        error = FT_Load_Glyph(face, idces[i], FT_LOAD_DEFAULT);
        if (error) {
            fprintf(stderr, "no glyph error\n");
            exit(1);
        }
        
        error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
        if (error) {
            fprintf(stderr, "glyph render error\n");
            exit(1);
        }
        
        draw(&face->glyph->bitmap,
                UGETA(pen.x) + face->glyph->bitmap_left,
                UGETA(pen.y) - face->glyph->bitmap_top);
        pen.x += face->glyph->advance.x;
    }
    fprintf(stderr, "\n");
    
    printf("P3\n%d %d\n%d\n", WIDTH, HEIGHT, 255);
    for (int y = 0; y < HEIGHT; y++) {
        for (int x = 0; x < WIDTH; x++) {
            if (paper[y][x]) {
                printf("%d %d %d ", 255, 255, 255);
            } else {
                printf("%d %d %d ", 0, 0, 0);
            }
            printf("\n");
        }
    }
}

さて、説明します。

#include <ft2build.h>
#include FT_FREETYPE_H

freetype のヘッダを include しています。 あまり見かけない使い方ですが、FT_FREETYPE_H はマクロです。

static unsigned int text[] = {
  // ...
};
#define LEN (sizeof text / sizeof text[0])

テキストを定義しています。前半がひらがな、後半がアルファベットです。 使いたい時に使いやすいように、unsigned int にしてあります。 また、LEN は文字数です。

#define WIDTH  1024
#define HEIGHT 256
#define LEFT 16
#define TOP 128
static unsigned char paper[HEIGHT][WIDTH];

WIDTH と HEIGHT は出力先エリアの幅と高さです。 paper が出力先エリアです。

LEFT と TOP は、文字を書き始める位置です。

static void draw(FT_Bitmap *bitmap, int x, int y)
{
  // ...
}

指定文字を paper の指定位置に描画する関数です。 文字は引数 bitmap にイメージとして格納されている前提です。 位置は引数 x, y に渡されてきます。

bitmap->buffer から1ドット読み取り、0 なら無視、0 でなければ paper 上でそのドットを塗りつぶしています。 paper はもともと 0 (黒) で塗りつぶされていて、そこに 255 (白) で描画します。 今回は灰色にはしません。

さて、ここから main 関数です。

    FT_Library library;
    int error;

これらの変数が必要なので定義しておきます。

    error = FT_Init_FreeType(&library);

freetype の初期化です。

    FT_Face face;
    error = FT_New_Face(
            library,
            "/usr/share/fonts/TTF/NasuM-Regular-20141215.ttf",
            0,
            &face);

フォントファイルを読み込んでいます。

    error = FT_Set_Char_Size(
            face,
            0,
            8 * 64,
            300,
            300);

文字のサイズをセットしています。 縦のサイズは 8ポイントです。

freetype では 26.6 の固定小数点が使われます。 26.6 とは、整数部を 26ビット、小数部を 6ビットで表現する、 という意味です。 イメージとしては、小数を 64倍して整数として扱う感じです。

1 は 64 という整数で扱い、1.5 は 96 という整数で扱います。 1 + 1.5 を計算したければ、64 + 96 で 160 です。加減算は そのまま計算できるので便利です。

#define GETA(x) ((x) << 6)
#define UGETA(x) ((x) >> 6)

<< 6 は64倍ですね。引数 x には整数を与えます。 GETA は「下駄を履かせる」的な意味で私が勝手に名付けました。

    FT_Vector pen;
    pen.x = GETA(LEFT);
    pen.y = GETA(TOP);

文字描画位置です。ここから始めます。

    unsigned int idces[LEN];

各文字は text[] に文字コードで与えられていますが、 フォントデータ中にアクセスするためには、 フォントデータ中のインデックスが必要です。 この配列にはそのインデックスを格納します。

ただ、配列でなくて良かった気はします。

    for (int i = 0; i < LEN; i++) {

ここから、各文字について処理します。

        if (text[i] == '\n') {
            fprintf(stderr, "%ld\n", pen.x);
            pen.x = GETA(LEFT);
            pen.y += GETA(32);
            continue;
        }

改行の場合の処理です。描画位置を次の行の左端に移動させています。 ここで、32 という改行幅は適当です。 今回は横幅にしか興味がないので、これで問題ありません。

        idces[i] = FT_Get_Char_Index(face, text[i]);

文字コードをフォントデータ中のインデックスに変換しています。

        error = FT_Load_Glyph(face, idces[i], FT_LOAD_DEFAULT);

その文字をロードします。

        error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);

それをイメージにします。

        draw(&face->glyph->bitmap,
                UGETA(pen.x) + face->glyph->bitmap_left,
                UGETA(pen.y) - face->glyph->bitmap_top);

先ほどの draw 関数で paper に描画します。 描画する際には、座標から下駄を外します。

        pen.x += face->glyph->advance.x;

描画位置を右へずらします。 どれだけずらすかは文字によります。

    printf("P3\n%d %d\n%d\n", WIDTH, HEIGHT, 255);
    for (int y = 0; y < HEIGHT; y++) {
      // ...
    }

画像として標準出力に出力します。

画像形式としては PNG, JPEG 等が有名ですが、扱いが面倒ですので、 簡単に出力できる PPM 形式を使います。Linux/UNIX では有名な形式で、 man ppm でマニュアルも読めます。

以上。たいしたことはしてなかったですよね。

  • 座標やサイズには 26.6 固定小数点を使う
  • 文字コードとインデックスは違う

チュートリアルを読みながら書けば、どうということはなかったです。

コンパイルする際には pkg-config を使います。

cc -g2 -O3 -Wall -o test `pkg-config --cflags --libs freetype2` main.c

こんな感じです。pkg-config --cflags --libs freetype2 って何? と思った方はこれだけを実行してみると良いです。

luna:ftsample % pkg-config --cflags --libs freetype2
-I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/harfbuzz -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-4 -pthread -lfreetype 

どこに include path を通して、 どこにある何ていうライブラリをリンクする必要があるかは、 freetype パッケージが知っていますので、それをそのまま指定している、という感じです。

そして実行して画像ビューワで見ると、以下のようになりました。

f:id:masm11:20220324003455p:plain

おぉ、揃ってない...

原因を探る

描画位置は face->glyph->advance.x ずつずらしていっただけですので、 この値を確認すれば良さそうです。

その結果、

  • 全角: 2112 ずつ
  • 半角: 1088 ずつ

となりました。64倍されててわかりにくいので、64 で割ります。

  • 全角: 33.0
  • 半角: 17.0

お? 半角2文字で 34ドットっていうことですから、全角1文字より 1ドット大きいですね。

なるほど、これが原因のようです。

linearHoriAdvance を使う

引き続き解決策を探します。 と言っても宛もないので、チュートリアルを引き続き読んでみます。 以下に2ページめがあります。

https://freetype.org/freetype2/docs/tutorial/step2.html

すると、linearHoriAdvance に出会いました。

horiAdvance (= advance.x) は整数 (= 26.6 なので 64 の倍数) に 丸められているけど、こちらは 16.16 固定小数点で、 丸める前の値が入っているそうです。

使ってみます。

プログラムの違いは以下の通りです。

--- 26-6.c      2022-02-23 22:44:07.635491014 +0900
+++ 16-16.c     2022-02-23 22:47:19.587834672 +0900
@@ -78,8 +78,8 @@
        exit(1);
     }
     
-#define GETA(x) ((x) << 6)
-#define UGETA(x) ((x) >> 6)
+#define GETA(x) ((x) << 16)
+#define UGETA(x) ((x) >> 16)
     FT_Vector pen;
     pen.x = GETA(LEFT);
     pen.y = GETA(TOP);
@@ -109,8 +109,8 @@
        draw(&face->glyph->bitmap,
                UGETA(pen.x) + face->glyph->bitmap_left,
                UGETA(pen.y) - face->glyph->bitmap_top);
-       fprintf(stderr, "%ld\n", face->glyph->advance.x);
-       pen.x += face->glyph->advance.x;
+       fprintf(stderr, "%ld\n", face->glyph->linearHoriAdvance);
+       pen.x += face->glyph->linearHoriAdvance;
     }
     fprintf(stderr, "\n");
     

これだけです。下駄のビット数と、描画位置の進め方だけですね。

これを実行した結果が以下になります。

f:id:masm11:20220324003512p:plain

横幅が揃いました!!

この時、描画位置がどれだけずつ進んでいたかというと、

  • 全角: 2184192
  • 半角: 1092096

16.16 なので 65536 で割ると、

  • 全角: 33.328125
  • 半角: 16.6640625

ぴったり、全角:半角=2:1 ですね!

そして、最初のプログラムで得た 33.0 と 17.0 が、 これの小数点以下を四捨五入したものっぽいこともわかりました。

まとめ

結局、 16.16 形式の linearHoriAdvance だと正確なのですが、 26.6 形式の advance.x には誤差が含まれていて、 そのため、全角:半角が綺麗に 2:1 になっていなかった、 ということでした。

でも、それが判ったからといって、だから何なのでしょう? 私のテストプログラムでは綺麗に揃えることができましたが、 結局エディタでは揃わないままです。

私は Emacs を使っています。Emacs では文字幅は整数で あることが前提のようです。 フォントの扱いを linearHoriAdvance に対応させたとしても、 結局、最終的に文字幅は整数ですので、誤差が発生してしまいます。 そして、Emacs を改造して文字幅を固定小数点にするのは (文字位置を固定小数点にするのも含め) 困難でしょう。 どちらかと言えば、フォントファイルをいじる方が可能性がありそうに思います。

…などと引き続き悩みつつ、今回はここまでにします。

インゲージでは引き続きエンジニアを募集しています。 詳細は以下からお願いします!

https://ingage.co.jp/recruit/