にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

フォントの気持ちになる

フォントの気持ちになるですよ。
f:id:nixeneko:20161007231818p:plain

……正確にはフォントというより、フォントレンダリングソフトウェアの気持ちになってフォントを読んでみよう、という感じでごぜーますが。

フォントの準備

…………。
さて。

読むフォントは、容量が小さいとありがたいのですが、ちょっと探しただけでは見つからなかったので、Noto Sans RegularをFont Squirrel Webfont Generator使ってASCIIの小文字アルファベットだけにしたものを用意しました。だいたい9KB位。

用意したフォントはここからダウンロードできます: font.ttf


さて、適当なバイナリエディタでフォントを開き、16進ダンプを印刷します。
f:id:nixeneko:20161007234742j:plain
一応、印刷したPDFのリンクを張っておきます: font.pdf


……えーと、別に印刷する必要なかったですね。バイナリエディタで見ていきます。今回はStirlingを使いました。


今回はWindows用の情報をたどって、"cat"を出力することにしましょう。UnicodeでU+0063 U+0061 U+0074です。

あと、長くなってしまったので今回表示に関係ないところについてはデフォルトで表示されないようにしています。クリックで展開できるので興味があれば開いてください。

OpenType Font File

Microsoft Typography - OpenType Specification
このOpenTypeの仕様をみながら読んでいきます。

特に、OpenTypeフォントファイルの仕様についての次のページを参考にします。
The OpenType Font File
最初にデータ型について書かれています。
特に説明を要するものもなさそうですが、固定小数点数を10進数に直すには、整数として読み出し、2小数部分のビット数で割ると実際の値になるようです。


さて、初めから読んでいきます。OpenTypeフォーマットはバイトオーダがビッグエンディアンなので直感的ですね。
最初に来るのが0x00010000だから(TTC Headerでなく)Offset Tableなので、TTCでない単一のフォントでかつTrueTypeアウトラインのものだとわかります。

Offset Table

Type Name Value 備考
ULONG sfntVersion 0x00010000 TrueType Outline
USHORT numTables 0x0013 19
USHORT searchRange 0x0100 256
USHORT entrySelector 0x0004 4
USHORT rangeShift 0x0030 48

numTablesはテーブルの数で19個あるとわかり、そのあとの3つはnumTablesから計算される数です。
entrySelectorは  2^n \leq \text{numTables} なる最大の整数 nで、他は  \text{numTables} = 16\cdot2^\text{entrySelector},  \text{rangeShift} = 16\cdot\text{numTables} - \text{searchRange} だそうです。計算してみると上の表のとおりになることが分かります。

Table Record

Type Name Description
ULONG tag テーブルを特定するための4文字の識別子
ULONG checkSum テーブルのチェックサム
ULONG offset テーブルのフォントファイルの先頭からのオフセット
ULONG length テーブルの長さ

直後にくるのがTable Recordで、これがOffset TableのnumTables……つまり19個並んでいるはずです。読みだすと次のようになっていました。
チェックサムの確認は面倒なのでしませんでしたが、offsetとlengthから計算できる値で計算方法は仕様を参照。

tag checkSum offset length
'FFTM' 0x61D6EAF7 0x013C (316) 0x001C (28)
'GDEF' 0x005C0024 0x0158 (344) 0x0028 (40)
'GPOS' 0xA176C2E4 0x0180 (384) 0x00D0 (208)
'GSUB' 0x9966BC6F 0x0250 (592) 0x0076 (118)
'OS/2' 0x79FC780F 0x02C8 (712) 0x0060 (96)
'cmap' 0xD6E8B4E8 0x0328 (808) 0x0182 (386)
'cvt ' 0x0F5312F5 0x04AC (1196) 0x0048 (72)
'fpgm' 0x53B42FA7 0x04F4 (1268) 0x0265 (613)
'gasp' 0x00160023 0x075C (1884) 0x0010 (16)
'glyf' 0xF470EA57 0x076C (1900) 0x1164 (4452)
'head' 0x0D957EF6 0x18D0 (6352) 0x0036 (54)
'hhea' 0x0EF404F9 0x1908 (6408) 0x0024 (36)
'hmtx' 0x9AC30ABF 0x192C (6444) 0x00B4 (180)
'loca' 0x7CD081CC 0x19E0 (6624) 0x005C (92)
'maxp' 0x014700FD 0x1A3C (6716) 0x0020 (32)
'name' 0x8B41D5BC 0x1A5C (6748) 0x06A4 (1700)
'post' 0xE1EBCFD6 0x2100 (8448) 0x0103 (259)
'prep' 0x7730038F 0x2204 (8708) 0x015A (346)
'webf' 0x62B657D9 0x2360 (9056) 0x0006 (6)

必ずしもあるテーブルについのoffset+lengthが次のテーブルのoffsetとなっていないのは、テーブルが4バイト境界にアラインメントされるためで、lengthには実際のテーブルの長さが記録されるのだそうです。実際のテーブルから次の4バイト境界までの隙間は0で埋められます。

