にせねこメモ

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

AutoHotkeyで入力キーボートレイアウトの切替を行う

Windowsでは複数の入力言語やキーボードレイアウト(IMEを含む)を設定できる。日本語IMEは日本語入力のオンオフを切り替えられるのでそれだけを利用している場合には触れることは少ないかもしれないが、Google日本語入力ATOKのような別のIMEを併用したり、中国語、タイ語など他の言語のIMEやキーボードを使う場合、切替が必要となる。

この切替は「Alt+Shift」(入力言語の切替)、「Ctrl+Shift」(同言語でのキーボードレイアウトの切替)、あるいはWindows 8以降では「Windowsロゴキー+Space」(入力言語・キーボードレイアウトの切替)のショートカットキーで行うことができる。また、設定で特定のキーボードレイアウトへ切り替えるショートカットを指定することができるが、設定できるキーの組合せに制限があったり、うまく設定が反映されなかったりすることがあるので、AutoHotkeyでキーボードレイアウトの切替を定義できれば便利だろう。


AutoHotkeyはSendMessage命令を利用してWindows APIのウィンドウメッセージを投げることができる。これで望む入力言語・キーボードに対応するinput locale identifier (入力ロケール識別子)を指定してWM_INPUTLANGCHANGEREQUESTを投げると、入力言語・キーボードを切り替えることができる。
したがって、このinput locale identifierを適切に取得すれば、指定したキーボードレイアウトを切り替えるホットキーが定義できる。これを取得するために、Windows APIのLoadKeyboardLayout関数やGetKeyboardLayout関数を利用することができる。

検証環境

Windows デスクトップ アプリケーションのいくつかで動作を確認したが、動かないアプリケーションもあるかもしれない。

方法1: LoadKeyboardLayout関数を使う場合

LoadKeyboardLayout関数のsyntaxは次のようになっている。

HKL WINAPI LoadKeyboardLayout(
  _In_ LPCTSTR pwszKLID,
  _In_ UINT    Flags
);

LoadKeyboardLayoutの引数のpwszKLIDは「input locale identifierの名前」であり、16進数の数字8文字のからなる文字列で、レジストリ

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Keyboard Layouts

の下にある項目のものである。ここから、利用したいキーボードを表す8桁の16進数字をメモしておく。

なお、既定のキーボードについては次のページからも参照できる。


目的のキーボードを探すのに役立つ情報として、pwszKLIDの下4ケタはlanguage identifierである。language identifierは次のページから参照できる。

また、上4桁については、その言語のデフォルトキーボードは0000となるようである。ある言語のキーボードレイアウトにいくつか種類がある場合、0001, 0002, と上4桁の番号が順番に増えたりするらしい。一方でMSKLCなどでキーボードレイアウトを自作した場合などは上4桁がa000などとなる場合もある。

実装例

ここでは、例として「日本語・Microsoft IME」と「ロシア語 (タイプライター)」に切り替えるホットキーを設定してみる。
まず、pwszKLIDとして与えるべきものは

キーボード pwszKLID
日本語・Microsoft IME 00000411
ロシア語 (タイプライター) 00010419

である。

これらを引数に指定してLoadKeyboardLayout関数を呼び出してinput locale identifierを取得し、取得したinput locale identifierを指定してPostMessageでWM_INPUTLANGCHANGEREQUEST (0x50)メッセージをアクティブウィンドウに投げれば切り替えができる。

F11で日本語・Microsoft IME、F12でロシア語 (タイプライター)に切り替えるAutoHotkeyスクリプトは次のように書ける。

F11::
  ;日本語・MS-IMEへの切替
  ja := DllCall("LoadKeyboardLayout", "Str", "00000411", "Int", 1)
  PostMessage 0x50, 0, ja,, A  ;WM_INPUTLANGCHANGEREQUEST
Return

F12::
  ;ロシア語 (タイプライター)への切替
  ru := DllCall("LoadKeyboardLayout", "Str", "00010419", "Int", 1)
  PostMessage 0x50, 0, ru,, A  ;WM_INPUTLANGCHANGEREQUEST
Return

方法2: GetKeyboardLayout関数を使う場合

いちいちレジストリエディタ開いてinput locale identifierの名前を調べるのは面倒である。どうせ切り替えるキーボードは普段使っているものなので、現在使用しているキーボードから直接input locale identifierを取得してしまえばよい。


GetKeyboardLayout関数は現在選択されているinput locale identifierを返す。
しかし、LoadKeyboardLayoutの場合と異なり、32ビットのinput locale identifierのMSB (最上位ビット)が1の場合でも正整数が返ってくる。要するに32ビット符号なし整数扱いになっているようだ。
一方でPostMessegeでWM_INPUTLANGCHANGEREQUESTを投げる時に指定するinput locale identifierは符号あり整数にする必要があるようで、GetKeyboardLayout関数が返した数値をそのまま用いると、32ビットアプリでは動くが、64ビットアプリでは動作しないようである。
そのため、自分で変換する必要がある。


まず、次のようなスクリプトを使って、使っている入力言語・キーボードレイアウトのinput locale identifierを取得する。
結果はキー入力として出力されるので、テキストエディタなどを開いて調べたいキーボードレイアウトに切り替えた状態でF10を押す。

