にせねこメモ

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

PPEM・ポイントサイズを表示するフォント

TrueType命令で遊ぶシリーズ。
前回までの記事はこちら:

何をするのか

ラスタライザのTrueType命令回りの挙動を見るのに便利かなあと思って、TrueType命令から取得できるPPEM(EMあたりのピクセル数)やポイント数を可視化できるようなフォントを作ってみる。

完成品

PPEM

f:id:nixeneko:20170129005803p:plain

Point Size

f:id:nixeneko:20170129005837p:plain

ダウンロード

尚、この記事の末尾にTrueType命令のソースコードをまとめて記載した。

追記(2017-02-01 4時ころ)

gitlab.com
同様のものがすでにあったみたいですね。しかもこっちのが高機能だし……。

というか、PPEMは縦・横で異なる可能性があるのですね*1。長体・平体がかかった時は、確かに縦横でppemが異なりそう。

今回作成したものでは特にprojection vectorの指定をしてないので、MPPEMの前にSTVCA命令などでppemを計る軸を指定した方が良さそう。
(追記終)

方針

伝統ある7セグメント数字を使う。

一つの桁について、初めは8の字が表示されるように7つのセグメントを用意しておく。各セグメントには下図のように番号を割り振っておく。
f:id:nixeneko:20170129010346p:plain

このセグメントについて、その桁の数字が何であるかを見て、各セグメントを必要であれば適切に消去する。これによってその数字を構成するセグメントのみが残り、数字が表示できるという訳である。

セグメントNo. セグメントあり セグメントなし
0 0,2,3,5,6,7,8,9 1,4
1 0,4,5,6,8,9 1,2,3,7
2 0,1,2,3,4,7,8,9 5,6
3 2,3,4,5,6,8,9 0,1,7
4 0,2,6,8 1,3,4,5,7,9
5 0,1,3,4,5,6,7,8,9 2
6 0,2,3,5,6,8,9 1,4,7

実装

次のようなグリフを用意した。
f:id:nixeneko:20170129011619p:plain
各セグメントは4つの制御点からなり、セグメントの並び順は上図の丸囲み数字の番号が若い順に並ぶようにした。
丸囲みの数字をセグメント番号sとすると、セグメントsは制御点4s~4s+3からなる。

また、四角で囲んだ数字(mとする)は、 10^mの位の数字を表す。各桁に対応する数字位置におけるセグメントn ( 0 \le n \le 6)を考えると、
 s = 7m+n
となるようになっている。

なお、セグメントの並べ替えは、.ttfに書き出し→ttxで.ttxに変換→テキストエディタで並べ替え→.ttfに変換という風にして行った。

関数0: 指定した制御点を原点に移動

次にTrueType命令を考えていく。
さて、まずは、以前作った、指定した番号の制御点を原点に移動する関数0をもってきて使うことにする。

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

関数1: 指定したセグメントを消去

次に、指定した番号のセグメントを消去する関数1を考える。
入力は消去したいセグメント番号sとする。
セグメント番号sは、右からm番目( 0 \le m \le 3)の数字のセグメントn ( 0 \le m \le 6)について s = 7m+nで計算したものとする。
このとき、関数1は、制御点番号4s, 4s+1, 4s+2, 4s+3を原点に移動する。

/* func1: removes segment s */
/* initial stack ...|s|     */
/* final stack   ...|       */
PUSHB_1 1
FDEF        /* ...|s|                       */
PUSHW_1 256 /* ...|s|4.0|                   */
MUL         /* ...|4*s|                     */
DUP         /* ...|4*s|4*s|                 */
PUSHB_1 1   /* ...|4*s|4*s|1|               */
ADD         /* ...|4*s|4*s+1|               */
DUP         /* ...|4*s|4*s+1|4*s+1|         */
PUSHB_1 1   /* ...|4*s|4*s+1|4*s+1|1|       */
ADD         /* ...|4*s|4*s+1|4*s+2|         */
DUP         /* ...|4*s|4*s+1|4*s+2|4*s+2|   */
PUSHB_1 1   /* ...|4*s|4*s+1|4*s+2|4*s+2|1| */
ADD         /* ...|4*s|4*s+1|4*s+2|4*s+3|   */
PUSHB_1 0   /* ...|4*s|4*s+1|4*s+2|4*s+3|0| */
CALL        /* ...|4*s|4*s+1|4*s+2|         */ /*call func0 with 4*s+3*/
PUSHB_1 0   /* ...|4*s|4*s+1|4*s+2|0|       */
CALL        /* ...|4*s|4*s+1|               */ /*call func0 with 4*s+2*/
PUSHB_1 0   /* ...|4*s|4*s+1|0|             */
CALL        /* ...|4*s|                     */ /*call func0 with 4*s+1*/
PUSHB_1 0   /* ...|4*s|0|                   */
CALL        /* ...|                         */ /*call func0 with 4*s  */
ENDF

