にせねこメモ

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

フォントサイズに合わせて回転するフォントを作る(1)

TrueType命令で遊ぶシリーズ第2弾。第1弾は次を参照:


TrueType命令を利用して、フォントサイズに応じた角度だけ回転するようなフォントを作った。

完成品

f:id:nixeneko:20170118014251p:plain
コンピュータの世界広がりすぎ。

ダウンロード


アウトラインとして、M+ Fontsのmplus-1p-regular.ttfを利用した。

TrueType Instructionのコード

最終的に実装したコードは次のようになった。なおFontforgeで読み込める記述となっている(というより、Fontforgeによる逆アセンブル結果と言った方がいいか)。

'fpgm': 関数定義
PUSHB_1
 0
FDEF
PUSHB_1
 2
ADD
SVTCA[x-axis]
GC[orig]
PUSHB_1
 128
DIV
PUSHB_1
 0
SWAP
WS
PUSHB_1
 1
RCVT
PUSHB_1
 1
SWAP
WS
ENDF
PUSHB_1
 1
FDEF
DUP
ROLL
DUP
ROLL
PUSHB_1
 3
RS
MUL
SWAP
PUSHB_1
 2
RS
MUL
SUB
PUSHW_1
 4096
DIV
ROLL
ROLL
PUSHB_1
 3
RS
MUL
SWAP
PUSHB_1
 2
RS
MUL
ADD
PUSHW_1
 4096
DIV
SWAP
ENDF
PUSHB_1
 2
FDEF
DUP
DUP
SVTCA[x-axis]
GC[cur]
SWAP
SVTCA[y-axis]
GC[cur]
PUSHB_1
 1
RS
SUB
SWAP
PUSHB_1
 0
RS
SUB
MPS
PUSHB_1
 1
LOOPCALL
PUSHB_1
 0
RS
ADD
SWAP
PUSHB_1
 1
RS
ADD
SVTCA[y-axis]
PUSHB_1
 3
CINDEX
SWAP
SCFS
SVTCA[x-axis]
SCFS
POP
ENDF
PUSHB_1
 3
FDEF
DUP
PUSHB_1
 0
CALL
DUP
DUP
PUSHB_1
 0
GTEQ
IF
PUSHB_1
 2
CINDEX
PUSHB_1
 2
CINDEX
PUSHB_1
 2
CALL
PUSHB_1
 1
SUB
PUSHB_1
 20
NEG
JMPR
EIF
POP
POP
ENDF
'prep': 定数のStorage Areaへの格納
PUSHB_2
 2
 64
PUSHW_1
 4096
MUL
PUSHW_1
 512
DIV
WS
PUSHW_3
 3
 8128
 4096
MUL
PUSHW_1
 8192
DIV
WS
'cvt ': Control Value Table
index val
0 0
1 379

cvtテーブルは事前にFunit(フォントエディタで図形を定義している時の座標)で数値を記述しておき、RCVT[ ]で読みだすときにはフォントサイズ(PPEM)に応じたピクセル数となって読みだされる。書きこむ際には書きこむ単位によってWCVTF[ ] (Funit), WCVTP[ ] (Pixel unit)の2命令が使える。
0番は0にしておいたが、実際には使わなかった。
1番は回転中心のy座標である。

グリフ固有のTrueType Instruction('glyf'内)
PUSHB_2
 11
 3
CALL

11の部分にはそのグリフの最大の制御点番号が入る。全部のグリフへの適用については前回の記事を参照。


以下、どのように実装をおこなったかを説明する。

方針

グリフを回転させることを考える。回転中心はグリフの真ん中あたりとし、その座標を \left( x_c, y_c \right)とする。 \left( x_c, y_c \right)を中心に回転させるには、次のような手順をとる。

  1. 回転中心が原点に重なるよう平行移動
  2. (原点を中心に)回転
  3. 原点に移動していた回転中心が元の座標となるよう平行移動

制御点 \boldsymbol{p}_i = \left( x_i, y_i \right)に対する座標を計算する。

1. 回転中心が原点に重なるよう平行移動

制御点の座標か回転中心の座標を引く。
 \left(\begin{matrix}x_i' \\ y_i'\end{matrix}\right) = \left(\begin{matrix}x_i \\ y_i \end{matrix}\right) - \left(\begin{matrix}x_c \\ y_c\end{matrix}\right) = \left(\begin{matrix}x_i - x_c \\ y_i - y_c\end{matrix}\right)

2. (原点を中心に)回転

これは回転行列を掛けて
 \left(\begin{matrix}x_i'' \\ y_i''\end{matrix}\right) =  \left(\begin{matrix}\cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{matrix}\right) \left(\begin{matrix}x_i' \\ y_i'\end{matrix}\right) = \left(\begin{matrix}x_i'\cos\theta - y_i' \sin\theta \\ x_i' \sin\theta + y_i' \cos\theta \end{matrix}\right)
みたいな感じではあるが、 \thetaに対する \sin\theta, \cos\thetaを計算するのが面倒だったので、固定した角度 \theta_0に対応する定数 \sin\theta_0, \cos\theta_0の値を用いて、
 \left(\begin{matrix}x_i'' \\ y_i''\end{matrix}\right) =  R R R R \cdots R \left(\begin{matrix}x_i' \\ y_i'\end{matrix}\right), \ \ \ R = \left(\begin{matrix}\cos\theta_0 & -\sin\theta_0 \\ \sin\theta_0 & \cos\theta_0 \end{matrix}\right)
のように回転行列をフォントサイズに応じた回数適用することで、サイズに応じた回転を計算する。かなり効率が悪いのでどうにかしたいところではあるが……。*1

3. 原点に移動していた回転中心が元の座標となるよう平行移動

これは1.の逆である。
 \left(\begin{matrix}x_i''' \\ y_i'''\end{matrix}\right) = \left(\begin{matrix}x_i'' \\ y_i''\end{matrix}\right) + \left(\begin{matrix}x_c \\ y_c\end{matrix}\right) = \left(\begin{matrix}x_i'' + x_c \\ y_i'' + y_c\end{matrix}\right)

回転後の座標 \left( x_i''', y_i''' \right)が計算できたので、制御点 \boldsymbol{p}_iをそこに移動する。これをグリフに含まれる全ての制御点について繰り返せば、グリフの回転が計算できるという訳である。

実装

さて、より実装に近い部分について。

Storage Areaへの座標の割り付け

index 割り付け
0  x_c (回転中心x座標)
1  y_c (回転中心y座標)
2  64\sin\theta_0
3  64\cos\theta_0

Storage Areaにはこんな感じで割り付けを行い、一時的な変数として利用したり、あるいは定数を格納して使ったりしている。

回転中心の座標の取得

回転中心の取得は、y座標についてはグリフに依らず事前に値を決め打ちし、'cvt 'テーブルに1番地に座標を(Funitで)書き込んでおく。

x座標についてはphantom pointによって行う。phantom pointとは、グリフの幅や高さを取得(したり、制御)するためにラスタライザが自動で付加する制御点であり、取り扱うグリフにn個の制御点 \boldsymbol{p}_0, \cdots, \boldsymbol{p}_{n-1}がある場合、 \boldsymbol{p}_n, \boldsymbol{p}_{n+1}, \boldsymbol{p}_{n+2}, \boldsymbol{p}_{n+3}がphantom pointとなる。次に図次するように、 \boldsymbol{p}_nがx位置左端、 \boldsymbol{p}_{n+1}が右端、 \boldsymbol{p}_{n+2}が上端、 \boldsymbol{p}_{n+3}が下端を示す。

f:id:nixeneko:20170118205015p:plain

なお、 \boldsymbol{p}_{n+2}, \boldsymbol{p}_{n+3}は古いバージョン(Microsoft rasterizer v.1.7より前)では使えないので、GETINFOでバージョンを取得し、MS rasterizerのバージョンが1.7以上であることを確認してから使わないといけないそうである。
Phantom Pointsについて、詳しくは、https://www.microsoft.com/typography/otspec/ttinst.htmのInstructing TrueType Glyphsを参照。

今回はグリフ幅の中心を求めるので、 \boldsymbol{p}_{n+1}=(x_{n+1}, y_{n+1})について \frac{x_{n+1}}{2}を計算するようにする。 \boldsymbol{p}_nは原点にくるので特に値を参照しなくても幅の中心が求まる。


回転中心の座標を取得し、Storage Areaに書き込むコードを考える。

回転中心のx座標

x座標についてはphantom point  \boldsymbol{p}_{n+1}の座標、すなわちグリフの幅を取得し、それを2で割ることによって中心を得る。

グリフAに対しては次のようになる。最初にpushしている11がグリフの最大の制御点番号n-1である。

PUSHB_1
 11
PUSHB_1
 2
ADD
SVTCA[x-axis]
GC[orig]
PUSHB_1
 128
DIV
PUSHB_1
 0
SWAP
WS
GC

座標の取得は命令GCを使う。リファレンスによると、制御点番号p(整数)をpopし、その番号に対応する座標cをpushする。pushされる座標は描画するフォントサイズ(PPEM)におけるピクセル単位で表され、F26Dot6つまり小数点以下6bitの固定小数点数で表現される。

