SATySFiで素敵な文字を出力する

> これは「[SATySFi Advent Caleandar 2018](https://adventar.org/calendars/3418)」の8日目の記事です。
（7日目は [hanachinさん](https://github.com/hanachin/SATySFiAdventCalendar2018/releases/tag/20181207) でした。9日目は [bd_gfngfnさん](https://github.com/gfngfn/SATySFi/wiki/SATySFi%E3%81%AE%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E5%87%BA%E5%8A%9B%E3%83%A2%E3%83%BC%E3%83%89%E3%81%AE%E3%83%97%E3%83%AD%E3%83%88%E3%82%BF%E3%82%A4%E3%83%97) です。）

[SATySFi]の重要な目的の一つは「LaTeXのよりよい代替となること」です。そして、皆さんご存知の通り[^known]、LaTeXといえば**[ゆきだるま][snowman-1]**:snowman:です。となると、SATySFiでも:snowman:ができることが求められるのは当然の帰結でしょう。

というわけで、早速SATySFiで素敵な:snowman:の絵を描く話……といきたいところですが、なんと、[どっかの誰か][ZR]かSATySFiの正式リリースの当日に早々と[そのネタを済ませて][snowman-2]しまっています。困った事態です。

そこで、チョット目先を変えて、素敵な:snowman:の「絵」でなく「字」を書くことにしましょう。すなわち、**SATySFiで“☃”(U+2603)の文字を書く**ことを目指します。

※本記事に登場するソースコードの完全版をGistに置きました：
→[SATySFiで素敵な文字を出力する](https://gist.github.com/zr-tex8r/2c1ce5f6ded16a55d1ccb0462c7f4da6)（Gist:zr-tex8r）

[SATySFi]: https://github.com/gfngfn/SATySFi
[^known]: えっ、ご存知でしょ？☃
[snowman-1]: http://acetaminophen.hatenablog.com/entry/2015/12/13/080226
[snowman-2]: http://d.hatena.ne.jp/zrbabbler/20180211/1518303817
[ZR]: https://qiita.com/zr_tex8r

# ☃を書けば☃が……出ない

とはいっても、SATySFiはUnicodeに十分に対応していてソースファイルをUTF-8で記述するようになっているはずです。それゆえ、Unicode文字である☃(U+2603)を出力するのは自明な気もするでしょう。とにかく試してみましょう。

```text:sample0.saty
@require: stdja

% '☃'(U+2603)を含むインラインテキスト
%※解説の都合上, 出力テキストをマクロに切り出した
let-inline \my-test = {少年老い易く☃成り難し}

% あとはいつものStdJa.documentの通り
in
document (|
  title = {\SATySFi;でゆきだるま文字したい話};
  author = {某ゆきだるま者};
  show-title = false; show-toc = false;
|) '< % 先のマクロを使用
  +p {\my-test;}
>
```

ただし、フォントのデフォルト設定はオリジナルの通りとします。つまり和文フォントは「IPAexフォント」です。これなら“☃”(U+2603)のグリフを含むので大丈夫でしょう。コンパイルしてみましょう。

```text
 ---- ---- ---- ----
  target file: 'sample0.pdf'
  dump file: 'sample0.satysfi-aux' (will be created)
  parsing 'sample0.saty' ...
  parsing 'stdja.satyh' ...
……(略)
  reading 'sample.saty' ...
  type check passed. (document)
 ---- ---- ---- ----
……(略)
 ---- ---- ---- ----
  output written on 'sample.pdf'.
FontFormat> No glyph is associated with U+2603.
```

アレレ、コンパイルは通りましたが、なんか「U+2603のグリフがない」と怒られてしまいました。嫌な予感とともに結果のsample0.pdfを開くと……。

![sample-1.png](https://qiita-image-store.s3.amazonaws.com/0/14550/abdf4ca9-dfc8-023f-68a2-680e7039483a.png)

**☃できません！** 何てことでしょう！

## ☃のために必要なもの

気を取り直して出力をよく見てみましょう。グリフ欠落を示す“?”は欧文フォント（[Junicode]）で出力されていて、さらにその周りに和欧文間空白（四分空き）があります。ここから推測すると、「“☃”の文字が**欧文扱いされた**ため、欧文フォントで出力しようとした」と考えられます（残念ながらJunicodeにはU+2603のグリフはありません）。

理由が判ったので対策を考えましょう。☃が欧文扱いされるのが問題なので、☃の文字を**「強制的に和文として出力する」**ための仕組が必要です。そこで次のような命令を考えましょう。

  * `\my-ja{...}`: [inline-text] inline-cmd<br>`引数のテキストを「強制的に和文として」出力する。

仮にこのような命令`\my-ja`があるとすると、先のsample0.satyの中の`\my-text`の定義を次のように変えれば素敵な☃が無事に出力できるはずです[^general]。

```text
let-inline \my-test = {少年老い易く\my-ja{☃}成り難し}
```

以下では、`\my-ja`を実装することを目標とします。

[Junicode]: http://junicode.sourceforge.net/
[^general]: 別に「和文の☃を出力する命令」を定義しても構わないのですが、汎用的に使える`\my-ja`の方がより妥当でしょう。

## とにかくフォントを変えてみる

SATySFiにおける文字とフォントの扱いについては[『The SATySFibook』][SATySFibook]の第8章「文字組版」で解説されています。ここで必要な要点を抜粋します。

  * SATySFiではUnicode文字の集合を4つの**文字体系**(script)に分類[^script-class]していて、文字体系ごとに異なるフォントを割り当てることができる[^script-space]。
  * 文字体系を表す型が**script型**であり、これは`HanIdeographic`（漢字）・`Kana`（仮名）・`Latin`（ラテン文字）・`OtherScript`（その他の文字体系）の4つの定数値からなる。
  * `set-font`: script → string ∗ float ∗ float → context → context<br>`set-font` *scr* (*fname*, *r*, *rb*) で、文字体系 *scr* に対するフォント[^font]を (*fname*, *r*, *rb*) に更新する[^setter]。

これを利用して、フォントを（アドホックに）変更する命令`\my-font`を実装してみます[^poly-type]。

```text:sample1.saty
@require: stdja

%% my-set-for-each-script: (script -> context -> context) -> context -> context
% setf scr が文脈更新函数値である場合に, my-set-for-each-script setf は
% "全てのscript値 scr について setf scr による更新を行う"という更新を行う.
let my-set-for-each-script setf ctx =
  ctx |> setf HanIdeographic |> setf Kana
      |> setf Latin |> setf OtherScript

%% my-set-all-fonts: (script * float * float) -> context -> context
% my-set-all-fonts fspec で全ての文字体系に対するフォントを fspec に更新する.
% (fspec はフォントを表す3つ組.)
let my-set-all-fonts fspec =
  my-set-for-each-script (fun scr -> set-font scr fspec)

%% \my-font: [string; inline-text] inline-cmd
% \my-font(fname){...} は引数のテキストをフォント fname で出力する.
% ※fname はフォント名で, スケールは1, ベースライン補正は0で決め打ち.
let-inline ctx \my-font fname it =
  let ctx1 = my-set-all-fonts (fname, 1., 0.) ctx in
  read-inline ctx1 it

% 明示的にフォント'ipaexm'を指定した
let-inline \my-test = {少年老い易く\my-font(`ipaexm`){☃}成り難し}

in
% document 以降はsample0.satyと同じ
```

  * `\my-font`では、引数のテキストにどんな文字体系の文字が含まれていてもよいように、全ての文字体系についてフォント設定を更新しています。
  * SATySFiの既定設定において、「IPAex明朝」は`ipaexm`というフォント名で登録さているため、このフォントで☃を出力するためには``\my-font(`ipaexm`){☃}``と書くことになります。

このsample1.satyのコンパイル結果（本文部分）は以下のようになりました。

![sample-2.png](https://qiita-image-store.s3.amazonaws.com/0/14550/6d9380a7-17d0-02b3-515a-80567db6002c.png)

今度はちゃんと「IPAex明朝」の☃が出力されています。

[SATySFibook]: https://github.com/gfngfn/SATySFi/wiki/The-SATySFi%E2%80%8Bbook-Web%E5%85%AC%E9%96%8B%E7%89%88-%E7%AC%AC1%E7%89%88
[^script-class]: つまり、これまでの話では便宜的に「欧文」「和文」と呼んでいましたが、実際のSATySFiでは分類は4つあるということです。どの文字がどの文字体系に属するかはUnicodeにおける文字体系の規定（Script属性値）により決められます。なお、☃(U+2603)および他の多くの記号はUnicodeでは「どの文字体系にも属さない」（Script値が`Common`）となっていますが、SATySFiでは「どの文字体系にも属さない」文字はSATySFiにおける**寡占的文字体系**(dominant scripts)の設定に基づいて特定の文字体系に“吸収”されて扱われます。結局、既定の状態では、☃は`Latin`に“吸収”されるため、ラテン文字と同じくJunicodeフォントで出力する（ことを試みる）ことになります。
[^script-space]: 「和欧文間空白」の有無や空き量についても、実際には「script同士の間の設定」という扱いです。
[^font]: SATySFiにおいて「フォント」は「（SATySFiが管理する）フォント名（string）」「スケール値（float）」「ベースライン補正値（float）」の3つ組で表されます。
[^setter]: ある context → context 型の函数について「その函数が引数のテキスト処理文脈を受け取って、……に更新した新たなテキスト処理文脈を返す」という言明を、この記事では単に「その函数で……に更新する」と表現することにします。またこのような函数を便宜的に「**文脈更新函数**」と呼ぶことにします。
[^poly-type]: コメント中に函数の型を記していますが、型註釈を入れているわけではないため、実際に定められる型はより広いものになっている場合があることに注意してください。

## 番外編：好きなフォントを使いたい

本題からは少し外れますが、せっかく「明示的にフォント名を指定してフォントを変更する」ような命令を実装したので、「自分がもっている**好きなフォントで文字を出力する**」ための方法を解説しておきます。例として、「[源真ゴシック] Regular」（ファイル名はGenShinGothic-Regular.ttf）で素敵な☃を出力してみます。

※フォントの登録関連の話は『[The SATySFibook][SATySFibook]』の7章「システムと設定ファイル」で解説されています。

※ここでは実効の（実際に中に dist/ が存在する）ライブラリルートのディレクトリが ~/.satysfi/ であると仮定します。SATySFiをopamを通じて自分でコンパイルしてインストールした場合はそれで合っているはずです[^libroot]。

まずは、「[源真ゴシック] Regular」のファイルをSATySFiで使えるように“登録”します。

 1. フォントファイル GenShinGothic-Regular.ttf を ~/.satysfi/dist/fonts/ の直下に配置します。

 2. **フォントハッシュファイル**(font hash file) ~/.satysfi/dist/hash/fonts.satysfi-hash を編集して「源真ゴシック」のためのエントリを追加します[^ttc-file]。ここではSATySFiにおけるフォント名を`GenShinGothic`としています。

    ```text:fonts.satysfi-hash
    {
    //↓この1行を追加する
      "GenShinGothic": <Single: {"src-dist": "GenShinGothic-Regular.ttf"}>,
      "ipaexm"    : <Single: {"src-dist": "ipaexm.ttf"}>,
      "ipaexg"    : <Single: {"src-dist": "ipaexg.ttf"}>,
    ……(略)
    ```

これで準備は整いました。後はSATySFi文書で実際に「源真ゴシック」を指定するのですが、これは先ほど実装した`\my-font`にフォント名`GenShinGothic`を与えるだけです。

```text:sample2.saty
% \my-test の定義を以下に変更する.
let-inline \my-test = {少年老い易く\my-font(`GenShinGothic`){☃}成り難し}
```

![sample-3.png](https://qiita-image-store.s3.amazonaws.com/0/14550/b4e8d78d-6af2-4ced-e6a8-486d7dd06197.png)

「源真ゴシック」の☃が出力されました！

[源真ゴシック]: http://jikasei.me/font/genshin/
[^libroot]: **ライブラリルート**(library root)とは何かについては『The SATySFibook』を参照してください。「SATySFi for Windows」ではライブラリルートは環境変数`SATySFi_RUNTIME`の値となります。UNIX的OSで何らかのパッケージマネージャでSATySFiのバイナリをインストールした場合は恐らく /usr/local/share/satysfi/ になるのだと思われます。なお、実際にはライブラリルートは“複数同時に”配置できるのですが、ここでは元々存在するもののみを使うこととします。
[^ttc-file]: なお、TrueType（またはOpenType）Collection形式のフォントファイルを登録する場合は書き方が少し誓います。

## “和文の”フォントで出力したい

さて本題に戻りましょう。`\my-font`ではユーザが明示的にフォント名を与える方式でしたが、最終的に作りたい`\my-ja`では“和文の”フォント、すなわち「文字体系`Kana`に対するフォント」を強制的に適用する必要があります。既定の設定では`Kana`のフォントは「IPAexフォント」ですが、この値はユーザの環境設定や文書クラスの指定によって当然変わってきます。従って、「現在のフォント設定内容」を読み出す操作が必要になります。これを行うのが`get-font`プリミティヴです。

  * `get-font`: script → context → string ∗ float ∗ float<br>`get-font` *scr* *ctx* は文字体系 *scr* に対する現在のフォント。

ではこれを利用して、「指定の文字体系用と同じフォントで出力する」ための命令`\my-font-script`を実装してみます。

```text:sample3.saty
@require: stdja

%……(sample1.satyの my-set-for-each-script の定義を書く)
%……(sample1.satyの my-set-all-fonts の定義を書く)

%% \my-font-script: [script; inline-text] inline-cmd
% \my-font-script(scr){...} は引数のテキストを"文字体系 scr 用のフォント"で出力する.
let-inline ctx \my-font-script scr it =
  let ctx1 = my-set-all-fonts (get-font scr ctx) ctx in
  read-inline ctx1 it

% "和文(Kana)用と同じフォント"で出力する
let-inline \my-test = {少年老い易く\my-font-script(Kana){☃}成り難し}

in
% document 以降はsample0.satyと同じ
```

  * `\my-font`の定義では`(fname,1.,0.)`と明示的にフォント名を指定していたところを、`\my-font-script`では`(get-font scr ctx)`としています。この式の値は「フォントの3つ組」ですから、そのまま`my-set-all-fonts`に渡せます。

出力結果は以下の通りです。和文フォントは「IPAexフォント」なので、sample1.satyと同様に☃が出ました[^ja-scale]。

![sample-4.png](https://qiita-image-store.s3.amazonaws.com/0/14550/ab9e66b0-5733-b25a-e7d0-154bf14cd072.png)

これでほぼ目的のものができているようにも見えますが、実はまだ問題があります。sample3.satyの出力では☃の周りに四分空きが入っています[^script-jaspace]。しかし本当に“和文として”出力したのであれば和文同士ということで空きは入らないはずです。

[^ja-scale]: 実はsample1.satyとsample3.satyの出力は微妙に異なります。stdjaの設定では和文（`Kana`と`HanIdeographic`）のフォントに対してスケール値0.88が指定されているため、sample3の☃はsample1のものに比べて若干小さく（周りの和文文字と同じサイズに）なっています。
[^script-jaspace]: SATySFiの既定では、“和文”（`Kana`と`HanIdeographic`）と`Latin`の間に四分空きが入ります。既に脚注で述べた通り、SATySFiでは☃(U+2603)は`Latin`として扱われるのでした。

## 真の“和文扱い”を求めて

つまり、ここで必要なのは、引数のテキストを「その周りから見て“和文”(`Kana`)であるように見せかける」という処置です。そして、SATySFiにはこれを行うためのプリミティヴも存在します。

  * `script-guard`: script → inline-boxes → inline-boxes<br>`script-guard` *scr* *ib* は、インラインボックス列 *ib* を前後の空き量に関して「文字体系 *scr* のテキストである」ように見せかけたもの。

これを利用して、sample3の`\my-font-script`を改良してみましょう。命令名は`my-force-script`とします。


```text:sample4.saty
@require: stdja

%……(sample1.satyの my-set-for-each-script の定義を書く)
%……(sample1.satyの my-set-all-fonts の定義を書く)

%% \my-force-script: [script; inline-text] inline-cmd
% \my-force-script(scr){...} は引数のテキストを"文字体系 scr のテキストと同じ扱い"を
% 適用した上で出力する.
let-inline ctx \my-force-script scr it =
  let ctx1 = my-set-all-fonts (get-font scr ctx) ctx in
  script-guard scr (read-inline ctx1 it)

%(続く)
```

  * `read-inline`が返す値がinline-boxes型であるため、これに対して`script-guard scr`を適用します。

この`\my-force-script`を使えば最終目標である`\my-ja`は簡単に作れます。ついでに“欧文扱い”にするための`\my-al`も定義してみました。

```text:sample4.saty(続き)

%% \my-ja{...}: [inline-text] inline-cmd
% テキストを"和文扱い"で出力する.
let-inline \my-ja it = {\my-force-script(Kana){#it;}}
%% \my-al{...}: [inline-text] inline-cmd
% テキストを"欧文扱い"で出力する.
let-inline \my-al it = {\my-force-script(Latin){#it;}}

% "和文扱い"で出力する
let-inline \my-test = {少年老い易く\my-ja{☃}成り難し}

in
% document 以降はsample0.satyと同じ
```

![sample-5.png](https://qiita-image-store.s3.amazonaws.com/0/14550/0abdb810-1df4-a8b7-9766-838d19adcaa5.png)

和文扱いの素敵な☃が出力されました！ 完璧ですね！

## まとめ

絵:snowman:もいいけど、文字☃もいいよ♪