CFF: CharString

Type2 CharString
>> The Type 2 Charstring Format (PDF)

CFF のグリフデータ (Type2 CharString) は、CharStrings INDEX 内に、バイナリデータとして格納されています。

値→命令の順で、可変バイトの数値が連続に格納されており、それぞれの数値は、先頭のバイト値を元に、以下の方法でデコードします。
※複数バイトは、ビッグエンディアン扱い。

先頭のバイト値全体のバイト数値の意味
0-111byte命令
122byteエスケープ (次のバイトが追加の命令)
13-181byte命令
19,202byte〜(可変)命令 (hintmask, cntrmask)
21-271byte命令
283byte数値: -32768〜32767
次の 2byte を、符号付き 16bit とする。
29-311byte命令
32-2461byte数値: -107〜107。
decode: n - 139
247-2502byte数値: 108〜1131。
decode: (n1 - 247) * 256 + n2 + 108
251-2542byte数値: -108〜-1131。
decode: -((n1 - 251) * 256) - n2 - 108
2555byte固定小数点数 (16:16)
次の 4byte を符号付き 32bit として、16bit の固定小数点数とする。

先頭から順に、数値なら値をスタックの後ろに追加していき、命令が来たら、スタックにある値を後ろから取り出して、その命令の値として使います。
複数個の値が必要な場合は、スタックの後ろから指定個数分の位置を先頭にして、前から順に取り出していきます。

可変個数の場合、命令によって、奇数か偶数個かは決まっているので、スタック内における最大個数分を取り出します。
命令が可変引数で、偶数個の値を必要としていて、スタック内の値が奇数個の場合、先頭の1個はスタックに残ります。
数の制限
引数スタック48
(数値をどれだけ保持できるか)
ステムヒント総数96
サブルーチンネスト10
データ全体のサイズ65535
データの並び
[送り幅] [ヒント命令] [パス構成] endchar

先頭に数値がある場合、その値は、グリフの送り幅となります。
(細かく言うと、最初に hstem/hstemhm/vstem/vstemhm/cntrmask/hintmask/hmoveto/vmoveto/rmoveto/endchar が来た時、その命令を実行した後に、スタックに値が残っている場合)
その値に、Private DICT の nominalWidthX で指定された値を加算したものが、実際の送り幅になります。

なぜ nominalWidthX の値を加算する形になっているかというと、値をできるだけ少ないバイト数で指定できるようにするためです。
実際の送り幅が 1000 でも、nominalWidthX が 1000 なら、指定する値は 0 となり、1byte で表現できます。

先頭に数値がない場合、送り幅は、Private DICT の defaultWidthX で指定された値が使われます。

endchar 命令で、グリフデータは終了します。
命令
  • () 内は、命令を表す値。
    (12 34) の場合は、先頭バイトが 12 で、次のバイトが 34。
  • { } はグループ (一つのかたまり)。
  • + は1回以上の繰り返し。* は 0 回以上の繰り返し。? は 0 または 1回。
  • $ は、数値のスタックをクリアする。