Pops p: point number (uint32)
Pushes c: coordinate location (F26Dot6)

GCには2種類あり、

  • GC[0]: 現在の点pの座標
  • GC[1]: オリジナルのアウトラインにおける点pの座標

である。これらは点を移動した場合などに得られる値が変わってくる。

x座標, y座標それぞれについて座標を取得する場合はそれぞれSVTCA[1], SVTCA[0]を事前に実行しておく。

なお、ここでF26Dot6は小数点以下6bitの固定小数点数である。これは実質、小数点数に64をかけた整数であり、F26Dot6を整数として評価した場合に64で割ると実際の数値が得られる。
例えば1.0は整数として評価すると64であり、2.0は128となる。

DIV

割り算命令DIVは、リファレンスによると

Pops n2: divisor (F26Dot6)
n1: dividend (F26Dot6)
Pushes (n1 * 64)/n2: quotient (F26Dot6)

となっていて、F26Dot6に対して計算が定義されている。
スタックが

… | n1 | n2 | 

となっているとき、n1, n2を整数として考えると(n1 * 64)/n2が計算される。

… | n1/n2(F26Dot6) | 

n1, n2はF26Dot6で与えなければいけないため、2で割るためにはn2の部分が128になる。


従って、上のコードはGC[1]で取得したx座標を2.0(128)で割っている。

WS

最終的にWS命令でStorage Areaの0番に計算した値を保存する。

Pops v: storage area value (uint32)
l: storage area location (uint32)
before … | l | v |
after  … |

Storage Areaのインデックスiに値vを書きこむ。

SWAP

なお、命令が要求するようにスタックの順番を合わせるために、SWAP命令を使っている。SWAP命令はトップ2項目の順番を入れ替える。

before: … | e1 | e2 |
after:  … | e2 | e1 |

スタックの入れ替えには他にもROLL(上から3番目の項目を先頭に)やMINDEX(指定したnについて、上からn番目の項目を先頭に)が使える。

回転中心のy座標

次に回転中心のy座標の取得をする。

PUSHB_1
 1
RCVT
PUSHB_1
 1
SWAP
WS

事前に'cvt 'テーブルの1番地に書いておいた値をRCVT命令で読みだしている。

Pops location: CVT entry number (uint32)
Pushes value: CVT value (F26Dot6)

CVTに事前に値を設定しておく場合はFunitで書くが、RCVTで読みだした値は現在のPPEMに応じたピクセル数単位に変換されて読みだされる。
上のコードでは、読みだした値をWSでStorage Areaの1番地に保存している。

関数化: 関数0

これを関数化した。
回転中心の座標の取得を行い、回転中心のx座標をStorage Areaの0番地に、y座標を1番地に書きこむ関数が関数0である。引数としてグリフの最大の制御点番号(n-1)をとる。参考までにスタックの状態とコメントを書いた*2

PUSHB_1
 0
FDEF          /* ..| n-1 |             *//* ← initial stack */
PUSHB_1       /* ..| n-1 | 2 |         */ 
 2
ADD           /* ..| n+1 |             *//* n+1: phantom point n+1 に対応 */
SVTCA[x-axis]                            /* 座標の測定をx軸方向にする */
GC[orig]      /* ..| x_(n+1) |         *//* phantom point n+1の(X)座標取得 */
PUSHB_1       /* ..| x_(n+1) | 2.0 |   *//* Uint 128 == F24Dot6 2.0 */
 128
DIV           /* ..| x_(n+1) / 2 |     */
PUSHB_1       /* ..| x_(n+1) / 2 | 0 | */
 0
SWAP          /* ..| 0 | x_(n+1) / 2 | */
WS            /* ..|                   *//* StorageArea[0] = x_(n+1) / 2 */
PUSHB_1       /* ..| 1 |               */
 1
RCVT          /* ..| CVT[1] |          *//* read control value table */
PUSHB_1       /* ..| CVT[1] | 1 |      */     /* CVT[1]は回転中心y_cに対応 */
 1
SWAP          /* ..| 1 | CVT[1] |      */
WS            /* ..|                   *//* StorageArea[1] = CVT[1] */
ENDF

グリフの回転

次に回転を計算する部分を考える。
回転行列の計算は地道に計算していくしかないのだが、定数 \cos\theta_0, \sin\theta_0について、小数点以下に12ビット使うようにすることで精度を上げようとした(つまり、6ビット分下駄をはかせている)*3

定数のセット

'prep'において、Storage Areaの2番地に \sin\theta_0 \approx \frac{1}{8}, 3番地に \cos\theta_0 \approx \frac{127}{128}の近似値を書きこんでいる。 \theta_0 \approx 0.125 \approx 7.2\mbox{°}程度の近似である。
参考:


別にこんなことしなくても、定数をPUSHWしてWSすればいいって話でもある。

PUSHB_2  /* |2|1.0|              */
 2
 64
PUSHW_1  /* |2|1.0|64.0|         */
 4096
MUL      /* |2|1.0*64.0|         */ /* 64(6ビット分)下駄をはかせる */
PUSHW_1  /* |2|1.0*64.0|8.0|     */
 512
DIV      /* |2|0.125*64.0|       */
WS       /* |                    */ /* StorageArea[2] = 0.125*64.0 */
PUSHW_3  /* |3|127.0|64.0|       */
 3
 8128
 4096
MUL      /* |3|127.0*64.0|       */ /* 64(6ビット分)下駄をはかせる */
PUSHW_1  /* |3|127.0*64.0|128.0| */
 8192
DIV      /* |3|(127/128)*64.0|   */
WS       /* |                    */ /* StorageArea[3] = (127/128)*64.0 */

'prep'はフォントサイズ(PPEM)が変更される度に呼び出されるプログラムである。

制御点の座標を取得

最初に移動させたい制御点の番号kがスタクトップに乗っているとする。

DUP           /* ..|k|k| */
DUP           /* ..|k|k|k| */
SVTCA[x-axis]                   /* X座標に設定 */
GC[cur]       /* ..|k|k|x_k| */
SWAP          /* ..|k|x_k|k| */
SVTCA[y-axis]                   /* Y座標に設定 */
GC[cur]       /* ..|k|x_k|y_k| */

STVCAで座標軸を変更しながら、GC[0]で制御点kの現在の座標の値を取得している。

平行移動

Storage Areaから回転中心のx, y座標を読みだして、座標から引いている。

PUSHB_1       /* ..|k|x_k|y_k|1|           */
 1
RS            /* ..|k|x_k|y_k|y_c|         */ /* y_c = SA[1]: 回転中心Y座標 */
SUB           /* ..|k|x_k|y_k - y_c|       */
SWAP          /* ..|k|y_k - y_c|x_k|       */
PUSHB_1       /* ..|k|y_k - y_c|x_k|0|     */
 0
RS            /* ..|k|y_k - y_c|x_k|x_c|   */ /* x_c = SA[0]: 回転中心X座標 */
SUB           /* ..|k|y_k - y_c|x_k - x_c| */
回転

回転を行う関数が関数1である。

/* Function 1: Calculates rotated coordinates with pre-defined angle T */
/* initial stack ..| y | x |             x' = x*cosT - y*sinT          */
/* final stack   ..| y'| x'|      while  y' = x*sinT + y*cosT          */
PUSHB_1
 1
FDEF     /* ..|y|x|                                 */
DUP      /* ..|y|x|x|                               */
ROLL     /* ..|x|x|y|                               */
DUP      /* ..|x|x|y|y|                             */
ROLL     /* ..|x|y|y|x|                             */
PUSHB_1  /* ..|x|y|y|x|3|                           */
 3
RS       /* ..|x|y|y|x|64*cosT|                     */ /* SA[3] = 64*cosT */
MUL      /* ..|x|y|y|64*x*cosT|                     */
SWAP     /* ..|x|y|64*x*cosT|y|                     */
PUSHB_1  /* ..|x|y|64*x*cosT|y|2|                   */
 2
RS       /* ..|x|y|64*x*cosT|y|64*sinT|             */ /* SA[2] = 64*sinT */
MUL      /* ..|x|y|64*x*cosT|64*y*sinT|             */
SUB      /* ..|x|y|64*(x*cosT - y*sinT)|            */
PUSHW_1  /* ..|x|y|64*(x*cosT - y*sinT)|64.0|       */
 4096
DIV      /* ..|x|y|x*cosT - y*sinT|                 */
ROLL     /* ..|y|x*cosT - y*sinT|x|                 */
ROLL     /* ..|x*cosT - y*sinT|x|y|                 */
PUSHB_1  /* ..|x*cosT - y*sinT|x|y|3|               */
 3
RS       /* ..|x*cosT - y*sinT|x|y|64*cosT|         */ /* SA[3] = 64*cosT */
MUL      /* ..|x*cosT - y*sinT|x|64*y*cosT|         */
SWAP     /* ..|x*cosT - y*sinT|64*y*cosT|x|         */
PUSHB_1  /* ..|x*cosT - y*sinT|64*y*cosT|x|2|       */
 2
