にせねこメモ

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

OpenType/CFFのフォントを読んでみる

フォントと仲良くなりたい。
どうすれば仲良くなれるのか。フォントの構造を知れば多少は仲良くなったと言えるのではないか。という訳で、現在コンピュータで一般的に使われているOpenTypeフォントを、構造を調べつつ読んでいくことにする。


OpenTypeフォントはアウトライン形式にTrueType形式かPostScript (CFF)形式のどちらかを選ぶことができる。TrueTypeアウトラインのフォントを読むのは以前やった。

今回は、PostScriptアウトラインのフォントを読むのをやってみる。
実際に読んでいくフォントとして、Noto Sans CJK JP RegularのVersion 1.004を用意した。(NotoSansCJKjp-Regular.otf)

「ねこ」という文字列を描画してみたい。


全部のテーブルを読むのは大変なので、アウトラインの描画に必要な部分だけ押さえていくことにする。
バイナリエディタ(今回はStirlingを利用した)を使ってNotoSansCJKjp-Regular.otfを開いて、見ていく。
f:id:nixeneko:20180518204021p:plain

OpenTypeの仕様書は次にあるので、これを参照しつつ読んでいく。

また、TrueTypeアウトラインのOpenTypeフォントと共通な部分については前回の記事も理解の役に立つかもしれない。

OpenType font file

まず、最初の4バイトが4F 54 54 4F'OTTO'であることからCFFアウトラインであることがわかる。
Offset Tableは次のようになっている。

Offset Table

Type Name Value 備考
uint32 sfntVersion 0x4F54544F ('OTTO') CFF
uint16 numTables 0x0010 (16) Number of tables.
uint16 searchRange 0x0100 (256) (Maximum power of 2 <= numTables) x 16.
uint16 entrySelector 0x0004 (4) Log2(maximum power of 2 <= numTables).
uint16 rangeShift 0x0000 (0) NumTables x 16-searchRange.

numTablesに示された通り、テーブルが16個ある。
テーブルは次のような形式である。

Table Records

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

に従って読む。どんなテーブルがあるかを見ていくと、次のようになっている。

tag checkSum offset length
'BASE' 0xEDFAF516 0x00FAA7EC 0x000000F0
'CFF ' 0x099D40CA 0x00045230 0x00EBA647
'DSIG' 0x00000001 0x00F3F594 0x00000008
'GPOS' 0x6016F4D4 0x00F3F59C 0x0000A97A
'GSUB' 0x4B9CB038 0x00F49F18 0x0002054A
'OS/2' 0x9F6317EE 0x00000170 0x00000060
'VORG' 0xBB25F31E 0x00F6A464 0x000003A0
'cmap' 0x28008683 0x00000FB8 0x00044255
'head' 0x09E560BE 0x0000010C 0x00000036
'hhea' 0x0C1209A5 0x00000144 0x00000024
'hmtx' 0x934BC902 0x00F6A804 0x0003FFC4
'maxp' 0xFFFF5000 0x00000168 0x00000006
'name' 0x4CE51A83 0x000001D0 0x00000DE5
'post' 0xFF860032 0x00045210 0x00000020
'vhea' 0x097F15AE 0x00FAA7C8 0x00000024
'vmtx' 0xD03AB332 0x00EFF878 0x0003FD1C

ここで、

  • TrueType・CFF共通の必須テーブルがcmap, head, hhea, hmtx, maxp, name, OS/2, post
  • CFF独自のものがCFF, VORG (VORGはoptional)
  • 高度組版機能向けがBASE, GDEF, GPOS, GSUB
  • 他のoptionalテーブルがvhea, vmtx

である。

'head' - Font Header Table

Offset Tableから、フォントファイル先頭から数えて0x10Cバイト目から0x142バイト目までが'head'である。

uint16 majorVersion 0x0001 1固定
uint16 minorVersion 0x0000 0固定
Fixed fontRevision 0x00010106 およそ1.004
uint32 checkSumAdjustment 0x9B4A5969
uint32 magicNumber 0x5F0F3CF5 0x5F0F3CF5固定
uint16 flags 0x0003 Bit 0: Baseline for font at y=0;
Bit 1: Left sidebearing point at x=0
uint16 unitsPerEm 0x03E8 1000
LONGDATETIME created 0x00000000D1A40DF0 2015-06-15T05:06:56+00:00
LONGDATETIME modified 0x00000000D1A40DF0 同上
int16 xMin 0xFC16 -1002; バウンディングボックスのx最小値
int16 yMin 0xFBE8 -1048; 同y最小値
int16 xMax 0x0B70 2928; 同x最大値
int16 yMax 0x0710 1808; 同y最大値
uint16 macStyle 0x0000
uint16 lowestRecPPEM 0x0003 最小可読サイズ3px
int16 fontDirectionHint 0x0002 2固定
int16 indexToLocFormat 0x0000 short offsets
int16 glyphDataFormat 0x0000 0固定

'maxp' - Maximum Profile

0x00000168から6バイト。

Type Name Value 備考
Fixed version 0x00005000 0x00005000 for version 0.5
uint16 numGlyphs 0xFFFF フォントに含まれるグリフの数

グリフ数がOpenTypeの仕様が許す最大(65535個)になっている。

'cmap' - Character To Glyph Index Mapping Table

文字コードからGlyph ID (GID)を引くための情報が記録されている。offset 0x00000FB8から読む。

cmap Header

Type Name Value 備考
uint16 version 0x0000 0
uint16 numTables 0x0006 encoding tableの数: 6個

6つのencoding tableが存在しているのがわかる。Encoding tableの形式は

Type Name 備考
uint16 platformID Platform ID
uint16 encodingID Platform固有encoding ID
Offset32 offset このテーブル先頭からのオフセット

なので、読み出すと、次のようになっている。

platformID encodingID offset
0x0000 (Unicode) 0x0003 (Unicode 2.0 and onwards semantics, Unicode BMP only) 0x00006AC1
0x0000 (Unicode) 0x0004 (Unicode 2.0 and onwards semantics, Unicode full repertoire) 0x00014CF5
0x0000 (Unicode) 0x0005 (Unicode Variation Sequences) 0x00000034
0x0001 (Macintosh) 0x0001 (Japanese) 0x00006AB5
0x0003 (Windows) 0x0001 (Unicode BMP (UCS-2)) 0x00006AC1
0x0003 (Windows) 0x000A (Unicode UCS-4) 0x00014CF5

