OpenType形式のフォントファイルを読む
ttfファイルを読んだりしているのでそのメモ
OpenType/TrueTypeの仕様について
OpenTypeはTrueTypeの拡張として作られたもの
Microsoft
Apple(TrueType)
日本語の解説
描画の実装
Sixlabors/Fonts
ImageSharpのフォント描画ライブラリ
FreeType
広く使われているフォントの描画ライブラリ。クリスタにもライセンスの表記があった。
OpenTypeで扱うデータの型
BigEndianで表現される。
名前 | .NET型 | 説明 |
---|---|---|
uint8 | byte | - |
int8 | sbyte | - |
uint16 | ushort | - |
int16 | short | - |
uint24 | - | - |
uint32 | uint | - |
int32 | int | - |
Fixed | - | 32bit (整数部16bit.小数部16bit)固定小数点数 |
FWORD | short | - |
UFWORD | ushort | - |
F2DOT14 | - | 16bit (2bit.14bit)固定小数点数 |
LONGDATETIME | long/DateTime | long(1904年1月1日午前0時からの経過秒数) |
Tag | uint | 0x20~0x7Eの1byte文字 * 4 |
Offset16 | ushort | オフセット値を表す |
Offset32 | uint | 同上 |
Version16Dot16 | - | 16bitずつでメジャーバージョンとマイナーバージョンを指す |
ファイル構造
ファイルの先頭4byteを読み、0x74746366
("ttcf"
)なら複数のフォント(Font Collection)、0x00010000
または0x4F54544F
("OTTO"
)なら単一フォントのデータのみを含む。
単一のフォントを収録したファイルの場合、Table Directoryが先頭にあり、そのあとにTable Directory内のTable Recordにデータのあるテーブルが続く。
Font Collectionの場合はTTCヘッダが先頭に来て、その後に単一フォントのデータと同様のものが複数続く。
Table Directory
Type | Name | Description |
---|---|---|
uint32 | sfntVersion |
0x00010000 or 0x4F54544F ("OTTO" ) |
uint16 | numTables | テーブルの個数 |
uint16 | searchRange | |
uint16 | entrySelector |
|
uint16 | rangeShift | |
tableRecord | tableRecords[numTables] | テーブルレコードの配列 |
searchRange
, entrySelector
, rangeShift
はtableRecord
のバイナリサーチ用のデータですが、互換性の為に残されており、現在はセキュリティを考慮してnumTables
の値から直接取得することが推奨されています。
TableRecord
Type | Name | Description |
---|---|---|
Tag | tableTag | 4文字のテーブル識別子("cmap" など) |
uint32 | checksum | テーブルのチェックサム |
Offset32 | offset | ファイル先頭からのオフセット値(byte) |
uint32 | length | テーブルデータのbyte長 |
tableTag の値の昇順で並べられている。 |
||
各テーブルの長さが4の倍数でない場合、実際は4の倍数の長さとなるよう0 で残りが埋められている。 |
例
Version:0x00010000, Number of tables:21
Search range:256, Entry selector:4, Range shift:80
Tag:'BASE', Checksum:0x1B8E200D Offset:0x00C1D668, Length:228
Tag:'DSIG', Checksum:0x885A67B0 Offset:0x00C79114, Length:8580
Tag:'GPOS', Checksum:0x3F2C92BF Offset:0x00C1D74C, Length:75682
Tag:'GSUB', Checksum:0xBFD0784C Offset:0x00C2FEF0, Length:205816
Tag:'OS/2', Checksum:0x484237BA Offset:0x000001D8, Length:96
Tag:'cmap', Checksum:0xA1CC9359 Offset:0x00016FD0, Length:256818
Tag:'cvt ', Checksum:0x50922DDE Offset:0x00056D80, Length:414
Tag:'fpgm', Checksum:0x19671D43 Offset:0x00055B04, Length:3578
Tag:'gasp', Checksum:0x0007001B Offset:0x00C1D65C, Length:12
Tag:'glyf', Checksum:0x8807E014 Offset:0x0006DCBC, Length:12251732
Tag:'head', Checksum:0x07B36F0F Offset:0x0000015C, Length:54
Tag:'hhea', Checksum:0x0CFF615C Offset:0x00000194, Length:36
Tag:'hmtx', Checksum:0xC7044A02 Offset:0x00000238, Length:93592
Tag:'loca', Checksum:0x1849366E Offset:0x00056F20, Length:93596
Tag:'maxp', Checksum:0x6C1F1048 Offset:0x000001B8, Length:32
Tag:'meta', Checksum:0x5DF676BC Offset:0x00C622E8, Length:114
Tag:'name', Checksum:0xDA0804DB Offset:0x00C1CF10, Length:1836
Tag:'post', Checksum:0xFF360068 Offset:0x00C1D63C, Length:32
Tag:'prep', Checksum:0xE373DC4F Offset:0x00056900, Length:1152
Tag:'vhea', Checksum:0x0DB36491 Offset:0x00C6235C, Length:36
Tag:'vmtx', Checksum:0x1763F675 Offset:0x00C62380, Length:93588
Version:0x00010000, Number of tables:20
Search range:256, Entry selector:4, Range shift:64
Tag:'DSIG', Checksum:0x868E1516 Offset:0x0006E3F8, Length:7604
Tag:'GDEF', Checksum:0xBAF8C4D1 Offset:0x000652C4, Length:898
Tag:'GPOS', Checksum:0x0CA76988 Offset:0x00065648, Length:26870
Tag:'GSUB', Checksum:0x648F7049 Offset:0x0006BF40, Length:9304
Tag:'MERG', Checksum:0x00160001 Offset:0x0006E398, Length:12
Tag:'OS/2', Checksum:0x4D5098BC Offset:0x000001C8, Length:96
Tag:'cmap', Checksum:0x2EBDE0CD Offset:0x00002E70, Length:10410
Tag:'cvt ', Checksum:0xEC4CCC7B Offset:0x00007CE8, Length:1350
Tag:'fpgm', Checksum:0x4F2E51F5 Offset:0x0000571C, Length:3370
Tag:'gasp', Checksum:0x001D0023 Offset:0x000652B4, Length:16
Tag:'glyf', Checksum:0xED4F6F7F Offset:0x0000B18C, Length:366166
Tag:'head', Checksum:0xF38D2166 Offset:0x0000014C, Length:54
Tag:'hhea', Checksum:0x09480AF7 Offset:0x00000184, Length:36
Tag:'hmtx', Checksum:0x9A9634EF Offset:0x00000228, Length:11336
Tag:'loca', Checksum:0x1FFE4C1A Offset:0x00008230, Length:12124
Tag:'maxp', Checksum:0x197407DA Offset:0x000001A8, Length:32
Tag:'meta', Checksum:0x2EAD345C Offset:0x0006E3A4, Length:84
Tag:'name', Checksum:0x8A2120E1 Offset:0x000647E4, Length:2736
Tag:'post', Checksum:0xFEF90091 Offset:0x00065294, Length:32
Tag:'prep', Checksum:0x301ACFBE Offset:0x00006448, Length:6303
Tag
の昇順に並んでいるが実際はOffset
の順で並べた方が取り回しは良さそう。
Checksumの計算
foreach(var rec in tableRecords)
{
// テーブルデータ読み取り用のバッファ確保
var rentArray = ArrayPool<byte>.Shared.Rent((int)rec.Length);
// ファイルストリームの位置をTableRecordのオフセット値にセット
fp.Position = rec.Offset;
// パディングを考慮しテーブル長を4の倍数にする
var length = (int)(rec.Length % 4 == 0 ? rec.Length : (rec.Length / 4) * 4 + 4);
// フォントファイルから各テーブルのデータを読み取る
fp.Read(rentArray.AsSpan(0, length));
// SpanReaderは自実装のBigEndian用ref構造体版BinaryReader
var tableReader = new SpanReader(rentArray.AsSpan(0, length));
// 4byte分をuintとして読んで足していく
var checksum = 0u;
for(var i=0; i<(length / sizeof(uint)); i++)
{
checksum += tableReader.ReadUInt32();
}
// 計算値とテーブルデータが異なっていればコンソールに表示
if(checksum != rec.Checksum) Console.WriteLine($"Invalid checksum {checksum:X8}");
// 借りているメモリの返却
ArrayPool<byte>.Shared.Return(rentArray);
}
head
テーブルだけChecksumAdjustment
という調整用の値を持っている為、普通に計算すると必ず異なる値となります。
だいたいこれらのテーブルで描画できそう
必須テーブル
Tag | 説明 |
---|---|
'cmap' | 文字コードからグリフIDへのマッピングを行う |
'head' | フォントのヘッダ。フォント描画の基本的な情報が格納されている |
'hhea' | 水平方向のレイアウト情報が格納されている |
'hmtx' | 左端からの距離や文字と文字との間隔などが格納されている |
'maxp' | 様々な最大値の情報 |
'name' | フォントの名称や著作者の情報が文字データで入っている |
OS/2 | OS/2とWindows固有の指標 |
'post' | PostScriptの情報 |
TrueTypeに関係するテーブル
Tag | 説明 |
---|---|
'cvt ' | Control Value Table (optional table) |
'fpgm' | Font program (optional table) |
'glyf' | グリフの輪郭データが格納されている |
'loca' | 各グリフの場所が格納されている |
'prep' | CVT Program (optional table) |
'gasp' | Grid-fitting/Scan-conversion (optional table) |
ただの翻訳文と化してきたので次はcmap
からGlyphIDを取得してglyf
からグリフの頂点データの一覧を取得することを書くかもしれない。
C#ならSystem.Text.Rune
を使うと便利そう。
そろそろRustで書く時がやってきたかもしれない…。FreeType互換のクレートはあったとは思うけど。