F10::
  SetFormat, Integer, H
  WinGet, WinID,, A
  ThreadID:=DllCall("GetWindowThreadProcessId", "UInt", WinID, "UInt", 0)
  InputLocaleID:=DllCall("GetKeyboardLayout", "UInt", ThreadID, "UInt")
  Send, %InputLocaleID%
Return


すると、次のようなinput locale identifierが取得できる。

キーボード input locale id
日本語・Microsoft IME 0x4110411
ロシア語 (タイプライター) 0xF0080419

日本語・Microsoft IMEの場合は32ビットのMSBが0であるため問題ないが、ロシア語 (タイプライター)の場合はMSBが1なため負数で表す必要がある。

次のWebサービスのようなものを用いて、32ビット16進数表記を符号あり10進整数に変換する。

すると、0xF0080419は-267910119であることがわかる。これをWM_INPUTLANGCHANGEREQUESTメッセージのlParamに指定する。


F11で日本語・Microsoft IME、F12でロシア語 (タイプライター)に切り替えるAutoHotkeyスクリプトは次の様になる。

F11::
  ;日本語・MS-IMEへの切替
  PostMessage 0x50, 0, 0x4110411,, A  ;WM_INPUTLANGCHANGEREQUEST
Return

F12::
  ;ロシア語 (タイプライター)への切替
  PostMessage 0x50, 0, -267910119,, A  ;WM_INPUTLANGCHANGEREQUEST
Return

文字のデザインに筆記具が与える影響

Type& 2015「ロゴの多言語化:デバナガリとアラビア文字

2015年11月21日にType& 2015の「[3]ロゴの多言語化:デバナガリとアラビア文字」を聴講した。これは、まずデーヴァナーガリー及びアラビア文字の特徴についてそれぞれタイプデザイナーであるVaibhav Singh氏およびNadine Chahine氏から簡単に紹介がなされ、さらにラテン文字のロゴと各スクリプト*1版作成の実例が紹介された。その後、実在の日本の会社のロゴタイプを題材に、実際に両氏が各スクリプト版を試作したというものであった。


この後半のローカライズロゴ試作においてお題に使われたうちの一つがWonders! by Panasonicのロゴであった。
ここで注目したいのが感嘆符「!」で、上の縦棒部分の下端がカーブして切り取られ、左側にとがっている。一方で、試作されたデーヴァナーガリー及びアラビア文字版ロゴではそれが左右逆になっていた。図解すると次のようである*2
f:id:nixeneko:20151124001702p:plain
アラビア文字ラテン文字とは逆に右から左へと書き、疑問符などの記号も左右反転させて使う(“؟”)ので、デザインが左右反転しているのも納得できるが、一方でラテン文字同様左から右へ書くデーヴァナーガリーでもデザインが反転しているというのは不思議である。
このことは公演中にデーヴァナーガリー版ロゴについて小林章氏がVaibhav Singh氏に質問していたことなのだが、Vaibhav Singh氏はその理由について「その方が自然だから」と返答していた。

筆記具が影響した?

ここからは私の仮説であるが、伝統的に文字を書くのに使われていた筆記具が影響しているのではないかと思う。各スクリプトが伝統的にどのような筆記具で書かれていたかをみていくことで確認してみたい。

ラテン文字

ラテン文字は長らく羽ペンで筆記されてきた伝統をもつ。次の動画で羽ペンの作り方や筆記のやり方が解説されている。現在のラテン文字セリフ書体のデザインは羽ペンで書かれた文字の形を元にしているという。
www.youtube.com
羽ペンは平らなニブ(ペン先)をもち、縦方向に動かすと太く、横方向に動かすと細く線がかける。
現在の欧文カリグラフィーで使われるペンにも同様に平たいニブをもつものがあるが、ニブ(ペン先)を上に向けて外側からみたときに、傾きがなく横まっすぐになっているか、あるいはゆるやかに右肩上がりになっている*3かのどちらかのようである。
ペンを当てるときはペン先が水平から反時計回りに角度がついた状態で書かれることが多いようだ。これは、oなどの円形の文字の左上と右下が細くなり、反対に左下と右上が太くなることに影響している。
f:id:nixeneko:20180113164152p:plain
(フォントはTimes New Roman)

アラビア文字

イスラム世界ではアラビア文字による書道が発達している。アラビア書道用のペンは葦ペン等の平らなニブをもつ筆記具が用いられる。ペン先は斜めにカットされていて、ニブを上に向けて外側からみたときに右肩上がりになる場合が多いようだ。アラビア文字を書く場合はペン先の角度が直角に近く、ペンを横に向けてに持つ感じになる。次の動画でペンの持ち方が解説されている。
www.youtube.com

アラビア文字フォントでも、ペン先を傾ける方向はラテン文字と同様反時計回りでありながら、角度はかなり急峻になっているのがみてとれる。
f:id:nixeneko:20180113171052p:plain
(フォントはTimes New Roman)

デーヴァナーガリー

デーヴァナーガリーは伝統的に葦ペンでかかれる。