「ねこ」(U+306D, U+3053)はBMP内で収まるし、Unicode BMP (UCS-2)が妥当だろう。Windowsのつもりでいくと、platformID=0x0003 (Windows), encodingID=0x0001 (Unicode BMP (UCS-2))のサブテーブルはオフセットが0x00006AC1なので*1、0x00000FB8+0x00006AC1=0x7A79番地から読んでいく。

00 04で始まるので'cmap' Subtable Format 4であることがわかる。サブテーブルの長さが0xE234=57908と長い。

cmap Subtable Format 4
Type Name Value 備考
uint16 format 0x0004 Format number 4
uint16 length 0xE234 57908; このsubtableの長さ
uint16 language 0x0000 Mac用以外では0
uint16 segCountX2 0x0CB6 3254; 2 × segCount. よってsegCount=1627
uint16 searchRange 0x0800 2 × (2**floor(log2(segCount)))
uint16 entrySelector 0x000A log2(searchRange/2)
uint16 rangeShift 0x04B6 2 × segCount - searchRange
uint16*1627 endCount[segCount] (0x7A87番地~) End characterCode for each segment, last=0xFFFF.
uint16 reservedPad 0x0000 0固定 (0x873D番地)
uint16*1627 startCount[segCount] (0x873F番地~) Start character code for each segment.
int16*1627 idDelta[segCount] (0x93F5番地~) Delta for all character codes in segment.
uint16*1627 idRangeOffset[segCount] (0xA0AB番地~) Offsets into glyphIdArray or 0
uint16 glyphIdArray[ ] Glyph index array (arbitrary length)

「ねこ」(U+306D, U+3053)なので、ひらがなの辺りに固まって定義されているだろうと考えられる。
endCountを順に見ていって、初めて0x306D, 0x3053を超えるところを調べると、0x3096 (0x7BBB番地)がそれであり、そのindexは(0x7BBB-0x7A87)/2 = 0x9A (154)である。対応するstartCountは0x873F+0x9A*2=0x8873番地の0x3041である。idDeltaは0xD56C (0x9529番地)であり、idRangeOffsetは0x0000 (0xA1DF番地)である。

index endCount startCount idDelta idRangeOffset
154 0x3096 0x3041 0xD56C (-10900) 0

ここから、U+306D, U+3053はstartCount~endCountの間に挟まれているのでマッピングが定義されていることがわかる。idRangeOffsetが0なので、idDeltaを加算してGlyph IDを計算すると、

文字 文字コード Glyph ID
0x306D 0x5D9
0x3053 0x5BF

となる。

'hmtx' - Horizontal Metrics

グリフ幅と左サイドベアリングが記録されている。

Offsetが0x00F6A804なのでそこから読む。Glyph IDに対応する場所(ね: 0x00F6A804+0x5D9*4=0xF6BF68, こ: 0x00F6A804+0x5BF*4=0xF6BF00)を読むと、次のようになっている。

文字 Glyph ID advanceWidth lsb
0x5D9 0x03E8 (1000) 0x003B
0x5BF 0x03E8 (1000) 0x00B0

advanceWidth、つまりグリフ幅は両方とも1000であることが分かる。

'CFF ' - Compact Font Format table

さて、PostScriptアウトラインが格納されているCFFフォントの本体である。0x00045230番地から0x00EBA647バイト長のテーブルである。
OpenType仕様書にはCFFに関する仕様がほとんどなく、Adobeの技術文書を参照せよということになっている*2

The Compact Font Format SpecificationはCFFにどんな構造でデータが格納されているかを定義してあり、Type 2 Charstring Formatはアウトラインデータの記述方法を定義している。また、Type 2 CharstringはAdobe Type 1 Font Formatの知識を前提としている部分があり、そちらの仕様書も参照した方がよいかもしれない。
まずはThe Compact Font Format Specificationを参照しながら、CFFテーブルを解析していく。

また、日本語では次のサイトも参考になる。

データ型(type)

CFFのデータ型としては次のものがある。

Name Range バイト数 説明
Card8 0-255 1 符号無し整数
Card16 0-65536 2 符号無し整数
OffSize 1-4 1 符号無し整数。オフセットのバイト数を指定する
Offset 可変 1-4 オフセット(サイズはOffSizeで指定されたバイト数)
SID 0-64999 2 文字列ID

SIDは文字列を指定する数値である。

CFFのデータ構造

Entry コメント
Header 先頭固定
Name INDEX Headerの次固定
Top DICT INDEX Name INDEXの次固定
String INDEX Top DICT INDEXの次固定
Global Subr INDEX String INDEXの次固定
Encodings OpenTypeフォントのCFFテーブルとしては存在なし
Charsets
FDSelect CIDフォントのみ
CharStrings INDEX フォント毎
Font DICT INDEX フォント毎、CIDフォントのみ
Private DICT フォント毎
Local Subr INDEX フォント毎、またはCIDフォントのPrivate DICT毎

データ構造としてINDEXとDICTという構造があり、INDEXはリストや配列のようなデータを、DICTはキーとそれに対する値の組を保持する。
Header~Name INDEX~Top DICT INDEX~String INDEX~Global Subr INDEXは必ず存在し、この順番で隙間なく配置されるため、CFFデータの頭から順番に読んでいかないといけない。
他のデータは順番が不定であり、Top DICT INDEX等に含まれるオフセットを使って参照することになる。Encodings, Charsetsは存在しなくてもよい。

Header

最初にHeaderがくる。(0x00045230番地)

Type Name Value Description
Card8 major 0x01 Format major version (starting at 1)
Card8 minor 0x00 Format minor version (starting at 0)
Card8 hdrSize 0x04 ヘッダーのサイズ (バイト)
OffSize offSize 0x03 Absolute offset (0) size