'FFTM'はFontForge Time StampでFontForgeが出力するデータ、'webf'は不明ですがおそらくWebfontの略か、それぞれ独自テーブルなので無視します。

さて、OpenType tableのリストとその位置が得られたので、見ていきます。
どういう順番で見てくのがいいんでしょうか。とりあえずheadから行きます。

'head' - Font Header Table

フォントヘッダだそうです。開始オフセットが0x18D0なのでそこから見ていきます。54バイトなので短い。

Type Name Value 備考
USHORT majorVersion 0x0001 1固定
USHORT minorVersion 0x0000 0固定
Fixed fontRevision 0x00010F5C 1.06くらい
ULONG checkSumAdjustment 0xEC47630E
ULONG magicNumber 0x5F0F3CF5 0x5F0F3CF5固定
USHORT flags 0x001F ベースラインがy=0;
Left sidebearing point at x=0;
Instructions may depend on point size;
Force ppem to integer values for all internal scaler math;
Instructions may alter advance width
USHORT unitsPerEm 0x0800 2048
LONGDATETIME created 0x00000000D3FF1335 2016-09-14T14:46:13+00:00
mac時間らしい
LONGDATETIME modified 0x00000000D3FF1335 2016-09-14T14:46:13+00:00
SHORT xMin 0xFF8F -113 バウンディングボックスのx最小値
SHORT yMin 0xFE14 -492 同y最小値
SHORT xMax 0x06D5 1749 同x最大値
SHORT yMax 0x061F 1567 同y最大値
USHORT macStyle 0x0000 なし
USHORT lowestRecPPEM 0x0008 最小可読サイズ。8px
SHORT fontDirectionHint 0x0002 2固定。廃止
SHORT indexToLocFormat 0x0000 short offsets
SHORT glyphDataFormat 0x0000 0固定

'OS/2' - OS/2 and Windows Metrics Table

offsetが0x02C8なのでそこから読みます。
The 'OS/2' & Windows Metrics Table (Version 5)
実質Windows専用なんですかね。
はじめ2バイトのversionが0x0004であるからこれがVersion 4のOS/2テーブルということが分かります。
The 'OS/2' & Windows Metrics Table (Version 4)

Type Name of Entry Value comment
USHORT version 0x0004 4
SHORT xAvgCharWidth 0x0387 903; 幅が0でないグリフの幅の算術平均
USHORT usWeightClass 0x0190 400 = Normal (Regular)
USHORT usWidthClass 0x0005 5 = Medium (normal)
USHORT fsType 0x0004 4 = 表示・印刷用にフォント埋め込み可(編集不可)
SHORT ySubscriptXSize 0x059A 1434; 下付き文字の大きさの目安
SHORT ySubscriptYSize 0x0533 1331
SHORT ySubscriptXOffset 0x0000 0; 下付き文字の位置の目安
SHORT ySubscriptYOffset 0x011F 287
SHORT ySuperscriptXSize 0x059A 1434; 上付き文字の大きさの目安
SHORT ySuperscriptYSize 0x0533 1331
SHORT ySuperscriptXOffset 0x0000 0; 上付き文字の位置の目安
SHORT ySuperscriptYOffset 0x03D1 977
SHORT yStrikeoutSize 0x0066 102; 打消線の幅
SHORT yStrikeoutPosition 0x0200 512; 打消線の上側の位置
SHORT sFamilyClass 0x0802 2050
BYTE panose[10] 0x020B0502040504020204
ULONG ulUnicodeRange1 0xE00002FF Basic Latin, Latin-1 Supplement, Latin Extended-A, Latin Extended-B, IPA Extensions, Phonetic Extensions, Phonetic Extensions Supplement, Spacing Modifier Letters, Modifier Tone Letters, Combining Diacritical Marks, Combining Diacritical Marks Supplement, Greek and Coptic, Cyrillic, Cyrillic Supplement, Cyrillic Extended-A, Cyrillic Extended-B, Latin Extended Additional, Latin Extended-C, Latin Extended-D, Greek Extended, General Punctuation, Supplemental Punctuation
ULONG ulUnicodeRange2 0x00000000
ULONG ulUnicodeRange3 0x00000000
ULONG ulUnicodeRange4 0x00000000
CHAR achVendID[4] 0x474F4F47 'GOOG'; 大文字IDはMicrosoftに登録しないと使えないらしい
USHORT fsSelection 0x0040 REGULAR
USHORT usFirstCharIndex 0x000D CR…フォント内の最小のUnicode index
USHORT usLastCharIndex 0x25FC '◼'…フォント内の最大のUnicode index
SHORT sTypoAscender 0x088D 2189; typographic ascender; 'hhea'のAscenderとは異なる
SHORT sTypoDescender 0xFDA8 -600; typographic descender; 'hhea'のDescenderとは異なる
SHORT sTypoLineGap 0x0000 0; typographic line gap; 'hhea'のLineGapとは異なる
USHORT usWinAscent 0x088D 2189; ascender metric for Windows
USHORT usWinDescent 0x0258 600; descender metric for Windows
ULONG ulCodePageRange1 0x2000019F サポートするコードページ: Latin 1, Latin 2: Eastern Europe, Cyrillic, Greek, Turkish, Windows Baltic, Vietnamese, Macintosh Character Set (US Roman),
ULONG ulCodePageRange2 0xDFD70000 IBM Greek, MS-DOS Russian, MS-DOS Nordic, MS-DOS Canadian French, MS-DOS Icelandic, MS-DOS Portuguese, IBM Turkish, IBM Cyrillic; primarily Russian, Latin 2, MS-DOS Baltic, Greek; former 437 G, WE/Latin 1
SHORT sxHeight 0x044A 1098; エックスハイト
SHORT sCapHeight 0x0000 0; キャップハイトあるいは大文字Hの高さ
USHORT usDefaultChar 0x0000 0
USHORT usBreakChar 0x0020 ' ' スペース
USHORT usMaxContext 0x0002 2 = ペアカーニングや2文字までを対象したフィーチャしかない