次の動画では、デーヴァナーガリー用のカリグラフィペンは、ペン先が欧文カリグラフィー用とは逆の傾きをもち、右肩下がりになっているという説明がある。
www.youtube.com
実際に文字を書いているところや書かれた文字をみると、ペン先の当て方がラテン文字アラビア文字とは傾きが逆で右肩下がりになっている。
www.youtube.com
次の動画では基本的なストロークを紹介しているが、ここで書かれている円は右上と左下が細く、左上と右下が太くなっている。
www.youtube.com

全体として、ニブの当て方が逆であることから、ラテン文字アラビア文字とは線の方向と太さの関係が逆になっていることがみてとれる。線の端についても、左上から右下に傾いていることがわかり、これもラテン文字と逆である。
f:id:nixeneko:20180113172941p:plain
(フォントはAdobe Devanagari)

まとめ

カリグラフィ用のペンのニブの形状(傾き)の違いと、ニブをどの向きで紙に当てるかについてを次図にまとめた。
f:id:nixeneko:20180113173314p:plain

ここで、なぜアラビア文字デーヴァナーガリーで試作ロゴの感嘆符「!」の向きが一致したのかを検討してみたい。

アラビア文字で感嘆符が反転したのは書字方向が反対になるのに合わせたということだろう。

一方で、デーヴァナーガリーは、伝統的にラテン文字とは逆方向の右肩下がりにペン先を当てるため、線の端は右肩下がりになるのが自然であり、線の端が右肩上がりになる要素は不自然であると考えられる。そのためラテン文字と書字方向は同じだが、「!」の縦線が右肩下がりになる形を採用し、デーヴァナーガリーのエレメントに合わせたのではないか。


結果としてアラビア文字デーヴァナーガリーで「!」の形が一緒になったものの、どうして元のラテン文字のロゴから左右反転したような形を採用したのか、その理由は2つのスクリプトで異なっているようにも感じられる。
文字のデザインはそのスクリプトが書かれていた筆記具に影響されるというのは正しそうではあるが、必ずしもそれだけでデザインが決定される訳ではなく、他の要因との複合で最終的な形が作られるのだろう。

*1:この「スクリプト」は、ラテン文字アラビア文字デーヴァナーガリーなどの文字体系のことを指す。用字系、書字系などとも。script.

*2:蛇足ではあるが、“Wonders!”の“s”に当たる部分を、デーヴァナーガリーではs, アラビア文字ではzに相当する文字で転写しているのも面白い。

*3:ペン先に角度がついているものをobliqueという。Difference between an italic, an oblique, and a stub? | Classic Fountain Pensによると、usually about 15 degreesとのこと。

キーボードの言語によってDvorakJの有効無効を切り替える

随分前に、DvorakJを利用して英字入力はDvorakにし、日本語入力はJapanist 2003を利用して親指シフト(NICOLA)をエミュレーション利用していた。一方で、ロシア語やタイ語等を入力する要求がでてきて、DvorakJはこの様な日本語以外のキーボード言語の入力時に無効にする設定ができず、まともに入力ができないため、Dvorak配列を使用するの自体をやめてしまっていた。

やりたいこと

キーボードの配列を次のようにしたい。

  • 日本語入力時(キーボード言語が日本語かつIMEオン): 親指シフト(NICOLA)
  • 英字入力時(キーボード言語が日本語かつIMEオフ): Dvorak
  • 他言語入力時(キーボード言語が日本語以外): Qwerty相当 (キーボードの入力そのまま)


これは、2回の場合分けによって表現できる。

  1. キーボード言語による場合分け(日本語か?)
    • 日本語でない→キー変換を行わない
    • 日本語である→2へ
  2. IMEのオンオフによる場合分け
    • オン→日本語入力用配列
    • オフ→直接入力用配列


ここで、現行のDvorakJ (2014-06-07版)ではIMEのオンオフの検出で日本語入力時と直接入力時で別々の配列を適用することができる(つまり、2.は実装されている)が、一方で日本語以外の言語に切り替えたときも直接入力用の配列が適用されてしまって、日本語以外の言語を入力することができない。
なので、うまいことキーボードの言語が日本語であるかどうかで場合分けを行い、日本語である場合にだけキーリマップを適用する様にしたい。

方針

DvorakJはAutoHotkey_L用のスクリプトが公開されているので、それを改変し、条件分岐部分を追加すればいいのではないかと思う。

AutoHotkeyで現在のキーボード言語を取得するには次のページのようにするといいらしい。

ここに挙げられたコードを引用する。ただし、このコードはコンソールでは使えないとのことである。

F11::
  SetFormat, Integer, H
  WinGet, WinID,, A
  ThreadID:=DllCall("GetWindowThreadProcessId", "UInt", WinID, "UInt", 0)
  InputLocaleID:=DllCall("GetKeyboardLayout", "UInt", ThreadID, "UInt")
  MsgBox, %InputLocaleID%
Return

要するにInputLocaleIDが現在のキーボードを識別するIDのようである。

WinAPIのGetKeyboardLayoutの項によると、

The return value is the input locale identifier for the thread. The low word contains a Language Identifier for the input language and the high word contains a device handle to the physical layout of the keyboard.

GetKeyboardLayout function (Windows)

だそうで、下位2バイトは入力言語のlanguage code、上位2バイトはキーボードレイアウトへのデバイスハンドルらしい。

最新のWindows 10 (バージョン1709)において、実際に取得したInputLocaleIDは次のようになっている。