RS       /* ..|x*cosT - y*sinT|64*y*cosT|x|64*sinT| */ /* SA[2] = 64*sinT */
MUL      /* ..|x*cosT - y*sinT|64*y*cosT|64*x*sinT| */
ADD      /* ..|x*cosT - y*sinT|64*(x*sinT + y*cosT)| */
PUSHW_1  /* ..|x*cosT - y*sinT|64*(x*sinT + y*cosT)|64.0| */
 4096
DIV      /* ..|x*cosT - y*sinT|x*sinT + y*cosT|     */
SWAP     /* ..|x*sinT + y*cosT|x*cosT - y*sinT|     */ 
ENDF                        /* ↑関数呼び出し時とx, yの順番を合わせている */

これは、スタックにy座標、x座標が積まれた状態で呼び出す。だいたい7.2°の回転を計算し、計算結果も同様にスタックに積まれる。関数が終了するときのスタック状態が関数開始時と同型になっているので、複数回関数を実行すればそれだけ回転が計算されるというわけである。

回転の繰り返し

回転回数の取得はポイント数取得命令MPSによって得られた整数値をそのまま使っている。頭悪い。
LOOPCALL命令を利用して、(MPSで得られた値)回だけ関数1を実行している。

MPS      /* ..|k|y_k-y_c|x_k-x_c|PS|   */ /* measure point size. */
                                          /* PS…ポイントサイズ, Uint */
PUSHB_1  /* ..|k|y_k-y_c|x_k-x_c|PS|1| */
 1
LOOPCALL /* ..|k|y_k-y_c|x_k-x_c|      */ /* call function 1 PS times */
             /* y_k' = y_k-y_c, x_k' = x_k-x_c are changed to y_k'', x_k'' */
平行移動
PUSHB_1       /* ..|k|y_k''|x_k''|0|       */ 
 0
RS            /* ..|k|y_k''|x_k''|x_c|     */ /* x_c = SA[0]: 回転中心X座標 */
ADD           /* ..|k|y_k''|x_k''+x_c|     */
SWAP          /* ..|k|x_k''+x_c|y_k''|     */
PUSHB_1       /* ..|k|x_k''+x_c|y_k''|1|   */
 1
RS            /* ..|k|x_k''+x_c|y_k''|y_c| */ /* y_c = SA[1]: 回転中心Y座標 */
ADD           /* ..|k|x_k''+x_c|y_k''+y_c| */

Storage Areaから回転中心のx, y座標を読みだして、計算した座標に足し算している。

制御点の移動

STVCAで座標軸を切り替えながら、SCFSを利用して、指定した制御点kを計算した座標に移動させている。

SVTCA[y-axis]                              /* Y座標に設定 */
PUSHB_1  /* ..|k|x_k''+x_c|y_k''+y_c|3| */ 
 3
CINDEX   /* ..|k|x_k''+x_c|y_k''+y_c|k| */ /*stack先頭から3番目をトップにコピー*/
SWAP     /* ..|k|x_k''+x_c|k|y_k''+y_c| */
SCFS     /* ..|k|x_k''+x_c|             */ /* 制御点kのY座標を y_k''+y_c に */
SVTCA[x-axis]                              /* X座標に設定 */
SCFS     /* ..|                         */ /* 制御点kのX座標を x_k''+x_c に */
CINDEX

ここで、CINDEXは、整数kをpopし、スタックトップからk番目の要素をコピーしてスタックに積む命令である。

Pops k: stack element number (int32)
Pushes ek: kth stack element (StkElt)

k=1のとき、DUPと同じ動作となる。

関数化: 関数2

これらの、一つの制御点kを移動する命令群を関数2としてまとめた。引数としてグリフ最大の制御点番号n-1, 編集したい制御点番号kを与えて呼び出すと制御点kを回転した位置に移動する。n-1は考えたら使ってないのでもっとシンプルにできるはず。

/* Function 2: moves a control point k to the rotated coordinates         */
/* initial stack ..| n-1 | k |    n-1: 最大の制御点番号, k: 編集する制御点番号 */
/* final stack   ..|                                                      */
PUSHB_1
 2
FDEF     /* ..|n-1|k| */
DUP      /* ..|n-1|k|k| */
DUP      /* ..|n-1|k|k|k| */
SVTCA[x-axis]                                  /* X座標に設定 */
GC[cur]  /* ..|n-1|k|k|x_k| */
SWAP     /* ..|n-1|k|x_k|k| */
SVTCA[y-axis]                                  /* Y座標に設定 */
GC[cur]  /* ..|n-1|k|x_k|y_k| */
PUSHB_1  /* ..|n-1|k|x_k|y_k|1| */
 1
RS       /* ..|n-1|k|x_k|y_k|y_c|           */ /* y_c = SA[1]: 回転中心Y座標 */
SUB      /* ..|n-1|k|x_k|y_k-y_c|           */
SWAP     /* ..|n-1|k|y_k-y_c|x_k|           */
PUSHB_1  /* ..|n-1|k|y_k-y_c|x_k|0|         */
 0
RS       /* ..|n-1|k|y_k-y_c|x_k|x_c|       */ /* x_c = SA[0]: 回転中心X座標 */
SUB      /* ..|n-1|k|y_k-y_c|x_k-x_c|       */
MPS      /* ..|n-1|k|y_k-y_c|x_k-x_c|PS|    */ /* measure point size.*/
PUSHB_1  /* ..|n-1|k|y_k-y_c|x_k-x_c|PS|1|  */
 1
LOOPCALL /* ..|n-1|k|y_k-y_c|x_k-x_c|       */ /* call function 1 PS times */
             /* y_k' = y_k-y_c, x_k' = x_k-x_c are changed to y_k'', x_k'' */
PUSHB_1  /* ..|n-1|k|y_k''|x_k''|0|         */ 
 0
RS       /* ..|n-1|k|y_k''|x_k''|x_c|       */ /* x_c = SA[0]: 回転中心X座標 */
ADD      /* ..|n-1|k|y_k''|x_k''+x_c|       */
SWAP     /* ..|n-1|k|x_k''+x_c|y_k''|       */
PUSHB_1  /* ..|n-1|k|x_k''+x_c|y_k''|1|     */
 1
RS       /* ..|n-1|k|x_k''+x_c|y_k''|y_c|   */ /* y_c = SA[1]: 回転中心Y座標 */
ADD      /* ..|n-1|k|x_k''+x_c|y_k''+y_c|   */
SVTCA[y-axis]                                 /* Y座標に設定 */
PUSHB_1  /* ..|n-1|k|x_k''+x_c|y_k''+y_c|3| */ 
 3
CINDEX   /* ..|n-1|k|x_k''+x_c|y_k''+y_c|k| * stack先頭から3番目をトップにコピー*/
SWAP     /* ..|n-1|k|x_k''+x_c|k|y_k''+y_c| */
SCFS     /* ..|n-1|k|x_k''+x_c|            *//* 制御点kのY座標を y_k''+y_c に */
SVTCA[x-axis]                                 /* X座標に設定 */
SCFS     /* ..|n-1|                        *//* 制御点kのX座標を x_k''+x_c に */
POP      /* ..|                    */ /*そういえばこのn-1は関数内で全く使ってない*/
ENDF

全ての制御点に対し適用

イデア前回の記事で使ったのと同じ。関数に渡す値を用意する部分がやや複雑になっている。
最初にスタックにn-1が積まれてることとする。

                                 /* i = n-1 とおく */
DUP       /* ..|n-1|i|         */ 
                                 /* jump_to_here */
DUP       /* ..|n-1|i|i|       */
PUSHB_1   /* ..|n-1|i|i|0|     */
 0
GTEQ      /* ..|n-1|i|i<0|     */
IF        /* ..|n-1|i|         *//* if i<0 then */
  PUSHB_1 /* ..|n-1|i|2|       */
   2
  CINDEX  /* ..|n-1|i|n-1|     */
  PUSHB_1 /* ..|n-1|i|n-1|2|   */
   2
  CINDEX  /* ..|n-1|i|n-1|i|   */
  PUSHB_1 /* ..|n-1|i|n-1|i|2| */
   2
  CALL    /* ..|n-1|i|n-1|i|   *//* call function 2 */ 
  PUSHB_1 /* ..|n-1|i|1|       */ 
   1
  SUB     /* ..|n-1|i-1|       */
  PUSHB_1 /* ..|n-1|i-1|20|    */
   20
  NEG     /* ..|n-1|i-1|-20|   */
  JMPR    /* ..|n-1|i-1|       *//* goto jump_to_here with i=i-1*/
EIF                              /* end if */

疑似コードで書くとこんな感じ。

    int i = n-1;
jump_to_here:
    if (i>=0){
      func2(n-1, i);
      i--;
      goto jump_to_here;
    }
関数化: 関数3

最終的にグリフから実行する処理を関数3としてまとめた。回転中心座標の取得と、全ての制御点に対し回転を実行することをやっている。引数としてグリフの最大の制御点番号n-1を与えて実行する。

PUSHB_1
 3