Windows用のソフトでは、sTypoAscender, sTypoDescender, sTypoLineGapを利用して標準のline-spaceingを計算するのが推奨されてるらしいです。

'hhea' - Horizontal Header Table

offsetが0x1908なのでそこから読んでいきます。
The Horizontal Header Table

Type Name of Entry Value comment
USHORT majorVersion 0x0001 1固定
USHORT minorVersion 0x0000 0固定
FWORD Ascender 0x088D 2189; Typographic ascent
FWORD Descender 0xFDA8 -600; Typographic descent
FWORD LineGap 0x0000 0; Typographic line gap
UFWORD advanceWidthMax 0x077B 1915; Maximum advance width value in 'hmtx'
FWORD minLeftSideBearing 0xFF8F -113; Minimum left sidebearing value in 'hmtx'
FWORD minRightSideBearing 0xFFA8 -88
FWORD xMaxExtent 0x06D5 1749; Max(lsb + (xMax - xMin))
SHORT caretSlopeRise 0x0001 1 for vertical
SHORT caretSlopeRun 0x0000 0 for vertical
SHORT caretOffset 0x0000 0 for non-slanted fonts
SHORT (reserved) 0x0000 0固定
SHORT (reserved) 0x0000 0固定
SHORT (reserved) 0x0000 0固定
SHORT (reserved) 0x0000 0固定
SHORT metricDataFormat 0x0000 0固定
USHORT numberOfHMetrics 0x002D 45; 'hmtx'におけるhMetricエントリの数

ascender, descender, linegapはMac用。とはいえ'OS/2'にあるものと一致してますね。

'maxp' - Maximum Profile

offsetが0x1A3Cなのでそこから読みます。
The Maximum Profile Table
最初が0x00010000なのでversion 1.0であることがわかります。それに従って読んでいくと……

Type Name value comment
Fixed Table version number 0x00010000 version 1.0
USHORT numGlyphs 0x002D 45; グリフの数
USHORT maxPoints 0x0029 41; Maximum points in a non-composite glyph.
USHORT maxContours 0x0002 2; Maximum contours in a non-composite glyph.
USHORT maxCompositePoints 0x0000 0; Maximum points in a composite glyph.
USHORT maxCompositeContours 0x0000 0; Maximum contours in a composite glyph.
USHORT maxZones 0x0002 instructions use the twilight zone (Z0); 大体は2
USHORT maxTwilightPoints 0x0001 1; Maximum points used in Z0.
USHORT maxStorage 0x0002 2; Number of Storage Area locations.
USHORT maxFunctionDefs 0x0016 22; Number of FDEFs
USHORT maxInstructionDefs 0x0000 0; Number of IDEFs.
USHORT maxStackElements 0x0100 256; Maximum stack depth.
USHORT maxSizeOfInstructions 0x00D0 208; Maximum byte count for glyph instructions.
USHORT maxComponentElements 0x0000 0; Maximum number of components referenced at “top level” for any composite glyph.
USHORT maxComponentDepth 0x0000 0; Maximum levels of recursion

'cmap' - Character To Glyph Index Mapping Table

offsetが0x0328からなのでそこから読みます。
Character/Glyph Index Mapping
文字コードを実際のグリフの内部コード(Glyph IDもしくはGID)にマッピングするテーブルです。

cmap Header

Type Name value comment
USHORT version 0x0000 0
USHORT numTables 0x0003 3; エンコーディングテーブルの数

Encoding Record

つぎに来るのがencoding recordの列で、cmapヘッダから3つあることがわかります。レコードの形式は次の様になっています。

Type Name 備考
USHORT platformID
USHORT encodingID
ULONG offset サブテーブルへのオフセット

platformIDやencodingIDは'name'テーブルのものと同じらしい。さらにサブテーブルがあるようですね。

これに従って読んでいくと……

platformID encodingID offset
0x0000 (Unicode) 0x0003 (Unicode 2.0 and onwards semantics, Unicode BMP only) 0x0000001C
0x0001 (Macintosh) 0x0000 (Roman) 0x0000007C
0x0003 (Windows) 0x0001 (Unicode BMP (UCS-2)) 0x0000001C

