にせねこメモ

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

初めての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でも問題なくできる気がする。