wordで文章を書くとき、MS明朝とかArialとかフォントを選べますよね。

あれはC:\Windows\Fonts に入っている.ttfファイルから選んでいるのです。

今回はArial 標準と表示されているarial.ttfファイルを見ていこうと思います。

目的は文字の形を表すグリフのデータ本体を見ることです。


構造を知る

見ていくといってもどんな構造か知らないとただ数字が並んでいるようにしか見えません。

そこで

フォントについて

OpenType® Specification

これらを参考にしながら実際にバイナリを見ていこうと思います。


まず全体像を把握しておきます。

・オフセットテーブル

    フォントのデータはテーブルという単位でまとめられているそうです。

    そのテーブルデータがどこにあるのかが書いてあります。

・複数のテーブルデータ

    フォントのデータです。

    文字コードを文字の形であるグリフに割り当てられたIDに変換する表や、フォント名、       グリフの幅とかいろいろ書いてあります。


オフセットテーブル

最初はオフセットテーブルです。
オフセットテーブルの中身は以下のような感じです。

uint32 sfntVersion 0x00010000 か 0x4F54544F ('OTTO')
uint16 numTables     テーブルの数
uint16 searchRange よくわからん
uint16 entrySelector ふえ?
uint16 rangeShift    ほえ?
tableRecord 以下の4つから成る。この4つがテーブルの数だけある。
    Tag テーブルの名前 (4バイト)
    uint32 チェックサムの数値
    Offset32 テーブルデータへのオフセット
    uint32 テーブルデータの長さ

uint32は符号なしの32bit (4バイト)の整数です。
符号付きだとuがないint32とかで、先頭ビットが1で負の数を表すため表せる数が少ない。

実際に見てみると

最初の5つは赤で、青はtableRecordを表しています。
最初は4バイトはバージョンで、0x10000はTrueTypeアウトラインを表すそうです。
詳しい話はあとで出てきます。(たぶん)
次にテーブルが0x19個 (25個)あることがわかります。
青いやつのtablerecordの最初はDSIGというテーブル名で、0x0D13B4にあって、0x1DB4のサイズだとわかります。
DSIGはデジタル署名で改ざんを防ぐためのやつです。
次はGDEFというテーブルの情報が同じように書かれています。

テーブル

テーブルは必須なものとなくてもいいものがあります。
ここではテキトーに何個か見ていきます。

必須テーブル
cmap    文字コードからグリフIDへの変換
head    基本的な情報
name    フォント名など

TrueTypeアウトラインに関するテーブル
glyf グリフデータ
loca 各グリフデータの場所

ここら辺を見ていこうと思います。
見るためにはまずさっきのtablerecordから場所を探します。

cmap 0x000212AC
head 0x0000019C
name 0x000CE4C8
glyf 0x00029F0C
loca 0x000258AC

それぞれの場所がわかったので見ていきます。

head

headにはバージョンやチェックサム、作成日時、最小のフォントサイズとかが書かれています。
細かいことはこれとかこれを見ればわかります。
実際に見てみると

あまり知りたい情報はありませんでした。まあ作成日時が1904/1/1 0時から0xA2E3272A秒 (2,732,795,690秒)経ったとき、すなわち86年たった1990年だということはわかりましたね。
wikiだと1982年に設計されたみたいなので合ってそうですね。

あとでindexToLocFormatの値が必要になるみたいです。
この値は1のため、locaテーブルのオフセットのデータ型がOffset32となります。

name

フォント名などの文字列の場所が書いてあるそうです。
(詳しくはms, aznote)

uint16 format フォーマット番号。0 or 1
uint16 count 文字列データの数
Offset16 stringOffset 文字列がある領域のオフセット(nameテーブルの先頭から)
NameRecord nameRecord[count] NameRecord の配列がcount個だけある。
    uint16 platformID プラットフォーム ID
    uint16 encodingID エンコーディング ID
    uint16 languageID 言語 ID
    uint16 nameID 名前 ID
    uint16 length 文字列の長さ (バイト数)
    Offset16 offset 文字列へのオフセット位置

実際に見てみます。

文字列データは0x3A (58)個あり、文字列の先頭オフセットが0x2BEより絶対アドレスは0xCE4C8+0x2BE=0xCE786だとわかります。
青で示したnamerecordの1つ目を見てみると、nameIDが0のため著作権についての文字列だそうです。オフセットは0のため、0xCE786を見てみると
長いので全部は見ませんが、copyrightがありました。

次の水色のを見てみるとnameIDが1のためフォントのファミリー名を表し、0xCE786+0x21A=0xCE9A0をみると