よく見てみると、UnicodeWindowsのレコードのoffsetが同一です。サブテーブルは実際には2つだけ含まれているということでしょう。

Unicode, Windows用サブテーブル

0x0328 + 0x001C = 0x0344 から読みます。というか前と連続してますね。

先頭が0x0004なのでFormat 4サブテーブルのようです。

Type Name value 備考
USHORT format 0x0004 Format number 4
USHORT length 0x0060 96; このsubtableの長さ
USHORT language 0x0000 Mac用以外では0
USHORT segCountX2 0x0014 20; 2×segCount= 20 よって segCount=10
USHORT searchRange 0x0010 16; segCount以上で最小の2の冪乗数
USHORT entrySelector 0x0003 3; log2(searchRange/2)
USHORT rangeShift 0x0004 4; 2×segCount - searchRange = 20 - 16 = 4
USHORT endCount[segCount] 0x0000 End characterCode for each segment
0x000D
0x0020
0x007A
0x00A0
0x200A
0x202F
0x205F
0x25FC
0xFFFF last=0xFFFF
USHORT reservedPad 0x0000 0固定
USHORT startCount[segCount] 0x0000 Start character code for each segment
0x000D
0x0020
0x0061
0x00A0
0x2000
0x202F
0x205F
0x25FC
0xFFFF
SHORT idDelta[segCount] 0x0001 Delta for all character codes in segment
0xFFF5
0xFFE3
0xFFA3
0xFF7E
0xE01F
0xDFFB
0xDFCC
0xDA30
0x0001
USHORT idRangeOffset[segCount] 0x0000 0; Offsets into glyphIdArray or 0
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
0x0000
USHORT glyphIdArray[ ] なし

segCountが分からないとendCount以降が読み込めないので、先にsegCountを読んで配列の個数を確定させてから読んでいく必要があります。
さて、このformat 4は文字コードとglyph indexが連続して対応してる場合にコンパクトに記録できる方式のようです。

まとめます。

index endCount startCount idDelta idRangeOffset
0 0x0000 0x0000 0x0001 (1) 0x0000
1 0x000D 0x000D 0xFFF5 (-11) 0x0000
2 0x0020 0x0020 0xFFE3 (-29) 0x0000
3 0x007A 0x0061 0xFFA3 (-93) 0x0000
4 0x00A0 0x00A0 0xFF7E (-130) 0x0000
5 0x200A 0x2000 0xE01F (-8161) 0x0000
6 0x202F 0x202F 0xDFFB (-8197) 0x0000
7 0x205F 0x205F 0xDFCC (-8244) 0x0000
8 0x25FC 0x25FC 0xDA30 (-9680) 0x0000
9 0xFFFF 0xFFFF 0x0001 (1) 0x0000

さて、Unicodeスカラ値とグリフの対応を見てみましょう。今回欲しいのは“cat”(0x0063, 0x0061, 0x0074)なので、それに対応するものを探しましょう。
これらはendCount順にソートされています。endCountを順に見ていくと、4番目(index = 3)の0x007Aで初めてc (0x0063), a (0x0061), t (0x0074)の値以上になります。
そのためそれに対応するstartCountを見ると0x0061となっていて、c, a, tはいずれもそれ以上であり、したがってマッピングが定義されていることが分かります。
そこで、対応するidDeltaとidRangeOffset (ここでは0なので気にしない)からGlyph IDを計算します。ここでは、文字コードにidDelta = -93を加えると(=93を引くと)求めるGlyph IDになります。

文字 文字コード Glyph ID
c 0x0063 6
a 0x0061 4
t 0x0074 23

Macintosh用subtable

0x0328 + 0x007C = 0x03A4 から。

'hmtx' - Horizontal Metrics

offsetが0x192Cなのでそこから読みます。
The Horizontal Metrics Table
グリフ幅と左サイドベアリングが記録されているようです。

Field Type
hMetrics longHorMetric[numberOfHMetrics]
leftSideBearing SHORT[ ]

'hhea'のnumberOfHMetricsが45であることから、45個hMetricsデータが連なってるはずです。

という訳で順番に読んでいきます。なおlongHorMetricは

  • USHORT advanceWidth;
  • SHORT lsb;

からなる構造体です。(lsbは左サイドベアリング)

hMetrics

