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
//! ブラウザで目で見て確かめるための点検ページを書き出す
//!
//! 使い方:
//! cargo run --example check > check.html && open check.html
//! cargo run --example check -- /path/to/other-math.otf > check.html
//!
//! 代表的な式を 10 個、3 通りの書き出しで縦に並べる。3 つは同じ形になるはずなので、
//! 食い違っていればそこが誤りである。ベースラインに薄い線を引いてあるので、
//! 縦の位置がそろっているかも見られる。
use suki::font::{self, Font};
use suki::{Mode, Shape, escape, render_html, render_svg, style};
const DEFAULT_FONT: &str = "font/latinmodern-math.otf";
/// (見出し, 式, 文中に置く前置き, 見るところ)
const CHECKS: &[(&str, &str, &str, &str)] = &[
(
"上付きと空き",
r"x^2 + y^2 = r^2",
"円の式は",
"+ の左右より = の左右のほうが広く空いている。2 が x の右肩に乗り、字の高さを超えていない。",
),
(
"分数と根号",
r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
"二次方程式の解は",
"分数線が = と同じ高さにある。根号の横線が記号の頭とつながり、中身をすべて覆っている。\
先頭の − は引き算ではないので、左右が詰まっている。",
),
(
"大きな演算子と上下の添字",
r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}",
"1 から n までの和は",
"∑ が真上と真下に添字を持ち、どちらも中央にそろっている。∑ そのものが地の文字より大きい。\
文中に置いたほうは添字が右側に回り、行の高さを崩していない。",
),
(
"積分と細い空き",
r"\int_0^1 x^2 \, dx = \frac{1}{3}",
"定積分は",
"∫ の添字は上下ではなく右側に付く。x^2 と dx のあいだにわずかな空きがある。\
dx の d は立体ではなく斜体。",
),
(
"伸びる括弧",
r"\lim_{x \to \infty} \left( 1 + \frac{1}{x} \right)^{x} = e",
"ネイピア数は",
"括弧が中身の分数に合わせて背を伸ばし、上下に十分はみ出している。括弧の右肩に x が乗っている。\
lim は立体で、その下に添字が付く。",
),
(
"行列",
r"A = \begin{pmatrix} a & b \\ c & d \end{pmatrix}",
"2 次の正方行列は",
"丸括弧が 2 行ぶんの高さまで伸びている。列が縦にそろい、行の間隔が等しい。\
行列全体の中心が = の高さに来ている。",
),
(
"場合分け",
r"f(x) = \begin{cases} 1 & x > 0 \\ 0 & x \leq 0 \end{cases}",
"階段関数は",
"中括弧が 2 行を覆っている。左の列と右の列がそれぞれ左そろえになっている。",
),
(
"指数つきの根号と入れ子",
r"\sqrt[3]{\frac{x + 1}{x - 1}}",
"立方根は",
"3 が根号の左肩に入り、記号に重なっていない。根号が入れ子の分数を丸ごと覆っている。",
),
(
"上に付ける印と線",
r"\hat{p} \quad \vec{v} \quad \overline{z_1 z_2} \quad \widehat{xyz}",
"推定量と共役は",
"印が字の真上に来ている。z_1 z_2 の線が 2 つの字を通して 1 本になっている。\
\\widehat{xyz} の印は 3 文字ぶんに広がっており、\\hat{p} のものより横に長い。",
),
(
"装いと文章",
concat!(
r"\mathbf{A}\mathbf{x} = \lambda \mathbf{x}, \quad ",
r"\mathbb{Z} \subset \mathbb{R}, \quad \text{固有値}"
),
"固有値の式は",
"A と x が太字の立体、λ が斜体。ℤ と ℝ が黒板太字。\\text の中の日本語が地の文と同じ書体で、\
字が重ならずに並んでいる。",
),
];
const PAGE_STYLE: &str = "\
body{margin:0;padding:56px 44px 120px;color:#1f1f1f;background:#fff;\
font-family:system-ui,-apple-system,sans-serif;font-size:14px;line-height:1.9}
h1{font-size:19px;font-weight:500;margin:0 0 10px}
h2{font-size:14px;font-weight:500;margin:0 0 10px}
.lead{max-width:48em;margin:0 0 12px;color:#767676}
.item{margin-bottom:52px;max-width:60em}
.source{color:#767676;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;\
line-height:1.7;margin-bottom:18px;word-break:break-all}
.row{position:relative;margin-bottom:14px;display:flex;align-items:baseline;gap:16px}
.name{color:#767676;font-size:11px;width:62px;flex:none;text-align:right}
.stage{position:relative;flex:1;min-width:0;overflow-x:auto}
.guide{display:inline-block;width:100%;height:0;margin-right:-100%;\
border-top:1px dashed #dcdcdc;vertical-align:baseline}
.formula{font-size:27px;position:relative}
.sentence{margin-top:20px;font-size:15px;max-width:46em}
.watch{margin-top:12px;color:#767676;font-size:13px;max-width:46em}
.note{color:#767676;font-size:13px}";
fn main() {
let arguments: Vec<String> = std::env::args().skip(1).collect();
let path = arguments.first().map_or(DEFAULT_FONT, |value| value.as_str());
let data = match std::fs::read(path) {
Ok(data) => data,
Err(reason) => {
eprintln!("フォントを読めません: {path}: {reason}");
std::process::exit(1);
}
};
let font = match font::load(data) {
Ok(font) => font,
Err(reason) => {
eprintln!("フォントを読み込めません: {path}: {reason}");
std::process::exit(1);
}
};
write_page(&font, path);
}
fn write_page(font: &Font, path: &str) {
println!("<!DOCTYPE html>");
println!("<html lang=\"ja\">");
println!("<head>");
println!("<meta charset=\"utf-8\">");
println!("<title>suki の点検</title>");
println!("<style>");
// 見ている機械にこの書体が入っているとはかぎらないので、ファイルから読ませる。
// 輪郭で組んだものはこれがなくても正しく出る
println!(
"@font-face{{font-family:\"{}\";src:url(\"{}\")}}",
escape(font::family(font)),
escape(path)
);
println!("{}{PAGE_STYLE}", style(font));
println!("</style>");
println!("</head>");
println!("<body>");
println!("<h1>suki の点検</h1>");
println!(
"<p class=\"lead\">書体は {} です。代表的な式を 10 個、3 通りの書き出しで並べています。</p>",
escape(font::family(font))
);
println!(
"<p class=\"lead\">HTML は文字を並べたもの、SVG 文字は SVG の中に文字を置いたもの、\
SVG 輪郭は字形を輪郭で描いたものです。3 つは同じ形に見えるはずなので、\
どれか 1 つだけ違っていればそこが誤りです。</p>"
);
println!(
"<p class=\"lead\">伸びる括弧と根号、それに display の大きな演算子は、\
どの書き出し方でも輪郭で描いています。フォントはこれらを大きさ違いの字形として持っていますが、\
その字形には文字が割り当てられておらず、文字としては書き出せないためです。</p>"
);
println!(
"<p class=\"lead\">薄い破線はベースラインです。式がこの線の上に乗っていれば、\
まわりの文字と高さが合います。それぞれの式の下に、地の文に混ぜたものと、見るところを書いています。</p>"
);
for (index, (heading, source, lead, watch)) in CHECKS.iter().enumerate() {
write_item(font, index + 1, heading, source, lead, watch);
}
println!("</body>");
println!("</html>");
}
fn write_item(font: &Font, number: usize, heading: &str, source: &str, lead: &str, watch: &str) {
println!("<div class=\"item\">");
println!("<h2>{number}. {}</h2>", escape(heading));
println!("<div class=\"source\">{}</div>", escape(source));
write_row("HTML", &render_html(source, font, Mode::Display));
write_row("SVG 文字", &render_svg(source, font, Mode::Display, Shape::Text));
write_row("SVG 輪郭", &render_svg(source, font, Mode::Display, Shape::Outline));
let inline = match render_svg(source, font, Mode::Inline, Shape::Outline) {
Ok(markup) => markup,
Err(reason) => escape(&reason.to_string()),
};
println!(
"<p class=\"sentence\">{} {inline} です。この行の高さが崩れていないかを見ます。</p>",
escape(lead)
);
println!("<p class=\"watch\">見るところ: {}</p>", escape(watch));
println!("</div>");
}
fn write_row(name: &str, result: &Result<String, suki::Error>) {
let body = match result {
Ok(markup) => format!("<span class=\"guide\"></span><span class=\"formula\">{markup}</span>"),
Err(reason) => format!("<span class=\"note\">エラー: {}</span>", escape(&reason.to_string())),
};
println!("<div class=\"row\"><div class=\"name\">{name}</div><div class=\"stage\">{body}</div></div>");
}