Open9

OpenType形式のフォントファイルを読む

meshimeshi

ttfファイルを読んだりしているのでそのメモ

OpenType/TrueTypeの仕様について

OpenTypeはTrueTypeの拡張として作られたもの

Microsoft

https://docs.microsoft.com/en-us/typography/opentype/spec/

Apple(TrueType)

https://developer.apple.com/fonts/TrueType-Reference-Manual/

日本語の解説

https://aznote.jakou.com/prog/opentype/index.html

描画の実装

Sixlabors/Fonts

ImageSharpのフォント描画ライブラリ
https://github.com/SixLabors/Fonts

FreeType

広く使われているフォントの描画ライブラリ。クリスタにもライセンスの表記があった。
https://www.freetype.org/

meshimeshi

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ずつでメジャーバージョンとマイナーバージョンを指す
meshimeshi

ファイル構造

ファイルの先頭4byteを読み、0x74746366("ttcf")なら複数のフォント(Font Collection)、0x00010000または0x4F54544F ("OTTO")なら単一フォントのデータのみを含む。

単一のフォントを収録したファイルの場合、Table Directoryが先頭にあり、そのあとにTable Directory内のTable Recordにデータのあるテーブルが続く。

Font Collectionの場合はTTCヘッダが先頭に来て、その後に単一フォントのデータと同様のものが複数続く。

Table Directory

https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font

Type Name Description
uint32 sfntVersion 0x00010000 or 0x4F54544F ("OTTO")
uint16 numTables テーブルの個数
uint16 searchRange \def\bar#1{#1^{\log_2\mathrm{numTables}}} \bar{2} \sdot 16
uint16 entrySelector \log_2{\mathrm{(searchRange/16)}} or \mathrm{Floor}(\log_2{\mathrm{numTables}})
uint16 rangeShift \mathrm{16 \sdot numTables - searchRange}
tableRecord tableRecords[numTables] テーブルレコードの配列

searchRange, entrySelector, rangeShifttableRecordのバイナリサーチ用のデータですが、互換性の為に残されており、現在はセキュリティを考慮してnumTablesの値から直接取得することが推奨されています。

TableRecord

Type Name Description
Tag tableTag 4文字のテーブル識別子("cmap"など)
uint32 checksum テーブルのチェックサム
Offset32 offset ファイル先頭からのオフセット値(byte)
uint32 length テーブルデータのbyte長
tableTagの値の昇順で並べられている。
各テーブルの長さが4の倍数でない場合、実際は4の倍数の長さとなるよう0で残りが埋められている。

yumin.ttf
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
consola.ttf
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という調整用の値を持っている為、普通に計算すると必ず異なる値となります。

meshimeshi

だいたいこれらのテーブルで描画できそう

必須テーブル

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)
meshimeshi

ただの翻訳文と化してきたので次はcmapからGlyphIDを取得してglyfからグリフの頂点データの一覧を取得することを書くかもしれない。
C#ならSystem.Text.Runeを使うと便利そう。

meshimeshi

そろそろRustで書く時がやってきたかもしれない…。FreeType互換のクレートはあったとは思うけど。