index advanceWidth lsb
0 0x02EC 0x0044
1 0x0000 0x0000
2 0x0414 0x0000
3 0x0214 0x0000
4 0x047D 0x005E
5 0x04EC 0x00AE
6 0x03D7 0x0071
7 0x04EC 0x0071
8 0x0483 0x0071
9 0x02C1 0x001F
10 0x04EC 0x0071
11 0x04F2 0x00AE
12 0x0210 0x00A0
13 0x0210 0xFF8F
14 0x0446 0x00AE
15 0x0210 0x00AE
16 0x077B 0x00AE
17 0x04F2 0x00AE
18 0x04D7 0x0071
19 0x04EC 0x00AE
20 0x04EC 0x0071
21 0x034E 0x00AE
22 0x03D5 0x0068
23 0x02E3 0x0021
24 0x04F2 0x00A2
25 0x0410 0x0000
26 0x064A 0x0017
27 0x043B 0x0025
28 0x0414 0x0002
29 0x03C3 0x0050
30 0x0214 0x0000
31 0x030F 0x0000
32 0x061F 0x0000
33 0x030F 0x0000
34 0x061F 0x0000
35 0x020A 0x0000
36 0x0187 0x0000
37 0x0105 0x0000
38 0x0105 0x0000
39 0x00C3 0x0000
40 0x0139 0x0000
41 0x0057 0x0000
42 0x0139 0x0000
43 0x0187 0x0000
44 0x044C 0x0000

あれ、これでこのテーブル終わりのようです。leftSideBearingフィールドの入る余地がないですね。
leftSideBearingの個数は'maxp'のnumGlyphsを使って numGlyphs - numberOfHMetrics で計算されるらしいので、 45 - 45 = 0 なのであってます。主に等幅のグリフのためのものだとか。

"cat"の3文字についてまとめます。

文字 Glyph ID advanceWidth lsb
c 6 0x03D7 (983) 0x0071 (113)
a 4 0x047D (1149) 0x005E (94)
t 23 0x02E3 (739) 0x0021 (33)

'name' - Naming Table

offsetは0x1A5Cなのでそこから読んでいきます。
The Naming Table
グリフを得るのには特に必要なさそうです。

'post' - PostScript Table

offsetは0x2100なのでそこから読みます。
The PostScript Table
PostScriptプリンタのために必要な情報を記録するところとのことで、グリフ名とかが格納されているようです。

'GDEF'

offsetは0x0158。
The Glyph Definition Table
グリフの属するクラスを定義したり、リガチャのグリフのキャレット位置を指定したりするテーブルの様です。

'GPOS'

offsetは0x0180
The Glyph Positioning Table
カーニングの調整、合成記号がどこにつくかの指定など、条件つきのグリフの位置調整を指定するテーブルです。

'GSUB'

offsetは0x0250。
The Glyph Substitution Table
合字や、アラビア文字などにおける独立・語頭・語中・語末形の切り替えなど、特定の条件でグリフを置き換えることを指定するテーブルです。

'gasp' — Grid-fitting And Scan-conversion Procedure Table

offsetは0x075C。
The gasp Table
PPEMサイズの違いによりラスタライザの描画品質を向上させるためのフラグ設定等が書かれているようです。

'cvt ' - Control Value Table

offsetは0x04AC。
The Control Value Table

TrueType instructionから参照される値がつまってるようです。

FWORDの値が並んでいます。

0 1 2 3 4 5 6 7 8 9
0x 0x0000 0x044A 0x05B6 0x00A2 0x00E5 0x006A 0x007D 0x008C 0x0091 0x0095
1x 0x009A 0x009D 0x0054 0x00B4 0x00D3 0x0081 0x008F 0x0094 0x00AA 0x00AE
2x 0x00B4 0x00B8 0x00BE 0x00C2 0x006B 0x00A6 0x00BA 0x0084 0x00B2 0x008A
3x 0x0077 0x007B 0x00AC 0x00B0 0x0044 0x0511

'fpgm' - Font Program

offsetは0x04F4。
The Font Program
BYTE[ n ]
最初に一回だけ実行されるプログラムだそうで、TrueType命令によって関数定義を行っているようです。

また、TrueType命令については、最近のMac OSからは単純に無視される様です。高DPI環境には不要ということでしょうか。今回描画に必要でないので省略します。

'prep' - Control Value Program

offsetは0x2204。
The Control Value Program

フォントサイズや変換行列などが変わる度に呼び出されるプログラムらしいです。グリフ固有の処理はできません。TrueType命令が並んでいます。

'loca' - Index to Location

offsetは0x19E0。
The Index to Location Table

このテーブルには2種類の形式がありますが、headテーブルのindexToLocFormat = 0であることからshort版であることがわかります。

indexToLoc table short version

Type Name 説明
USHORT offsets[n] 2で割ったローカルオフセット
n = numGlyphs('maxp') + 1

maxpテーブルのnumGlyphs = 45なので、USHORT型が46個並んだ配列となっているはずです。

index 0 1 2 3 4 5 6 7
0o00 0x0000 0x002C 0x002C 0x002C 0x002C 0x00A4 0x010E 0x0154
0o10 0x01BA 0x021E 0x0272 0x02EC 0x033C 0x0376 0x03C2 0x03FE
0o20 0x0418 0x0482 0x04CC 0x051A 0x0580 0x05E6 0x0622 0x0688
0o30 0x06EC 0x073A 0x0766 0x0806 0x0838 0x0878 0x08A6 0x08A6
0o40 0x08A6 0x08A6 0x08A6 0x08A6 0x08A6 0x08A6 0x08A6 0x08A6
0o50 0x08A6 0x08A6 0x08A6 0x08A6 0x08A6 0x08B2

