こんにちは、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
ずつずらしていっただけですので、
この値を確認すれば良さそうです。
その結果、
となりました。64倍されててわかりにくいので、64 で割ります。
お? 半角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");
これだけです。下駄のビット数と、描画位置の進め方だけですね。
これを実行した結果が以下になります。

横幅が揃いました!!
この時、描画位置がどれだけずつ進んでいたかというと、
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/