言語 キーボード InputLocaleID
日本語 Microsoft IME 0x4110411
タイ語 タイ語 Kedmanee 0x41E041E
ロシア語 ロシア語 0x4190419
ロシア語 ロシア語 - ニーモニック 0xF0330419
ロシア語 ロシア語 (タイプライター) 0xF0080419
モンゴル語 伝統的なモンゴル文字 0xF0B20850

その言語の標準的なキーボード配列においては上4桁(2バイト)は下4桁と同じになっているように見える。
キーボードによって条件を変化させるのであれば全体を比較すればよいし、言語によって変化させるのであれば下2バイトだけ比較すればよいことになる。

Language Codeについては、AutoHotkeyのドキュメントにも次の様な記術があるが、

WindowsのLanguage Codeの一覧は次のページから参照することができる。


DvorakJのソースをみてみたところ、DvorakJ全体を一時停止するキーショートカットが設定できるので、これに使われてるロジックを利用して、キーボードが日本語以外に切り替わった場合に一時停止するようにし、反対に日本語に切り替わった場合には一時停止を解除するようにしてみる。

環境

改変

DvorakJのAutoHotkey_Lスクリプト版の src/init/set_various_timers.ahk を開き、27行目付近の

	;;; IME の状態を定期的に調べる
	SetTimer, IME_GET, %IMEms%

の下に

	SetTimer, Keyboard_GET, %IMEms%

を挿入し、更に末尾に

;; キーボードの言語を一定間隔毎に取得し、日本語以外だったら動作を停止する。
;; 日本語だったら動作を再開する。
Keyboard_GET:
  SetFormat, Integer, H
  WinGet, WinID,, A
  ThreadID:=DllCall("GetWindowThreadProcessId", "UInt", WinID, "UInt", 0)
  InputLocaleID:=DllCall("GetKeyboardLayout", "UInt", ThreadID, "UInt")
  If ( InputLocaleID & 0xFFFF = 0x411 ){ ;Japanese
    If ( A_IsSuspended ){
      toggle_status_of_dvorakj(False)
    }
  } Else { ;otherwise
    If ( !(A_IsSuspended) ){
      toggle_status_of_dvorakj(True)
    }
  }
return

を追加した。

要するに、一定時間ごと(今回はIMEの状態を定期的に調べる間隔(IMEms)を流用した)に現在のキーボードの入力言語を取得し、

  1. 入力言語が日本語であり、かつ、DvorakJが無効である(Suspendされている)場合は、Suspendを解除する
  2. 入力言語が日本語でない、かつ、DvorakJが有効である(Suspendされてない)場合は、Suspendする

という処理をもともとのソースコードに追加している。

感想・課題

しばらく使ってみたが、親指シフト回りがあまり快適でなかったのでやめてしまった。DvorakJ自体を利用するのが久しぶりであるのに加え、親指シフトのエミュレーション用として利用したことがなかったため、問題の切り分けができていない。そのためはっきりしたことが言えないので今後も検証が必要である。

  • 親指シフトのエミュレーションの同時押しの検出がやまぶきRより精度が低い感じがする。そのため、入力ミスが多発した。
    • 急いで入力すると「にゅうりょく」が「にけをりょく」や「にゅうりいる」になったり。やまぶきRでも時々なるけれど…。
    • やまぶきRと同様の設定にしてるつもりであるが、設定によって改善が可能かもしれない。要検証。
  • ウィンドウを切り替えたときや別の入力フィールドに移ったときなどにうまく入力ができなくなる場合があった。
    • 改変部分が原因かもしれないが、素のDvorakJを最近使ってなかったので比較ができない。
    • やまぶきRでもなったことがあるように思うので一概に何ともわからない。別のウィンドウ切り替えた後で戻すと復活したりする。
  • DvorakJに用意されていた一時停止機能を勝手に利用しているため、一時停止してもキーボードが日本語であればすぐ再開されてしまい、一時停止機能が使えない状態になっている。
  • 本当にこんな改変で大丈夫なのか……?


頂いた情報によると、DvorakJとやまぶきRではいろいろと同時押しエミュレーションのロジックが異なるので入力感は同じにならないとのこと。


なかなか難しい。

Adobe Illustrator CS6日本語版でタイ文字やアラビア文字を組む

Adobe CS6ソフトウェアを学生版の買い切りで購入して以来愛用しているが、InDesignはともかくIllustratorはタイ文字やアラビア文字といった複雑なテキスト配置を要する用字系(complex script)をまともに表示できなかったりと不満があった。しかし、Illustratorでも裏技的な方法でタイ文字やアラビア文字などを正しく表示することができるらしい。

Adobe InDesign CS6日本語版の多言語対応

Adobe InDesign CS6日本語版では「段落」パネルのメニューから「Adobe 多言語対応コンポーザー」が選択でき、それによってタイ文字やアラビア文字を一応まともに組むことができる。
f:id:nixeneko:20171213120147p:plain
文字を入力してみると次のようになる。
f:id:nixeneko:20171213160221p:plain
フォントはアラビア文字Times New Roman、タイ文字はAngsana New、ヒンディー語Adobe Devanagariを指定した。