関数2: 各桁についての数字を表示

さて、次に、右からm番目( 0 \le m \le 3)の数字の値dについて適切にセグメントを消去する関数2を考える。これはまあ大体気合で。

/* func2: removes some segments of the m-th digit to show the number d */
/* initial stack ...|d|m| */
/* final stack   ...|     */
PUSHB_1 2
FDEF         /* ...|d|m|           */

PUSHW_1 448  /* ...|d|m|7.0|       */
MUL          /* ...|d|7*m|         */
SWAP         /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 0    /* ...|7*m|d|d|0|     */
EQ           /* ...|7*m|d|d==0|    */
IF           /* ...|7*m|d|         */ /* if d==0 */ /* 消去: 3 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 3  /* ...|d|7*m|7*m|3|   */
  ADD        /* ...|d|7*m|7*m+3|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+3|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+3 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 1    /* ...|7*m|d|d|1|     */
EQ           /* ...|7*m|d|d==1|    */
IF           /* ...|7*m|d|         */ /* if d==1 */ /* 消去: 0,1,3,4,6 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 1  /* ...|d|7*m|7*m|1|   */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 1  /* ...|d|7*m|7*m|1|   */
  ADD        /* ...|d|7*m|7*m+1|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+1|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+1 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 3  /* ...|d|7*m|7*m|3|   */
  ADD        /* ...|d|7*m|7*m+3|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+3|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+3 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 4  /* ...|d|7*m|7*m|4|   */
  ADD        /* ...|d|7*m|7*m+4|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+4|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+4 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 6  /* ...|d|7*m|7*m|6|   */
  ADD        /* ...|d|7*m|7*m+6|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+6|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+6 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 2    /* ...|7*m|d|d|2|     */
EQ           /* ...|7*m|d|d==2|    */
IF           /* ...|7*m|d|         */ /* if d==2 */ /* 消去: 1,5 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 1  /* ...|d|7*m|7*m|1|   */
  ADD        /* ...|d|7*m|7*m+1|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+1|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+1 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 5  /* ...|d|7*m|7*m|5|   */
  ADD        /* ...|d|7*m|7*m+5|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+5|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+5 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 3    /* ...|7*m|d|d|3|     */
EQ           /* ...|7*m|d|d==3|    */
IF           /* ...|7*m|d|         */ /* if d==3 */ /* 消去: 1,4 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 1  /* ...|d|7*m|7*m|1|   */
  ADD        /* ...|d|7*m|7*m+1|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+1|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+1 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 4  /* ...|d|7*m|7*m|4|   */
  ADD        /* ...|d|7*m|7*m+4|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+4|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+4 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 4    /* ...|7*m|d|d|4|     */
EQ           /* ...|7*m|d|d==4|    */
IF           /* ...|7*m|d|         */ /* if d==4 */ /* 消去: 0,4,6 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 1  /* ...|d|7*m|7*m|1|   */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 4  /* ...|d|7*m|7*m|4|   */
  ADD        /* ...|d|7*m|7*m+4|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+4|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+4 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 6  /* ...|d|7*m|7*m|6|   */
  ADD        /* ...|d|7*m|7*m+6|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+6|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+6 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 5    /* ...|7*m|d|d|5|     */
EQ           /* ...|7*m|d|d==5|    */
IF           /* ...|7*m|d|         */ /* if d==5 */ /* 消去: 2,4 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 2  /* ...|d|7*m|7*m|2|   */
  ADD        /* ...|d|7*m|7*m+2|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+2|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+2 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 4  /* ...|d|7*m|7*m|4|   */
  ADD        /* ...|d|7*m|7*m+4|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+4|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+4 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 6    /* ...|7*m|d|d|6|     */