FDEF      /* ..|n-1|           */
DUP       /* ..|n-1|n-1|       */
PUSHB_1   /* ..|n-1|n-1|0|     */
 0
CALL      /* ..|n-1|           */ /* call function 0 */
DUP       /* ..|n-1|i|         */ /* i = n-1 とおく */ 
                                  /* jump_to_here */
DUP       /* ..|n-1|i|i|       */
PUSHB_1   /* ..|n-1|i|i|0|     */
 0
GTEQ      /* ..|n-1|i|i<0|     */
IF        /* ..|n-1|i|         */ /* if i<0 then */
  PUSHB_1 /* ..|n-1|i|2|       */
   2
  CINDEX  /* ..|n-1|i|n-1|     */
  PUSHB_1 /* ..|n-1|i|n-1|2|   */
   2
  CINDEX  /* ..|n-1|i|n-1|i|   */
  PUSHB_1 /* ..|n-1|i|n-1|i|2| */
   2
  CALL    /* ..|n-1|i|n-1|i|   */ /* call function 2 */ 
  PUSHB_1 /* ..|n-1|i|1|       */ 
   1
  SUB     /* ..|n-1|i-1|       */
  PUSHB_1 /* ..|n-1|i-1|20|    */
   20
  NEG     /* ..|n-1|i-1|-20|   */
  JMPR    /* ..|n-1|i-1|       */ /* goto jump_to_here */
EIF                               /* end if */
POP       /* ..|n-1|           */
POP       /* ..|               */
ENDF

よく考えてみれば関数2でn-1の値を使ってないので、ループの時点でn-1の値を保存しなくてよくて、もっとシンプルにできたはず。

各グリフのプログラム

PUSHB_2
 11
 3
CALL

11の部分にはそのグリフの最大の制御点番号が入る。
関数3を、グリフの最大の制御点番号を引数にして呼び出している。

感想

計算量がバカにならないので、割と最新のパソコンでもフォントビューワで表示するとフォントサイズごとに上からポンポンポンと順番に出てくるのがわかる。回転部分をもっと賢くさせたい。
PPEM変わるたびに呼び出されるのが'prep'なので、そこでPPEMを測定しそれに応じた回転行列のパラメータを計算しておくのがいいのだろうと思う。まあそれは追い追い。

*1:今回はグリフの制御点に対して毎回計算しているが、合成された回転行列RRRR…Rの部分はここではフォントサイズ(つまりRの個数)のみに依存するため、prepテーブルで計算しておくと随分マシになるだろうと思う。

*2:厳しいことに、Fontforgeのデバッガではコメントが使えない。空行も危ない。

*3:どこまで意味があるんだろう……。

UbuntuにX11転送を有効にしてsshで接続したらgeditなどがSegmentation Faultした

sshX11 forwardingを有効にしてUbuntuにログインし、ログイン先のnautilusやgeditなどを起動しようとすると、Segmentation Faultと表示されて動作しなかった。
一方で、xtermなどは正常に実行でき、ちゃんとそのウィンドウが表示された。

ログイン先のOSはUbuntu 16.04 64bitで、Nvidiaのグラボを利用している。オンボードグラフィックがないマザーボードを使っている。

解決策

Open GUI apps on a Ubuntu 16.04 machine via SSH from an Ubuntu 14.04 machine - Ask Ubuntu
これを見て解決した。

原因はNvidiaのドライバかなんかにあるらしい。 libGLX_indirect.so.0 がうまく見つけられてないとのこと。
次の様にリンクを張ると動作する様になった。

cd /usr/lib/x86_64-linux-gnu
sudo ln -s /usr/lib/nvidia-361/libGLX_indirect.so.0

nvidia-361の部分は環境(ドライバ)に合わせてといった感じになると思う。

GPOSのCursive Attachment Positioningについて

序説

OpenTypeフォントの高度組版機能においてグリフの位置調整を行うのがGPOSである。GPOSは主にペアカーニングやダイアクリティカルマークを適切な位置に表示するために利用される。

GPOSの中には更に、筆記体の接続を実現するためのCursive Attachment Positioningというものがある。筆記体の接続と言う名前ではあるが、文字が水平に並ぶ英語などの筆記体のためのものではなく、ペルシア語やウルドゥー語で用いられるアラビア文字ナスタアリーク体などのために追加されたものだろう。ナスアリーク体では文字の塊ごとに文字が右上から右下に流れ、グリフの上下位置が一定しないために、この機能がないと適切な表示ができない(例などは参考サイト参照)。

特にウルドゥー語などではこれがないと標準的な組版もできない様な機能ではあるが、日本語や欧文では不要である。
それでも、グリフ間の位置調整ができれば便利だという気持ちもあるので、使えないか調べてみる。

Fontforgeによる設定方法

さて、Fontforgeでどのように利用するかを見ながら実際の動作を見ていく。
M+フォントのmplus-1p-regular.ttfを利用することにする。

まず、Fontforgeでmplus-1p-regular.ttfを開く。最初にASCII外のグリフを削除し、GSUB, GPOSについても既存のものを削除しておいた。

GPOS lookupとanchor classの作成

メインウィンドウのメニューから、エレメント→フォント情報を開く。
Lookupsタブ→GPOSタブを開き、Add Lookupをクリックする。
f:id:nixeneko:20170114153645p:plain


次に開くLookupウィンドウで、

  • 種類: 「Cursive Position」を選択。
  • 機能: 「curs」を選択。
  • 用字系と言語は適切に。デフォルトで動くはず。
  • Lookup Name: 適当に設定。デフォルトでも良い。

など設定し、OKをクリック。
f:id:nixeneko:20170114154120p:plain


すると今設定したlookupが追加されるので、追加したlookupを選択した状態で「Add Subtable」をクリック。
f:id:nixeneko:20170114154312p:plain


サブテーブルの名前を聞かれるダイアログが開くので適当に名前をつけて(デフォルトでよい)、OKをクリック。
するとAnchor Class Nameを設定するダイアログが開くのでクラスを追加していく。<New Anchor Class>をクリックするとスロットが追加されるので適当に名前を入力して(ここでは“cursive”)、OKを押す。
f:id:nixeneko:20170114154833p:plain


「フォント情報」ウィンドウをOKを押して閉じる。

これによってアンカークラスが追加されたので、アンカー点が設定できるようになる。

Anchor pointの設定

適当なグリフ(ここではa)をダブルクリックしアウトラインウィンドウを開く。
メニューから、点→アンカーを追加(あるいはアウトライン領域で右クリック→アンカーを追加)を選ぶ。
f:id:nixeneko:20170114155520p:plain


するとアンカー点の情報が表示されるので、先ほど追加したcursiveクラスを選び、「筆記体の始点」を選択、OKを押す。座標はこのダイアログで設定してもいいし、青い+字点をドラッグして移動することもできる。このダイアログは、青い+字点を右クリック→情報を得る、などから再度表示させることができる。
f:id:nixeneko:20170114155840p:plain


同様に筆記体の終点を追加する。前とほぼ一緒で、違いは「筆記体の終点」を選択するところだけである。

f:id:nixeneko:20170114162155p:plain
追加したのが上の画像である。始点を(0, 0)に、終点を(548, 0)に置いてみた。y座標が一緒でx座標の差分がグリフの幅に一致しているため、この状態だとcursを有効にしてもなにも変化しない、はずである。

試しにメトリックウィンドウを開いて確認してみる。
f:id:nixeneko:20170114162617p:plain
有効無効によって変化がない。同じアンカークラスに属するものについて、次のグリフの始点が前のグリフの終点に重なるように移動する、というのがアンカーポイントによる位置調整なので、当然の動作であるように思う。


さて、今度はcursive終点の位置を少し上側にずらしてみる。
f:id:nixeneko:20170114162929p:plain


今度は次にくるaはcursive終点の位置に移動するはずなので、やや上に上がって表示されるはずだ。メトリックウィンドウで試してみる。
f:id:nixeneko:20170114163134p:plain
期待通りである。

同様に別のグリフにも対してもアンカー点を追加していってみる。小文字には始点・終点を設定し、大文字には終点だけ設定してみる。コンマピリオドにも始点を設定してみる。

最終的に、TrueTypeアウトラインのフォントとして出力する。

ダウンロード

出力したフォントは次からダウンロードできる。

ソフトウェア側の対応

さて、フォントが対応していてもフォントを扱う側のソフトウェアが対応していないと正しく表示できないのがフォントの世界である。この機能が必須であるウルドゥー語などを除いては機能を無効にしているなどといった可能性もある。

それでは、ソフトウェア側の対応を見ていく。OpenTypeタグ'curs'が有効にできればいいのであるが。なお以下、すべてWindows 10で動かした結果である。

Adobe InDesign CS6

Adobe InDesign CS6はダメっぽい。そもそも'curs'に対応する設定項目が見当たらない。他のAdobeソフトも対応は期待できなさそうな気がする。

Microsoft Word 2013

設定項目が見当たらないのでダメっぽい。

各種ブラウザ

CSS3でfont-feature-settingsプロパティが追加され、それによってOpenType featureが個別に有効を設定できる。期待できそう。
CSSだと、次のような設定をすることになる。