ただし、「段落」パネルは書字方向の設定(RTL)に対応していない様で、RTL言語を組むための機能が揃っているとはいえないようだ。RLM (U+200F RIGHT-TO-LEFT MARK)を挿入することによって一応右横書きとして妥当な表示にはなるので、日本語の中にアラビア語を挿入するなどの場合には十分使えるのではないかと思う。

Adobe Illustrator CS6日本語版の多言語対応

一方、Adobe Illustrator CS6日本語版では多言語対応コンポーザーなどは選択できず、「Adobe 日本語コンポーザ―」のみとなっている。
f:id:nixeneko:20171213121442p:plain
これでは、アラビア文字のような右から左に書くもの(RTL)であったり、同じ文字が語中の位置に形が変わる書字系には対応できない。また、タイ文字の様に記号の位置調整が重要なものも正しく表示されない。次の画像が入力例である。
f:id:nixeneko:20171213155615p:plain
アラビア文字のひどさは言うまでもないが、タイ文字の2行目において、文字の上方で縦方向に2段積み重なっていた記号が重なってしまっている。また、ヒンディー語は母音の再配置ができていない。これでは使い物にならない。

World-Ready Comopser

ところで、調べていたら偶然、CS4以降のAdobeソフトウェア(InDesign, Photoshop, Illustrator)にはWorld-Ready Composerが実装されているということを知った。次の記事である。

これによると、ソフトウェアの中東版向けに右から左への横書きや語の位置による字形の置き換え(contextual substitution)のような機能が存在していたが、CS4からそれが中東版以外のソフトにもWorld-Ready Composerとして実装されたとのことである。

ただし、Illustratorなどにおいては、World-Ready ComposerはAPIとしては存在しているが、先に挙げたようにUIからアクセスすることはできない。とはいえ、実装自体はされているので、中東版のソフトウェアで作ったテキストエリアを含むファイルを開きコピペすることで、CS4のIllustratorでもWorld-Ready Composerを利用できるという。

前掲のWebサイトにおいて、World-Ready Composerを利用したテンプレートファイルが配布されている(“Templates for ID, Ai & PS”でページ内検索するとよい)。
これを開いてテキストエリアを新しいドキュメントにコピペするとWorld-Ready Composerが使えるようになる。


実際にIllustrator CS6 日本語版やってみたのが次である。
f:id:nixeneko:20171213155007p:plain
すごい! 完璧! ちなみに文字の再配置を要するデーヴナーグリー(ヒンディー語)などにも対応しているようである。
任意の用字系に対応している訳ではないので、前掲のブログ記事の中のリスト等で確認するとよいと思う。何にせよ、正しく組めているかは(分かる人が)自分の目で見て確認する必要があるだろう。

バージョンCC以降

さて、World-Ready Composerが実装されたのは、全世界的に機能を提供するためであるというのは想像に難くない。実際に、Adobe Illustrator CCにおいて、中東やインド言語のサポートが追加されている。

「環境設定」の「テキスト」から「インド言語のオプションを表示」をオンにすると「中東言語および南アジア言語コンポーザー」が利用できるようになるとのことである。

素晴らしい。CC契約しようかな……。


(20171213 16時追記: アラビア語をコピペミスってたので画像を修正。ちゃんと確認してないのがバレバレですね……。)

フォントのアウトラインを法線方向に太らせたり細らせたりしてみる

フォントのアウトラインの制御点を法線方向に沿って動かしたら文字の線をうまいこと太らせたり細らせたりできるのではないかと思ってやってみて、結果小さい移動量においてはそこそこになったように思う。

読まなくてもいい前書き

現在一般のコンピュータで使われているフォントは、文字を表す図形の輪郭をベクタフォーマットで収録したアウトラインフォントである。そのため、線の太さはアウトライン毎に固定され、線の太さを変えたければ別のアウトラインを用意するしかない*1
そこで、一つの書体のアウトラインを変形して文字の線の太さをうまいこと変化させられれば、太さの違う書体を用意しなくてもよいということになる*2


以下は、フォントのアウトラインの制御点を法線方向に沿って動かせばうまいこと太らせたり細らせたりできるんじゃないか!?と思いついたのでやってみたという記録である。恐らくこのような処理を実装しているソフトウェアはあるだろうし、これよりさらに良いアルゴリズムもあるだろうので、歪な車輪の再発明なのではという気はする。

ここで、アウトラインを「フォントの」と限定しているのは、アウトラインフォントに含まれるcontourがすべてclosed pathであり、open pathがないからという理由である。すなわち、contourによって表現される図形の外側と内側がはっきりとしているため、法線ベクトルを一意に定めることができる。

OpenTypeフォントではアウトラインを直線およびベジエ(Bézier)曲線の集合によって表現する。OpenTypeにはアウトラインデータの格納方式としてCFF形式とTrueType形式を選ぶことができるが、PostScript由来のCFFベースのもの(一般にこれが「OpenTypeフォント」と呼ばれる)では3次ベジエ曲線が使われ、TrueTypeベースのもの(これは一般に「TrueTypeフォント」とよばれている)は2次ベジエ曲線が曲線の表現に使われている。
この記事においてはTrueType形式のものについて扱うが、CFF形式の方でも同様に操作可能であると考えられる。