EQ           /* ...|7*m|d|d==6|    */
IF           /* ...|7*m|d|         */ /* if d==6 */ /* 消去: 2 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 2  /* ...|d|7*m|7*m|2|   */
  ADD        /* ...|d|7*m|7*m+2|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+2|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+2 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 7    /* ...|7*m|d|d|7|     */
EQ           /* ...|7*m|d|d==7|    */
IF           /* ...|7*m|d|         */ /* if d==7 */ /*消去: 1,3,4,6 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 1  /* ...|d|7*m|7*m|1|   */
  ADD        /* ...|d|7*m|7*m+1|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+1|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+1 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 3  /* ...|d|7*m|7*m|3|   */
  ADD        /* ...|d|7*m|7*m+3|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+3|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+3 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 4  /* ...|d|7*m|7*m|4|   */
  ADD        /* ...|d|7*m|7*m+4|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+4|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+4 */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 6  /* ...|d|7*m|7*m|6|   */
  ADD        /* ...|d|7*m|7*m+6|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+6|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+6 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

                                      /*(if d==8)*/ /* 消去:なし */

DUP          /* ...|7*m|d|d|       */
PUSHB_1 9    /* ...|7*m|d|d|9|     */
EQ           /* ...|7*m|d|d==9|    */
IF           /* ...|7*m|d|         */ /* if d==9 */ /* 消去: 4 */
  SWAP       /* ...|d|7*m|         */
  DUP        /* ...|d|7*m|7*m|     */
  PUSHB_1 4  /* ...|d|7*m|7*m|4|   */
  ADD        /* ...|d|7*m|7*m+4|   */
  PUSHB_1 1  /* ...|d|7*m|7*m+4|1| */
  CALL       /* ...|d|7*m|         */ /* call func1 with 7*m+4 */
  SWAP       /* ...|7*m|d|         */
EIF          /* ...|7*m|d|         */

POP          /* ...|7*m|           */
POP          /* ...|               */
ENDF

数字で場合分けして、セグメントを消去する関数を呼び出してるというだけだが、全部スタックをつかって値を扱ってるので妙に長くなる。うーん、気合感。

関数3: 剰余の計算

次に、各桁の数値を計算して関数2を呼び出すような関数を作りたいところだが、先に剰余を計算する関数3を考える。

剰余の計算は、
 a \div b = c \ \mbox{余り} \ d
のとき、
 a = bc + d
が成り立つから、
 d = a -bc
を計算すればいいことがわかる。cは結果が整数になる様な割り算  c = \mathrm{int}(a/b) で求められるので、実際は
 d = a -b \cdot \mathrm{int}(a/b)
といった感じの計算になる。

さて、関数を実装するが、ここで、MULやDIVは整数ではなくF26Dot6に対して定義されている命令であり、整数a, bに対して

MUL: a * b / 64
DIV: a * 64 / b

という風な計算になることに注意する。

/* func3: calculates remainder a % b */
/* initial stack: ...|dividend a(int)|divisor b(int)|          */
/*   final stack: ...|remainder(int) = dividend a % divisor b| */
PUSHB_1
 3
FDEF     /* ...|a|b|                 */
PUSHB_1  /* ...|a|b|2|               */
 2
CINDEX   /* ...|a|b|a|               */
PUSHB_1  /* ...|a|b|2|               */
 2
CINDEX   /* ...|a|b|a|b|             */
PUSHW_1  /* ...|a|b|a|b|64.0|        */
 4096
MUL      /* ...|a|b|a|b*64|          */
DIV      /* ...|a|b|int(a/b)|        */ /* DIV: (a)*64 / (b*64) */
PUSHB_1  /* ...|a|b|int(a/b)|1.0/64| */
 1
DIV      /* ...|a|b|int(a/b)*64|     */
MUL      /* ...|a|b*int(a/b)|        */ /* MUL: (b) * (int(a/b)*64) / 64 */
SUB      /* ...|a - b*int(a/b)|      */
ENDF

関数4: 数値を表示する

さて、ここで、各桁の数値を計算して関数2を呼び出し、数値を表示する関数4を作る。

要するに、数値xについて、
x%10, x/10 % 10, x/100 % 10, x/1000 % 10
を計算して、それらの数字が各桁に表示されるように関数2を適用する。 / は整数の除算、 % は剰余を表す。