font-feature-settings: "curs";

これを実際にはフォント指定と組み合わせる。

Webフォントで表示させてみたのが次である。

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

各ブラウザの対応を見ていく。

Firefox (50.1.0)

f:id:nixeneko:20170114174319p:plain

Google Chrome (55.0.2883.87 m)

f:id:nixeneko:20170114174308p:plain

Microsoft Edge (25.10586.672.0)

f:id:nixeneko:20170114175208p:plain
EdgeやIE

font-feature-settings: curs;

ではだめで、

font-feature-settings: 'curs';

のように(シングルでもダブルでもいいけど)クォーテーションでくくらないとちゃんと動かないみたい。

さて、最近のブラウザ正しく動いてるようである。しかし、滅茶苦茶不安になるな、これ……。

LaTeX

LaTeXは、fontspecパッケージによって、OpenType featuresを個別に設定することができる。

\setmainfont[Path=./,Extension=.ttf,RawFeature=+curs]{Acurstest}
\newfontfamily\curstest[Path=./,Extension=.ttf,RawFeature=+curs]{Acurstest}

みたいにフォント設定時にRawFeature=+cursオプションを設定するか、あるいは、オプションを適用したいテキストの直前で

\addfontfeature{RawFeature=+curs}

と書いて指定する。

XeLaTeXとLuaLaTeXのみ検討する。なお以下はどちらもCygwinで導入したTeX Live 2016のものによる出力である。

XeLaTeX

f:id:nixeneko:20170114202027p:plain
U+2019を削除してしまったのでXeLaTeXではアポストロフィ'が豆腐になっている。

LuaLaTeX

f:id:nixeneko:20170114202152p:plain

どちらでも正しく動いているようであるが、aaaaa...の行が上と重ならないなど、行間の扱いがブラウザの場合と異なっているのがわかる。また、aaaaa...の行を見るに、XeLaTeXとLuaLaTeXでも行間の扱いが異なっているようである。どうなってるの…。

なお、XeLaTeXでは\begin{document}より前に\addfontfeatures{RawFeature=+curs}を書いても有効になったのだが、LuaLaTeXでは有効にならなかった。微妙に動作が異なるらしい。

まとめ

ソフトウェアの対応が悪い。使用できる環境が限られるので、万能ではない。先に使いたい環境で使えるかを確かめてからやらないといけなさそう。

参考サイト

『ギリシャ文字・キリル文字・ラテン文字』

サークル“ヒュアリニオス”として頒布した『ギリシャ文字キリル文字ラテン文字』(初出: コミックマーケット90)を公開します。文字の対応を見ながら、ギリシャ文字からキリル文字が作られた過程をラテン文字を絡めて説明している感じの漫画です。

サポートページ


ダウンロード

PDFファイルのダウンロードはこちらから: greek_latin_cyrillic.pdf (3.91MB)

こちらもどうぞ

ロシア語で使われているキリル文字の歴史の話です。

本文

f:id:nixeneko:20170105222233p:plain

続きを読む

LaTeX (TikZ)でキーボード配列表を作成

XeLaTeXでTikZを使ってキーボード配列表を作っていた。101キーボード向け。
多言語用のキーボード配列を作るときに便利…なはず。とても泥臭いので誰か改良してください。

ちなみにXeTeXを使ったのはモンゴル文字への対応がLuaLaTeXは良くないためで、試してないがfontspecが使えればLuaLaTeXなどでも普通に使えるかもしれない。

ソース

\documentclass{standalone}
\usepackage{fontspec}
\usepackage{lmodern}
\setmainfont{Times New Roman}

\usepackage{readarray}

\usepackage{calc}
\usepackage{xparse}
\usepackage{listings}
\usepackage{tikz}
\usetikzlibrary{positioning,shapes,fit}

\tikzstyle{abstract}=[rectangle, draw=black, rounded corners, fill=white,text centered, text=black, text width=8mm]
\tikzstyle{gkey}=[rectangle, draw=black, rounded corners, fill=gray!20,text centered, text=black, text width=8mm]

\newcommand{\mykey}[2]{%
\begin{tikzpicture} \node (Item) [abstract, rectangle split, rectangle split, rectangle split parts=2]%
{#1 \nodepart{second}#2};%
\end{tikzpicture}}


\tikzset{
    pics/vhsplit/.style n args = {4}{
        code = {
        \node[anchor=west] (A) at (-3mm,-0.5mm) {#1};
        \node[anchor=west] (B) at (-3mm,-6.5mm) {#2};
        \node[anchor=east] (C) at (9mm,-0.5mm) {#3};
        \node[anchor=east] (D) at (9mm,-6.5mm) {#4};
        \draw[rounded corners=1mm] (-3mm,-9.5mm) rectangle (9mm,2.5mm); 
        }
    }
}
\tikzset{
    pics/specialkey/.style n args = {2}{
        code = {
        \draw[rounded corners=1mm,fill=gray!20] (-3mm,-9.5mm) rectangle (#2-1mm,2.5mm);  
        \node[inner sep=1mm,anchor=south west,text width=#2,align=center] (A) at (-3mm,-9.5mm) {#1};  
        }
    }
}


\makeatletter

\newlength{\my@keylength}
\setlength{\my@keylength}{13mm}

% {name}{label}{xpos}{ypos}{width}
\def\my@specialkey#1#2#3#4#5{%
  \path pic (#1) at (#3\my@keylength,#4\my@keylength) {specialkey={#2}{#5\my@keylength}};
}
% {name}{index}{north-west}{south-west}{north-east}{south-east}{vertical-index}{left-offset}
\def\my@keypath#1#2#3#4#5#6#7#8{%
  \path pic (#1) at ([xshift=#8\my@keylength]#2\my@keylength,#7\my@keylength) {vhsplit={#3}{#4}{#5}{#6}};
  \typeout{#1/#2/}
}
% \keyassign{1st}{2nd}{3rd}{4th line}
\def\keyassign#1#2#3#4{%
  \begin{tikzpicture}%
    \newcount\my@keycount %
    % 1st line
    \my@keycount=0
    \@for\lp@elem:=#1\do{%
      \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} %
      \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter a\my@cnum}} %
      \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} %
      \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{0}{0} %
      \advance\my@keycount by 1 %
    } %
    \my@specialkey{BS}{Back\\Space}{13}{0}{1.27}
    % 2nd line
    \my@specialkey{Tab}{Tab}{0}{-1}{1.27}
    \my@keycount=0
    \@for\lp@elem:=#2\do{%
      \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} %
      \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter b\my@cnum}} %
      \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} %
      \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{-1}{1.5} %
      \advance\my@keycount by 1 %
    } %
    % 3rd line
    \my@specialkey{CapsLock}{Caps Lock}{0}{-2}{1.57}
    \my@keycount=0
    \@for\lp@elem:=#3\do{%
      \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} %
      \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter c\my@cnum}} %
      \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} %
      \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{-2}{1.8} %
      \advance\my@keycount by 1 %
    } %
    \my@specialkey{Enter}{Enter}{12.80}{-2}{1.47}
    % 4th line
    \my@specialkey{Shift}{Shift}{0}{-3}{2.07}
    \my@keycount=0
    \@for\lp@elem:=#4\do{%
      \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} %
      \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter d\my@cnum}} %
      \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} %
      \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{-3}{2.3} %
      \advance\my@keycount by 1 %
    } %
    \my@specialkey{Shift}{Shift}{12.3}{-3}{1.97}
  \end{tikzpicture}%
}
\makeatother


\begin{document}

