こんにちは、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 パッケージが知っていますので、それをそのまま指定している、という感じです。
そして実行して画像ビューワで見ると、以下のようになりました。
おぉ、揃ってない...
原因を探る
描画位置は 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");
これだけです。下駄のビット数と、描画位置の進め方だけですね。
これを実行した結果が以下になります。
横幅が揃いました!!
この時、描画位置がどれだけずつ進んでいたかというと、
- 全角: 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 を改造して文字幅を固定小数点にするのは (文字位置を固定小数点にするのも含め) 困難でしょう。 どちらかと言えば、フォントファイルをいじる方が可能性がありそうに思います。
…などと引き続き悩みつつ、今回はここまでにします。
インゲージでは引き続きエンジニアを募集しています。 詳細は以下からお願いします!