/* func4: shows the given number x */
/* initial stack: ...|x|           */
/*   final stack: ...|             */
PUSHB_1 4
FDEF          /* ...|x|                  */
DUP           /* ...|x|x|                */
PUSHB_1 10 3  /* ...|x|x|10|3|           */
CALL          /* ...|x|x%10|             */ /*call func3 with x, 10 */
PUSHB_2 0 2   /* ...|x|x%10|0|2|         */
CALL          /* ...|x|                  */ /*call func2 with x%10, 0 */
PUSHW_1 640   /* ...|x|10.0|             */
DIV           /* ...|x/10|               */
DUP           /* ...|x/10|x/10|          */
PUSHB_2 10 3  /* ...|x/10|x/10|10|3|     */
CALL          /* ...|x/10|(x/10)%10|     */ /*call func3 with x/10, 10 */
PUSHB_2 1 2   /* ...|x/10|(x/10)%10|1|2| */
CALL          /* ...|x/10|               */ /*call func2 with (x/10)%10, 1 */
PUSHW_1 640   /* ...|x/10|10.0|          */
DIV           /* ...|x/100|              */
DUP           /* ...|x/100|x/100|        */
PUSHB_2 10 3  /* ...|x/100|x/100|10|3|   */
CALL          /* ...|x/100|(x/100)%10|   */ /*call func3 with x/100, 10 */
PUSHB_2 2 2   /* ...|x/100|(x/100)%10|2|2|   */
CALL          /* ...|x/100|              */ /*call func2 with (x/100)%10, 2 */
PUSHW_1 640   /* ...|x/100|10.0|         */
DIV           /* ...|x/1000|             */
DUP           /* ...|x/1000|x/1000|      */
PUSHB_2 10 3  /* ...|x/1000|x/1000|10|3| */
CALL          /* ...|x/1000|(x/1000)%10| */ /*call func3 with x/1000, 10 */
PUSHB_2 3 2   /* ...|x/1000|(x/1000)%10|3|2| */
CALL          /* ...|x/1000|             */ /*call func2 with (x/1000)%10, 3*/
POP           /* ...|                    */
ENDF

グリフのプログラム

さて、この関数4をグリフから呼び出す。
引数の数値としては、PPEMやポイントサイズを与えてみようと思う。

PPEM

PPEMとは、Pixels/EMのことで、EMのサイズが何ピクセルで描画されるかということを示した値である。

TrueType fundamentals - Typography | Microsoft Docsによると、ppemは、
ppem = pointSize * dpi / 72
によって計算される。

PPEMを測定するのはMPPEM命令を使う。PPEMを表示するようなプログラムは次のようになる。

MPPEM     /* |PPEM|   */
PUSHB_1 4 /* |PPEM|4| */
CALL      /* |        */ /* call func4 with PPEM */
ポイントサイズ

ポイントサイズを取得するのはMPS命令を使う。

MPS       /* |PS|   */ /* Point Size */
PUSHB_1 4 /* |PS|4| */
CALL      /* |      */ /* call func4 with PS */

いろんなソフトの挙動をみる

以下で挙げた他にも、Windows 10上のFirefox, Google Chromeでも動作するのを確認した。

Windows 10 フォントビューア

PPEM:
f:id:nixeneko:20170129005803p:plain

Point Size:
f:id:nixeneko:20170129005837p:plain
冒頭でも上げたが、PPEMとPoint Sizeが別の値を指している。PPEMの計算式から、DPIが96であることがわかる(要するに、72ptの時のPPEMがDPIである)。
なお、PPEMの値はディスプレイのDPI設定を変えると変わる。
まともな実装だと思う。

Ubuntu フォントビューア

gnome-tweak-toolでヒンティングを有効にしてある。

PPEM:
f:id:nixeneko:20170129164627p:plain

Point Size:
f:id:nixeneko:20170129164707p:plain

PPEMとポイントサイズが全く同じ値を示している。dpi=72固定ということだろうか。
システム設定→ディスプレイ→「メニューとタイトルバーの拡大縮小」をいじってみたけど変化なし。

Adobe ソフト

なんとAdobe IllustratorでもTrueType命令は動く。Photoshopでも動く。以下のスクリーンショットWindows版のCS6のバージョンのものである。

Photoshop

PPEM:
f:id:nixeneko:20170129020643p:plain
謎の挙動を示す。上に挙げたppemの計算式に対し4掛けしたものが400以下かどうかで場合分けされるらしい。

if (4 * pointSize * dpi / 72 <= 400)
     ppem = 4 * pointSize * dpi / 72;