一続きの直線あるいはベジエ曲線によって表現される図形をcontourとよび、制御点列によって表現される。一つのグリフ(フォントによって描画を行う際の操作単位で、大抵は文字と一対一で対応付けられる)は0個以上のcontourからなる。

手法

一般に点に対して法線は求まらないが、ここでは、制御点の法線を、その制御点に隣接する2制御点とその制御点のなす2辺について、それぞれの単位法線ベクトルを足し合わせた方向のものとする。
つまり、 m個の制御点からなるあるcontourについて、制御点が \boldsymbol{p}_{k-1},\ \boldsymbol{p}_k,\ \boldsymbol{p}_{k+1} ( 0 \le k \le m-1)の順で並んでいたとき( \boldsymbol{p}_{-1} = \boldsymbol{p}_{m-1},\ \boldsymbol{p}_{m} = \boldsymbol{p}_{0}とする)、点 \boldsymbol{p}_{k}における法線ベクトル \boldsymbol{n}_{\boldsymbol{p}k}は、2点 \boldsymbol{p}_{k-1},  \boldsymbol{p}_{k}を通る直線の単位法線ベクトル \boldsymbol{n}_{k-1}と、2点 \boldsymbol{p}_{k},  \boldsymbol{p}_{k+1}を通る直線の単位法線ベクトル \boldsymbol{n}_{k}を用いて、
 \boldsymbol{n}_{\boldsymbol{p}k} = s(\boldsymbol{n}_{k-1} +  \boldsymbol{n}_{k} ),\ s > 0
と表せる。
なお、 s \displaystyle s = \frac{1}{\|\boldsymbol{n}_{k-1} + \boldsymbol{n}_{k}\|}と定めると単位ベクトルになる( \|\cdot\|ユークリッドノルムを指す)。
f:id:nixeneko:20171211190003p:plain
TrueTypeアウトラインにおいて、contourの制御点を順番にたどっていった時の右手側が塗りつぶされることになると決まっているので、 \boldsymbol{n}_{k} = (x_k,\ y_k)^\topとしたとき、 \boldsymbol{n}_{k}は、スクリーン座標系(左上原点)において次のように計算できる。
 \displaystyle \boldsymbol{n}_{k} =\frac{1}{\| n_k - n_{k-1} \|} (y_k - y_{k-1},\  x_{k-1} - x_k )^\top

ここで、フォントの座標系は左下原点だがスクリーン座標系は左上原点であり、y軸正方向が反転していることに気を付ける。

実験1

各頂点における単位法線ベクトルに沿って移動させ、色を変えて描画した図を次に示す。黒が元々のアウトラインであり、外側に移動させたものは緑~赤、内側に移動させたもの緑~青で描画している。
f:id:nixeneko:20171211230418p:plain
見ればわかる様に、頂点の前後の辺がなす角度に関わらず頂点の移動量が一定のため、全体として形が崩れていると感じられる。

実験2

次に、 s
 \displaystyle s = \frac{1}{1+\cos\theta} ( \thetaはその頂点を通る2辺の法線のなす角、上図参照)
とした場合の法線ベクトルの定数倍制御点を移動させてみる。
f:id:nixeneko:20171211231642p:plain
そうすると、移動量が大きい部分については自己交差して破綻している部分もあるが、小さい移動量においては割と全体的の形を保ったまま移動できているように思う。

ちなみに、 \displaystyle s = \frac{1}{1+\cos\theta}というのは、2辺の法線のなす角度が \theta = 0のときは \displaystyle s = \frac{1}{2} \displaystyle \theta = \frac{\pi}{2}のときは s = 1となるように定めた。

コード

描画に使用したPython 3コードを次に挙げる。描画に用いたフォントはM+フォントのmplus-1p-regular.ttfである。
フォントの読み込み~アウトラインの制御点列の抽出はFontToolsを利用した。
アウトラインの描画・画像の出力はPillowを利用した。
gist.github.com
制御点座標の操作はsetで表して泥臭くやったが、Google関わっているフォント系のライブラリにおいては制御点の表現に複素数型を使っていたし、あるいはNumPyのndarrayなどによって制御点を表すと座標計算が楽になるのではないかと思う。

考察

ベジエ曲線は凸包性(convex hull property)をもつ。凸包性とは、(n+1)個の制御点によって定義されるn次ベジエ曲線が(n+1)個の制御点の凸包(convex hull)の内部に含まれるという性質であり、要するに制御点がなす多角形によって曲線のだいたいの形が推測できるということで、これによって、ベジエ曲線の集まりで構成されるcontourについて、制御点のなす多角形(polygon)が均等に太るように動かすとそれによって表現されるベジエ曲線等もまあまあ均等に太るようになっている気がする。
何にせよ、ベジエ曲線(からなる図形)の操作に多角形の操作手法が適用できるというのは言えるのではないかと思われる。3DCGでオブジェクトをメッシュの法線方向に拡縮する手法があるので、それを適用してみてもいいかもしれない。

調べたら次のサイトが引っかかった。

このサイトによると、ベジエ曲線をオフセットした曲線を(同次)のベジエ曲線で表現することは不可能である、とのことであり、適当に近似するしかない。紹介されている手法では、適当にベジエ曲線を分割し、それらに対して法線方向に移動するというものである。