'glyf' - Glyf Data

offsetは0x076C。
The Glyph Data Table

これにはグリフのデータが並んでいます。実際に描画される文字図形の本体ですね。フォントファイルの大部分を占めています。
locaテーブルに書かれているGlyphIDに対応するオフセットにアクセスするとそのグリフが得られるようです。

さすがに全部順番に読んでいくのは無理なので、今回描こうとしている“cat”の三文字について見ていきましょうか。

cmapテーブルから、c, a, tのGlyph IDはそれぞれ6, 4, 23であることがわかります。locaテーブルから対応するオフセットを求めると……

文字 glyph ID offset アドレス
c 6 0x021C 0x0988 - 0x0A14
a 4 0x0058 0x07C4 - 0x08B4
t 23 0x0D10 0x147C - 0x1544

となるので、それぞれを見ていきましょう。

'c' 0x0988 - 0x0A14

ヘッダー
Type Name value 備考
SHORT numberOfContours 0x0001 1; >= 0 なのでsingle glyph
SHORT xMin 0x0071 Minimum x for coordinate data.
SHORT yMin 0xFFEC Minimum y for coordinate data.
SHORT xMax 0x0393 Maximum x for coordinate data.
SHORT yMax 0x045E Maximum y for coordinate data.

numberOfCoutours >= 0 で単体のグリフなので、次のデータが来ます。

Simple Glyph Description
Type Name value Description
USHORT endPtsOfContours[n] 0x0016 22; Array of last points of each contour; n = numberOfCoutours
USHORT instructionLength 0x003D 61; instructionsのバイト数
BYTE instructions[n] 以下 Array of instructions for each glyph; n is the number of instructions.
BYTE flags[n] 以下 Array of flags for each coordinate in outline; n is the number of flags.
BYTE or SHORT xCoordinates[ ] 以下 First coordinates relative to (0,0); others are relative to previous point.
BYTE or SHORT yCoordinates[ ] 以下 First coordinates relative to (0,0); others are relative to previous point.
TrueType命令 (0x0996~0x9D3)

制御点列とその座標

flagsの配列の大きさは最初から確定できないので順番に読んでいく必要があるようです。インデックスがendPtsOfContoursの最大の値に等しくなるまで読んでいきます。
flagには、x,yがそれぞれBYTE(さらに正か負)かSHORTか前の値と同じかについての情報が含まれているので、それに従ってまず各制御点のx座標を、次にy座標を読みだしていきます。記録されている座標の値は前の座標との差分になっていて、記録スペースを圧縮することができるようになっているようです。

1バイト正数を+1b, 1バイト負数を-1b, 2バイト整数を2b, 前と変化なしをsameとします。

idx flag On Curve x y Δx Δy x座標 y座標
0 0x13 (0b00010011) +1b 2b +113 +543 113 543
1 0x10 (0b00010000) same 2b 0 +276 113 819
2 0x00 (0b00000000) 2b 2b +267 +299 380 1118
3 0x33 (0b00110011) +1b same +247 0 627 1118
4 0x32 (0b00110010) +1b same +80 0 707 1118
5 0x16 (0b00010110) +1b -1b +157 -33 864 1085
6 0x17 (0b00010111) +1b -1b +51 -26 915 1059
7 0x07 (0b00000111) -1b -1b -55 -150 860 909
8 0x26 (0b00100110) -1b +1b -139 +52 721 961
9 0x23 (0b00100011) -1b same -98 0 623 961
10 0x22 (0b00100010) -1b same -166 0 457 961
11 0x06 (0b00000110) -1b -1b -158 -209 299 752
12 0x15 (0b00010101) same -1b 0 -207 299 545
13 0x14 (0b00010100) same -1b 0 -199 299 346
14 0x16 (0b00010110) +1b -1b +158 -211 457 135
15 0x33 (0b00110011) +1b same +155 0 612 135
16 0x32 (0b00110010) +1b same +145 0 757 135
17 0x37 (0b00110111) +1b +1b +140 +64 897 199
18 0x15 (0b00010101) same -1b 0 -160 897 39
19 0x06 (0b00000110) -1b -1b -114 -59 783 -20
20 0x23 (0b00100011) -1b same -169 0 614 -20
21 0x22 (0b00100010) -1b same -237 0 377 -20
22 0x00 (0b00000000) 2b 2b -264 +291 113 271

座標の単位はFUNITです。

とりあえず制御点列が得られたので、プロットしていきましょう。簡
単のため、今回は10 FUNITを1mmとします。このフォントはEMが2048 FUNITなので、EMスクエアは204.8mmとなる計算です。
ここでは、黒い点がon-curve point、赤い×がoff-curve pointです。
f:id:nixeneko:20161001195629p:plain

それっぽいですね。

'a' 0x07C4 - 0x08B4

同様に読んでいきます。