offSizeによって、CFFデータ先頭からのオフセット(“offset (0)”)が3バイトであると指定されている。
CFFの仕様書ではCFFテーブル先頭からのオフセットを“offset (0)”、各データ構造の先頭からのオフセットを“offset (self)”と書いて区別している。HeaderのoffSizeはすべての“offset (0)”のサイズを指定する。

Name INDEX

続いてName INDEXがくる。

INDEX

INDEXは次のような形式である。

Type Name 説明
Card16 count INDEXに含まれるオブジェクトの個数
OffSize offSize オフセット列の各要素のバイト数
Offset offset[count+1] オフセット列
Card8 data[] オブジェクトデータ

INDEXは配列やリストのようなデータを保管する。
countがオブジェクトの個数を表す。オブジェクトが一個もない空のINDEXの場合はcountが0となり、他のフィールドは存在せず、全体として2バイトとなる。
offSizeは続いて指定するオフセット列に含まれる一つ一つのオフセットのサイズ(バイト数)を指定する。
offsetには(データの個数+1)個のオフセットが並ぶ。dataにはオブジェクトが隙間なく並べてあり、オフセットでアクセスする。オフセットはオブジェクトデータ先頭の直前のバイトからのオフセットであり、オブジェクトデータ先頭は1となる。n番目(1 ≦ n ≦ count)のオブジェクトは、オフセット列のn番目とn+1番目のオフセットをつかってアクセスできる。n+1番目のオフセットはn+1番目のオブジェクトの先頭を表すので、n+1番目のオフセットの指す位置の直前までがn番目のオブジェクトである。

これにしたがってName INDEXを解析すると

Type Name Value 備考
Card16 count 0x0001 データ個数1
OffSize offSize 0x01 1バイト
Offset offset[count+1] 0x01 0x16 オフセットが1から22まで
Card8 data[] NotoSansCJKjp-Regular

となる。
Name INDEXはCFFデータに含まれるフォントの名前(PostScript language name)を指定する。CFFデータ自体はフォントを複数含むことができ、フォントの数だけフォント名が含まれる。しかし、OpenTypeのテーブルとしてのCFFデータではName INDEXのデータは必ず一つだけで、つまり一つのフォントしか含むことはできない。ここでは“NotoSansCJKjp-Regular”である。この名前はOpenTypeの'name'テーブルのID 6で指定されるものと一致している必要がある。

Top DICT INDEX

続いて0x04524E番地からTop DICTを入れたINDEXがくる。

Type Name Value 備考
Card16 count 0x0001 データ個数1
OffSize offSize 0x01 1バイト
Offset offset[count+1] 0x01 0x56 オフセットが1から86まで
Card8 data[] (Top DICT Data)

CFFデータに含まれるフォントの数が1つなので、フォントに対応するTop DICTも1つである。

Top DICT Data

読みだされたTop DICTのデータは次のバイナリである。

F8 1B F8 1C 8B 0C 1E F8 1D 01 F8 1E 02 F8 1F 03 
F8 18 04 FB 2A 0C 03 FE 7E FE AC 1D 00 00 0B 70 
1D 00 00 07 10 05 8C 96 1D 00 8D 83 C8 0E 1E 1A 
00 4F 0C 1F 1D 00 00 FF FF 0C 22 1D 00 00 3C B6 
0F 1D 00 00 3C BB 0C 25 1D 00 DC 32 6B 0C 24 1D 
00 00 3E 13 11
DICTデータ

DICTデータはキーと値のペアをバイト長を小さくなるような形で記録したものである。
一つ以上の値が並び、その後にキーがくることで、キーと値のペアを表現する。例えば、次のような感じになる。

[値A] [キーA] [値B1] [値B2] [値B3] [値B4] [キーB]

ここで、0~21の範囲のバイトがキーを示し(12のみ例外で、後続のバイトと合わせて2バイトでキーとなる)、それ以外は値を示す。値の表現は、データのバイト長を削減するために複雑になっており、詳しくは仕様書を参照。これがDICTデータの終端まで続く。

仕様書を読むと分かるが、人が読めるようにはなっていないので、プログラムを書いて何とかする。これを人が読める形にするための簡易的なPythonスクリプトを作成した。出力がキーは数値が< >で囲まれるようにして値と区別している。

このスクリプトで先ほどのDICTを読み出すと次のようになった。キーによって指定される項目について名前や説明を付記した。いくつかの項目はOpenTypeのテーブルと同じ値が格納されている。

Value(s) Key Name Operand 説明
391 392 0 12 30 ROS SID SID number CID用. Registry Ordering Supplement
393 1 Notice SID
394 2 FullName SID
395 3 FamilyName SID
388 4 Weight SID
-150 12 3 UndelinePosition number
-1002 -1048 2928 1808 5 FontBBox array
1 11 9274312 14 XUID array
1.004 12 31 CIDFontVersion number CID用
65535 12 34 CIDCount number CID用
15542 15 charset number charset offset (0)
15547 12 37 FDSelect number CID用. FDSelect offset(0)
14430827 12 36 FDArray number CID用. Font DICT (FD) INDEX offset(0)
15891 17 CharStrings number CharStrings offset (0)

最初のROSは、次に読んでいくString INDEXの文字列を参照することにより、Adobe-Identity-0であるとわかる。これは独自定義のCIDである。

String INDEX

続いて0x452A8番地からString INDEXがくる。フォント名以外の文字列が格納されているらしい。アクセスにはstring identifier (SID)を使う。SIDの一部は事前定義されており、Appendix AによるとSID 390までがstandard stringとして定義済みとなっている。そのため、SID 391がString INDEXの0番目の項目となる。

Type Name Value 備考
Card16 count 0x0018 データ個数24
OffSize offSize 0x02 2バイト

offsetとdataは次のようになる。