次のサイトでもいくつかの手法が紹介されているが、制御点を移動してみて、精度が十分でなかったら分割するという方針らしい。

結局、太らせたり細らせたりする方向にベジエ曲線を移動したものを同じ制御点数で正確に表現できないので、本手法では限界があるようではある。しかし、本手法で楽なのは、制御点の数が変化せず、また曲線を分割する必要がないため単純に前後の制御点をみて移動させるだけで済むという部分である。自己交差さえ何とかなれば何かに使えるかもしれない。

*1:2016年にリリースされたOpenType 1.8で導入されたvaribale fontではユーザーが書体の線の太さ等を動的に変えられるが、フォントデータには太さ等の違う2種類以上のアウトライン(に相当する)情報を含ませる必要がある。

*2:実際には、市販の書体ファミリー、特に極太書体では、字が潰れない様に部分的に細くしたりバランスを整えるために再配置したり要素を融合させたりといった調整を行っているため、機械的な太さの変更では一般に品質は低下すると考えられる。

OpenTypeフォントにSVGアニメーションを突っ込んでみる

Twitterを眺めてたらOpenType-SVGを実装した話が流れてきた。

ちなみにその実装ではラスタ画像をSVGのベクタ形式に変換しているが、SVGの<image>タグのxlink:href属性にPNG画像などをdata URI schemeを使って埋め込むとラスタ画像も利用できる。


最近はAdobeのソフトウェア(PhotoshopIndesignなど)がOpenType-SVGカラーフォントをサポートし始めたり、最新のWindows 10の描画エンジンでもサポートが始まり、Edgeなどでも表示できるようになっているらしく、今後が期待できる。


そういえば、OpenType-SVGではSMILによるアニメーションは問題なく突っ込めるという話があった*1

OpenType fonts with either TrueType or CFF outlines may also contain an optional 'SVG ' table, which allows some or all glyphs in the font to be defined with color, gradients, or animation.

https://www.microsoft.com/typography/otspec/svg.htm

せっかくなので試してみる。

SVGのアニメーションといえば前になんか作ってたなあと思ったので次のページで作成したSVGをフォントに突っ込んでみることにする。

手順

編集する下地のフォントとして、M+フォントのmplus-1p-medium.ttfからASCIIコード外のグリフをごっそり削除したものを利用した(編集にはFontforgeを利用した)。
これは次のリンクからダウンロードできる。

まず用意した下地のフォント(mplus-mod.ttf)をTTXでXML形式にダンプする。

ttx mplus-mod.ttf

するとmplus-mod.ttxが出来上がるので、このファイルをテキストエディタなどで開いて編集する。

今回はアルファベットの“W”の代わりにSVG画像が表示されるようにしてみる*2
まず、<GlyphOrder>タグで囲まれた要素を見て行って、WのglyphIDを調べる。今回は、次の記述から、58であることがわかった。

    <GlyphID id="58" name="W"/>


これをもとに、ルート要素である<ttFont>の子として次のように<SVG>タグを追加する。

  <SVG>
    <svgDoc endGlyphID="58" startGlyphID="58">
      <![CDATA[<svg xmlns="http://www.w3.org/2000/svg"
                    xmlns:xlink="http://www.w3.org/1999/xlink"
                    width="768" height="432" 
                    viewBox="0 600 764 432" id="glyph58">
        (SVGの描画される要素がここに入る)
      </svg>]]>
    </svgDoc>
  </SVG>

ここで、<svgDoc>タグにはstartGlyphIDおよびendGlyphID両属性に58(WのglyphID)を指定している。
また、<svg>タグはhttp://nixeneko.2-d.jp/hatenablog/20170330-svganim/animate.svgの中身を突っ込んだものであるが、viewBoxの2番目の値を600に変更し、さらにWのglyphIDの58に合わせてid="glyph58"の指定を追加している。<svg>タグの子孫要素は長いので記載を省略した。


最後に編集した.ttxファイルをTTXで.ttfフォントファイルに変換する。

ttx -o out.ttf mplus-base.ttx

完成したフォントは次のリンクからダウンロード可能:

サンプル

次のページで実際に試すことができる:


実際にFirefoxで見てみると次のようになった。
f:id:nixeneko:20171031204634g:plain
こいつ…動くぞ…!?


なお、Edgeで見てみたら次のような感じになり、アニメーションは動かなかった*3
f:id:nixeneko:20171031210837p:plain
ここで、SVG画像の表示される高さや大きさが、Firefoxの場合と異なっている。SVGファイルをそのまま突っ込んだだけで幅・高さの辻褄を合わせていないから実装依存になってしまっているのだと思う。

ちょっとした解説

OpenType-SVGでは複数のグリフに対して一つのSVG文書(画像)を指定することができる。

つまり、TTXの形式では、一つの<svgDoc>を複数のglyphIDに対応付けることができる。この対応付けはstartGlyphIDとendGlyphIDで指定し、指定されたstartGlyphID~endGlyphID間に含まれるglyphIDに対応するグリフがそのSVG文書に結び付けられる。

また、<svgDoc>内に埋め込まれるSVG文書には、id="glyph58"のように、idに“glyph<glyphID>”の形で(その<svgDoc>と結びつけられた)glyphIDに対応するid属性を指定した要素が含まれていないといけない。グリフが描画されるとき、そのglyphIDに対応するidを持つ要素が画面に表示されることになる。