座標は (0,0) から始まり、パスの座標は、前の位置から追加していく形になる。
パス
rmoveto(21)$ dx1 dy1 rmoveto(21) $現在位置を相対移動
hmoveto(22)$ dx1 hmoveto(22) $水平移動
vmoveto(4)$ dy1 vmoveto(4) $垂直移動
rlineto(5)$ {dxa dya}+ rlineto(5) $現在位置から、(dxa, dya) の相対位置に線を引く。
複数の座標を連続指定可。
hlineto(6)$ dx1 {dya dxb}* hlineto(6) $
$ {dxa dyb}+ hlineto(6) $
現在位置から (dx1, 0) の相対位置に水平線を引く。
引数が奇数の場合、残りはペアの値で、(0, dya) の垂直線、(dxb, 0) の水平線を交互に描画する。
偶数の場合、(dxa, 0) の水平線、(0, dyb) の垂直線を交互に描画する。
vlineto(7)$ dy1 {dxa dyb}* vlineto(7) $
$ {dya dxb}+ vlineto(7) $
現在位置から (0, dy1) の相対位置に垂直線を引く。
引数が奇数の場合、残りはペアの値で、(dxa, 0) の水平線、(0, dyb) の垂直線を交互に描画する。
偶数の場合、(0, dya) の垂直線、(dxb, 0) の水平線を交互に描画する。
rrcurveto(8)$ {dxa dya dxb dyb dxc dyc}+ rrcurveto(8) $現在位置から、3次ベジェ曲線を描画。
始点が現在位置で、a,b が制御点、c が終点。
hhcurveto(27)$ dy1? {dxa dxb dyb dxc}+ hhcurveto(27) $現在位置から、3次ベジェ曲線を描画。
dya = 0, dyc = 0 として、水平線で始まって、水平線で終わる。
奇数の場合は、dya = dyc = dy1 となる。
hvcurveto(31)$ dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf? hvcurveto(31) $現在位置から、3次ベジェ曲線を描画。
最初は水平で、最後が垂直。(dx1, 0, dx2, dy2, 0, dy3)
その後、垂直と水平となり (0, dya, dxb, dyb, dxc, 0)、交互に繰り返す。
rcurveline(24)$ {dxa dya dxb dyb dxc dyc}+ dxd dyd rcurveline(24) $複数のベジェ曲線の描画後、1つの直線を引く。
rlinecurve(25)$ {dxa dya}+ dxb dyb dxc dyc dxd dyd rlinecurve(25) $複数の直線の後、1つのベジェ曲線を描画。
vhcurveto(30)$ dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf? vhcurveto(30) $
$ {dya dxb dyb dxc dxd dxe dye dyf}+ dxf? vhcurveto(30) $
垂直-水平のベジェ曲線
vvcurveto(26)$ dx1? {dya dxb dyb dyc}+ vvcurveto(26) $垂直-垂直のベジェ曲線
flex(12 35)$ dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex(12 35) $dy1 と dy3 からなる flex depth が fd / 100 デバイスピクセル未満の場合は、直線として描画され、それ以上の場合はベジェ曲線で描画する。
水平線に近い場合は直線として描画する。
hflex(12 34)$ dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex(12 34) $2つのベジェ曲線の flex depth が 0.5 未満 (fd = 50) の場合は直線、それ以上ならベジェ曲線。
hflex1(12 36)$ dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1(12 36) $hflex と同じ。
flex1(12 37)$ dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1(12 37) $-
ほか
endchar(14)- endchar(14) $アウトラインの終了。
サブルーチン内で、endchar によって終了することもできる。
空白文字などは、"送り幅 + endchar" のみで構成できる。
hstem(1)$ y dy {dya dyb}* hstem(1) $一つ以上の水平ステムを指定する。
ステムは、座標の昇順 (小→大) で指定する必要がある。

y は座標値で、dy は y からの距離 (ステム幅)。
以降は、前の位置 (y + dy) からの相対値。
dy = -20 は上端または右端、dy = -21 は下端または左端として予約されている。他の負の値は未定義。

hstem1 = y 280 - y 380, hstem2 = y 310 - 350 の場合、
"280 100 -70 40 hstem" となる。
vstem(3)$ x dx {dxa dxb}* vstem(3) $一つ以上の垂直ステムを指定する。
hstemhm(18)$ y dy {dya dyb}* hstemhm(18) $CharString に1つ以上の hintmask が含まれている場合、hstem の代わりに使用する
vstemhm(23)$ x dx {dxa dxb}* vstemhm(23) $CharString に1つ以上の hintmask が含まれている場合、vstem の代わりに使用する
hintmask(19)$ hintmask(19 + mask) $どのヒントがアクティブで、どのヒントが非アクティブかを指定する。
ヒントは最初にすべて列挙されるため、状況によって無効/有効にしたいものは、これで変更する。
この後のパス演算子が影響を受ける。

後続の可変バイトで、ビットフラグを指定する。
バイト数は、CharString 開始時からのステムヒント数 x 1bit で計算。

最初のバイトの最上位ビットから始まり、ヒントが宣言された順で続く。
ビットが 1 で ON。未使用ビットは 0 にする。

hstem/hstemhm と vstem/vstemhm が連続し、その直後に hintmask/cntrmask が続く場合、vstem は省略する。
(hintmask の前に偶数の引数がある場合、常に vstem の引数として扱う)
スタックの値が 48 個を超える場合、複数回 vstem を指定する必要があり、".. hstemhm .. vstemhm .. hintmask" という形になる場合がある。

例: 280 100 –70 40 hstemhm 400 50 hintmask 0xa0
cntrmask(20)$ cntrmask(20 + mask) $制御するカウンタ空間とその相対的な優先度を指定。

後続のバイトは、hintmask と同じビットデータ。
最初の cntrmask で 1 に設定されたビットが最優先され、それ以降の cntrmask で、優先度の低いカウンターを指定する。
callsubr(10)subr# callsubr(10) -ローカルサブルーチンを呼び出す。
スタックの最後の値が、サブルーチンのインデックス。
それより前の値は、サブルーチンから引数として使用できる。
値を返す場合は、スタックに置く。
callgsubr(29)subr# callgsubr(29) -グローバルサブルーチンを呼び出す
return(11)- return(11) -サブルーチンから戻る