ヘッダー
Type Name value 備考
SHORT numberOfContours 0x0002 2; >= 0 なのでsingle glyph
SHORT xMin 0x005E Minimum x for coordinate data.
SHORT yMin 0xFFEC Minimum y for coordinate data.
SHORT xMax 0x03D7 Maximum x for coordinate data.
SHORT yMax 0x045C Maximum y for coordinate data.

numberOfCoutours >= 0 なので、つまり合成グリフでないので、次のテーブルが後続します。

Simple Glyph Description
Type Name value Description
USHORT endPtsOfContours[n] 0x001A 26; Array of last points of each contour; n = numberOCoutours
0x0025 37
USHORT instructionLength 0x007B 123; instructionsのバイト数
BYTE instructions[n] 以下 TrueType命令
BYTE flags[n] 以下 このフラグに従って座標データを読む
BYTE or SHORT xCoordinates[ ] 以下 x座標データ差分
BYTE or SHORT yCoordinates[ ] 以下 y座標データ差分
TrueType命令: 0x07D4~0x084F

制御点列とその座標
idx flag On Curve x y Δx Δy x座標 y座標
0 0x13 (0b00010011) +1b 2b +94 +305 94 305
1 0x10 (0b00010000) same 2b 0 +334 94 639
2 0x25 (0b00100101) 2b +1b +527 +16 621 655
3 0x37 (0b00110111) +1b +1b +186 +7 807 662
4 0x35 (0b00110101) same +1b 0 +65 807 727
5 0x34 (0b00110100) same +1b 0 +125 807 852
6 0x26 (0b00100110) -1b +1b -108 +119 699 971
7 0x23 (0b00100011) -1b same -119 0 580 971
8 0x22 (0b00100010) -1b same -87 0 493 971
9 0x06 (0b00000110) -1b -1b -155 -52 338 919
10 0x07 (0b00000111) -1b -1b -68 -32 270 887
11 0x27 (0b00100111) -1b +1b -55 +135 215 1022
12 0x3E (0b00111110) +1b +1b +83 +44 298 1066
13 0x01 +1b +1b +196 +50 494 1116
14 0x33 (0b00110011) +1b same +96 0 590 1116
15 0x32 (0b00110010) +1b same +199 0 789 1116
16 0x16 (0b00010110) +1b -1b +194 -176 983 940
17 0x15 (0b00010101) same -1b 0 -192 983 748
18 0x11 (0b00010001) same 2b 0 -748 983 0
19 0x23 (0b00100011) -1b same -131 0 852 0
20 0x27 (0b00100111) -1b +1b -35 +156 817 156
21 0x23 (0b00100011) -1b same -8 0 809 156
22 0x0E (0b00001110) -1b -1b -82 -103 727 53
23 0x01 -1b -1b -163 -73 564 -20
24 0x23 (0b00100011) -1b same -124 0 440 -20
25 0x22 (0b00100010) -1b same -162 0 278 -20
26 0x26 (0b00100110) -1b +1b -184 +170 94 150
27 0x37 (0b00110111) +1b +1b +187 +153 281 303
28 0x14 (0b00010100) same -1b 0 -86 281 217
29 0x16 (0b00010110) +1b -1b +105 -92 386 125
30 0x33 (0b00110011) +1b same +95 0 481 125
31 0x32 (0b00110010) +1b same +151 0 632 125
32 0x36 (0b00110110) +1b +1b +173 +163 805 288
33 0x3D (0b00111101) same +1b 0 +150 805 438
34 0x01 same +1b 0 +99 805 537
35 0x07 (0b00000111) -1b -1b -162 -7 643 530
36 0x0E (0b00001110) -1b -1b -189 -7 454 523
37 0x01 -1b -1b -173 106 281 417

制御点の0-26と27-37が別々のcontourになります。
cと同様にaの制御点列もプロットしていきましょう。'hmtx'で見たようにcは幅983なので、aは制御点のx座標に983を加えた場所にプロットします。
f:id:nixeneko:20161001201014p:plain

't' 0x147C - 0x1544

これも同様に読んでいきます。

ヘッダー
Type Name value 備考
SHORT numberOfContours 0x0001 1; >= 0 なのでsingle glyph
SHORT xMin 0x0021 Minimum x for coordinate data.
SHORT yMin 0xFFEC Minimum y for coordinate data.
SHORT xMax 0x02B6 Maximum x for coordinate data.
SHORT yMax 0x0546 Maximum y for coordinate data.

numberOfCoutours >= 0 なので、つまり合成グリフでないので、次のテーブルが後続します。

Simple Glyph Description
Type Name value Description
USHORT endPtsOfContours[n] 0x0016 22; Array of last points of each contour; n = numberOCoutours
USHORT instructionLength 0x007A 122; instructionsのバイト数
BYTE instructions[n] 以下 TrueType命令
BYTE flags[n] 以下 このフラグに従って座標データを読む
BYTE or SHORT xCoordinates[ ] 以下 x座標データ差分
BYTE or SHORT yCoordinates[ ] 以下 y座標データ差分
TrueType命令 (0x148A-0x1504)