詳しくは仕様書を参照: The SVG Glyph Outlines Table


あと、svgのviewBoxの2番目の要素を変化させると、SVGがフォントとして表示される際の位置を上下に動かすことができる。今回使ったSVG画像は最初viewBox="0 0 764 432"と指定されていたが、その状態では丁度ベースラインからぶら下がる位置に表示された。なので、もう少し上の位置に表示させるためにviewBox="0 600 764 432"を指定した。

今回はSVG画像の幅や高さは特に気にせず突っ込んだが(それでも動く…!)、高さはフォントのEMのユニット数、幅はグリフのadvanceWidthに合わせるといいのではないかという気がする。描画時にどのように描画サイズが計算されるのかはちゃんと確認した方がよさそう。

まとめ

  • SVG画像で表示したいグリフに対応するよう指定してSVG画像を突っ込めばOpenType-SVGカラーフォントは作成できる
  • FirefoxではSVGのアニメーションまで動く
  • EdgeではOpenType-SVGは表示できるがアニメーションは動かない
  • SVGの幅や高さの指定がフォントの高さやグリフの幅と合ってない場合、表示位置や大きさは実装依存っぽい

*1:セキュリティ上の理由から、Javascriptは動かない。

*2:なぜWかというと、Windowsのフォントビューアのサンプルテキストの最初に来る文字だからである。なお、Windowsのフォントビューア自体はSVGフォントに対応してないので普通にWが表示された。

*3:そもそもEdgeってSMILアニメーションに対応してなかった…。

ins/delタグのdatetimeで指定された追記/削除時間を表示する

HTML文書を書いているとき、<ins>や<del>などのタグで追記や削除を行う場合に、その追記や削除を行った時間を表示しておけたらいいと思うことがある。これらのタグにはdatetime属性で時間が指定できるので、そこで指定された日時に追記あるいは削除された、ということを自動で表示できるようにしたい。

要するに、例えば、

<ins class="test" datetime="2017-10-28T01:23:45+09:00">こんばんは。</ins>

と書かれた場合、

(2017年10月28日追記)こんばんは。

などと表示できたらうれしい。


ここで、タグの前に文字列を挿入するのはCSSの擬似要素を使えばできるのだが、固定テキストやタグに指定された属性の内容等を挿入することはできる一方、それらを組み合わせたり複雑な処理を行うことはCSSのみではできないようだ。


次掲のページによると、Javascriptで直接擬似要素をいじることはできないが、擬似要素に適当な属性を指定しておくとそれ経由でJavascriptで動的に表示を変更できるらしい。


というわけで、作ってみた。

実行例

おはようございます。こんばんは。今朝今晩もいい天気です。今日も頑張っていきもう寝ましょう。

datetimeに何も指定しない場合は特に何も挿入しないようにした。

ソース

<style type="text/css">
ins::before {
  content: attr(data-before);
}
ins{
  text-decoration-line: underline;
  text-decoration-style: dashed;
}
del::before {
  content: attr(data-before);
}
</style>

<script type="text/javascript">
//forEachに渡すdata-before要素を書き換える関数を返す関数。
//追記と削除で同形式だから統合した。
function getRewriteAttrFunc(inserttext){
  return function(elem){
    datestr = elem.getAttribute('datetime');
    if (datestr){ //datetime要素が空でない場合
      t = new Date(datestr); //Date型に変換
      writestr = "("+t.toLocaleDateString()+inserttext+") "; 
                                            //(2017/10/28に追記) とか
      elem.setAttribute('data-before', writestr);
    }
  }
}
//HTMLのロード完了時に実行
document.addEventListener("DOMContentLoaded", function(event) {
  Array.prototype.forEach.call(document.getElementsByTagName("ins"), 
                               getRewriteAttrFunc("追記"));
  Array.prototype.forEach.call(document.getElementsByTagName("del"), 
                               getRewriteAttrFunc("削除"));
});
</script>

<del datetime="2017-10-30T01:27:42+09:00">おはようございます。</del><ins 
datetime="2017-10-30T01:27:42+09:00">こんばんは。</ins><del>今朝</del><ins
>今晩</ins>もいい天気です。<del datetime="2017-10-30T01:27:42+09:00"
>今日も頑張っていき</del><ins datetime="2017-10-30T01:27:42+09:00">もう寝</ins
>ましょう。

日付を表す文字列を用意するのにtoLocaleDateStringを使ってるけど、要するにロケール異なるとまた違う出力になりそうなので、代わりにgetFullYear, getMonth, getDateあたりのメソッドから泥臭く作る方がrobustかも。

datetime属性の指定が壊れてる場合に“(Invalid Date追記)”などという表示になる。対応したほうが良いかもしれない。

あと、window.onload使うのも、他に使用しているJavascriptライブラリと競合してしまってよくない場合があるかもしれない。jQueryなどのライブラリを使ってる場合はそちらに投げてしまったほうがいい気がする。
コメントでご指摘いただいたように、window.onloadの代わりにdocument.addEventListener("DOMContentLoaded", function(event){ … });を使うように書き換えた。