これ以外にも、演算用の命令などがありますが、フォントのグリフで使われることはほぼないので、省略します。
サブルーチンのインデックス値
サブルーチンのインデックス値は、0〜65535 の値を指定できますが、CharString 内でその値を直接指定しようとすると、最大で 5byte になるため、効率が悪くなります。
そのため、サブルーチンインデックス値は、特殊な計算方法を使うことで、-32768〜32767 の最大 3byte に収めることができるようになっています。

サブルーチンの総数別に、元のインデックス値から指定値が引かれているので、デコードして実際のインデックス値を取得する際は、値を足す必要があります。

サブルーチンの総数インデックス値の範囲エンコード後の値足す値
1〜12380〜1237-107〜1131107
1239〜338990〜33898-1131〜327671131
33900〜655360〜65535-32768〜3276732768

例えば、ローカルサブルーチンの総数が 100 で、"-107 callsubr" が呼ばれた場合は、-107 + 107 = 0 が、インデックス値となります。
ステム
ステム命令は扱いが少し複雑なので、注意してください。

特に hintmask/cntrmask 命令は、後続のバイトでフラグ値を指定する必要がありますが、このデータのバイト数は、これまでのステム命令で指定されたステムの個数によって決まるので、(サブルーチンを含め)CharString の先頭から順に命令を解釈していかないと、必要なバイト数が判断できません。
ステム数 x 1bit で、バイト数が計算されます。

なお、hintmask は、ステムの一部を有効/無効にしたい時に使います。
命令の省略
... hstemhm ... vstemhm hintmask
 ↓
... hstemhm ... hintmask

上記のように、hstem/hstemhm と vstem/vstemhm が連続した直後に hintmask/cntrmask が続く場合、vstem 命令は省略されます。

つまり、hintmask/cntrmask が来た時に、スタックに値があれば、それを vstem の引数として扱い、vstem を実行します。

ただし、vstem の引数の値が 48 個を越える場合、スタックの制限に引っかかるため、命令を複数回に分ける必要があります。
その場合、「... hstemhm ....(48個) vstemhm ...(残り) hintmask」という形になる場合もあります。

vstem が連続する場合があり、hstem の後に hintmask が来るとは限らないので、注意してください。
データ例
== 元データ ==

FB 40 FB 31 76 FA 33 77 01 F7 40 F7 0C 03 F7 1A 55 15 EF F7 10 C5 F7 21
F7 4B 1A F7 12 5B F7 16 50 CD 1E 9E 7A 73 99 72 1B 7F 82 86 83 86 1F 8B
DA 20 FB 6C 1A FB 95 20 FB 65 FB 2F 25 1E 7C 8E 96 83 A1 1B 98 A1 8E 96
9F 1F B3 A1 BE B0 B5 BE 08 0E

== デコード後 == () が元データ

-172(FB 40) -157(FB 31) -21(76) 927(FA 33) -20(77) hstem(01) 172(F7 40) 120(F7 0C) vstem(03)
134(F7 1A) -54(55) rmoveto(15) 100(EF) 124(F7 10) 58(C5) 141(F7 21) 183(F7 4B) vvcurveto(1A)
126(F7 12) -48(5B) 130(F7 16) -59(50) 66(CD) vhcurveto(1E) 19(9E) -17(7A) -24(73) 14(99) -25(72)
hhcurveto(1B) -12(7F) -9(82) -5(86) -8(83) -5(86) hvcurveto(1F) 0(8B) 79(DA) -107(20) -216(FB 6C)
vvcurveto(1A) -257(FB 95) -107(20) -209(FB 65) -155(FB 2F) -102(25) vhcurveto(1E) -15(7C) 3(8E)
11(96) -8(83) 22(A1) hhcurveto(1B) 13(98) 22(A1) 3(8E) 11(96) 20(9F) hvcurveto(1F) 40(B3) 22(A1)
51(BE) 37(B0) 42(B5) 51(BE) rrcurveto(08) endchar(0E)

わかりやすく区切りを入れると、以下のようになります。
前の値が、命令の引数値です。

-172
-157 -21 927 -20 hstem
172 120 vstem
134 -54 rmoveto
100 124 58 141 183 vvcurveto
126 -48 130 -59 66 vhcurveto
19 -17 -24 14 -25 hhcurveto
-12 -9 -5 -8 -5 hvcurveto
0 79 -107 -216 vvcurveto
-257 -107 -209 -155 -102 vhcurveto
-15 3 11 -8 22 hhcurveto
13 22 3 11 20 hvcurveto
40 22 51 37 42 51 rrcurveto
endchar

最初に値が5個ありますが、hstem の引数は偶数個なので、先頭の値 (-172) が余ります。
よって、先頭の値は送り幅です。
Private DICT で nominalWidthX が 544 に設定されているので、実際の送り幅は -172 + 544 = 372 になります。

あとはステム命令と、パス構成が続き、最後に endchar で終わります。