制御点列とその座標
idx flag On Curve x y Δx Δy x座標 y座標
0 0x13 (0b00010011) +1b 2b +33 +958 33 958
1 0x35 (0b00110101) same +1b 0 +86 33 1044
2 0x3F (0b00111111) +1b +1b +157 +72 190 1116
3 0x01 +1b +1b +72 +234 262 1350
4 0x33 (0b00110011) +1b same +107 0 369 1350
5 0x15 (0b00010101) same -1b 0 -252 369 1098
6 0x21 (0b00100001) 2b same +317 0 686 1098
7 0x15 (0b00010101) same -1b 0 -140 686 958
8 0x21 (0b00100001) 2b same -317 0 369 958
9 0x11 (0b00010001) same 2b 0 -634 369 324
10 0x14 (0b00010100) same -1b 0 -95 369 229
11 0x16 (0b00010110) +1b -1b +91 -102 460 127
12 0x33 (0b00110011) +1b same +81 0 541 127
13 0x32 (0b00110010) +1b same +35 0 576 127
14 0x36 (0b00110110) +1b +1b +94 +14 670 141
15 0x37 (0b00110111) +1b +1b +24 +9 694 150
16 0x15 (0b00010101) same -1b 0 -138 694 12
17 0x0E (0b00001110) -1b -1b -25 -11 669 1
18 0x01 -1b -1b -105 -21 564 -20
19 0x23 (0b00100011) -1b same -54 0 510 -20
20 0x20 (0b00100000) 2b same -322 0 188 -20
21 0x19 (0b00011001) same 2b 0 +339 188 319
22 0x01 same 2b 0 +639 188 958

また、tについてもプロットしていきましょう。cとaの幅 = 983 + 1149をx座標に加えた位置にプロットします。
f:id:nixeneko:20161001201414p:plain

アウトライン描画

さて、これら制御点列からアウトラインを描いていきましょう。
on-curve pointはその名前の通りグリフの輪郭(=contour)の上にある点です。最終的なアウトラインはon-curve pointを通ります。
一方でoff-curve pointは曲線の曲がり方を制御する点で、最終的なアウトラインは普通この点を通りません。


on-curve pointが2つ連続すると直線になります。連続するoff-curve pointを黒線で結びます。
f:id:nixeneko:20161001201811p:plain
tは大体直線でできていますね。

次に、曲線を求めていかないといけないのですが、まず準備として制御点列を線で結びます。
f:id:nixeneko:20161001202006p:plain

そして、連続するoff-curve pointの中点にon-curve pointを打っていきます。
f:id:nixeneko:20161001202148p:plain
こうすることで、曲線の部分は2次ベジエ(Bézier)曲線の集まりとみなせます。


さて、2次ベジエを描いていきましょう。2次ベジエは3つの制御点からなる曲線で、両端の点を通り真ん中の点の方に近づくカーブを描きます。ここでは、on-curve・off-curve・on-curveの3点の並び(●-×-●の並び)が2次ベジエ曲線です。

今回は、ド・カステリョ(De Casteljau)のアルゴリズムの考え方によって、ベジエ曲線を半分半分に繰り返し分割していくことで求めて行きたいと思います。

まず、2次ベジエの隣り合う制御点の中点同士を結びます。
f:id:nixeneko:20161001203213p:plain
次にその中点を求めるとそれがon-curve pointとなります。
f:id:nixeneko:20161001203402p:plain

さて、分割されたベジエ曲線は次の図の●-+-●の並びになります。
f:id:nixeneko:20161001203553p:plain

間隔が十分に狭いところを除いて、さらに繰り返し分割していきます。
f:id:nixeneko:20161001204005p:plain
制御点を結んだ線がどんどん求める形に近づいていきますね。
もう一回分割します。
f:id:nixeneko:20161001204126p:plain
も、もういいかな……さらに分割してもいいですが面倒なのと狭くてうまく描けないのであとはフリーハンドで黒い点をなめらかに結んでいきます。
f:id:nixeneko:20161001204306p:plain
完成です! 良さそうですね! これをさらに、制御点を順番にたどっていったときの右手側を塗りつぶすとフォントのレンダリング結果になります。

さて、実際にPhotoshopでフォントを表示したものを重ねてみます。
f:id:nixeneko:20161001204527p:plain
いい感じに一致してますね!

感想

  • こうやって見ていくと自然に仕様書とにらめっこすることになり、フォントファイルについての理解が深まりますね。マゾい暇な人にお勧めします。
  • こんなことするより仕様に従ってフォントを読み込むソフトウェア作ったほうが生産性があるかもしれません。
  • データを格納する際に、容量が小さくなるような工夫が随所にあるのがわかります。オフセット指定によって内容が同一なものは複数記録する必要がなかったり、座標は差分のみを記録することで容量を節約したり、ランレングス圧縮的な要素もあります。
  • フォントヒンティングに関するTrueType命令の動作を把握することについては、フォントから制御点の座標を読み出す以上に面倒そうなので途中で投げました。詳しい人に動作を教わりたいです。
  • もうやらない。