FreeTypeを使う
概要
この記事はFreeTypeを使うであって、FreeTypeをまとめる記事ではないです。なので遊び場程度の記事になります。遊べるかは謎です。Font情報をどれくらい操作できるのか試してみるところです。目標はPDFを作るためのグリフ番号取得と、フォントプログラムの再構成。サブセットフォントを作るということです。
そこまですることなんか?とは思っています。フォントの構造を知ることに重きを置きながらがんばります。
まずはコンソールプログラムでチュートリアルっぽいことを。
ってチュートリアルのとおり、作り始めたらQt64bitコンソールアプリケーションを作ってるせいで、64bitのライブラリが必要になってしまった。開発時にダイナミックリンクを使うと勉強の質が低下してしまうので、64bit32bit混在で動かすわけにもいかず、似非64bitのfreetypeライブラリを作成するために、libpngで似非64bitプラットフォームを作成した。似非というのは、何もプログラムを変更しないで、ただ64bit宣言するだけのことです。bit演算が凄まじいアプリケーション群なので、これが原因でアドレスの使い方が変わってバグる可能性はあるが、莫大なコードを目の前に、全てを潰していくのはサハラ砂漠にダイアモンドを埋めたのを探すようなモノ。無謀な道とは知りつつ、しばらく、これで突き進む。
ちな
似非を作るにはコチラの手順として記載。
Qtコンソールプログラムの書き始めは以下のような状態。Qtについては、Qt導入の記事をみて下さい。なんでQtなん?って思う人いるとおもいますけど、さほどQtの要素は使わないので、安心して下さい。Qtやってるっていう見せかけですよ。
#include <QtCore/QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
return a.exec();
}
自分は64bit版ですが、64bit版を生成するのが面倒な人はQtの32bit版のLibraryを導入して新規作成するとよいね。Qtの64bit版を使ってるつもりで説明します。ところどころ32bit版の人向けのコメントも添えます。
まずはプロジェクトの設定を変えて、freetypeが使える状態にします。
インクルードファイルのありかを設定します。ライブラリを使うときはまずはヘッダファイルで関数の全ての情報が必要です。標準関数でさえ#include <stdio.h>って設定するもんね。同じこと。
ソリューション構成をDebug、ソリューションプラットフォームをx64(32bitに人はWin32)に設定します。ツールバーのリストを選ぶところですね。次に、メニューの[プロジェクト]-[プロパティ]を選択します。
[VC++ディレクトリ]の項目の中の[外部インクルードディレクトリ]に以下を追加します。
C:\…\…\…\…\freetype-2.9.1\include
[VC++ディレクトリ]の項目の中の[ライブラリディレクトリ]に以下を追加します。
C:\…\…\…\…\freetype-2.9.1\objs\x64\Debug
絶対パスなので、…の部分はそれぞれの、freetypeの配置した場所に、freetype-2.9.1となっているところもそれぞれのバージョン番号だったり、名前を変えてfreetypeにしたりしてる場合も合わせて下さい。64bitの場合はx64ですが、32bitのQtアプリの場合はx64ではなくWin32です。
構成のRelease版も合わせて変更しておくとよいでしょう。インクルードディレクトリは同じで、ライブラリディレクトリはDebugがReleaseに代わります。
実際にデバッグでもなんでもテストでアプリを動かす時は、ダイナミックリンクライブラリを使うので、dllをがうごかせるように実行ファイルと同じディレクトリ置くとか、環境変数のPathの参照可能な範囲に配置する指定をしないと駄目です。プロジェクトごとにどの環境で動かすのかを見極めて環境変数の設定しないとだめです。プログラマなら実行ファイルが完成するまではDebug版のダイナミックリンクライブラリを使うはずなので、環境変数Pathにfreetype.libのx64 (Win32をQtアプリの人はWin32) のDebug版にPathを設定しましょう。
コンパネのシステムの右側の詳細設定から詳細。環境変数の中のPathに
C:¥…\…\…\…\…\freetype-2.9.1\objs\x64\Debug
を追加しましょう。コマンドプロンプトでwhere %path% freetype.dll とすると、フルパスが帰ってきたら、パスが通っていて、freetype.dllがどの実行ファイルからも参照できるようになっていることを意味します。フルパスが表示されず。また次のプロンプトだけが表示された場合は失敗しています。もう一度確認しましょう。デバッグ押しても、もちろんアプリを起動できません。
そうすると、うまく設定できたかを確かめるために、少しだけプログラムを記述してビルドして確かめてみます。
#include <QtCore/QCoreApplication>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
FT_Library library;
int error;
error = FT_Init_FreeType(&library);
if (error) {
fprintf(stderr, qPrintable(""));
exit(1);
}
return a.exec();
}
6行目から14行目を追加しました。これで一度ビルド Ctrl+Shift+B します。ちゃんと動くか確かめるためです。もちろん管理人はスーパープログラマなので、うまく動かせました。え。なにがスーパーやねん。わかります。へっぽこでした。
うまくいくと例の安心の出力になります。この正常終了って文字。プログラマにとってはアドレナリン・ドーパミン?だっけがでますね。
ビルドを開始しました...
1>------ ビルド開始: プロジェクト: QtConsole###########, 構成: Debug x64 ------
1>QtConsole###########.vcxproj -> C:\…\…\…\…\QtConsole###########\x64\Debug\QtConsole###########.exe
========== ビルド: 1 正常終了、0 失敗、0 更新不要、0 スキップ ==========
サンプル1.Qt ConsoleApplicationでテキスト出力
ここまできたらFreetypeを使える状態になったと言えます。いよいよ使っていきましょう。ところでfreetypeって、どういうことができるのか、
ひとことで言えば、フォントから文字を描画するのに必要な情報を取り出す作業ができる。どんな情報?っていうのが詳しい説明になるわけですが、
- フォントデータからプログラマが指定したフォントサイズ(ポイント)をもとに、指定したグリフ番号のビットマップモノトーン値をピクセル毎に取得できます。
- なかなかえぐい計算量ですが、まぁ最近のPCの早いこと。しゅっとフォントがビットマップになります。
- 前項のような作業のために文字コード番号から標準のグリフを取得したり、文字コード+付帯情報でその関連グリフの番号を取得できる。
- つまり、PDFでやりたかった文字コードからグリフ番号を取得する作業ができるということです。このライブラリを使えば目的は一つ達成できる。
これが最初のチュートリアルで紹介するサンプルの機能です。公式サイトにはQtでのサンプルもありますので、管理人と同じくQtで頑張っている人と共に勉強していけるはずです。
#include <QtCore/QCoreApplication>
#include <QString>
#include <QFile>
#include <QTextStream>
#include <QImage>
#include <QColor>
#include <QSize>
#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, //a、b、c、d
0x00000065, 0x00000066, 0x00000067, 0x00000068, //e、f、g、h
0x00000069, 0x0000006a, 0x0000006b, 0x0000006c, //i、j、k、l
0x0000006d, 0x0000006e, 0x0000006f, 0x00000070, //m、n、o、p
0x00000071, 0x00000072, 0x00000073, 0x00000074, //q、r、s、t
0x00000075, 0x00000076, 0x00000077, 0x00000078, //u、v、w、x
0x00000079, 0x0000007a, //y, z
0x00000061, 0x00000062, 0x00000063, 0x00000064, //a、b、c、d
0x00000065, 0x00000066, 0x00000067, 0x00000068, //e、f、g、h
0x00000069, 0x0000006a, 0x0000006b, 0x0000006c, //i、j、k、l
0x0000006d, 0x0000006e, 0x0000006f, 0x00000070, //m、n、
0x0000000a, //改行
};
#define LEN (sizeof text / sizeof text[0])
#define WIDTH 1024
#define HEIGHT 256
#define LEFT 16
#define TOP 32
static unsigned char canvas[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) {
canvas[y + j][x + i] = c;
}
}
}
}
}
}
int main(int argc, char* argv[])
{
QCoreApplication a(argc, argv);
QString QString_FileName = "C:/FreeTypeStep01Test.txt";
QString QString_ImageFileName = "C:/FreeTypeStep01Test.bmp";
QFile QFile_Text(QString_FileName);
QString QString_ImageRowMonoBuffer = "";
QSize QSize_xy(WIDTH, HEIGHT);
QImage QImage_Canvas(QSize_xy, QImage::Format_ARGB32_Premultiplied);//空イメージ
FT_Library library;
int error;
error = FT_Init_FreeType(&library);
if (error) {
fprintf(stderr, qPrintable("ft init error\n"));
exit(1);
}
FT_Face face;
error = FT_New_Face(library, qPrintable("C:\\Windows\\Fonts\\KozGoPr6N-Medium.otf"), 0, &face);
if (error) {
fprintf(stderr, qPrintable("new face error\n"));
exit(1);
}
error = FT_Set_Char_Size(face, 0, 8 * 64, 300, 300);
if (error) {
fprintf(stderr, qPrintable("set size error\n"));
exit(1);
}
#define LEFT64(x) ((x) << 6) // 1/64が1の 32 bit 26.6 ニィロク ロク固定小数。
#define RIGHT64(x) ((x) >> 6)
FT_Vector pen;
pen.x = LEFT64(LEFT);
pen.y = LEFT64(TOP);
unsigned int idces[LEN];
for (int i = 0; i < LEN; i++) {
if (text[i] == '\n') {
fprintf(stderr, qPrintable("%ld\n"), pen.x);
pen.x = LEFT64(LEFT);
pen.y += LEFT64(48);
continue;
}
idces[i] = FT_Get_Char_Index(face, text[i]);
error = FT_Load_Glyph(face, idces[i], FT_LOAD_DEFAULT);
if (error) {
fprintf(stderr, qPrintable("no glyph error/\n"));
exit(1);
}
error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
if (error) {
fprintf(stderr, qPrintable("glyph render error\n"));
exit(1);
}
draw(&face->glyph->bitmap,
RIGHT64(pen.x) + face->glyph->bitmap_left,
RIGHT64(pen.y) + face->glyph->bitmap_top);
pen.x += face->glyph->advance.x;
}
fprintf(stderr, qPrintable("\n"));
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
QImage_Canvas.setPixel(x, y, qRgb(canvas[y][x], canvas[y][x], canvas[y][x]));
}
}
QImage_Canvas.save(QString_ImageFileName, "BMP");
printf(qPrintable("P3\n%d %d\n%d\n"), WIDTH, HEIGHT, 255);
if (QFile_Text.open(QIODevice::WriteOnly)) {
QTextStream out(&QFile_Text);
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
if (canvas[y][x]) {
QString_ImageRowMonoBuffer = QString_ImageRowMonoBuffer + " 1";
//printf(qPrintable("%2d"), 1);
}
else {
QString_ImageRowMonoBuffer = QString_ImageRowMonoBuffer + " 0";
//printf(qPrintable("%2d"), 0);
}
}
QString_ImageRowMonoBuffer = QString_ImageRowMonoBuffer + "\n";
//printf(qPrintable("\n"));
out << QString_ImageRowMonoBuffer;
QString_ImageRowMonoBuffer = "";
}
QFile_Text.close();
}
else {
QString QString_Error(qPrintable("No Open or Create Error\n"));
QString_Error = QString_Error + QFile_Text.errorString();
fprintf(stderr, qPrintable(QString_Error));
}
return a.exec();
}
テキストファイルと画像を出力するQtアプリケーションfreetype連動プログラムになっています。
生成される画像
文字列は白黒を1ビットづつ見やすいように出力したものです。画像の方はNormalRenderingのアンチエイリアスがかかっていますので少し綺麗。文字が上にいったり下にいったりしてるのは、Adobe AcrobatReaderの小塚ゴシックを参照したため。この仕組みで、違反者をさがしてるのかな。それともだいたいのフォントがこんな風にずれるようになってるのか。気になる。そのまえに、FreeTypeの基本について説明していかないとな。
ついでにQtの勉強にもなるという。いいね。このSiteは、いいねボタンないけどね。
FreeTypeの勉強っていうけど、一般の人からしたら、だから何がうれしいのっていうようなプログラムですね。日々、目にしているグラフィックツールのテキストレンダリングは、これぐらい膨大なプログラミングによって実現されているっていうね。テレビや動画や音楽もしかり、実はものすごい技術です。LEDで文字が表示されてる看板とかね。この形の文字になるように、だれかが作ってんだよって言ってやりたいね。手で書いてるのとは訳が違う。そういえば、コンピュータのごとく手で書いちゃう人もいましたね。あれはあれであれのほうが凄い。コンピュータは誰かが苦労して書いたやつをみんなで再利用してるという効率の良さ。原理を知らない人ばかりになるとどうなるんだこの世界。
サンプル1.Qt ConsoleApplicationでテキスト画像表示の解説
プログラム最初らへんのQで始まるヘッダファイル、Qt 関連のインクルード挿入。割と最小構成でインクルードする手間のかかる指定をする癖があります。それほど無駄がないと確信したら、巨大なインクルードで済ませることもありますが、こういう巨大なライブラリだと延々と理解できないので、無駄が多くなりがち。以下のように対応しています。
- <QtCore/QCoreApplication>
- QCoreApplication クラスと、そのメンバ関数 exec()、 qPrintable()関数
- <QString>
- QString クラス
- <QFile>
- QFile クラス
- <QTextStream>
- QTextStream クラス
- <QImage>
- QImage クラス
- <QColor>
- qRGB() QColorの静的メンバ関数
- <QSize>
- QImage 関数の初期化に使う。QSize クラス
freetype ライブラリを使うためのヘッダファイルを宣言し、そのヘッダファイルで宣言されているヘッダ名マクロをインクルードします。
#include <ft2build.h>
#include FT_FREETYPE_H
12行目~32行目:処理したい文字配列です。8桁の16進数で、文字コードの「あ~と」までの20文字。番号がとびとびになっているのはUnicodeでは「あ」の後は「ぁ」とか濁点が付いたモノのように順番になっているからです。フォントファイルの中では実際はグリフIDというのが必要ですが、CMapのような構造をフォントファイル内に保持していて、どのような順番になっているかは、フォントファイルの構造を掌握していて、対応表を検出できるようなプログラム。例えば、今回のfreetypeのようなものが必要です。一番最後にLENというマクロが配列の大きさを保持するように設定しています。
34行目35行目のWIDTHとHEIGHTは、キャンバスの幅と高さについてのピクセル数を保持するマクロです。符号なし文字型(符号なし1バイト数値) canvasという型でその配列の大きさを利用します。値はcanvasの型どおりですが、0~255の値を保持します。白黒なので256段階の数値が保持できれば十分です。
LEFT RIGHTマクロは最初の「あ」を描画する位置を保持します。単位はポイントで8ポイントで1文字ぶんくらいの余白になります。
41~54行目はdrawという関数で、RGB=#000000の真っ黒の描画エリア=canvasに対して、受け取る引数。1文字分のグリフのbitmap情報とxとyによる文字描画位置から1文字分の全ピクセルの範囲について白黒濃度値を設定していきます。全部の文字分処理、LENの数値分、繰り返したら、全部を反映したことになります。text 配列の配列数分がLENです。その繰り返しの回数だけ関数が呼び出されます。
60行目からはメイン処理の追加文です。
QtのQStringで画像ファイルとテキストファイルのパスを指定します。長いパスは疲れるのでC:¥とCドライブの直下に置くことにしました。
テキストクラスのQFileに初期値として、パス名を与えることで、初期化できるので利用した感じです。あとから違うファイルを開くためにファイルをcloseした状態でならファイルパスを変更することもできると思いますが、今回はやっていません。
QString_ImageRowMonoBufferというQStringクラスの変数を実体化していますが、これは後々、キャンバスサイズの1行分の白黒情報を2桁の整数で吐き出します。実際は0と1の2値なので、1文字空白が発生するようになります。テキストでみたときに2文字が全角1文字なので、1文字が正方形で表せるため縦横比がテキストファイル上でいい感じにとらえることができます。
67行目でQImageクラスの変数を実体化しています。空の画像格納用変数になります。最初の引数に画像の横幅と縦幅を格納したQSizeクラスの値が必要なので、その上の行で、QSize クラスの変数を実体化しています。第二引数はどういう形式の画像にするかというフラグ設定列挙子です。今回はフルカラーRGBと透明度が設定可能な形式。
69行目からはfreetypeのおきまりの初期化が始まります。91行目までは、このようなサンプルのおきまりの初期化の流れになります。freetypeの初期化をlibraryというクラス変数で実施して、次にその初期化されたクラス変数設定状態を有した状態でフォント情報を保持したfaceというクラスを生成。
81行目
FT_New_Face(library, qPrintable("C:\\Windows\\Fonts\\KozGoPr6N-Medium.otf"), 0, &face);
のようにして、face変数に第一引数のフォント名を関連付けたオブジェクトとして操作できるようになる。
そして87行目で1文字を読み取るためのグリフサイズをポイント値で指定。第一引数はこれまでの初期化で生成されたオブジェクトfaceで、次の2つ引数は横幅、縦幅の大きさをポイント*64倍の値を設定する。ここで8は8ポイントを意味していて、ここに指定する1単位は1/64倍されたポイント数になるので、8ポイントにするための8/64を64倍して、8ポイントとして扱うことができる。このとき、このポイント数は26.6(ニィロク ロク)固定小数と呼ぶ形式になっていると言える。どちらかに値が設定されていればいい。片方は0なら、フォント自身が持つ縦横比が使われる。両方をしていした場合は縦横比が変形する。その後ろの2つつづく300は横・縦の内部解像度の扱いを指定している。こちらも片方が0なら、どちらかの値と同じになる。表示する部分がせいぜい8ポイント程度の画面上の表示でも1インチあたりは300個の情報を内部的に保持する形式になり、印刷してもそれなりに綺麗にみえる。300dpiというが、これは印刷業界の一般的な知識です。300dpiなら、人間にはそれなりに見えるということだ。画面上で拡大していくと汚くても。印刷したら大丈夫。これが大事。
ここまでの処理には、不具合があるとError番号に0以外が渡されるので、それを元にエラーがあって強制終了したことを標準エラー出力画面に表示させるStdErrでプログラマに知らせる形式となっている。
93・94行目は26.6形式に対応するべく、64倍するという操作をマクロ化しています。何回か使うのでスッキリ表示したいというものです。 << とか >> は演算子のオーバーロードが働いているC++のストリーム標準出力とはちがい、オーバーロード宣言される前の標準演算子で言えば、論理シフトという操作になります。 << は左論理シフトです。 >>は右論理シフト。 論理シフトを行うと 2進数の 0b0010が左だと0b0100のように左にひとつ移動し、右からは0が挿入されます。このとき10進数で言えば、2から4に増えています。実は1つ移動するごとに2倍されいます。つまり6回移動すると64倍です。2, 4 , 8, 16, 32, 64 ね。逆に右へ移動させると、1/2倍されていきます。
しらないと、何やってんのコレって思っちゃいますが、わかるとなんてことはない感じです。
97・98行目では算出した初期値へ移動するため、FT_Vector型の座標を記憶できる構造のクラスのメンバ変数 pen.x, pen.y にそれぞれ、LEFT 16とTOP 32のポイント値を64倍した値を記憶させています。
そして次の行で、テキストの文字数にあたる大きさの配列を準備しています。あとで、それぞれのテキスト番号に対応するグリフ番号を取得する予定です。
そして100行目から126行目の間を、テキストの文字数分だけ繰り返します。このとき繰り返しごとに増えていく数値が操作する入力テキスト文字コード配列の番号と対応します。
まず101行目のif文ではテキスト文字が改行コードかどうかを判断します。該当したら、pen.y への移動量を設定して、キャンバス上の縦への移動を行います。pen.xは復帰の扱いの処理で、初期値のLEFT 16 のマクロ値を設定します。このときも、64倍した値です。yは48ピクセル下へ移動します。6文字分くらい下ですね。300dpiでは8ポイントが32ピクセル相当です。
改行コードの場合はこのif文の中の処理がおわったら、ほかの処理はもう必要ないので、全部を無視して、for文の最初にもどり、繰り返し処理を再開します。
107行目は、文字コードを含んだ text 配列の for文処理の対象文字について、フォントファイルのCMapと照合して、グリフ番号を返してくれる命令です。
idces[i] = FT_Get_Char_Index(face, text[i]);
引数にはフォント情報をもっている face を第一引数として設定、第二引数で文字コード番号を含んだ数値を設定。これで戻り値にはグリフ番号が取得でき、結果として、for文の処理が終わった所で文字コードをグリフ番号に変換することを実現している。この部分は、PDFで使った文字コードをグリフ番号で保持するPDFの仕様に対応できる部分だ。
次の行ではフォントの情報を保持できるクラスに対して、対象のグリフ番号の情報を保持するという処理を行う。まずは、情報を読み込む処理として
error = FT_Load_Glyph(face, idces[i], FT_LOAD_DEFAULT);
109行目のFT_Load_Glyphを実行する。第一引数は一文字分のグリフ情報を保持するためのFT_Face クラスの変数 face を使う。先のグリフ番号取得でも使った face の役割はいくつかありそう。そうして読み込んだ状態では、そのメンバ関数がグリフに関する情報をさらに保持していけるので、次の命令を実行できる。
それが115行目だ。
error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
FT_Render_Glyphの第一引数の face->glyph メンバポインタ変数に描画後の情報の標準的なbitmap値を生成する。FT_RENDER_MODE_NORMALという列挙子によってbitmapに生成する仕組みがかわるようだ。これで、使えそうなbitmapが生成されたので、次の命令によって、キャンバスに反映させる。ここではキャンバスに反映させるが、ここはプログラムの用途によっていろいろな方法で情報を受け取ることになると思う。
このチュートリアルでは逐一、テキスト配列を処理していく毎にキャンバスの色情報保持配列に反映させてしまう。それが121行目から125行目の処理となる。
ビットマップ値はFT_Face型の変数faceを使って &face->glyph->bitmapのアドレス番号を先頭に保持されているので、この情報をプログラマ側で作ったキャンバスへ反映するdrawという関数の第一引数に指定する。この関数は、描画位置情報のxとyの値も必要としているので、改行したり、文字を描画がしたときに次のpenの位置を移動したりする操作をしていく。
draw関数では、face->glyph->bitmap_leftに読み込んだビットマップの左側の座標を渡す。topは上側だ。かならずしも端の座標は 0 とは限らない。フォントをはみ出してマイナスの位置からグリフ情報をもっている可能性もあるためだ、その場合、少し戻った位置にたいしてキャンバスへの描画をおこなう必要がある。
文字の送り幅自体はface->glyph->advance.xでpen.xに反映させる。125行目
字詰めという処理を必要とするプロポーショナルフォントの場合は特に上記のような操作にしておかないと、はみ出た分の文字のデザインが描画されないばかりか、バランスがおかしくなる。
これがおわるとキャンバスの配列ができあがる。これをビットマップに吐き出したり、標準出力に吐き出したり、テキストとして保存してみたりというのが、この先の処理だ。結果を目にみえるものにするのは楽しい。やってることは、単純だが、ここまで苦労して得られたものは、プログラマにしかわからない優越感を味わえる。フォントをイチカラ活用してやったぜ。っていう感じです。
129~134行目であらかじめ準備していたQImageの空のイメージ空間を使って、キャンバスの全ピクセル値を使って白黒のグレースケールイメージファイルを構成し、保存する処理を実施している。
137行目から最後まではテキストファイルの生成処理を実施している。このあたりはfreetypeとはもう関係ないので説明しない。
サンプル2. 公式サイト版 のQtConsolApp
#include <QtCore/QCoreApplication>
#include <QObject>
#include <QString>
#include <QFile>
#include <QTextStream>
#include <QImage>
#include <QColor>
#include <QSize>
#pragma execution_character_set("utf-8")
#include <ft2build.h>
#include FT_FREETYPE_H
#define WIDTH 640
#define HEIGHT 480
#define LEFT 32
#define TOP 32
static unsigned char canvas[HEIGHT][WIDTH];
static void draw_bitmap(FT_Bitmap* bitmap, FT_Int x, FT_Int y) {
FT_Int i, j, p, q;
FT_Int x_max = x + bitmap->width;
FT_Int y_max = y + bitmap->rows;
for (i = x, p = 0; i < x_max; i++, p++) {
for (j = y, q = 0; j < y_max; j++, q++) {
if (i < 0 || j < 0 || i >= WIDTH || j >= HEIGHT) {
continue;
}
canvas[j][i] |= bitmap->buffer[q * bitmap->width + p];
}
}
}
int main(int argc, char* argv[])
{
QCoreApplication a(argc, argv);
QString QString_FileName = "C:/FreeTypeStep02Test.txt";
QString QString_ImageFileName = "C:/FreeTypeStep02Test.bmp";
QFile QFile_Text(QString_FileName);
QString QString_ImageRowMonoBuffer = "";
QSize QSize_xy(WIDTH, HEIGHT);
QImage QImage_Canvas(QSize_xy, QImage::Format_ARGB32_Premultiplied);//空イメージ
FT_Library library;
FT_Face face;
FT_GlyphSlot slot;
FT_Matrix matrix;
FT_Vector pen;
FT_Error error;
QString text = QString::fromUtf8(u8"あいさがしの果てに\n辿り付いた寝屋川市駅\nというtest文章");
// ヒラギノ角5 DShirkg5.ttc BIZ UD ゴシック BIZ-UDGothicR.ttc KozGoPr6N-Medium.otf
QString filename = "C:\\Windows\\Fonts\\BIZ-UDGothicR.ttc";
QString SingleStr = "";
double angle;
int target_height;
int n;
int num_chars;
num_chars = text.length();
angle = (0.0 / 360.0) * 3.141592 * 2.0;
target_height = HEIGHT;
error = FT_Init_FreeType(&library);
if (error) {
fprintf(stderr, qPrintable("ft init error\n"));
exit(1);
}
error = FT_New_Face(library, filename.toUtf8(), 0, &face);
if (error) {
fprintf(stderr, qPrintable("new face error\n"));
exit(1);
}
error = FT_Set_Char_Size(face, 30 * 64, 0, 100, 0);
if (error) {
fprintf(stderr, qPrintable("set size error\n"));
exit(1);
}
slot = face->glyph;
#define LEFT64(x) ((x) << 6) // 1/64が1の 32 bit 26.6 ニィロク ロク固定小数。
#define RIGHT64(x) ((x) >> 6)
pen.x = LEFT64(LEFT);
pen.y = LEFT64(target_height - 200);
matrix.xx = (FT_Fixed)(cos(angle) * 0x10000L);
matrix.xy = (FT_Fixed)(-sin(angle) * 0x10000L);
matrix.yx = (FT_Fixed)(sin(angle) * 0x10000L);
matrix.yy = (FT_Fixed)(cos(angle) * 0x10000L);
unsigned int* idces = new unsigned int[num_chars];
for (int i = 0; i < num_chars; i++) {
if (text.at(i) == '\n') {
fprintf(stderr, qPrintable("%ld\n"), pen.x);
pen.x = LEFT64(LEFT);
pen.y += LEFT64(48);
continue;
}
SingleStr = text.at(i);
FT_Set_Transform(face, &matrix, &pen);
QChar ch = SingleStr.at(0);
FT_ULong FT_ULong_code = ch.unicode();
idces[i] = FT_Get_Char_Index(face, FT_ULong_code);
error = FT_Load_Char(face, FT_ULong_code, FT_LOAD_RENDER);
if (error) {
continue;
}
draw_bitmap(&slot->bitmap, slot->bitmap_left, HEIGHT - slot->bitmap_top);
pen.x += slot->advance.x;
pen.y += slot->advance.y;
}
fprintf(stderr, qPrintable("\n"));
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
QImage_Canvas.setPixel(x, y, qRgb(canvas[y][x], canvas[y][x], canvas[y][x]));
}
}
QImage_Canvas.save(QString_ImageFileName, "BMP");
printf(qPrintable("P3\n%d %d\n%d\n"), WIDTH, HEIGHT, 255);
if (QFile_Text.open(QIODevice::WriteOnly)) {
QTextStream out(&QFile_Text);
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
if (canvas[y][x]) {
QString_ImageRowMonoBuffer = QString_ImageRowMonoBuffer + " 1";
//printf(qPrintable("%2d"), 1);
}
else {
QString_ImageRowMonoBuffer = QString_ImageRowMonoBuffer + " 0";
//printf(qPrintable("%2d"), 0);
}
}
QString_ImageRowMonoBuffer = QString_ImageRowMonoBuffer + "\n";
//printf(qPrintable("\n"));
out << QString_ImageRowMonoBuffer;
QString_ImageRowMonoBuffer = "";
}
QFile_Text.close();
}
else {
QString QString_Error(qPrintable("No Open or Create Error\n"));
QString_Error = QString_Error + QFile_Text.errorString();
fprintf(stderr, qPrintable(QString_Error));
}
delete[] idces;
FT_done_Face (face);
FT_Done_FreeType (library);
return a.exec();
}
出力結果ファイル
今回は公式サイトの最初のチュートリアルをQtConsoleApp版にしたものです。うごきも少し違う工夫がなされていて、下への移動のつもりは、下から上に書いていくチュートリアルになってしまっていました。キャンバスへの引数はY座標が逆転する要素があるようになっていますね。解説はしません。あと勉強になったのは2点
- プログラムの最後の方にある。オブジェクトの消滅作業ですね。
- Initによる初期化で動的に生成されたオブジェクトをこの二つの方法で綺麗に掃除しています。これが行儀のいい書き方なのですね。もっと大きいソリューションとして構築する場合はデストラクタとかで処理するようにしたいものです。
- グリフ番号を取得しなくてもFT_Load_Charで、ユニコード番号だけで、グリフのビットマップが得られる方法がある。
- 最初のチュートリアルでグリフ番号を取得してグリフのビットマップを取得していました。PDFを作るという目的においては重要ですが、グリフを描画するという処理だけならあまり意識はしたくない処理が省けるというのもいい感じです。おもしろい。
freetypeで独自のフォントの変形処理を作るのが醍醐味なのかもしれません。ここまでのチュートリアルがわかっただけでも、相当いろいろな変形処理が作れそう。任意の半径の丸い円弧状にそって文字を配置したりもできるようになりそうです。出来栄えを確かめたりする作業をもった素早くするには、それなりのユーザインタフェースを作らないと駄目ですね。ややこしそう。画像をあやつる一歩を踏み出した感じ。いいね。この例にも文字を変形させる処理があります。FT_Set_Transformという関数とそのちょっとまえのFT_Matrixというクラス変数のmatrixというオブジェクトの準備作業が必要なようです。angleの計算の割り算の優先度を上げているところでカッコの中の数字のどちらかに小数点がないと、なにをやっても計算結果が整数の 0 になって、angleは常に 0 になってしまいます。注意されたし。
グリフ毎にある要素の意味は以下のサイトで画像付きで表記されていて、わかりやすいですね。これがglyph->advancedの意味かぁとかね。
もうちょっとあともういくつか公式のサンプルがあるので理解をすすめてみたいと思います。
サンプル3.公式サイトのErik Mollerさん作成 Qt版
エリックさんが作ったBという文字をアウトライン装飾ありで、文字の色とアウトラインの色を指定してレンダリングするというものです。ここまで手をかけておきながら、Bだけの固定のサンプルになっているところは、なかなか尖ったサンプルです。縁取りはこうやるということが伝わればいいわけで、色々な形をみれるという汎用性はバサッと切り捨て。カッコいい。
出力がTGAファイルなのもイカす。TGAはTruVision社が作った。Graphic形式です。ちょっと改造しないとVisual Studioではヘッダー部分が壊れてしまう感じでした。具体的にいうと、ヘッダーで使われているunsigned int の uint16 だと4バイトになってしまいます。2バイトでなければならない情報がすべて4バイトになって位置ズレをおこしてしまいます。
出力された画像はirfanviewとかで見るのがいいんじゃないかな。それだと面倒だと感じる人もいるかもしれないので、bitmapも出力するようにTGAを出力する次の行に追加して変更しておきました。
#include <QtCore/QCoreApplication>
#include <QObject>
#include <QString>
#include <QFile>
#include <QTextStream>
#include <QImage>
#include <QColor>
#include <QSize>
#pragma execution_character_set("utf-8")
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_STROKER_H
#include <vector>
#include <fstream>
#include <iostream>
#ifdef _MSC_VER
#define MIN __min
#define MAX __max
#else
#define MIN std::min
#define MAX std::max
#endif
typedef unsigned char uint8;
typedef unsigned uint16;
typedef unsigned int uint32;
#if (('1234' >> 24) == '1')
//0x31 0x32 0x33 0x34 >> 24 = 0x00 0x00 0x00 0x31 '1'
// or
//0x34 0x33 0x32 0x31 >> 24 = 0x00 0x00 0x00 0x34 '4'
//BIG ENDIAN方式なら下の動きなのif文は否定。
#elif (('4321' >> 24) == '1')
#define BIG_ENDIAN
#else
#error "Couldn't determine the endianness!"
#endif
#define WIDTH 640
#define HEIGHT 480
#define LEFT 32
#define TOP 32
static unsigned char canvas[HEIGHT][WIDTH];
union Pixel32 {
Pixel32() : integer(0) {}
Pixel32(uint8 bi, uint8 gi, uint ri, uint ai = 255) {
b = bi;
g = gi;
r = ri;
a = ai;
} uint32 integer;
struct{
#ifdef BIG_ENDIAN
uint8 a, r, g, b;
#else
uint8 b, g, r, a;
#endif
};
};
struct Vec2 {
Vec2() {}
Vec2(float a, float b) : x(a), y(b) {}
float x, y;
};
struct Rect {
Rect() {}
Rect(float left, float top, float right, float bottom) : xmin(left), xmax(right), ymin(top), ymax(bottom){}
void Include(const Vec2& r) {
xmin = MIN(xmin, r.x);
ymin = MIN(ymin, r.y);
xmax = MAX(xmax, r.x);
ymax = MAX(ymax, r.y);
}
float Width() const { return xmax - xmin + 1; }
float Height() const { return ymax - ymin + 1; }
float xmin, xmax, ymin, ymax;
};
#if defined(_MSC_VER) || defined(__GNUC__)
#pragma pack(push, 1)
#pragma pack(1)
#endif
struct TGAHeader
{
uint8 idLength, paletteType, imageType;
unsigned short firstPaletteEntry, numPaletteEntries;
// uint16 firstPaletteEntry, numPaletteEntries;
uint8 paletteBits;
unsigned short x, y, width, height;
// uint16 x, y, width, height;
uint8 depth, descriptor;
};
#if defined(_MSC_VER) || defined(__GNUC__)
#pragma pack(pop)
#endif
bool WriteTGA(const std::string& filename, const Pixel32* pxl, uint16 width, uint16 height) {
std::ofstream file(filename.c_str(), std::ios::binary);
if (file) {
TGAHeader header;
memset(&header, 0, sizeof(TGAHeader));
//Header部の長さを指定。指定がない場合は固定の長さ。
header.idLength = 0; // 1byte 0~255
//Color Map 0★=無, 1=有(この場合0~127はTruVisionが予約している色、128~255はファイルで定義)
header.paletteType = 0; // 1byte
//TGAファイルの形式
//0- 画像データは含まれていません。
//1- 非圧縮、カラーマップ画像
//2★- 非圧縮、トゥルーカラー画像
//3- 非圧縮、白黒画像
//9- ランレングスエンコード、カラーマップ画像
//10- ランレングスエンコード、トゥルーカラー画像
//11-ランレングスエンコード、白黒画像
header.imageType = 2; // 1byte
//カラーマップ///////////////////////////////
//カラーマップを使う場合の最初の番号
header.firstPaletteEntry = 0; // 2byte
//カラーマップを使う場合の総数
header.numPaletteEntries = 0; // 2byte
//カラーマップを使う場合の色を表すのに使うビット数。専用カードがある場合でも変わる。
header.paletteBits = 0; // 1byte
//////////////////////////////////////////////
//画像原点位置/////////////////////////////
//左下原点としたときの画像開始水平位置
header.x = 0; // 1byte
//左下原点としたときの画像開始垂直位置
header.y = 0; // 1byte
//////////////////////////////////////////////
//画像サイズ///////////////////////////////
//横幅
header.width = width; // 2byte
//縦幅
header.height = height; // 2byte
//////////////////////////////////////////////
//色表現bit数
header.depth = 32; // 1byte
//ヘッダ説明部 ビットによって細分化されている
//下位 0~3bit 1ピクセル毎のアルファチャンネルビット数
//上位 4bit 1なら右から左へ描画
//上位 5bit 1なら上から下へ描画
//上位 6~7bit 未使用 通常は00で固定
header.descriptor = 0x20; // = 0b0010 0000 1byte
file.write((const char*)&header, sizeof(TGAHeader));
file.write((const char*)pxl, sizeof(Pixel32) * width * height);
return true;
}
return false;
}
struct Span{
Span() {}
Span(int _x, int _y, int _width, int _coverage) : x(_x), y(_y), width(_width), coverage(_coverage) {}
int x, y, width, coverage;
};
typedef std::vector<Span> Spans;
void RasterCallback(const int y, const int count, const FT_Span* const spans, void* const user) {
Spans* sptr = (Spans*)user;
for (int i = 0; i < count; i++) {
sptr->push_back(Span(spans[i].x, y, spans[i].len, spans[i].coverage));
}
}
void RenderSpans(FT_Library& library, FT_Outline* const outline, Spans* spans) {
FT_Raster_Params params;
memset(¶ms, 0, sizeof(params));
params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;
params.gray_spans = RasterCallback;
params.user = spans;
FT_Outline_Render(library, outline, ¶ms);
}
void WriteBMP(const std::string& filename, const Pixel32* pxl, uint16 width, uint16 height) {
QSize QSize_xy(width, height);
QString QString_ImageFileName = filename.data();
QImage QImage_Canvas(QSize_xy, QImage::Format_ARGB32_Premultiplied);
int i = 0;
for (int h = 0; h < height; ++h) {
for (int w = 0; w < width; ++w) {
Pixel32 dst = pxl[(int)(width * h + w)];
QImage_Canvas.setPixel(w, h, qRgba(dst.r, dst.g, dst.b, dst.a));
}
}
QImage_Canvas.save(QString_ImageFileName, "BMP");
}
void WriteGlyphAsTGA(FT_Library& library, const std::string& fileName, wchar_t ch, FT_Face& face, int size,
const Pixel32& fontCol, const Pixel32 outlineCol, float outlineWidth) {
if (FT_Set_Char_Size(face, size << 6, size << 6, 90, 90) == 0){
FT_UInt gindex = FT_Get_Char_Index(face, ch);
if (FT_Load_Glyph(face, gindex, FT_LOAD_NO_BITMAP) == 0 ){
if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) {
Spans spans;
RenderSpans(library, &face->glyph->outline, &spans);
Spans outlineSpans;
FT_Stroker stroker;
FT_Stroker_New(library, &stroker);
FT_Stroker_Set(stroker, (int)(outlineWidth * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
FT_Glyph glyph;
if (FT_Get_Glyph(face->glyph, &glyph) == 0) {
FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1);
if (glyph->format == FT_GLYPH_FORMAT_OUTLINE) {
FT_Outline* o = &reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
RenderSpans(library, o, &outlineSpans);
}
FT_Stroker_Done(stroker);
FT_Done_Glyph(glyph);
if (!spans.empty()) {
Rect rect(spans.front().x, spans.front().y, spans.front().x, spans.front().y);
for (Spans::iterator s = spans.begin(); s != spans.end(); ++s) {
rect.Include(Vec2(s->x, s->y));
rect.Include(Vec2(s->x + s->width - 1, s->y));
}
for (Spans::iterator s = outlineSpans.begin(); s != outlineSpans.end(); ++s) {
rect.Include(Vec2(s->x, s->y));
rect.Include(Vec2(s->x + s->width - 1, s->y));
}
#if 0
float bearingX = face->glyph->metrics.horiBearingX >> 6;
float bearingY = face->glyph->metrics.horiBearingY >> 6;
float advance = face->glyph->advance.x >> 6;
#endif
int imgWidth = rect.Width(), imgHeight = rect.Height(), imgSize = imgWidth * imgHeight;
Pixel32* pxl = new Pixel32[imgSize];
memset(pxl, 0, sizeof(Pixel32) * imgSize);
for (Spans::iterator s = outlineSpans.begin(); s != outlineSpans.end(); ++s) {
for (int w = 0; w < s->width; ++w) {
pxl[(int)((imgHeight - 1 - (s->y - rect.ymin)) * imgWidth + s->x - rect.xmin + w)] =
Pixel32(outlineCol.r, outlineCol.g, outlineCol.b, s->coverage);
}
}
for (Spans::iterator s = spans.begin(); s != spans.end(); ++s) {
for (int w = 0; w < s->width; ++w) {
Pixel32& dst = pxl[(int)((imgHeight - 1 - (s->y - rect.ymin)) * imgWidth + s->x - rect.xmin + w)];
Pixel32 src = Pixel32(fontCol.r, fontCol.g, fontCol.b, s->coverage);
dst.r = (int)(dst.r + ((src.r - dst.r) * src.a) / 255.0f);
dst.g = (int)(dst.g + ((src.g - dst.g) * src.a) / 255.0f);
dst.b = (int)(dst.b + ((src.b - dst.b) * src.a) / 255.0f);
dst.a = MIN(255, dst.a + src.a);
}
}
WriteTGA(fileName, pxl, imgWidth, imgHeight);
WriteBMP("C:/FreeTypeStep03Test.bmp", pxl, imgWidth, imgHeight);
delete[] pxl;
}
}
}
}
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// if (argc == 3) {
if (argc == 3) {
std::cerr << "Render letter `B' of given font as a TGA image.\n";
std::cerr << "\n";
std::cerr << "usage: example2 <font> TGA-file\n";
// return 1;
}
QString QString_FileName = "C:/FreeTypeStep03Test.txt";
QString QString_ImageFileName = "C:/FreeTypeStep03Test.bmp";
QString filename = "C:\\Windows\\Fonts\\BIZ-UDGothicR.ttc";
QString QString_TgaFileName = "C:/FreeTypeStep03Test.tga";
QFile QFile_Text(QString_FileName);
QString QString_ImageRowMonoBuffer = "";
QSize QSize_xy(WIDTH, HEIGHT);
QImage QImage_Canvas(QSize_xy, QImage::Format_ARGB32_Premultiplied);//空イメージ
FT_Library library;
FT_Init_FreeType(&library);
FT_Face face;
//std::ifstream fontFile(argv[1], std::ios::binary);
std::ifstream fontFile(filename.toUtf8(), std::ios::binary);
if (fontFile) { /*フォントファイル名情報処理*/
fontFile.seekg(0, std::ios::end); //読み取り位置を 最後 に
std::fstream::pos_type fontFileSize = fontFile.tellg(); 現在位置を pos_type型 変数へ格納
fontFile.seekg(0);//読み取り位置を 0 に
unsigned char* fontBuffer = new unsigned char[fontFileSize];
//読み取り位置最後はファイルサイズなので、その大きさのchar型変数配列 fontBufferを作成
fontFile.read((char*)fontBuffer, fontFileSize);
//Fontファイルの内容をfontBufferへfontFileSizeの長さだけフォントファイル名取得
FT_New_Memory_Face(library, fontBuffer, fontFileSize, 0, &face);
//読み取ったフォントファイル名情報で&faceを初期化
WriteGlyphAsTGA(library, "C:/FreeTypeStep03Test.tga"/*QString_TgaFileName.constData()*/, L'B', face, 100, Pixel32(255, 90, 30), Pixel32(255, 255, 255), 3.0f);
delete[] fontBuffer;
}
return a.exec();
}
出力結果
サンプル4.公式サイトのErik Mollerさん作成その2 Qt版
これは、FreeType2.3.10以降で動作するとされているサンプルで、スタンドアロン B/W(Black/White:白黒:モノトーン)ラスタライザftraster.cの使用方法だそうです。なにが、それほど画期的なモノなのかよくわかっていませんが、フォントファイルを使わずにグラフィックを生成できます。内側と外側の概念があって塗りつぶしもできるようになっています。内側の中に外側だけの図形を描画して、穴をあけたような、図を描画できるかまでは把握していません。文字からの図形にかぎられていたのが、簡単な図形なら、描画できるようになったのが画期的なのかもしれません。
#include <QtCore/QCoreApplication>
#include "ftraster.c"
#include "ftraster.h"
#include "ftmisc.h"
#include <fstream>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_SYSTEM_H
#include FT_OUTLINE_H
struct Vec2 {
Vec2(float a, float b) : x(a), y(b){}
float x, y;
};
static Vec2 k_shape[] = {
Vec2(- 3, -18), Vec2( 0, -12), Vec2( 6, -10), Vec2( 12, - 6), Vec2( 12, - 4),
Vec2( 11, - 4), Vec2( 10, - 5), Vec2( 10, 1), Vec2( 9, 6), Vec2( 7, 10),
Vec2( 5, 12), Vec2( 4, 15), Vec2( 3, 14), Vec2( 1, 13), Vec2(- 1, 13),
Vec2(- 5, 11), Vec2(- 8, 8), Vec2(-11, 2), Vec2(-11, - 2), Vec2(-14, 0),
Vec2(-14, - 2), Vec2(-11, - 7), Vec2(- 9, - 9), Vec2(- 8, - 9), Vec2(- 5, -12),
Vec2(- 5, -14), Vec2(- 7, -15), Vec2(- 8, -14), Vec2(- 9, -15), Vec2(- 9, -17),
Vec2(- 7, -17), Vec2(- 6, -18)
};
void* MY_Alloc_Func(FT_Memory memory, long size){
return malloc((size_t)size);
}
void MY_Free_Func(FT_Memory memory, void* block) {
free(block);
}
void* MY_Realloc_Func(FT_Memory, long cur_size, long new_size, void* block) {
return realloc(block, (size_t)new_size);
}
static FT_Memory mem;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
mem = new FT_MemoryRec_;
mem->alloc = MY_Alloc_Func;
mem->free = MY_Free_Func;
mem->realloc = MY_Realloc_Func;
FT_Outline_ outline;
outline.n_contours = 1;
outline.n_points = sizeof(k_shape) / sizeof(Vec2);
outline.points = new FT_Vector[outline.n_points];
for (int i = 0; i < outline.n_points; ++i) {
FT_Vector v;
v.x = (20 + k_shape[i].x) * 10 * 64;
v.y = (20 + k_shape[i].y) * 10 * 64;
outline.points[i] = v;
}
outline.tags = new char[outline.n_points];
for (int i = 0; i < outline.n_points; ++i) {
outline.tags[i] = 1;
}
outline.contours = new short[outline.n_contours];
outline.contours[0] = outline.n_points - 1;
outline.flags = 0;
const int width = 500;
const int rows = 400;
const int pitch_mono = (width + 7) >> 3;
FT_Bitmap bmp;
FT_Raster_Params params;
const int kRenderPoolSize = 1024 * 1024;
unsigned char* renderPool = new unsigned char[kRenderPoolSize];
bmp.buffer = new unsigned char[rows * pitch_mono];
memset(bmp.buffer, 0, rows * pitch_mono);
bmp.width = width;
bmp.rows = rows;
bmp.pitch = pitch_mono;
bmp.pixel_mode = FT_PIXEL_MODE_MONO;
memset(¶ms, 0, sizeof(params));
params.source = &outline;
params.target = &bmp;
FT_Raster raster;
ft_standard_raster.raster_new(mem, &raster);
ft_standard_raster.raster_reset(raster, renderPool, kRenderPoolSize);
ft_standard_raster.raster_render(raster, ¶ms);
std::ofstream out_mono("out-mono.pbm", std::ios::binary);
out_mono << "P4 " << width << " " << rows << "\n";
out_mono.write((const char*)bmp.buffer, rows * pitch_mono);
delete[] renderPool;
delete[] bmp.buffer;
delete[] outline.points;
delete[] outline.tags;
delete[] outline.contours;
delete mem;
return a.exec();
}
実行結果
上記のようなプログラムで更に、いろいろと必要なプログラムをつまびかないと動作させられませんでした。
プロジェクトにftimege.hとftmisc.hとfutil.cをライブラリディレクトリからコピーしてmain.cppと同じフォルダに追加。
プロジェクトのフォルダの中にftraster.cをコピーする必要があり、このファイルはインクルードすることでコンパリンの対象になります。そして、以下のインクルードをftraster.cの中に記載。
#include "C:\xxx\xxx\xxx\xxx\freetype-2.9.1\include\freetype\internal\internal.h" #include "C:\xxx\xxx\xxx\xxx\freetype-2.9.1\include\freetype\internal\ftdebug.h" #include "C:\xxx\xxx\xxx\xxx\freetype-2.9.1\include\freetype\internal\ftcalc.h"
編集するファイルはすべて、プロジェクト側のディレクトリに移動します。本格的にB/Wラスタライズ機能を繰り返し使う場合は、もっと構成を簡単に綺麗にした方がよいでしょう。自分は無理やりでも動かしてやるということに集中しました。正直、なんにも文献もないのに、全貌を読み解くのは面倒です。簡単には使えないということを理解しました。美しい使い方については、自分もスタンドアロンB/Wラスタライズを使うときに考えたいと思います。
FT_THROWは存在しない構文としてエラーになって面倒だったので、消しました。例外エラーがでたときにわかりやすくなる命令ではありますが、なくても問題ない。return FT_THROW(... となっている場合はreturn 1;に置き換え。if文の中に1行だけある場合は、int ii = 1; のような疑似プログラムを追加。
取り込んだftmisc.hも
/*
typedef struct FT_MemoryRec_
{
void* user;
FT_Alloc_Func alloc;
FT_Free_Func free;
FT_Realloc_Func realloc;
} FT_MemoryRec;
*/
の部分をコメントアウトが必要になります。重複する部分になります。
ftobjs.hも
#define FT_INTERNAL_MEMORY_H <freetype/internal/ftmemory.h>
#include FT_INTERNAL_MEMORY_H
#define FT_INTERNAL_GLYPH_LOADER_H <freetype/internal/ftgloadr.h>
#include FT_INTERNAL_GLYPH_LOADER_H
#define FT_INTERNAL_DRIVER_H <freetype/internal/ftdrv.h>
#include FT_INTERNAL_DRIVER_H
#define FT_INTERNAL_AUTOHINT_H <freetype/internal/autohint.h>
#include FT_INTERNAL_AUTOHINT_H
#define FT_INTERNAL_SERVICE_H <freetype/internal/ftserv.h>
#include FT_INTERNAL_SERVICE_H
#define FT_INTERNAL_PIC_H <freetype/internal/ftpic.h>
#include FT_INTERNAL_PIC_H
#define FT_INTERNAL_CALC_H <freetype/internal/ftcalc.h>
#include FT_INTERNAL_CALC_H
#defineの行を追加しなければならないところが発生しました。
ftutil.hも
#include "C:\xxx\xxx\xxx\xxx\freetype-2.9.1\include\freetype\internal\ftdebug.h"
#include "C:\xxx\xxx\xxx\xxx\freetype-2.9.1\include\freetype\internal\ftmemory.h"
#include ".\ftobjs.h"
のように編集が必要なヘッダファイルはプロジェクト内のものを相対パス参照で、編集が不要でインクルードが必要なモノはライブラリのものを読み込みます。絶対パスでxxxになっているところは、各自の環境に合わせてください。
これで編集が必要だった箇所を全部を書いたかはわかりませんが、基本的にはやることは同じです。コンパリンエラーになった結果にもとづいて、定義が見つからない場合は、必要なインクルードファイルを追記。リンクエラーになる場合は宣言しか読み込めていないってことなので、プログラムファイルを探して読み込みという感じの作業をします。ファイルの編集が必要な場合はライブラリのものは触らず、ファイルを自分のプロジェクトにコピーしてきて、それを使うように読み込みする仕組みにしたうえで編集をします。
こういう作業をすることは勉強にはなりません。これを当たり前のようにこなしてから、どうすることが正しかったのかを考えることが大事です。でも今回は管理人には必要ない知識なので、ここではやりません。人が作ったものを使いこなすのは大変だということです。ユーザ数が少ない。文献がすくなければなおさら面倒です。必要に駆られるまで手は出さない。動けばいいな。程度。1週間やってもだめなら諦めますね。パズルと同じです。
サンプル5.公式サイトのRóbert Márkiさん作成
Róbertが作成したQtプロジェクトでのサンプルですが、プロジェクトファイルが配布されているわけでもないので、再現するのにはどうすればいいのかが、よくわからなかったので、自分でイチカラQt Guiプロジェクト作りこみながら、再現してみました。qmakeファイル配布されてましたけど、何をどうしろというのかというような内容でして、ここまで来て思ったんですけど、FreeTypeのサンプル集はどれも冷たい。どうやってビルドするべきかを環境とツールごとに分けて記述しているプロジェクトもある昨今なのに、ソースファイルとよくわからない*.proファイルの配布。冷たい。
不満を並べてもしょうがないので、ビルド手順を簡単に。
1.VisualStudioでQt Wigets Applicationの新規作成を始めます。
2.今回の場合Wizardがいろいろと分類してくれるのは、ありがたくないのですが、Wizardのいうとおりに作るので、BaseClassを指定するところだけQMainWindowではなく、QWidgetに変更します。あとは規定値でよいです。
3.したらばプログラムを書きます。分割プログラムの形式なのでやや複雑ですが、以下の通り記述します。
まずmain.cpp
#include "QtSampleProject.h"
#include <QtWidgets/QApplication>
#include <QPainter>
#include <QFile>
#include <iostream>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_OUTLINE_H
QString g_usageText =
"usage:\n"
"example4 FONT_PATH CHARACTER SIZE DIRECT_RENDERING_MODE(1|0)";
int main(int argc, char *argv[])
{
bool status = false;
bool isSizeOk = false;
QString path = "C:\\Windows\\Fonts\\BIZ-UDGothicR.ttc";
QChar character = 'm';
int size = QString("60").toInt(&isSizeOk);
bool directRender = QString("1").toInt();
if (QFile::exists(path) && isSizeOk) {
status = true;
QApplication a(argc, argv);
QtSampleProject w(path, character, size, directRender);
w.show();
return a.exec();
}
if (!status) {
std::cout << qPrintable(g_usageText) << std::endl;
return 0;
}
}
プロジェクト名.cpp ここでは仮にQtSampleProjectとしておきます。
#include "QtSampleProject.h"
QtSampleProject::QtSampleProject(QWidget *parent)
: QWidget(parent)
{
m_face = 0;
m_library = 0;
m_directRender = 0;
ui.setupUi(this);
}
QtSampleProject::QtSampleProject(const QString& fileName, QChar character, int pointSize, bool directRender, QWidget* parent) : QWidget(parent), m_directRender(directRender)
{
FT_Error error = FT_Err_Ok;
m_face = 0;
m_library = 0;
error = FT_Init_FreeType(&m_library);
if(!error){
error = FT_New_Face(m_library, fileName.toLatin1().constData(), 0, &m_face);
if (!error) {
error = FT_Set_Char_Size(m_face, 0, pointSize * 64, physicalDpiX(), physicalDpiY());
if (!error) {
FT_UInt glyph_index = 0;
glyph_index = FT_Get_Char_Index(m_face, character.unicode());
error = FT_Load_Glyph(m_face, glyph_index, FT_LOAD_DEFAULT);
if (!error) {
FT_Pos left = m_face->glyph->metrics.horiBearingX;
FT_Pos right = left + m_face->glyph->metrics.width;
FT_Pos top = m_face->glyph->metrics.horiBearingY;
FT_Pos bottom = top - m_face->glyph->metrics.height;
m_glyphRect = QRect(QPoint(TRUNC(left),
-TRUNC(top) + 1),
QSize (TRUNC(right - left) + 1,
TRUNC(top - bottom) + 1)
);
setFixedSize(m_glyphRect.width(), m_glyphRect.height());
}
}
}
}
ui.setupUi(this);
}
QtSampleProject::~QtSampleProject()
{
if(m_face){
FT_Done_Face(m_face);
}
if(m_library){
FT_Done_FreeType(m_library);
}
}
void QtSampleProject::graySpans(int y, int count, const FT_Span_* spans, void* user) {
QPainter* painter = (QPainter*)user;
y = -y;
for (int i = 0; i < count; i++) {
const FT_Span span = spans[i];
qreal opacity = qreal(span.coverage) / 255.0;
painter->setOpacity(opacity);
if (span.len > 1) {
painter->drawLine(span.x, y, span.x + span.len - 1, y);
}
else {
painter->drawPoint(span.x, y);
}
}
}
void QtSampleProject::paintEvent(QPaintEvent* event) {
QWidget::paintEvent(event);
if (m_library && m_face) {
FT_Error error = FT_Err_Ok;
QPainter painter(this);
painter.translate(-m_glyphRect.x(), -m_glyphRect.y());
if (m_directRender) {
painter.setPen(Qt::black);
FT_Raster_Params params;
params.target = 0;
params.flags = FT_RASTER_FLAG_DIRECT | FT_RASTER_FLAG_AA;
params.user = &painter;
params.gray_spans = &QtSampleProject::graySpans;
params.black_spans = 0;
params.bit_set = 0;
params.bit_test = 0;
FT_Outline* outline = &m_face->glyph->outline;
FT_Outline_Render(m_library, outline, ¶ms);
}
else {
error = FT_Render_Glyph(m_face->glyph, FT_RENDER_MODE_NORMAL);
QImage glyphImage(m_face->glyph->bitmap.buffer,
m_face->glyph->bitmap.width,
m_face->glyph->bitmap.rows,
m_face->glyph->bitmap.pitch,
QImage::Format_Indexed8);
painter.translate(m_glyphRect.x(), m_glyphRect.y());
QVector<QRgb> colorTable;
for (int i = 0; i < 256; ++i) {
colorTable << qRgba(0, 0, 0, i);
}
glyphImage.setColorTable(colorTable);
painter.drawImage(QPoint(0, 0), glyphImage);
}
}
}
</Syntaxhighlight2>
プロジェクト名.h
<Syntaxhighlight lang="cpp" line start="1">
#pragma once
#include <QtWidgets/QWidget>
#include "ui_QtSampleProject.h"
#include <QPainter>
#include <QFile>
#include <iostream>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_OUTLINE_H
#define TRUNC(x) ((x) >> 6)
class QtSampleProject : public QWidget
{
Q_OBJECT
public:
QtSampleProject(QWidget* parent = nullptr);
QtSampleProject(const QString& fileName, QChar character, int pointSize, bool directRender, QWidget* parent = 0);
~QtSampleProject();
private:
bool m_directRender = 0;
FT_Library m_library = 0;
FT_Face m_face = 0;
QRect m_glyphRect;
static void graySpans(int y, int count, const FT_Span_* spans, void* user);
Ui::QtSampleProjectClass ui;
protected:
void paintEvent(QPaintEvent* event);
};
実行結果
ウィンドウっちっさ!サンプルの通り動かしたらコレなので、自信を持ってお届けする結果です。main.cppの中のmain関数の最初の方に利用するフォント名を設定する箇所、レンダリングに使う文字、文字の大きさ、directRenderとそうじゃない描画のどちらを使うかを決める0 or 1を指定する文字列で結果が、もう少し変わります。このサンプルをもうちょっと面白くするならこの手前でやったようなサンプルのように文字列を与える形式にするとウィンドウがもう少し大きくなって楽しいかもしれません。あるいは変形とかね。
サンプル6.公式サイトのStatic Jobs LLC作成
Static Jobs LLCという組織によって作られたサンプルがありますが、こちらは、ほぼ変更する必要もなく出力結果のSVGのXMLが標準出力に描画されます。単位はemですが、htmlに単純に貼りつけるとpixelとして扱われます。ものすごく大きい描画になりますが、具体的に活用するときになるまでに、これを編集すればいいことなので、大きな問題ではないです。
このプログラムのSVG Printで拡大率を縮小する方向にするため ビューポート情報を追加すると簡単に縮小された結果を得ることができると思います。そうやってUD BIZゴシックについてグリフQを書き出したも結果は以下のようになります。
<svg xmlns='http://www.w3.org/2000/svg'
xmlns:xlink='http://www.w3.org/1999/xlink'
width='9px' height='17px' viewBox='63 -1640 922 1796'>
<path d='
M 901 156
Q 812 61, 745 -49
Q 647 84, 502 84
Q 303 84, 183 -152
Q 63 -389, 63 -783
Q 63 -1179, 179 -1408
Q 295 -1640, 502 -1640
Q 704 -1640, 824 -1407
Q 946 -1171, 946 -783
Q 946 -414, 839 -166
Q 890 -73, 985 12
L 901 156
M 501 -563
Q 609 -448, 718 -295
Q 772 -456, 772 -772
Q 772 -1109, 699 -1299
Q 627 -1486, 504 -1486
Q 393 -1486, 322 -1330
Q 235 -1138, 235 -781
Q 235 -445, 311 -253
Q 385 -66, 507 -66
Q 597 -66, 647 -162
Q 540 -315, 423 -438
L 501 -563
'
fill='red'/>
</svg>
このようにフォントの描画を行うためのベジェ曲線の通過座標とその補助座標といった情報に分解されたような情報を得ることができます。Mではじまるのは、そのあとに書かれた座標への移動。Qはそのあとに書かれた2つ座標について、現在の始点は前回の移動点であるとして、1つの制御点と1つの終点をもっています。始点と終点を結んだ直線に対して、制御点が曲線の膨らみや凹みを表します。こういうものを2次ベジェと呼んでいます。3次ベジェはCで始まります。Lは、前回の移動点を始点として、1つの指定された座標を終点とする直線を描画します。詳しくはSVGについての記事を読む必要があると思います。フォント情報はほぼ、このM,L,Q,Cで構成されています。
これまで見てきたようにFreeTypeはオープンソースでありながらフォント情報から、グラフィックに描画するための情報を抜き出して、かつ、ビットマップにレンダリングする機能。単独で図形を描画する機能。フォントファイルに埋め込まれたベジェ曲線を描くのに必要な情報の抜き出しが出来ました。
文字コードに対応するグリフ番号を取得する命令も保有しています。フォント情報を編集することはできませんが、もうひとつのフォントファイルを再構築するような作業もやれなくはない感じがします。プログラムによってテキストや独自の形式で使われた文字コードを蓄積し、そのすべてのグリフ番号を取得することができます。その使われたフォント情報だけを抜き出して再構築するような作業もやれそうですが、freetypeだけでやれるのか。もう少し見ていく必要はありそうです。
freetypeは文献が少ないですが、普段、われわれが使ってるようなソフトウェアでも使われていたりと実績はかなりあるようです。FreeTypeを使ってPDFに必要な文字サブセット作成とグリフ番号取得への道のりを考えていましたが、もっと発展的に使えばフォントファイルをプログラムによってすべてを一定の手法によって改造した文字を作ることも可能です。角の処理だけを変えたりですね。大量の文字をプログラムによって編集したものを新たなフォントにできるのであれば、手軽でありながら画期的です。