else
     ppem =     pointSize * dpi / 72;

みたいな感じか。

Point Size:
f:id:nixeneko:20170129020925p:plain
72固定っぽい。

Illustrator


Photoshopと似たような挙動を示す。

  • 基本的に最終的に画面に表示されるサイズによってppemは計算される。ズームイン・ズームアウトでも値が変わる。
  • ドラッグで拡縮してるときはppemは(pointSize * dpi / 72)になり、マウスボタン離すと上のPhotoshopと同様の場合分けされた挙動をする。
  • ポイントサイズは、Shift+ドラッグによる拡縮時(つまり、長体あるいは平体がかかってない状態)では、72未満は反映されるが、マウスボタンを離すと72固定になる。
  • アウトライン化するときは、ppemは、100%表示にしたときのppemあるいは1024のどちらか大きい方になるっぽい。ポイントサイズは72固定。
InDesign

f:id:nixeneko:20170129163439p:plain
InDesignも、Illustratorと似た挙動をするっぽい。

  • 基本的に最終的に画面に表示されるサイズによってppemは計算される。ズームイン・ズームアウトでも値が変わる。
  • ppemはPhotoshop同様に場合分けになるっぽい。
  • point sizeは72固定
After Effects

ppemは1024固定っぽい。
f:id:nixeneko:20170128105149p:plain
同様にPoint Sizeも72固定。
f:id:nixeneko:20170128112211p:plain

*1:MPPEMはprojection vectorを利用しているので。

続きを読む

CygwinからNotepad++でファイルを開く: Unixパスの扱い

Cygwinでテキストファイルを編集したいとき、普段Windowsで使っているNotepad++を使ってコマンド一発で開ければ便利だなあと思った。
丁度Ubuntuでgeditを使うようなイメージで。

他のWindowsプログラムについても、同様に対応可能であると思う。


Notepad++の実行ファイルは次にある。

C:\Program Files (x86)\Notepad++\notepad++.exe

これはCygwinUnixパスにすると

/cygdrive/c/Program Files (x86)/Notepad++/notepad++.exe

である。

案1

notepad++に実行ファイルのUnixフルパスをaliasしてみる。

alias notepad++='"/cygdrive/c/Program Files (x86)/Notepad++/notepad++.exe"'

内側をダブルクォートで囲んでいるのは、パスの中にスペースが含まれているため、そこで区切られて、ファイルが見つかりませんということになるのを避けるためである。

これだと、しかし、Unixパスで指定されたもの(“~/hoge”, “/usr/local/hoge”など)が開けない。これはNotepad++の実行ファイルにパスを通した場合でも同様だと思う。

案2

なので、引数が与えられた場合、cygpathを使ってWindowsパスに変換してから実行するような関数を作ってみた。

notepad++ () {
  if [ $# -eq 0 ]; then
    '/cygdrive/c/Program Files (x86)/Notepad++/notepad++.exe' &
  else
    for x in "$@"; do
      '/cygdrive/c/Program Files (x86)/Notepad++/notepad++.exe' "`cygpath -wa \"$x\"`" &
    done
  fi
}

引数の個数が0ならそのまま実行し、そうでなければすべての引数についてcygpathでWindowsフルパスに変換したものを引数として与えて実行している。
Notepad++のプログラムは & をつけてバックグラウンドで実行されるようにした。これをしないと複数ファイルを開けないからである。

~/.bashrc に書いておいた。


さて、とりあえず動いているのだが、これでいいのかどうか。

Undertaleをやろうとしたらゲームパッドの上が効かない

症状

ゲームパッドを接続してUndertaleを起動したところ、ゲームパッド十字キーの上が押しても反応せず、右左下には動けるのに上には動けない、あるいは上がずっと押しっぱなし状態になるかのどちらかで困っていた。
なお、別のスーファミゲームパッド(Elecom 8 Button Gamepad JC-FR08T)でも同様の状態になった。

解決策

UndertaleのJoystick Configを開き、DIR CHOICEをANALOG ONLYに変更。
f:id:nixeneko:20170121111745p:plain

POVによる入力はうまく動かないようで、Analog入力を使うように設定しないといけないっぽい。


ゲームパッド十字キー上方向の入力が効く、この当たり前のことが、あなたを決意でいっぱいにした。

フォントサイズに合わせて回転するフォントを作る(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

続きを読む