1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# suki
LaTeX の数式を HTML と SVG に組み上げる Rust のライブラリです。依存クレートはありません。
組み方はフォントが決めます。OpenType のファイルをそのまま読み、字送りと字形の輪郭に加えて、
MATH テーブルから数式軸の高さ、線の太さ、添字の縮小率と持ち上げ量、根号の空きを取ります。
伸びる括弧と根号は引き伸ばさず、フォントが持つ大きさ違いの字形と、部品を積んだものを使います。
書体を替えれば、組み方もその書体のものに替わります。
英語の説明は [README.md](./README.md) にあります。
## なにが違うのか
**依存がありません。** OpenType の解析も、CFF と TrueType の字形も、MATH テーブルも自前で読みます。
入れるものはこのクレート 1 つだけです。
**組み方をフォントから取ります。** 数式軸、線の太さ、添字の位置、根号の空きは、
どれも書体が持つ値です。値を焼き付けていないので、書体を替えると組み方もその書体のものになります。
**伸びる括弧を引き伸ばしません。** フォントが持つ大きさ違いの字形を選び、
足りなければ部品を積みます。縦に潰したり伸ばしたりしないので、線の太さが歪みません。
**表示する側にフォントが要りません。** 字形を輪郭として書き出せば、どこで開いても同じ形になります。
文字として書き出す道も残してあるので、選んで写せる出し方も選べます。
**3 通りの書き出しで寸法が一致します。** 位置と大きさは組み上げが一度だけ決め、
書き出しはそれを写すだけです。HTML と SVG で形が食い違うことがありません。
**速いです。** 1000 本の式を組み終わるまでが、MathJax の 16 分の 1、KaTeX の 5 分の 1 でした。
数字と測り方は「速さ」にあります。
**共有する状態を持ちません。** 読み込んだフォントは書き換わらないので、1 つのフォントを
何本のスレッドからでも同時に読めます。待ち合わせは要りません。
## 使い方
```
cargo run > sample.html # 内蔵のサンプルを表示する
cargo run -- 'x^2 + y^2 = r^2' > out.html # 引数の数式を表示する
cargo run -- --font /path/to/other-math.otf > out.html # 別の書体で組む
cargo test
```
ライブラリとして使うときは次のようにします。
```
use suki::{Mode, Shape};
let font = suki::load_font(std::fs::read("font/latinmodern-math.otf")?)?;
let html = suki::render_html(r"\frac{a}{b}", &font, Mode::Inline)?;
let svg = suki::render_svg(r"\sum_{k=1}^{n} k", &font, Mode::Display, Shape::Outline)?;
```
`Mode::Display` は独立した行に置く組み方で、大きな演算子が大きくなり、`\sum` や `\lim` の添字が
真上と真下に移ります。`Mode::Inline` は文中に置く組み方です。どちらも寸法は em で書き出すので、
まわりの文字サイズに合わせて伸び縮みします。
`load_font` は 1 回だけ呼び、返ってきた `Font` を使い回します。
読み込みに 8.7 ミリ秒かかるのに対して、式 1 つを組むのは 23 マイクロ秒です。
式ごとに読み直すと、そこだけで 400 倍の時間を使うことになります。
失敗すると `suki::Error` を返します。文言のほかに、元の文字列の何文字目で失敗したかを持っています。
## 出力の形
書き出しは 3 通りで、1 回の呼び出しで 1 つだけ出ます。`Mode` は組み方であって、形式ではありません。
`render_html` は HTML を返します。文字を絶対配置で置くので、`suki::style(&font)` が返す CSS と、
同じフォントが表示する側に要ります。1 式あたり 4,205 バイトでした。
`render_svg` に `Shape::Text` を渡すと、SVG の中に `<text>` で文字を置きます。選んで写せますが、
表示する側にフォントが要ります。
`render_svg` に `Shape::Outline` を渡すと、字形を `<path>` と `<use>` で描きます。フォントが要らず、
どこで開いても同じ形になります。選んで写すことはできません。1 式あたり 5,829 バイトでした。
3 つとも寸法と位置は同じです。ただし伸ばした括弧・根号・display の大きな演算子だけは、
どの書き出しでも輪郭で描きます。それらの字形には文字が割り当てられていないためです。
書き出さずに大きさだけ知りたいときは `suki::metric::size_of_source` を使います。
MathML と、PNG のような画像ファイルは出しません。
## 速さ
Apple シリコンのマシン、release ビルドで測りました。式は `src/main.rs` のサンプル 22 本を
順に繰り返したものです。プロセスを起こしてから書き出し終わるまでを 7 回測り、その最速を載せます。
相手は KaTeX 0.16.47 と mathjax-full 3.2.2 で、どちらも node 22.14.0 の上で動かしました。
輪郭の SVG を 1000 本組み終わるまで。
```
suki render_svg(Shape::Outline) 36.7 ms 5,828,987 バイト
MathJax tex2svg (fontCache: local) 604.2 ms 6,048,527 バイト
```
文字を置く HTML を 1000 本組み終わるまで。
```
suki render_html 23.1 ms 4,205,164 バイト
KaTeX renderToString 116.3 ms 2,797,925 バイト
```
3000 本にしても比は動きません。輪郭の SVG が 90.2 ミリ秒に対して 1463.0 ミリ秒、
HTML が 47.5 ミリ秒に対して 232.3 ミリ秒でした。
数え方を 2 つ添えます。KaTeX は `output: 'html'` で測っています。既定の `htmlAndMathml` は
読み上げ用の MathML が付くぶん大きくなるので、そこを外した状態の数字です。
また suki は Rust のネイティブ、相手は node の上です。使う側から見た時間の比較であって、
組み方の良し悪しの比較ではありません。
書き出す量は形式によって向きが変わります。SVG では suki のほうが少し小さく、
HTML では suki のほうが 1.5 倍大きくなります。字ごとに絶対配置と支柱を置くためです。
プロセスの中で見ると、フォントの読み込みが 8.7 ミリ秒、式 1 つを組んで SVG にするのが
23 マイクロ秒です。読み込みの時間はほとんどが全 4,802 字形の上下を測るところで、
テーブルを読むところは合わせて 0.2 ミリ秒に届きません。組み上げのうち大きいのは、
字形の輪郭を SVG の d 属性にするところです。座標は丸めたあとに整数へ直して桁を並べています。
式 1 つごとにプロセスを起こすと、この差は消えます。1 本だけ組んで終わる場合は suki が 10.3 ミリ秒、
KaTeX が 27.1 ミリ秒、MathJax が 83.9 ミリ秒です。suki の 10.3 ミリ秒はほとんどが読み込みなので、
まとめて渡すか、`Font` を持ったまま常駐させてください。
## 並列に組む
`Font` は読み込んだあと書き換わりません。組み上げも書き出しも `&Font` を読むだけなので、
1 つのフォントを何本のスレッドからでも同時に使えます。待ち合わせの仕組みは要りません。
8 スレッドで同じフォントを共有し、8 種類の式を 400 回ずつ組んだところ、1 本で回したときと
結果が完全に一致し、3.94 倍速く終わりました。
ライブラリの側からスレッドを起こすことはしません。並列にするかどうかは呼ぶ側が決めます。
## 式を囲む書き方
式には囲みが付いていても付いていなくても構いません。付いていればそれが組み方を決め、
引数の `Mode` より優先されます。付いていなければ `Mode` がそのまま使われます。
- `$…$` `\(…\)` `\begin{math}…\end{math}` は Inline
- `$$…$$` `\[…\]` `\begin{equation}…\end{equation}` `\begin{equation*}`
- `\begin{displaymath}` も Display
呼ぶ側で囲みを外す必要はありません。外させると、`\\[` との見分けや `\text` の中の扱いまで
利用側に背負わせることになるためです。
行列や場合分けの `\begin{pmatrix}` は囲みではありません。数式の中の構造なので、外さずにそのまま
組み立てます。`align` や `gather` のような複数行の環境には対応しておらず、はっきり止めます。
`$` を文字として出すときは `\$` と書きます。囲みになっていない `$` が残っていれば、
黙って描かずに止めます。
## 字形の出し方
`Shape::Outline` は字形を輪郭として書き出します。表示する側にフォントが要らず、どこで開いても
同じ形になります。伸びる括弧はフォントの大きさ違いの字形と積み木で作るので、
引き伸ばした歪みが出ません。
選んで写すことはできません。
伸びる括弧は、中身とのあいだに少し空きを入れます。括弧の字形が自分で持っている内側の余白は
Latin Modern Math だと丸括弧で 0.057em、角括弧では 0.022em しかなく、字形が大きくなっても
ここは増えないので、背の高い括弧ほど中身に貼り付いて見えるためです。素の大きさのままの括弧には
足しません。地の文に書く `(x)` と `\left( x \right)` が違って見えないようにするためです。
上に付ける印と、上下に引く線は、中身から少し離します。印は MATH テーブルの決めるままだと
字の上に直に載り、線もフォントの値だけでは窮屈に見えるためです。印が中身より広いときは、
中身のほうを右へずらして、印の取り付け位置と中身の中心をそろえます。
斜体の字のあとには、その字が右へはみ出す量(フォントが持つイタリック補正)を空きとして足します。
`f(x)` のように括弧が続くと、字の頭が括弧に迫って窮屈になるためです。TeX も同じことをします。
添字が付いている字には足しません。はみ出す量を添字の位置決めにすでに使っているからです。
横に付ける添字は、土台の背に比例した空きを入れてから置きます。字形が持つ右の余白は
背が高くなっても増えないので、積分のように背の高い記号では比率として足りず、
添字が貼り付いて見えるためです。背の低い字では比例ぶんがほとんど出ないので、下限も決めています。
書体が持たない文字(`\text` の中の日本語など)を任せる先は、HTML と SVG でそろえてあります。
片方だけ別の書体に落ちると、同じ式が違って見えるためです。
行列は、左右の端に列と列のあいだの半分だけ空きを置きます。括弧を置く側だけで、
`cases` のように片側にしか括弧が付かないものは、付かないほうを空けません。
輪郭は少しだけ太らせて描いています。ブラウザは文字を描くとき、細い線が薄く沈まないように
太らせますが、輪郭にはそれが掛かりません。そのままだと同じ書体の地の文より薄く見えるので、
`svg::DARKEN`(0.011em)ぶん縁を足しています。忠実な輪郭がほしいときは
`svg::render_weighted` に 0 を渡してください。
`Shape::Text` は字形を文字として書き出します。選んで写せて、出力も小さくなります。そのかわり
表示する側に同じフォントが要ります。
横に伸びる印(`\vec` `\widehat` `\overbrace` `\xrightarrow` など)も、縦に伸びる括弧と同じく、
フォントが持つ大きさ違いの字形を選び、足りなければ部品を並べて作ります。
伸びる括弧と根号、それに display の大きな演算子だけは、`Shape::Text` でも輪郭で描きます。
フォントはこれらを大きさ違いの字形として持っていますが、その字形には文字が割り当てられておらず、
文字としては書き出せないためです。おかげで 2 つの書き出し方は同じ形になります。
大きさ違いの字形を持たない書体では、素の字を縦に引き伸ばします。
`render_html` は文字として書き出します。`suki::style(&font)` が返す CSS を style 要素に入れて、
同じフォントを表示する側にも用意してください。輪郭で出したいときは SVG をそのまま HTML に置きます。
書体が持たない文字(`\text` の中の日本語など)は輪郭を描けないので、そこだけ文字として置きます。
## フォント
読めるのは OpenType です。字形が CFF のもの(拡張子がたいてい .otf)と、glyf のもの(.ttf)の
どちらも読めます。フォントの集合(.ttc)は先頭の 1 つを使います。
MATH テーブルを持つ書体を強くすすめます。Latin Modern Math、TeX Gyre の各 Math、Libertinus Math、
STIX Two Math、Cambria Math などが該当します。MATH テーブルがない書体でも組めますが、そのときは
x の高さから TeX の既定値に相当するものを組み立てて使い、括弧は素の字を引き伸ばします。
括弧は中身に合わせて伸びず、変数は数式用の斜体ではなく素の立体になります。
`cargo run --example compare` で並べて見られます。
`font/latinmodern-math.otf` は試験と見本のために置いてあります。ライブラリ本体はここを読みません。
## 対応している記法
- 式の囲み `$…$` `$$…$$` `\(…\)` `\[…\]` `equation` `equation*` `displaymath` `math`
- 英字(斜体)、数字、記号、ギリシャ文字、関数名(`\sin` `\log` `\lim` `\det` など)
- 上付き `^`、下付き `_`、まとまり `{ ... }`、`\limits` と `\nolimits`
- 分数 `\frac`、二項係数 `\binom`、根号 `\sqrt` と `\sqrt[n]`
- 伸びる括弧 `\left( ... \right)`
- 行列 `matrix` `pmatrix` `bmatrix` `Bmatrix` `vmatrix` `Vmatrix`
- 表 `array`(列の指定 `{lcr}` と横線 `\hline`)、場合分け `cases`
- 上下の線 `\overline` `\underline` `\bar`
- 上に付ける印 `\hat` `\widehat` `\tilde` `\widetilde` `\vec` `\dot` `\ddot`
- そのほかの印 `\check` `\breve` `\acute` `\grave`
- 波括弧 `\overbrace` `\underbrace`、重ねる `\overset` `\underset` `\stackrel`
- 装い `\mathrm` `\mathbf` `\mathit` `\mathbb` `\mathcal` `\mathscr`
- 装い(続き)`\mathfrak` `\mathsf` `\mathtt` `\boldsymbol`
- 文章 `\text`、演算子名 `\operatorname`
- 場所だけ空ける `\phantom` `\hphantom` `\vphantom`
- 大きさを決めた括弧 `\big` `\Big` `\bigg` `\Bigg`(`\bigl` `\bigr` `\bigm` も)
- 伸びる矢印 `\xrightarrow` `\xleftarrow` `\xhookrightarrow` `\xrightleftharpoons`
(`[下]{上}` の両方を取れます)
- 伸びる上の矢印 `\overrightarrow` `\overleftarrow`
- 組み方の切り替え `\displaystyle` `\textstyle` `\scriptstyle` `\scriptscriptstyle`
- 連分数 `\cfrac`、複数行の添字 `\substack`、合同式 `\pmod` `\mod` `\bmod`
- 空き `\,` `\:` `\;` `\!` `\quad` `\qquad`
変数と太字は、字を傾けたり太らせたりするのではなく、数式用に引かれた字形(Unicode の数式英数字)を
使います。書体がその字形を持たないときだけ、素の字を傾けて代えます。
## 対応していない記法
`align` `gather` `split` `aligned` のような複数行の環境、色や大きさを変えるコマンド、
上下に伸びる矢印、マクロの定義には対応していません。
複数行の環境は、行を揃える機構を持っていないためです。
## 試す
`examples/` に確かめるためのコマンドを置いています。
```
cargo run --example check > check.html # ブラウザで目で見て確かめるページ
cargo run --example corpus # 有名な式が全部通るかを見る
cargo run --example corpus -- page > corpus.html
cargo run --example compare -- a.otf b.otf # 同じ式を書体だけ替えて並べる
cargo run --example probe -- a.otf # 書体が何を持っているかを見る
cargo run --release --example bench # 読み込みと組み上げの速さを測る
cargo run --release --example many -- svg 1000 > out.svg
```
`check` が出すページには、代表的な式が 10 個、3 通りの書き出しで縦に並びます。3 つは同じ形に
見えるはずなので、どれか 1 つだけ違っていればそこが誤りです。ベースラインに薄い線を引いてあるので、
まわりの文字と高さが合うかも見られます。式ごとに、どこを見ればよいかを書いています。
`many` は KaTeX と MathJax に同じ仕事をさせて比べるためのものです。相手側は `test/` にあります。
`cd test && npm install` のあと、`node test/many.js mathjax 1000` のように動かします。
## 組み立て
```
src/
├── lib.rs 公開 API
├── error.rs エラーの型
├── token.rs トークナイザ
├── node.rs 構文木の型
├── parse.rs パーサ
├── symbol.rs コマンドから文字と種別への表
├── opentype.rs OpenType のテーブル(head hhea maxp hmtx cmap name)
├── cff.rs CFF の字形と Type 2 charstring
├── glyf.rs TrueType の字形
├── math.rs MATH テーブル
├── outline.rs 輪郭と、それを囲む箱
├── font.rs フォント 1 つぶんの寸法と字形
├── layout.rs 構文木に寸法と位置を与える
├── metric.rs 組み上げた式の寸法
├── html.rs まとまりを HTML にする
├── svg.rs まとまりを SVG にする
├── text.rs エスケープと数値の書き出し
└── main.rs サンプルページを書き出すコマンド
```
`layout` が決めるのは、ルートの文字サイズを 1 とした em での幅と、ベースラインから上の高さ、
下の深さ、そして子をどこに置くかです。y は下向きを正とします。
HTML は文字を絶対配置で置いています。縦の位置は、高さだけを持つ支柱を先に置いて行の
ベースラインを決める方法で、フォントの上下寸法に頼らずに合わせています。
## 数式の評価
このライブラリは数式を組むだけで、値を計算することはしません。
## ライセンス
MIT と Apache-2.0 の二択です。好きなほうを選んでください。
- [LICENSE-MIT](./LICENSE-MIT)
- [LICENSE-APACHE](./LICENSE-APACHE)
同梱しているフォントは別のライセンスです。`font/latinmodern-math.otf` は GUST(ポーランドの
TeX ユーザー会)の Latin Modern Math 1.959 で、GUST Font License のもとに置かれています。
LaTeX Project Public License 1.3c に基づくもので、本文は
[font/GUST-FONT-LICENSE.txt](./font/GUST-FONT-LICENSE.txt) にあります。
このフォントは試験と見本のために同梱しているだけで、ライブラリ本体は読み込みません。