\keyassign{{}{}{\char"07E}{\char"060},{}{}!1,{}{}@2,{}{}\#3,{}{}\$4,{}{}\%5,{}{}{\char"05E}6,{}{}\&7,{}{}*8,{}{}(9,{}{})0,{}{}\_-,{}{}+=}%
{{}{}{}Q,{}{}{}W,{}{}{}E,{}{}{}R,{}{}{}T,{}{}{}Y,{}{}{}U,{}{}{}I,{}{}{}O,{}{}{}P,{}{}\{[,{}{}\}],{}{}|{\char"05C}}%
{{}{}{}A,{}{}{}S,{}{}{}D,{}{}{}F,{}{}{}G,{}{}{}H,{}{}{}J,{}{}{}K,{}{}{}L,{}{}{:}{;},{}{}{\char"022}{\char"027}}%
{{}{}{}Z,{}{}{}X,{}{}{}C,{}{}{}V,{}{}{}B,{}{}{}N,{}{}{}M,{}{}{<}{\char"02C},{}{}{>}.,{}{}?/}


\end{document}

これをXeLaTeXでコンパイルすると次の様に描画される。
f:id:nixeneko:20170106192541p:plain

雑な解説

簡単に解説しておくと、\keyassignマクロは4つの引数、すなわち順に(上から)第1行目(キー13個)、第2行目(キー13個)、第3行目(キー11個)、第4行目(キー10個)のキー配列を引数としてとり、キー配列を描画する。
キー配列はキーをカンマ“,”で区切ったものである。
キーは文字4つ(何も文字がないところには{}を置く)からなり、左上、左下、右上、右下の順である。複数文字を一つの場所に入れる場合には{}で囲む。なお(La)TeXの特殊記号となっているものはエスケープしたりしないといけない。


\begin{document}~\end{document}の中の\keyassignマクロの部分を変えると他のキーボード配列を作成することができる。例えばロシア語キーボード配列だと、

\keyassign{{\char"07E}{\`{}}{}Ё,!1{}{},@2{\char"022}{},\#3№{},\$4;{},\%5{}{},{\char"05E}6:{},\&7?{},*8{}{},(9{}{},)0{}{},\_-{}{},+={}{}}%
{Q{}{}Й,W{}{}Ц,E{}{}У,R{}{}К,T{}{}Е,Y{}{}Н,U{}{}Г,I{}{}Ш,O{}{}Щ,P{}{}З,\{[{}Х,\}]{}Ъ,|{\char"05C}/{}}%
{A{}{}Ф,S{}{}Ы,D{}{}В,F{}{}А,G{}{}П,H{}{}Р,J{}{}О,K{}{}Л,L{}{}Д,{:}{;}{}Ж,{\char"022}{\char"027}{}Э}%
{Z{}{}Я,X{}{}Ч,C{}{}С,V{}{}М,B{}{}И,N{}{}Т,M{}{}Ь,{<}{\char"02C}{}Б,{>}.{}Ю,?/.{\char"02C}}

のようにして、次のような画像が得られる。
f:id:nixeneko:20170106202632p:plain



なぜこんなものを作ったかというと、モンゴル文字キーボードのキー配列表を作りたかったからで、フォントの変更や文字の回転などを組み合わせて次のようになった。なかなかいい感じだと思う。
f:id:nixeneko:20170106200748p:plain


というか、作ってから時間たってるので、マクロ内部で何やってるのか覚えていない。何やら\expandafterとTikZに苦しんでいたのは覚えているが……。

無理な時に押すボタンをつくった

最近いろいろと無理なので無理な時に押すためのボタンをつくった。Arduino Unoを使って押すとむーりぃーって言うもの。

へぇボタンじゃねーか。

ハードウェア

Arduino Uno (R3)を使った。

全体図こんな感じ。
f:id:nixeneko:20161223184726p:plain

電源

Arduinoへの電源供給様に電池6Vを3端子レギュレータで5Vに降圧してArduinoのUSB端子につなげている。
普通にUSBで電源につなぐので問題ない。

ボタン

ボタンは100円ショップで売っている押すとオン・オフが切り替わるプッシュライトのスイッチを改造した。

上のサイトを参考に、スイッチ内部でカチカチと引っかかる部品を取り除き、押している間だけスイッチが切り替わるようにする。また、押している間通電するようにしたかったので元の配線から配線を付け替えている。
さらに、ライト部分も生かしたかったのでそこにもリード線をつけて外に引っ張りだした。

ライトを開けるとこんな感じ。
f:id:nixeneko:20161223185855p:plain

配線

こんな感じの回路図になると思う。入力ピンに繋がれる部分は、ボタンが押されてないときはGNDに、押されてる間は+5Vになる。
f:id:nixeneko:20161223182431p:plain

実装時には、あまり良くないけどプルダウン抵抗は省略した(要するにプッシュライトのLED用抵抗がプルダウン抵抗の役目を果たすことになる)が、一応問題なく動いているようである。
f:id:nixeneko:20161223191024p:plain

ソフトウェア

音声の出力については

を参考にした。

音声を用意する

今回はArduinoフラッシュメモリ(32kB)に書きこむので、データのサイズをメモリに乗る程度に小さくしないといけない。という訳でメモリぎりぎりのサイズになるようサンプリング周波数を調整した。

  • サンプリング周波数13800Hz
  • モノラル
  • 符号なし8bit
  • ヘッダなし

の音声ファイルをAudacityを使って用意した。音量は割れない程度に大きめにしておいた。
f:id:nixeneko:20161220003235p:plain
ファイル名はmuri13k8.rawとした。


次に、Arduinoで読み込めるように、.hファイルに8ビット符号なし整数の配列として書き出す。
適当なpythonスクリプトconv.pyを書いて変換する。

with open('muri13k8.raw', 'rb') as f:
    data = f.read()
print('const unsigned char muri[] PROGMEM = {')
for x in data[:-1]:
    print('{},'.format(x), end="")
print('{}}};'.format(data[-1]))

この出力をmuri.hとして保存し、スケッチのフォルダに突っ込んでおく。

python conv.py > muri.h

muri.hの内容はこんな感じになる。

const unsigned char muri[] PROGMEM = {
126,125,125,125,125,125,125,125,125,125,125,125,(以下略)
};

Arduinoコード

Arduinoのプログラムを書く。Arduino IDEのバージョンは1.6.11。

#include <avr/pgmspace.h>
#include "muri.h"
#define ARRAYSIZE(x)  (sizeof(x) / sizeof((x)[0]))

#define OUT_PIN 3
#define BUTTON 19

int i = 0;       // 音声の再生位置
int lastval = 0; // ボタン状態レジスタ

void setup()
{
  pinMode(BUTTON, INPUT);
  pinMode(OUT_PIN, OUTPUT); 

  // http://qiita.com/kinu/items/6cd5da0415e31834e7da から
  //   これを設定するとすごい速さで動くようになるらしい
  // Non-inverting fast PWM mode on Pin 3.
  // COM2B1:0 ==  10: Non-inverting mode
  // WGM22:0  == 011: Fast PWM mode, 256 cycle (16MHz / 256 == 62.5kHz)
  // CS2:0    == 001: No prescaler (runs at maximum rate, 62.5kHz)
  TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(CS20);

  i = ARRAYSIZE(muri); // 再生位置を最後に指定し自動再生されなくする
}

void loop()
{
    if(i < ARRAYSIZE(muri)){
      OCR2B = pgm_read_byte_near(&muri[i]); // 音声データ書き込み
      i++;
    }
    lastval = (lastval << 1) | digitalRead(BUTTON); // チャタリング回避
    if(lastval == 0x7FFF)            // ボタンが押されたら
      i=0;                           // 再生位置を0に

    delayMicroseconds(72);   // 1000000microsec / 13800Hz
}
  • setup()関数内でマイコンが早い周波数で動くように設定している。
  • 単純にloop()関数内で音声の周期(72μs)ごとに音データを書き込んでるだけで、ボタンが押されると音声データの配列のインデックスを0に更新することで最初から再生されるようにし、ボタンを連打できるようにしている。
  • チャタリング回避を行っている。具体的には、digitalReadの値がボタンが押されていないときに0、押されているときに1となるのを利用して、0のあと15回1が続いた場合、信号が安定したとみなして音の再生を行う。ボタンの数値のログを保管するのにはシフトレジスタを使い、毎回レジスタを1ビット左シフトしてLSBにdigitalReadの値を入れていく。シフトレジスタ(lastval)の値が0b0111 1111 1111 1111となったときにボタンが押されたときの操作を実行する(再生位置を0に更新する)。これによりチャタリングが抑制されボタンを離した時に音が再生されることがほぼなくなった。

最後に

むーりぃー
f:id:nixeneko:20161228020555p:plain

初めてのTrueType命令: Windowsでは見えないフォントをつくる

TrueType命令を利用して何も表示されないフォントを作った。
f:id:nixeneko:20161216002924p:plain

フォント概要

作成したフォントは、アウトライン情報は保持しているのだが、TrueType命令によってすべての点をグリフの原点へ移動し、描画されるビットマップが存在しない状態にすることで何も表示されなくしている。

Windowsでは何も表示されなくなる一方で、Macではヒント命令を実行しないので普通に文字が表示される。そのため、フォントレンダラがレンダリング時にTrueTypeヒント命令を実行するどうかを確認するために用いることができる。

ダウンロード

作り方・技術解説

まえがき

最近TrueType命令を触っている。
TrueType命令というのは、OpenTypeフォント規格でアウトラインの形式としてTrueTypeを選択した場合に使用できるヒンティング用プログラム言語である。もともとTrueTypeフォント規格に付随していた。
ヒンティングでは主に制御点を動かすことでレンダリングされるビットマップを調整するのだが、この命令セットが妙に高機能なので色々できそうだ、という訳である。

このTrueType命令だが、最近のmacOS (OS X)では単に無視されるらしい。ヒンティング自体が低解像度の画面で可読性の高い表示を行うためのものなので、高密度なretinaディスプレイの下ではお役御免といったところだろうか。

という訳で、それを使って何か作ってみよう、というのがこの記事の目的である。

なお、記事タイトルの「初めてのTrueType命令」というのは私が触るのが初めてだったからであり、初めての人がこの記事を読んでTrueType命令を書けるようになるかは定かでない。

方針

さて、見えないフォントを作ることにする。つまり、アウトライン情報は保持しているのだが、TrueType命令によってすべてを指定した点(グリフの原点)に移動してしまうことで描画されるビットマップが存在しない状態にしよう、というものである。

TrueType命令を実行しない場合にはグリフが表示されるため、レンダリングしている環境でTrueType命令が実行されているかを確認することに使える。前述したようにTrueType命令は最近のMacでは無視されるので、Windowsでは見えないフォントということになる。

Fontforgeのインストール

TrueType命令のデバッグにはFontforgeが使える。最近のWindowsインストーラでインストールされるものにはTrueTypeデバッガを有効にしてコンパイルしてあるもののようなのでインストールすればそのまま使える。今回はWindows版の2016-10-04 Release Installer (.exe)でインストールし使用した。

コンパイル方法については次のページに書いてあるのでもし必要があれば参照されたい。


また、必須ではないのだが、Fontforgeはファイルメニューの環境設定から「TTF」タブの「大変更時に命令を消去」をオフにしておいた方がいいかもしれない。というのは、誤って制御点を追加してしまい編集中の命令が全消去された経験があるからである。
f:id:nixeneko:20161215202944p:plain

使うフォントの用意

さて、制御点を動かすにはTrueType形式のアウトラインが必要である。
今回はM+フォントをダウンロードしてきて弄ることにする。mplus-1p-regular.ttfをFontforgeで開く。

Fontforgeのヒント命令編集機能

メインウィンドウの「ヒント」メニューからヒント命令の編集ができる。
f:id:nixeneko:20161215203613p:plain


また、アウトラインウィンドウの「ヒント」メニューで個別グリフのヒント命令を細かく編集したりデバッグを行うことができる。
f:id:nixeneko:20161215203829p:plain

前準備

さて、まずはヒント命令を削除する。
メインウィンドウの「ヒント」→「Remove Instr Tables」でTrueType命令を全削除できる。

また、M+フォントはなぜかEMサイズが1000なので、2の冪数である1024にしておく。「エレメント」→「フォント情報」からウィンドウを開き、「一般情報」タブで「EMの大きさ」を1024にしてOKを押す。

TrueType命令を書いていく

制御点の移動命令

さて、実際にTrueType命令を書いていこうと思う。

今回は、グリフの全ての点を原点(0.0, 0.0)に移動することがゴールである。
そのためには制御点を移動する必要がある。制御点を移動する命令は豊富にあるが、今回は座標を指定してそこに移動する命令SCFS[]を使うことにする。
仕様によると、命令は次の様になっている。

SCFS[] Sets Coordinate From the Stack using projection vector and freedom vector

Pops c:coordinate value (F26Dot6)
p:point number (uint32)
Pushes なし
Uses zp2, freedom vector, projection vector

座標c, 制御点番号pを受け取り、pで表される制御点を座標cに移動するというものである。制御点はfreedom vectorに沿って移動され、projection vectorに沿った座標がcとなる場所に移される、らしい。

スタック

ところで、TrueType命令を実行するものはスタックマシンである。データはスタックに積み上げられ、命令の引数というのもスタック上に積まれていく。上の表のPopsというのがスタックから取得するデータを表しており、取得した(popされた)データはスタックから取り除かれる。

上の表ではcとpがこの順番でpopされる。すなわち、この命令を実行する前に、スタックトップにはcが、その一つ下にpがある状態になっている必要がある。

                     ↓スタックトップ
 … | 点番号p | 座標c |

スタックにデータを積む(pushする)のは主にPUSHB命令とPUSHW命令を使う。それぞれ、命令に後続する1~8個のByte(8ビット符号なし整数)もしくはWord(16ビット符号あり整数)の値をスタックに積む。積む数値の個数は指定する必要がある(正確には個数によって別命令になっている)。スタックに積む際には数値は32ビットに拡張され、PUSHBでは符号なしの拡張、PUSHWでは符号拡張される。


次の例はFontforgeのTrueType命令編集機能で認識されるように書いたもので、TrueType命令のマニュアルに書かれている表記とも少し異なっている。

PUSHB_3
 1
 2
 3

とすると

              →スタックトップ
 … | 1 | 2 | 3 |

のようにスタックに積まれ、

PUSHW_2
 2456
 -100

とすると

              →スタックトップ
 … | 2456 | -100 |

のように積まれていく。PUSHB_3、PUSHW_2のように命令の右端についている数字はpushする数値の個数を表している。

制御点移動のコード(1)

さて、SCFSを実行するところに戻る。点番号1の制御点を座標0.0に移動するコードは次のようになる。

PUSHB_1
 1
PUSHW_1
 0x0000
SCFS

試しに実行してみよう。

グリフのアウトラインウィンドウの「ヒント」→「ヒント命令を編集」でヒント編集用のウィンドウを開き、「編集」ボタンを押す(ボタンが何も表示されていない場合、ウィンドウサイズを縦に拡大すると現れる)。上のコードをペーストしOKを押す。
次に「ヒント」→「デバッグ」を開く。「グリッド合わせのパラメータ」ダイアログが開くので適当にOKを押す。
f:id:nixeneko:20161215221739p:plain
上図のようにTrueType命令のステップ実行用のインタフェースが現れ、またTrueTypeのパラメータやスタックを表示するウィンドウが開かれる。

ステップ実行のボタンを押していくと命令をひとつづつ実行することができる。実行結果が次である。
f:id:nixeneko:20161215221912p:plain
点番号1の制御点がX座標0.0に移動していることがわかる。


要するに制御点は設定した一つの方向にしか移動できないということである。そのため、x座標軸、y座標軸両方に対して座標を指定して移動するためには、方向を変化させて2度別々に処理をする必要がある。

移動方向切り替え

移動方向を切り替えるコマンドがSVTCAである。

  • STVCA[0]でY軸方向
  • STVCA[1]でX軸方向

に移動ができるようになる。具体的には距離を測る方向(projection vector)と移動する方向(freedom vector)を座標軸に指定するということらしい。

制御点移動のコード(2): 2次元移動

さて、Y軸方向にも移動させてみよう。制御点1を原点(0.0, 0.0)に移動する。コードを次のように書き換える。

STVCA[1]
PUSHB_1
 1
PUSHW_1
 0x0000
SCFS
STVCA[0]
PUSHB_1
 1
PUSHW_1
 0x0000
SCFS

先ほどのコードの前にSTVCAで移動方向を切り替えるものがついて、先にX軸方向、次にY軸方向、と2回繰り返している訳である。

同様に実行してみる。
f:id:nixeneko:20161215223116p:plain
分かりづらいが原点に移動している。


さて、これを制御点の数だけ繰り返せば全ての点が原点に移動し何も表示されなくなるという訳である。
単純に上のコードを、点番号を変化させて制御点の数だけ並べてもいいが、さすがにアホっぽいのと容量が無駄なのでもっと効率化したい。

関数化

ここで関数が登場する。

関数は'fpgm'テーブルにおいて、FDEF命令で定義できる。FDEFはスタックから整数を一つpopして、その番号の関数の定義を行う。関数の終わりはENDFで示される。
例えば関数0の定義だと次のように書く。

PUSHB_1
0
FDEF
 <ここに関数の内容がくる>
ENDF

さて、先ほどの、制御点を指定して原点に移動させる、という機能を関数としてまとめてみたい。
移動する制御点を指定するために制御点番号を入力とする。呼び出されるときにスタックトップが制御点番号であることを期待するということであり、関数から抜ける際には引数として入力された数値はpopされてスタックから取り除かれた状態にする。

つまり、スタックが、コール時には

              ↓スタックトップ
 … | 点番号p |

となり、関数を抜けるときは

    ↓スタックトップ
 … | 

となるように設計する。

関数番号は0として定義してみよう。

PUSHB_1
0
FDEF
STVCA[1]
DUP
PUSHW_1
 0x0000
SCFS
STVCA[0]
PUSHW_1
 0x0000
SCFS
ENDF

ここで、DUPはスタックトップを複製する命令である。以下のようにスタックの中身を考えてみると動作がわかると思う。

PUSHB_1
0
FDEF      /* ...|p| */
STVCA[1]  /* ...|p| */
DUP       /* ...|p|p| */
PUSHW_1   /* ...|p|p|0.0| */
 0x0000
SCFS      /* ...|p| */
STVCA[0]  /* ...|p| */
PUSHW_1   /* ...|p|0.0| */
 0x0000
SCFS      /* ...| */
ENDF

実行前後のスタックは先ほど設計したのと合っている。

今定義した関数0を実行するのはCALL関数を使う。コードは次のようになる。

PUSHB_2
 1
 0
CALL

CALLは関数番号をpopし対応する関数を実行する。このコードでは0は関数番号、1は関数の引数とする制御点番号である。

さて、実際に関数が動くか実行してみよう。前述したとおり関数定義は'fpgm'テーブルに書かないといけない。これはFontforgeのメインウィンドウから「ヒント」→「'fpgm'テーブルを編集...」から設定することができる。ここで先ほどの関数定義を書き込み、OKを押す。
また、グリフのヒント命令を関数を実行する命令に書き換える。
そして実行してみる。
f:id:nixeneko:20161216020353p:plain
先ほどに実行した命令と同じ動作をしているので成功である。

制御点の数だけ繰り返す

さて、これを制御点の数だけ繰り返さないといけない。
単純に指定回数だけ関数の実行を繰り返すならLOOPCALLという命令が使えるのだが、今回はすべての制御点に対して操作を行うため何回目の実行かということを覚えておかないといけないため、条件分岐IFと相対ジャンプJMPRを使って実装することにした*1

疑似コードを次に示す。nmaxはグリフの最大の制御点番号を指定する。

  n_cur = nmax;
loop_label:
  if (ncur >= 0){
    func0(ncur);
    ncur--;
    goto loop_label;
  }

さて、実際にコードを書いてみる。
Aの制御点は最大の制御点番号は11なので、nmaxとして11を指定してやってみる。

PUSHB_1
 11
DUP
PUSHB_1
 0
GTEQ
IF
DUP
PUSHB_1
 0
CALL
PUSHB_1
 1
SUB
PUSHW_1
 -15
JMPR
EIF
POP

GTEQはスタックトップの2項目を比較しその結果をpushする命令である。

GTEQ            →スタックトップ
before …| a | b | 
after  …| a>=b |

a>=bのときは1、そうでないときは0がpushされる。

IFはスタックトップから整数を一つpopし、それが0でないときは次の命令を実行していき、0ならばEIFにジャンプするというものである。

JMPRは整数を一つpopし、それによって指定される相対位置へとジャンプする。マイナスだと遡る方向にジャンプする。

さて、同様にスタックの中身を考えてみる。

         /* …|              */
PUSHB_1  /* …|ncur<=nmax|   */
 11
         /* …|ncur|         */ /* JMPRでここに戻ってくる */
DUP      /* …|ncur|ncur|    */
PUSHB_1  /* …|ncur|ncur|0|  */
 0
GTEQ     /* …|ncur|ncur>=0| */ 
IF       /* …|ncur|         */ /* if ncur >= 0 then */
DUP      /* …|ncur|ncur|    */
PUSHB_1  /* …|ncur|ncur|0|  */
 0
CALL     /* …|ncur|         */ /* func0(ncur) */
PUSHB_1  /* …|ncur|1|       */
 1
SUB      /* …|ncur-1|       */ /* ncur--; */
PUSHW_1  /* …|ncur-1|-15|   */
 -15
JMPR     /* …|ncur-1|       */ /* goto loop_label */
EIF
POP      /* …|              */

良さそう。さてこのプログラムをグリフのTrueType命令に書き込んで実行してみる。
実行しながらスタックの中身を示すウィンドウを見ていくと、スタックの一番下にある値がひとつづつ減少していき最終的に-1となってループを抜けていることがわかる。
f:id:nixeneko:20161216222646p:plain
描画されるビットマップが何もないという状態になっている。成功である。

ループを関数化

さて、これも関数にする。
スタックがコールする前後で、

              →スタックトップ
before:  … | nmax |
after:   … | 

となるようにしよう。nmaxにはグリフの最大の制御点番号を指定する。
関数番号は1とする。

PUSHB_1
 1
FDEF
DUP
PUSHB_1
 0
GTEQ
IF
DUP
PUSHB_1
 0
CALL
PUSHB_1
 1
SUB
PUSHW_1
 -15
JMPR
EIF
POP
ENDF

これは単純に前のコードから、

PUSHB_1
 11

を除いて、

PUSHB_1
 1
FDEF
 ~
ENDF

で囲んだだけである。
これを'fpgm'テーブルに追記する。

さて、関数1をつくったのでグリフから呼ぶ必要がある。
Aの最大の制御点番号は11なので、11を引数として関数1を呼び出す。コードは次のようになる。

PUSHB_2
 11
 1
CALL

実行してみると次のようになる。
f:id:nixeneko:20161216222403p:plain
関数化前と同様に何も塗られるビットマップがないので成功である。


さて、ここではAにだけ消えるようなヒント命令を実装したが、これをすべてのグリフに対しても適用したい。
そこで、ひとまずフォントを出力する。

フォントを出力

フォント出力前に「ヒント」→「'maxp'テーブルを編集...」からmaxpテーブルの数値を編集する。
f:id:nixeneko:20161216000103p:plain

  • ゾーンは2固定。
  • ストレージは使ってないので0。
  • FDEFはさっき関数0, 1の2つを定義したので2としておこう。
  • トワイライトポイントは使ってないので個数0。
  • スタックの最大深さは……使ってたの5個位だった気がするけど多めに指定しておくか。
  • IDEFは使ってないので0。

とか指定する。使ってるインデックスがそれぞれの指定した値を超えるとうまく動かないことになるのだと思う。

フォント情報(エレメント→フォント情報)を適宜修正し、TrueTypeフォントを出力する(ファイル→フォントを出力)。
f:id:nixeneko:20161216000955p:plain

TTXでXMLに変換

さて、これをTTXを使ってXMLファイルに展開する。
TTXのインストールについては次のサイトを参考にされたい。

今回使用したTTXのバージョンは2.5である。
コマンドプロンプトなどで先ほど出力したフォントAdisappear.ttfがあるフォルダへと移動し、

ttx Adisappear.ttf

と指定して展開し、Adisppear.ttxを得る。

すべてのグリフに適用

Adisappear.ttxをテキストエディタで描き、Aのグリフを探し、紐づいているTrueType命令を調べる。<glyf>~</glyf>の中で<TTGlyph name="A">~</TTGlyph>がある。

    <TTGlyph name="A" xMin="36" yMin="0" xMax="674" yMax="748">
      <contour>
        <pt x="36" y="0" on="1"/>
        <pt x="309" y="748" on="1"/>
        <pt x="401" y="748" on="1"/>
        <pt x="674" y="0" on="1"/>
        <pt x="587" y="0" on="1"/>
        <pt x="511" y="218" on="1"/>
        <pt x="196" y="218" on="1"/>
        <pt x="121" y="0" on="1"/>
      </contour>
      <contour>
        <pt x="218" y="284" on="1"/>
        <pt x="488" y="284" on="1"/>
        <pt x="354" y="674" on="1"/>
        <pt x="352" y="674" on="1"/>
      </contour>
      <instructions><assembly>
          PUSH[ ]	/* 2 values pushed */
          11 1
          CALL[ ]	/* CallFunction */
        </assembly></instructions>
    </TTGlyph>

<instructions><assembly>~</assembly></instructions>に挟まれているのがAのグリフに対応する命令であり、その中の11がAの最大の制御点番号である。これはほかのグリフに適用する際、グリフに含まれる制御点<pt>の個数を数えて、(制御点の個数 - 1)個を指定すればよい。


これをすべてのグリフに対しても適用するようなpythonスクリプトを作成した。

#!/usr/bin/env python3
# conding: utf-8

import xml.etree.ElementTree as ET

INFILE = "Adisappear.ttx"
OUTFILE = "Adisappear-out.ttx"
xmltree = ET.parse(INFILE)
xmlroot = xmltree.getroot()

for glyph in xmlroot.find('glyf').findall('TTGlyph'):
    cnt = 0
    for contour in glyph.findall('contour'):
        cnt += len(contour.findall('pt'))
    if cnt > 0:
        prog ="""
          PUSH[ ]    /* 2 values pushed */
          {} 1
          CALL[ ]    /* CallFunction */
          """.format(cnt-1)
        glyph.find('instructions').find('assembly').text = prog

with open(OUTFILE, 'w') as w:
    w.write('<?xml version="1.0" encoding="UTF-8"?>\n')
    xmlstr = ET.tostring(xmlroot, method='xml', encoding="unicode")
    #xmltree.write(OUTFILE)
    w.write(xmlstr)

やってることは各<TTGlyph>に対して<pt>の個数を求めて、もし個数が1以上なら(個数 - 1)を埋め込んだTrueType命令を<instructions><assembly>~</assembly></instructions>間に挿入するというものである。

これをapplyall.pyという名前でAdisappear.ttxと同じフォルダに入れ実行する。pythonは3系でないとうまく動かない。

python3 applyall.py

実行するとAdisappear-out.ttxが出力されるので、これをTTXを使って.ttfファイルへと変換する。

ttx Adisappear-out.ttx

結果

最終的に得られるフォント(Adisppear-out.ttf)をWindows 10でプレビューしたのが次である。
f:id:nixeneko:20161216002924p:plain
やったー! 何も見えないよ……。

他のOSで見てみると……

Mac OS X 10.5 Yosemite

f:id:nixeneko:20161217153953p:plain
噂通りヒンティングはされていないようである。
まあRetinaだし、ヒントでアウトラインを歪めなくてもオリジナルのアウトラインを綺麗に表示できるので……。

Ubuntu Desktop 16.04

f:id:nixeneko:20161216224852p:plain
ヒント効いてない。

どうやら、標準ではヒントが効かなくなっているらしい?

このページによると、gnome-tweak-toolを使ってヒンティングを調整できるようである。
f:id:nixeneko:20161216225417p:plain
「フォント」タブにて、ヒンティングがデフォルトではSlightになっているところをFullに変更する。

そしてフォントビューアを起動しなおすと……
f:id:nixeneko:20161216225543p:plain
やった!

参考サイト

*1:とはいえ、関数の中にカウンタを用意すればLOOPCALLでも問題なくできる気がする。