Arialとちゃんとありました。
nameIDの1,2,3,4が連番にあり、Arial Regular Monotypeなどフォント名についてまとめてありました。
他にもnameID 9のデザイナーの名前のところを見てみるとちゃんとwikiと同じRobin Nicholas Patricia Saundersと書いてありました。

cmap

cmapは文字コードとグリフの対応表です。
(詳しくはms, aznote)

uint16 version バージョン = 0
uint16 numTables EncodingRecord テーブルの数
EncodingRecord encodingRecords[numTables] EncodingRecord テーブルの配列
    uint16 platformID プラットフォームID
    uint16 encodingID エンコーディングID
    Offset32 offset サブテーブルのオフセット(cmapの先頭から)


サブテーブルの構造にはいろいろなフォーマットがあるみたいですが、以下で実際に見たときの1つ目がフォーマット番号4だったのでそれを載せます。
そのフォーマットはUnicode BMP (U+0000〜U+FFFF)用のものです。

サブテーブルの構造
uint16 format フォーマット番号 = 4
uint16 len サブテーブルの長さ
uint16 language Mac 用。それ以外では 0
uint16 segCountX2 segCount * 2
uint16 searchRange
uint16 entrySelector
uint16 rangeShift 2 * segCount - searchRange
uint16 endCode[segCount] 各セグメントの終了文字コード(配列の最後 = 0xFFFF)
uint16 reservedPad 0
uint16 startCode[segCount] 各セグメントの開始文字コード (配列の最後 = 0xFFFF)
int16 idDelta[segCount] Unicode 値からグリフインデックスを算出するための増減値
uint16 idRangeOffset[segCount] glyphIDArray のオフセットまたは0
uint16 glyphIDArray[(可変)] グリフインデックスを格納するデータ領域


よくわからぬ。とりあえず見てみる。


赤が最初の2つ、青がencodingRecords、緑がサブテーブルを表しています。
サブテーブルが多すぎるので途中で緑のしるしは諦めました。
実体はサブテーブルなので、1つ目のサブテーブルを見てみます。
フォーマット番号4、サイズ0x1346バイトだということがわかりました。
あとはよくわからない数字がたくさん。。


とりあえず値をまとめてみます。
segCountX2 0x144 →segCount=0xA2
searchRange 0x100
entrySelector 0x7
rangeShift 0x44
endCode[0~segCount-1]
    [0]0x7E (アドレスは0x212D6)
    [1]0x1FF
... [A1]0xFFFF (アドレスは0x212D5+0xA2*2=0x21419。だから2倍してあったのか)
reservedPad   0
startCode[0~segCount-1]
    [0]0x20 (アドレスは0x2141C)
... [A1]0xFFFF (アドレスは0x2141B+0xA2*2=0x2155E)
idDelta[0~segCount-1]
    [0]0xFFE3 (アドレスは0x21560)
... [A1]0x100 (アドレスは0x2155F+0xA2*2=0x216A3)
idRangeOffset 0 (0のため0xA2個の配列ではなく0が2バイトだけ)
glyphIDArray[0~] なし?

まず文字コードからグリフIDを求める方法は2つあるようです。
ひとつめ:
グリフIDのアドレス(glyphIDArray[i])=idRangeOffset[i]/2 + (文字コード - startCode[i]) + idRangeOffset[i]のアドレス
これがidRangeOffsetが0でないときの求め方。

ふたつめ:
グリフID=(文字コード+idDelta[i])&0xFFFF
今回はidRangeOffsetが0だったためこの方法を用います。

idDelta[i]のiをどうやって決めるかというと、
startCode[i]<=文字コード<endCode[i]
となるようなiにします。
Unicodeにおいて'A'はU+0031でありi=0のとき
(※'A'はU+0041です。U+0031は'1'です。以降そゆことで。wikiのUnicode表1つずれたとこみてたわー)
0x20<=0x31<0x7E
となるためi=0と求まりました。
よってidDelta[0]=0xFFE3を用いて
'A'のグリフID=(0xFFE3+0x31)&0xFFFF=0x14 (20)となります。

loca

glyfテーブルのオフセットが格納されています。
(locaとglyfの詳細はaznote)
このオフセットのデータ型がheadテーブルのindexToLocFormatの値で決まり、先ほど見た通り1だったためOffset32となります。
Offset32 offsets[]
よって4バイトずつオフセットが格納されていることになります。

めんどいので途中で印をつけてないです。

'A'のグリフIDは20だったため、offsets[20]をみる。
0x000258AC+4*0x14=0x258FC
よって0x1248がグリフテーブルから'A'のグリフがあるアドレスまでのオフセットとなります。

glyf

遂に文字の形を表すグリフの本体にたどり着きました!
glyfの構造は以下の通りです。

