フォントと仲良くなりたい。
どうすれば仲良くなれるのか。フォントの構造を知れば多少は仲良くなったと言えるのではないか。という訳で、現在コンピュータで一般的に使われているOpenTypeフォントを、構造を調べつつ読んでいくことにする。
OpenTypeフォントはアウトライン形式にTrueType形式かPostScript (CFF)形式のどちらかを選ぶことができる。TrueTypeアウトラインのフォントを読むのは以前やった。
今回は、PostScriptアウトラインのフォントを読むのをやってみる。
実際に読んでいくフォントとして、Noto Sans CJK JP RegularのVersion 1.004を用意した。(NotoSansCJKjp-Regular.otf
)
「ねこ」という文字列を描画してみたい。
全部のテーブルを読むのは大変なので、アウトラインの描画に必要な部分だけ押さえていくことにする。
バイナリエディタ(今回はStirlingを利用した)を使ってNotoSansCJKjp-Regular.otf
を開いて、見ていく。
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。
- Adobe Technical Note #5176: “The Compact Font Format Specification”
- Adobe Technical Note #5177: “Type 2 Charstring Format”
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
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が来たので終了。パスの開始点へともどってきている。
感想
- 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先頭から解析していかないとわからない。