offset data
0x0001 Adobe
0x0006 Identity
0x000E Copyright 2014, 2015 Adobe Systems Incorporated (http://www.adobe.com/). Noto is a trademark of Google Inc.
0x0079 Noto Sans CJK JP Regular
0x0091 Noto Sans CJK JP
0x00A1 NotoSansCJKjp-Regular-Alphabetic
0x00C1 NotoSansCJKjp-Regular-AlphabeticDigits
0x00E7 NotoSansCJKjp-Regular-Bopomofo
0x0105 NotoSansCJKjp-Regular-Dingbats
0x0123 NotoSansCJKjp-Regular-DingbatsDigits
0x0147 NotoSansCJKjp-Regular-Generic
0x0164 NotoSansCJKjp-Regular-HDingbats
0x0183 NotoSansCJKjp-Regular-HHangul
0x01A0 NotoSansCJKjp-Regular-HKana
0x01BB NotoSansCJKjp-Regular-HWidth
0x01D7 NotoSansCJKjp-Regular-HWidthCJK
0x01F6 NotoSansCJKjp-Regular-HWidthDigits
0x0218 NotoSansCJKjp-Regular-Hangul
0x0234 NotoSansCJKjp-Regular-Ideographs
0x0254 NotoSansCJKjp-Regular-Kana
0x026E NotoSansCJKjp-Regular-Proportional
0x0290 NotoSansCJKjp-Regular-ProportionalCJK
0x02B5 NotoSansCJKjp-Regular-ProportionalDigits
0x02DD NotoSansCJKjp-Regular-VKana
0x02F8

Global Subr INDEX

次は0x455D4番地からGlobal Subr INDEXがくる。これは、文字のアウトラインの定義から呼び出すことのできるサブルーチンの集合で、globalとつくように、CFFデータに含まれるどのフォントからでも呼び出すことができる。サブルーチンが一つも存在しない場合は空のINDEXとなる。

最初を見ていくと、

Type Name Value 備考
Card16 count 0x06 4D データ個数1613
OffSize offSize 0x02 2バイト

となり、個数も多いので必要になったら参照することにする。

Encodings

Top DICTでencodingが存在する場合はオフセットが指定されるが、これはCIDフォントなのでencodingはない。また、OpenType/CFFでも省略される。

Charsets

グリフはSID (name-keyed)またはCID (CID-keyed)によって同定される。一方で、GID (glyph index)とSID/CIDは一致しているとは限らず*3マッピングを定義する必要がある。これを指定するのがcharsetである。GID 0は必ず“.notdef”なので、GID 0のグリフの指定は必須でなく、この場合charsetの配列のインデックスはGID 1から始まる。

Top DICTで指定されたようにオフセットが15542である。つまり0x00045230+15542=0x48EE6番地から始まる。
02…と始まっているのでこれはFormat 2のcharsetであることがわかり、次のように定義されている。

charset format 2
Type Name Description
Card8 format 2
struct Range2[] Range2 array
Range2 format
Type Name Description
SID first 範囲の最初のグリフ
Card16 nLeft 最初のグリフを除いた、範囲に含まれるグリフの個数

format 0はSIDがランダムな場合、format 1はそこそこまとまっている場合、format 2は東アジアのCIDフォントのようなlarge well-ordered charsetの場合に向くらしい。format 1と2はnLeftのバイト数が1か2かだけが異なる。

読み出して行くと、

first nLeft
0x0001 0xFFFD

これだけ。(GID 0の.notdef以外の)65534個、つまりすべてのグリフについてCIDはGIDに一致するということらしい。

CharStrings INDEX

Top DICTより、0x00045230+15891=0x49043番地から始まる。実際の文字のアウトラインの定義がType 2 Charstringの形式で記録されている。

Type Name Value 備考
Card16 count 0xFFFF データ個数65535
OffSize offSize 0x02 3バイト

Charstringのデータは仕様書をみるとGIDでアクセスするらしい。
必要なのはGID 0x5D9 (ね), 0x5BF (こ)であるので、これらを読みだす。

0x49043+3+0x5D9*3=0x4A1D1番地からの数値を読み出すと0x0167C6, 0x016885となっている。対応するデータを0x49043+3+0x10000*3+0x0167C6-1=0x8F80B~0x8F8CA番地の間で読みだすと

85 D2 F7 22 CC F7 90 DE 71 D1 12 F7 A1 D3 52 CE 
52 D2 F5 D0 F7 93 D7 13 D9 80 F9 15 CC 15 53 5E 
A1 BE B3 B8 A8 BE BA B8 7E 74 B6 1F 4A 75 64 62 
46 1B 13 D3 80 FB A3 F8 D4 15 3A 0A 13 E3 80 80 
89 67 88 5D 1E 58 82 51 85 6B 89 73 8A 78 8A 75 
8C 93 39 18 13 E5 80 31 0A 13 D9 80 24 0A B1 8C 
B7 8E B8 1E E8 E2 F7 14 EA E7 1B DD C8 3F FB 10 
59 89 5C 85 61 1F 9E 60 5B 96 56 1B 26 44 51 3C 
27 DB 62 EA EF C6 BB DF AC 1F A8 73 A9 6E A8 6A 
B5 CC 18 69 AF 67 AC 63 A7 08 94 BB 8F C2 C9 1A 
F7 38 44 F7 0C FB 1A 1E 13 D5 80 FB 00 FB 0F 33 
40 37 1F 8C 9C 8D 9B 8C 9B 08 13 D3 80 B8 0A

である。

0x49043+3+0x5BF*3=0x4A183番地からの数値を読み出すと0x015A39, 0x015A81となっている。対応するデータを
0x49043+3+0x10000*3+0x015A39-1=0x8EA7E~0x8EAC6番地の間で読みだすと

77 DD F8 B6 DB 01 F7 44 DD 03 F7 81 F8 FF 15 84 
DA DF 87 EE 1B E6 F7 01 92 90 CE 1F DD 07 84 44 
26 5C 0A 62 FB CA 15 82 62 CB 0A 49 F7 5D F7 21 
F7 12 9A 9E D2 1E 8A E1 05 74 60 0A BF D5 B0 93 
AF 97 B3 1F 39 93 05 0E

となっている。
これらはType 2 Charstringであり、その仕様書にあたる必要がある。後で読んでいく。

FDSelect

CIDフォントでは、描画に必要なPrivate DICTを指定するFont DICTの配列FDArrayが存在し、どのFont DICTを用いるかはFDSelectによって指定されている。
0x00045230+15547=0x48EEB番地からFDSelectが始まる。FDSelectの構造はcharsetと似ている。ここで、03から始まっているのでFormat 3だとわかる。

FDSelect Format 3
Type Name Value 説明
Card8 format 0x03 format 3
Card16 nRanges 0x0071 範囲の個数: 113個
struct Range3[nRanges] (略) Range3 array
Card16 sentinel 0xFFFF Sentinel GID

Range3は次のように定義されている。

Range3 Format
Type Name 説明
Card16 first 範囲の最初のグリフのインデックス
Card8 fd 範囲に含まれる全てのグリフに対応するFD index

あるGIDについて最初から順番にみていって、初めて求めるGIDより大きなfirstをもつRange3があったら、その直前のRange3構造体のもつfdが求めるFD indexとなる。
欲しいのはGID 0x5D9 (ね), 0x5BF (こ)に対応するFD indexであるから、0x48F90番地からの

first fd
0x05AD 0x0E
0x0603 0x03

から、GID 0x5D9 (ね), 0x5BF (こ)のFD indexはともに0x0E (14)であるとわかる。

FDArray

0x00045230+14430827=0xE0849B番地にFDArrayがある。これはFont DICTの入ったINDEXである。
ここから、FD indexが0x0E (14)のものを取り出してみる。

Type Name Value 備考
Card16 count 0x0013 データ個数19
OffSize offSize 0x01 1バイト

(0オリジンで)14, 15番目のoffsetをみると0x9B, 0xA6となっているので、その間(0xE084B2+0x9B-1=0xE0854C~0xE08557)を読むと、

F8 2E 0C 26 A1 1D 00 EB 78 6C 12

となっており、これを先ほどのDICTを読み出すpythonスクリプトで読みだすと

Value(s) Key Name Operand 説明
410 12 38 FontName SID NotoSansCJKjp-Regular-Kana
22 15431788 18 Private number number Private DICT size, offset (0)

となった。Appendix AによりSID 390までがstandard stringとして定義済みのようなので、SID 391がString INDEXの0番目の項目となる。FontNameのSID 410は、ここではString INDEXの410-391=19番目(最初の項目を0番目として数える)の項目であり、これはNotoSansCJKjp-Regular-Kanaである。仮名なので、正しそう。

そしてPrivate DICTのサイズと(CFFテーブル最初からの)オフセットが記載されている。
0x00045230+15431788=0xEFCA9C番地から22バイトであるとわかる。

FB 8E 8B 1D 00 00 05 46 8B 06 D3 0A D5 0B 8C 0C 
11 FA 7C 14 A1 13

同様にPythonスクリプトでDICTを解析すると、

Value(s) Key Name Operand 説明
-250 0 1350 0 6 BlueValues delta
72 10 StdHW number
74 11 StdVW number
1 12 17 LanguageGroup number
1000 20 defaultWidthX number
22 19 Subrs number Offset (self) to local subrs

defaultWidthXは重要で、この値と同じグリフ幅を持つ場合はcharstringでは省略できるらしい。

また、ローカルサブルーチンを格納したINDEXへのオフセットが記録されている。これはPrivate DICTの先頭からのオフセットである。

Local Subrs INDEX

0xEFCA9C+22=0xEFCAB2番地から。charstringあるいはサブルーチンから呼び出されるローカルサブルーチンが記録されている。先頭のみ解析しておく。

Type Name Value 備考
Card16 count 0x00B4 データ個数180
OffSize offSize 0x02 2バイト

あとは必要になったら読みだすこととする。

Charstringの解読

Charstringの数値はCFFのDICTとよく似ていて、人が読むには向かないので、Type 2 Charstringの仕様書に従って、一旦逆アセンブルすることにする。
解析するための簡易的なPythonスクリプトを作成した。次である。

「ね」

前に読みだした「ね」のcharstringを解析すると、次のようになっている。

-6 71 142 65 252 83 -26 70 hstemhm 269 72 -57 67 -57 71 106 69 255 76 
hintmask 0xD980 641 65 rmoveto -56 -45 22 51 40 45 29 51 47 45 -13 -23 43 hvcurveto 
-65 -22 -39 -41 -69 hhcurveto hintmask 0xD380 -271 576 rmoveto -81 callsubr 
hintmask 0xE380 -11 -2 -36 -3 -46 vhcurveto -51 -9 -58 -6 -32 -2 -24 -1 -19 -1 -22 
1 8 -82 rcurveline hintmask 0xE580 -90 callsubr hintmask 0xD980 -103 callsubr 38 1 
44 3 45 vhcurveto 93 87 128 95 92 hhcurveto 82 61 -76 -124 -50 -2 -47 -6 -42 
hvcurveto 19 -43 -48 11 -53 hhcurveto -101 -71 -58 -79 -100 80 -41 95 100 59 48 
84 33 hvcurveto 29 -24 30 -29 29 -33 42 65 rcurveline -34 36 -36 33 -40 28 
rrcurveto 9 48 4 55 62 vvcurveto 164 -71 120 -134 vhcurveto hintmask 0xD580 
-108 -123 -88 -75 -84 hvcurveto 1 17 2 16 1 16 rrcurveto hintmask 0xD380 45 
callsubr

少しずつ読んでいく。今回、ヒントは無視する。


グリフ幅はdefaultWidthX (1000)と同じであるので、最初にWidthが来ず、ヒントステムの指定がくる。

-6 71 142 65 252 83 -26 70 hstemhm 
269 72 -57 67 -57 71 106 69 255 76 
hintmask 0xD980

hstemhmはヒントの水平ステムを指定する。hstemhmに先行する数字列が指定された水平ステムである。この命令の後、hintmask命令がくるまでの値の列は垂直ステムvstemの指定であるらしい。
一つのステムは2つの値のペアで指定するため、水平ステム4個、垂直ステム5個である。hintmask命令は有効にするヒントステムを指定する命令で、マスクが後続し、ステムの数×1ビットのマスク用のフラグがつくため、ステムの数によって命令長が変わる*4。このグリフはステム9個なのでマスクが2バイトである。
以下、#以降はコメントとする。

641 65 rmoveto #(641, 65)

rmovetoによって現在位置を(0, 0)から(641, 65)に移動し、パスの描画を始める。このように、(hintmask, cntrmask命令を除いて)逆ポーランド記法的に引数が前に来るようになっている。数値がくると引数スタックに積まれて行く。命令によっては(特にパス定義命令は)引数が可変であり、引数の個数によって動作が変わることがある。
特にパス定義の命令などは実行されると引数スタックを空にする。スタックを空にする命令は仕様書の定義において

|- dx1 {dya dxb}* hlineto (6) |-

のように“|-”(スタックの底を表す記号)で終わっており、命令が引数スタックを空にすることを示す。また、先頭の“|-”はスタックの底から順番に引数にとっていくことを示している。

CFFフォントにおいては、アウトラインを直線と三次ベジエ曲線によって定義する。
一つの(三次)ベジエ曲線は4つの制御点で定義されるが、charstringにおいては基本的には座標の差分を表す6つの数値dxa, dya, dxb, dyb, dxc, dycで表現される。Type 2 Charstringには特別な条件でバイト数を節約できるようにいくつものオペレータが用意されている。
各オペレータがどのようにパスを定義するかは次掲のページの最後に図次されているものがわかりやすい。

これに従ってパスの定義をみていく。

-56 -45 22 51 40 45 29 51 47 45 -13 -23 43 hvcurveto 

hvcurveto命令は、最初の端点の接ベクトルが水平で最後の端点の接ベクトルが垂直であるベジエ曲線(と、最初の端点の接ベクトルが垂直で最後の端点の接ベクトルが水平であるベジエ曲線とが交互に並ぶ場合)を定義する。次のパスが得られる。

  • 曲線: (641, 65)-(585, 65)-(540, 87)-(540, 138)
  • 曲線: (540, 138)-(540, 178)-(585, 207)-(636, 207)
  • 曲線: (636, 207)-(683, 207)-(728, 194)-(771, 171)
-65 -22 -39 -41 -69 hhcurveto 

hhcurveto命令は端点の接ベクトルが水平のベジエ曲線(の並び)を定義する。次のパスを得る。

  • 曲線: (771, 171)-(749, 106)-(710, 65)-(641, 65)

最初の地点に戻ってきた。

hintmask 0xD380 #無視
-271 576 rmoveto #(370, 641)
-81 callsubr 

rmovetoで次のパスへ移動。続いてcallsubrでローカルサブルーチンの呼び出しを行っている。

callsubrの引数である-81はbiased indexであり、CFFの仕様書にバイアスの詳細があり、Local Subr INDEXに含まれるサブルーチンの個数によって場合分けが行われる。ここではLocal Subr INDEXの個数(count)が180 (<1240)なのでバイアス107を加える。よって、-81は26番目のローカルサブルーチンを表す。
26番目のローカルサブルーチンは、0xEFCAB5+26*2=0xEFCAE9番地から0x0513, 0x0526と書かれていることから、0xEFCAB5+(180+1)*2+0x0513-1=0xEFD131番地から0xEFCAB5+(180+1)*2+0x0526-1=0xEFD144番地までが26番目のサブルーチンである。次に挙げるが、charstringと同じ形式である。

86 89 92 D0 93 C3 90 A3 19 2C 8E 05 90 72 8A 71 74 1A 0B

先掲のPythonスクリプトを使ってType 2 Charstringを訳すと、

-5 -2 7 69 8 56 5 24 rlinecurve -95 3 rlineto 5 -25 -1 -26 -23 vvcurveto return

となる。これを読んでいく。

-5 -2 7 69 8 56 5 24 rlinecurve 

rlinecurveは一つ以上の直線のあとに曲線がくる場合に使われる。

  • 直線: (370, 641)-(365, 639)
  • 曲線: (365, 639)-(372, 708)-(380, 764)-(385, 788)
-95 3 rlineto 

rlinetoは直線を表現する。

  • 直線: (385, 788)-(290, 791)
5 -25 -1 -26 -23 vvcurveto return

vvcurvetoは、端点の接ベクトルが垂直なベジエ曲線(が並ぶ)場合を表現するのに適している。

  • 曲線: (290, 791)-(295, 766)-(294, 740)-(294, 717)

return命令によってサブルーチンを終了している。


charstringに戻って、

hintmask 0xE380 #無視
-11 -2 -36 -3 -46 vhcurveto 
  • 曲線: (294, 717)-(294, 706)-(292, 670)-(289, 624)
-51 -9 -58 -6 -32 -2 -24 -1 -19 -1 -22 1 8 -82 rcurveline 

rcurvelineは一つ以上の曲線の後に直線が続く場合を表現する。

  • 曲線: (289, 624)-(238, 615)-(180, 609)-(148, 607)
  • 曲線: (148, 607)-(124, 606)-(105, 605)-(83, 606)
  • 直線: (83, 606)-(91, 524)
hintmask 0xE580 #無視
-90 callsubr 

callsubrで、-90+107=17番目のサブルーチンを呼び出す。
0xEFCAB5+17*2=0xEFCAD7番地から0x040B, 0x0423と書かれているので、0xEFCAB5+(180+1)*2+0x040B-1=0xEFD029番地から0xEFCAB5+(180+1)*2+0x0423-1=0xEFD041までを読み出すと

C9 93 E1 97 B8 90 89 6C 89 6B 89 6B 59 3D FB 06 FB 2E 54 46 BD 46 18 0B

となっている。これを解読すると

62 8 86 12 45 5 -2 -31 -2 -32 -2 -32 -50 -78 -114 -154 -55 -69 50 -69 rcurveline 
return

となり、次のパスが得られる

  • 曲線: (91, 524)-(153, 532)-(239, 544)-(284, 549)
  • 曲線: (284, 549)-(282, 518)-(280, 486)-(278, 454)
  • 曲線: (278, 454)-(228, 376)-(114, 222)-(59, 153)
  • 直線: (59, 153)-(109, 84)


続いてcharstringに戻り、

hintmask 0xD980 #無視
-103 callsubr 

再度callsubrによるサブルーチン呼び出し。-103+107=4番目のサブルーチンを呼び出す。
0xEFCAB5+4*2=0xEFCABD番地から0x021D, 0x0247と書かれているので、0xEFCAB5+(180+1)*2+0x021D-1=0xEFCE3B番地から0xEFCAB5+(180+1)*2+0x0247-1=0xEFCE65番地までを読み出すと、

BB CD CC EA BB D4 08 7C 8A 7E 81 1A 89 FB 02 8B 
5A 8A 2C 08 7B 89 6E 8A 7D 1E E2 06 89 9D 89 A4 
8A 9C 08 87 E4 8B C7 E5 1A 0B

となっている。これを解読すると、

48 66 65 95 48 73 rrcurveto -15 -1 -13 -10 vvcurveto -2 -110 0 -49 -1 -95 rrcurveto 
-16 -2 -29 -1 -14 vhcurveto 87 hlineto -2 18 -2 25 -1 17 rrcurveto -4 89 0 60 90 
vvcurveto return

である。

順に読んでいくと、

48 66 65 95 48 73 rrcurveto 
  • 曲線: (109, 84)-(157, 150)-(222, 245)-(270, 318)
-15 -1 -13 -10 vvcurveto 
  • 曲線: (270, 318)-(270, 303)-(269, 290)-(269, 280)
-2 -110 0 -49 -1 -95 rrcurveto 
  • 曲線: (269, 280)-(267, 170)-(267, 121)-(266, 26)
-16 -2 -29 -1 -14 vhcurveto 
  • 曲線: (266, 26)-(266, 10)-(264, -19)-(263, -33)
87 hlineto 
  • 直線: (263, -33)-(350, -33)
-2 18 -2 25 -1 17 rrcurveto 
  • 曲線: (350, -33)-(348, -15)-(346, 10)-(345, 27)
-4 89 0 60 90 vvcurveto return
  • 曲線: (345, 27)-(341, 116)-(341, 176)-(341, 266)

サブルーチンは終了。


charstringに戻って、

38 1 44 3 45 vhcurveto 
  • 曲線: (341, 266)-(341, 304)-(342, 348)-(345, 393)
93 87 128 95 92 hhcurveto 
  • 曲線: (345, 393)-(432, 486)-(560, 581)-(652, 581)
82 61 -76 -124 -50 -2 -47 -6 -42 hvcurveto 
  • 曲線: (652, 581)-(734, 581)-(795, 505)-(795, 381)
  • 曲線: (795, 381)-(795, 331)-(793, 284)-(787, 242)
19 -43 -48 11 -53 hhcurveto 
  • 曲線: (787, 242)-(744, 261)-(696, 272)-(643, 272)
-101 -71 -58 -79 -100 80 -41 95 100 59 48 84 33 hvcurveto 
  • 曲線: (643, 272)-(542, 272)-(471, 214)-(471, 135)
  • 曲線: (471, 135)-(471, 35)-(551, -6)-(646, -6)
  • 曲線: (646, -6)-(746, -6)-(805, 42)-(838, 126)
29 -24 30 -29 29 -33 42 65 rcurveline 
  • 曲線: (838, 126)-(867, 102)-(897, 73)-(926, 40)
  • 直線: (926, 40)-(968, 105)
-34 36 -36 33 -40 28 rrcurveto 
  • 曲線: (968, 105)-(934, 141)-(898, 174)-(858, 202)
9 48 4 55 62 vvcurveto 
  • 曲線: (858, 202)-(867, 250)-(871, 305)-(871, 367)
164 -71 120 -134 vhcurveto 
  • 曲線: (871, 367)-(871, 531)-(800, 651)-(666, 651)
hintmask 0xD580 #無視
-108 -123 -88 -75 -84 hvcurveto 
  • 曲線: (666, 651)-(558, 651)-(435, 563)-(351, 488)
1 17 2 16 1 16 rrcurveto 
  • 曲線: (351, 488)-(352, 505)-(354, 521)-(355, 537)
hintmask 0xD380 #無視
45 callsubr

45+107=152番目のサブルーチンを呼び出す。
0xEFCAB5+152*2=0xEFCBE5番地から0x0B6E, 0x0B76と書かれているので、0xEFCAB5+(180+1)*2+0x0B6E-1=0xEFD78C番地から0xEFCAB5+(180+1)*2+0x0B76-1=0xEFD794番地までを読み出すと、

9A A3 9C A6 97 9D 08 0E

となっている。解読すると、

15 24 17 27 12 18 rrcurveto endchar
  • 曲線: (355, 537)-(370, 561)-(387, 588)-(399, 606)

となり、endcharで一つの文字のパスの定義が終了する。ここで、このパスの輪郭が(370, 641)から始まったことから、パスが閉じられていない。パスを閉じるためには次の直線が必要である。

  • 直線: (399, 606)-(370, 641)

以上で「ね」のアウトラインが得られた。

「こ」

続いて「こ」のcharstringは先掲のPythonスクリプトで解読すると次になる。

-20 82 546 80 hstem 176 82 vstem 237 619 rmoveto -7 79 84 -4 99 hhcurveto 91 109 
7 5 67 hvcurveto 82 vlineto -7 -71 -101 -47 callsubr -41 -310 rmoveto -9 -41 
64 callsubr -66 201 141 126 15 19 71 vhcurveto -1 86 rlineto -23 -43 callsubr 52 
74 37 8 36 12 40 hvcurveto -82 8 rlineto endchar

順に読んでいく。

-20 82 546 80 hstem #横stem
176 82 vstem #縦stem定義。stemは合計6個
237 619 rmoveto #(237, 619)

ヒントステムの合計は6個なので、hintmask, cntrmaskのマスクのバイト数は1バイトである。
rmovetoで(0, 0)から(237, 619)に移動、パスの定義をスタートさせる。

-7 79 84 -4 99 hhcurveto 
  • 曲線: (237, 619)-(316, 612)-(400, 608)-(499, 608)
91 109 7 5 67 hvcurveto 
  • 曲線: (499, 608)-(590, 608)-(699, 615)-(766, 620)
82 vlineto 

vlinetoは垂直な線を示す。

  • 直線: (766, 620)-(766, 702)
-7 -71 -101 -47 callsubr 

callsubrで-47+107=60番目のサブルーチンの呼び出し。
0xEFCAB5+60*2=0xEFCB2D番地から0x0776, 0x0782と書かれているので、0xEFCAB5+(180+1)*2+0x0776-1=0xEFD394番地から0xEFCAB5+(180+1)*2+0x0782-1=0xEFD3A0番地までを読み出すと、

84 2C 1B 28 30 8F 94 43 1F 39 07 0B

となっている。解読すると、

-7 -95 hhcurveto -99 -91 4 9 -72 hvcurveto -82 vlineto return

となる。
これを-7 -71 -101の引数をつけて呼び出すので、最初の命令は-7 -71 -101 -7 -95 hhcurveto ...となる。ここでは引数を[~]で囲んで表現しておく。

[-7 -71 -101] -7 -95 hhcurveto
  • 曲線: (766, 702)-(695, 695)-(594, 688)-(499, 688)
-99 -91 4 9 -72 hvcurveto
  • 曲線: (499, 688)-(400, 688)-(309, 692)-(237, 701)
-82 vlineto return
  • 直線: (237, 701)-(237, 619)

サブルーチンは終了。パスが始まった点に戻ってきた。


charstringに戻る。

-41 -310 rmoveto #次のパスへ (196, 309)
-9 -41 64 callsubr 

callsubrで64+107=171番目のサブルーチンの呼び出し。
0xEFCAB5+171*2=0xEFCC0B番地から0x0C12, 0x0C1Bと書かれているので、0xEFCAB5+(180+1)*2+0x0C12-1=0xEFD830番地から0xEFCAB5+(180+1)*2+0x0C1B-1=0xEFD839番地までを読み出すと、

80 5D 57 1A FB 10 F7 09 0B

となっている。解読すると、

-11 -46 -52 vvcurveto -124 117 return

である。

引数を合わせて読んでいくと、

[-9 -41] -11 -46 -52 vvcurveto
  • 曲線: (196, 309)-(187, 268)-(176, 222)-(176, 170)
-124 117 return

最後に数値が残った状態でサブルーチンがreturnされるので、戻った先でその値が使われることになる。


charstringに戻って、返された-124 117も併せて続きを読んでいく。

[-124 117] -66 201 141 126 15 19 71 vhcurveto 
  • 曲線: (176, 170)-(176, 46)-(293, -20)-(494, -20)
  • 曲線: (494, -20)-(635, -20)-(761, -5)-(832, 14)
-1 86 rlineto 
  • 直線: (832, 14)-(831, 100)
-23 -43 callsubr

callsubrで、-43+107=64番目のサブルーチンの呼び出し。
0xEFCAB5+64*2=0xEFCB35番地から0x0797, 0x07A2と書かれているので、0xEFCAB5+(180+1)*2+0x0797-1=0xEFD3B5番地から0xEFCAB5+(180+1)*2+0x07A2-1=0xEFD3C0番地までを読み出すと、

40 FB 13 7C FB 1D 1B FB 32 3F 0B

となっている。解読すると、

-75 -127 -15 -137 hhcurveto -158 -76 return

である。引数-23を合わせて読んでいくと、

[-23] -75 -127 -15 -137 hhcurveto
  • 曲線: (831, 100)-(756, 77)-(629, 62)-(492, 62)
-158 -76 return

サブルーチンは終了。使われなかった-158 -76が返される。


charstringに戻って、返された-158 -76も併せて読んでいく。

[-158 -76] 52 74 37 8 36 12 40 hvcurveto 
  • 曲線: (492, 62)-(334, 62)-(258, 114)-(258, 188)
  • 曲線: (258, 188)-(258, 225)-(266, 261)-(278, 301)
-82 8 rlineto endchar
  • 直線: (278, 301)-(196, 309)

endcharが来たので終了。パスの開始点へともどってきている。

プロット

直線と曲線の制御点の座標が求められたので、プロットしていく。「こ」は「ね」の幅1000 unitだけずらす。
f:id:nixeneko:20180610211334p:plain

Photoshopで実際のフォントのアウトラインを重ねてみると次のようになる。
f:id:nixeneko:20180610212135p:plain
曲線については、2つ飛ばしに制御点がアウトラインに乗っているように見え、良さそう。

適当に実線でアウトラインを描いていくと次のようになった。
f:id:nixeneko:20180610213110p:plain
実際のフォントのレンダリング結果を重ねてみると、おおよそ上手くいっているように見える。ずれは線の描画が雑なせいだと思う。
f:id:nixeneko:20180610214356p:plain

感想

  • OpenType/CFFは、TrueTypeと同様のテーブル構造(sfnt構造)にPostScriptフォントの一種であるCFFフォントを丸ごと突っ込んだ形をしている。フォントの中にフォントを突っ込むという気持ち悪い構造である。
  • CFFでアウトラインの定義に使われているType 2 Charstringはある種のプログラミング言語(PostScriptの仲間)である。なので、アウトラインを取得するためにはインタプリタを実装する必要がある。大変。
  • 仕様書によると、Type 2 Charstringにおいてはサブルーチン呼び出しを最大10回までネストすることができる。そのため、サブセットフォントをまともにつくろうとすると、サブルーチンがどこから呼び出されるかを全部辿って行かないといけないので、難しそう。
  • 歴史的な経緯でこうなったということは分かるのだが、現在新しくフォント形式を設計するとしたらこうはならないだろうな…という感じがする。

*1:実はplatformID=0x0000, encodingID=0x0003とplatformID=0x0003, encodingID=0x0001は同一のオフセットをもち、同じサブテーブルを指している。

*2:一方、OpenType 1.8でvariable font等に対応した際に追加された'CFF2'テーブルはOpenType仕様書にしっかり記載されている。基本は一緒のようなので、そちらを参照した方がアイデアはつかみやすいかもしれない。

*3:GIDは必ず隙間なく連続している必要があるが、CIDは空きがあって連続していなくてもよいので、CIDフォントにおいてはCID→GIDマッピングを定義しなければならない。

*4:なお、サブルーチンにおけるhintmask命令に後続するマスクの長さも、呼び出している側のcharstring先頭から解析していかないとわからない。