int16 numberOfContours 輪郭の数。
int16 xMin 輪郭の最小x
int16 yMin 輪郭の最小y
int16 xMax 輪郭の最大x
int16 yMax 輪郭の最大y
uint16 endPtsOfContours[numberOfContours] 各輪郭の最後の座標のインデックス値。
uint16 instructionLength グリフ命令の総バイト数。
uint8 instructions[instructionLength] グリフ命令
uint8 flags[可変] 各座標のフラグ値の配列。主に座標の数だけある。
uint8 or int16 xCoordinates[可変] 各ポイントの X 座標。前の座標からの相対位置。
uint8 or int16 yCoordinates[可変] 各ポイントの Y 座標

これを踏まえて'A'のグリフデータをみてみます。
0x00029F0C+0x1248=0x2B154
ここを見ると

赤が最初の輪郭について、青がグリフ命令、緑がフラグ、紫がX座標、ピンクがY座標。

輪郭の数:1
輪郭の最小(x,y): (0xDF,0)=(223,0)
輪郭の最大(x,y): (0x2FB, 0x5C0)=(763, 1472)
endPtsOfContours: 0xA=10 このインデックスは0から始まるため、11個の座標を持つ。
グリフ命令のバイト数:0x10E=270 byte
フラグ:
    座標インデックス
    [0]0x21=0b00100001 bit1=0 bit2=0 bit5=1 (int16,=)
    [1]0x23=0b00100011 bit2=0 bit5=1 (-uint8, =)
    [2]0x11=0b00010001 bit1=0 bit4=1 bit2=0 (=, int16)
    [3]0x06=0b00000110 (-uint8, -uint8)
    [4]0x06=0b00000110 (-uint8, -uint8)
    [5]0x07=0b00000111 (-uint8, -uint8)
    [6]0x35=0b00110101 bit1=0 bit4=1 (=, uint8)
    [7]0x36=0b00110110 (uint8, uint8)
    [8]0x36=0b00110110 (uint8, uint8)
    [9]0x37=0b00110111 (uint8, uint8)
    [10]0x33=0b00110011 bit2=0 bit5=1 (uint8, =) 

    x座標を省略して前と同じ値にするのはbit1=0 && bit4=1 のとき
    Y座標を省略して前と同じ値にするのはbit2=0 && bit5=1 のとき
    X座標はbit1=0でint16, 1でuint8
    Y座標はbit2=0でint16, 1でuint8
    X座標が負になるのはbit1=1 && bit4=0    
    Y座標が負になるのはbit2=1 && bit5=0    

座標
    [0](0x2FB, 0)→(0x2FB, 0)→(763, 0)
    [1](-0xB4, 0)→(0x247, 0)→(583, 0)
    [2](=, +0x47B)→(0x247, 0x47B)→(583, 1147)
    [3](-0x41, -0x3E)→(0x206, 0x43D)→(518, 1085)
    [4](-0xD3, -0x7C)→(0x133, 0x3C1)→(307, 961)
    [5](-0x54, -0x1F)→(0xDF, 0x3A2)→(223, 930)
    [6](=, +0xAE)→(0xDF, 0x450)→(223, 1104)
    [7](+0x97, +0x47)→(0x176, 0x497)→(374, 1175)
    [8](+0xE2, 0xCA)→(0x258, 0x561)→(600, 1377)
    [9](+0x2F, +0x5F)→(0x287, 0x5C0)→(647, 1472)
    [10](+0x74, =)→(0x2FB, 0x5C0)→(763, 1472)

グリフ命令とかはわかんないからとりあえず座標をつなげてみよう。
Excelのグラフで表示すると・・・
Aじゃなくて1が出てきた。
まじかよ。どっかでアドレスミスったか。
cmapの計算ミスってたのかなー。
つかれたー。今度間違いを探すとしますかーー。

発見したぁああ!!!!
'A'はU+0041でU+0031は'1'です!
まあちゃんとした文字でよかったです。
これで制御文字とかだったら途中で詰まってやり直しだったから助かった。
'1'でもちゃんと表示できたから、これでなんとなくフォントをバイナリから理解できたということで!!



書いた後読み返したら少し読みにくかった。この理由としては細かい情報をaznoteという参考にしたサイトに任せているために省略した情報が多かったからだと思われる。そのためこの記事しか読んでいない人には知らない情報が飛び込んでくることになる。あとは早くフォントの中身を知りたくて、丁寧に説明を書くより次に進みたかったのもある。あと結局OpenTypeの説明をしていなかった。OpenTypeというのはなんか新しめのフォントの規格の1つである。あと一番は表を使わないでそのまま文字として載せているところである。確かスプレッドシートとか使えば簡単に表を作れたような気がするが、やっぱりめんどい。