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
//! JavaScript から呼ぶための入口。wasm32 に組むときだけ入る
//!
//! 文字列は UTF-8 のバイト列で受け渡す。呼ぶ側は allocate で取った場所に書き込んでから関数を呼び、
//! 使い終えたら deallocate で返す。関数は成功なら 0、失敗なら 1 を返す。
//! 結果は resultPointer と resultLength が指す場所に置き、失敗のときはエラーの文言が入る。
//! 結果は次の呼び出しで上書きされるので、呼んだらすぐに読む。
//!
//! 公開する名前は JavaScript の書き方に合わせて、単語の区切りを大文字にしてある。
use crate::Mode;
use crate::font::{self, Font};
use crate::layout::{self, Content, Layout, Shape};
use crate::node::Style;
use crate::outline;
use crate::parse;
use crate::svg::DARKEN;
use crate::text::number;
use std::cell::RefCell;
thread_local! {
/// 読み込んだフォント。loadFont を呼ぶまでは空
static FONT: RefCell<Option<Font>> = const { RefCell::new(None) };
/// 直前の呼び出しの結果
static RESULT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// 受け渡しに使う場所を length バイトぶん取る
#[unsafe(no_mangle)]
pub extern "C" fn allocate(length: usize) -> *mut u8 {
let mut buffer: Vec<u8> = Vec::with_capacity(length);
let pointer = buffer.as_mut_ptr();
std::mem::forget(buffer);
pointer
}
/// allocate で取った場所を返す
///
/// # Safety
///
/// pointer と length は、allocate が返した場所と、そのとき渡した長さでなければならない
#[unsafe(no_mangle)]
pub unsafe extern "C" fn deallocate(pointer: *mut u8, length: usize) {
drop(unsafe { Vec::from_raw_parts(pointer, 0, length) });
}
/// 直前の呼び出しの結果が置いてある場所
#[unsafe(export_name = "resultPointer")]
pub extern "C" fn result_pointer() -> *const u8 {
RESULT.with(|slot| slot.borrow().as_ptr())
}
/// 直前の呼び出しの結果の長さ(バイト)
#[unsafe(export_name = "resultLength")]
pub extern "C" fn result_length() -> usize {
RESULT.with(|slot| slot.borrow().len())
}
/// フォントを読み込む。前に読み込んだものは捨てる
///
/// # Safety
///
/// pointer から length バイトが読める場所でなければならない
#[unsafe(export_name = "loadFont")]
pub unsafe extern "C" fn load_font(pointer: *const u8, length: usize) -> u32 {
let data = unsafe { std::slice::from_raw_parts(pointer, length) }.to_vec();
finish(match font::load(data) {
Ok(loaded) => {
FONT.with(|slot| *slot.borrow_mut() = Some(loaded));
Ok(String::new())
}
// 位置はフォントのファイルのバイト位置で、文字の位置ではないので、文言だけを返す
Err(reason) => Err(reason.message),
})
}
/// 式を輪郭の SVG にする。display が 0 でなければ独立した行の組み方にする。
/// 式に囲みが付いていれば、そちらが組み方を決める
///
/// # Safety
///
/// pointer から length バイトが読める場所でなければならない
#[unsafe(export_name = "renderSvg")]
pub unsafe extern "C" fn render_svg(pointer: *const u8, length: usize, display: u32) -> u32 {
let source = match unsafe { read_text(pointer, length) } {
Ok(source) => source,
Err(reason) => return finish(Err(reason)),
};
finish(with_font(|loaded| {
crate::render_svg(&source, loaded, mode_of(display), Shape::Outline)
.map_err(|reason| reason.to_string())
}))
}
/// 式をキャンバスに描くための配置を JSON にする。display の扱いは renderSvg と同じである
///
/// 寸法と位置はどれも em で、y は下向きが正、ベースラインが 0 である。字形はフォントの字形番号で指し、
/// 輪郭は glyphOutline で別に引く。書体にない字は輪郭を持たないので、文字として返す
///
/// # Safety
///
/// pointer から length バイトが読める場所でなければならない
#[unsafe(export_name = "layoutNotation")]
pub unsafe extern "C" fn layout_notation(pointer: *const u8, length: usize, display: u32) -> u32 {
let source = match unsafe { read_text(pointer, length) } {
Ok(source) => source,
Err(reason) => return finish(Err(reason)),
};
finish(with_font(|loaded| {
let (node, wrapper) = parse::parse_wrapped(&source).map_err(|reason| reason.to_string())?;
let shown = parse::display_of(wrapper, display != 0);
let placed = layout::layout(&node, loaded, 1.0, Style::Auto, shown);
Ok(notation_json(&placed, loaded))
}))
}
/// 字形の輪郭を SVG の d にする。座標はフォントの単位のままで、y は下向きである
#[unsafe(export_name = "glyphOutline")]
pub extern "C" fn glyph_outline(glyph: u32) -> u32 {
let Ok(glyph) = u16::try_from(glyph) else {
return finish(Err(format!("字形の番号が大きすぎます: {glyph}")));
};
finish(with_font(|loaded| {
Ok(outline::path_data(&font::outline_of(loaded, glyph), 1.0, 0.0, 0.0))
}))
}
/// 輪郭のまわりに引く線の太さ(em)。これを引くと、SVG に書き出したものと同じ太さに見える
#[unsafe(no_mangle)]
pub extern "C" fn darken() -> f64 {
DARKEN
}
/// 呼ぶ側が書き込んだ文字列を読む
///
/// # Safety
///
/// pointer から length バイトが読める場所でなければならない
unsafe fn read_text(pointer: *const u8, length: usize) -> Result<String, String> {
let bytes = unsafe { std::slice::from_raw_parts(pointer, length) };
match std::str::from_utf8(bytes) {
Ok(text) => Ok(text.to_string()),
Err(_) => Err("渡された文字列が UTF-8 ではありません".to_string()),
}
}
/// 結果を置いて、成功なら 0、失敗なら 1 を返す
fn finish(outcome: Result<String, String>) -> u32 {
let (status, text) = match outcome {
Ok(text) => (0, text),
Err(text) => (1, text),
};
RESULT.with(|slot| *slot.borrow_mut() = text.into_bytes());
status
}
/// 読み込んだフォントで組む。まだ読み込んでいなければ失敗にする
fn with_font(work: impl FnOnce(&Font) -> Result<String, String>) -> Result<String, String> {
FONT.with(|slot| match slot.borrow().as_ref() {
Some(loaded) => work(loaded),
None => Err("フォントを読み込む前に呼ばれました。先に loadFont を呼びます".to_string()),
})
}
fn mode_of(display: u32) -> Mode {
if display == 0 {Mode::Inline} else {Mode::Display}
}
/// 組み上げたまとまりを JSON にする。字形・線・文字を、それぞれ描く順に並べる
fn notation_json(placed: &Layout, font: &Font) -> String {
let mut glyph_out = String::new();
let mut rule_out = String::new();
let mut text_out = String::new();
collect(placed, font, 0.0, 0.0, &mut glyph_out, &mut rule_out, &mut text_out);
format!(
"{{\"width\":{},\"height\":{},\"depth\":{},\"glyph\":[{glyph_out}],\"rule\":[{rule_out}],\"text\":[{text_out}]}}",
number(placed.width),
number(placed.height),
number(placed.depth)
)
}
/// まとまりをたどり、字形・線・文字をそれぞれの並びへ書き足す。x と y はまとまりを置く位置
fn collect(
placed: &Layout,
font: &Font,
x: f64,
y: f64,
glyph_out: &mut String,
rule_out: &mut String,
text_out: &mut String,
) {
match &placed.content {
Content::Glyph {text, glyph, size, stretch, ..} => {
// 書体にない字(\text の中の日本語など)は輪郭を描けないので、文字として置く
if *glyph == 0 {
if text.is_empty() {
return;
}
separate(text_out);
text_out.push_str("{\"value\":");
push_string(text_out, text);
text_out.push_str(&format!(
",\"size\":{},\"offsetX\":{},\"offsetY\":{}}}",
number(*size),
number(x),
number(y)
));
return;
}
// 墨のない字形(空白など)は描くものがない。SVG の書き出しも同じく飛ばしている
if font::outline_of(font, *glyph).segment.is_empty() {
return;
}
let scale = size / font::units_per_em(font);
separate(glyph_out);
glyph_out.push_str(&format!(
"{{\"glyph\":{glyph},\"scaleX\":{},\"scaleY\":{},\"offsetX\":{},\"offsetY\":{}}}",
number(scale),
number(scale * stretch),
number(x),
number(y)
));
}
Content::Rule => {
separate(rule_out);
rule_out.push_str(&format!(
"{{\"offsetX\":{},\"offsetY\":{},\"width\":{},\"height\":{}}}",
number(x),
number(y - placed.height),
number(placed.width),
number(placed.height + placed.depth)
));
}
Content::Group(children) => {
for child in children {
collect(&child.layout, font, x + child.x, y + child.y, glyph_out, rule_out, text_out);
}
}
}
}
/// 並びに 2 つ目から足すときは、手前に区切りを置く
fn separate(out: &mut String) {
if !out.is_empty() {
out.push(',');
}
}
/// JSON の文字列にして書き足す
fn push_string(out: &mut String, value: &str) {
out.push('"');
for character in value.chars() {
match character {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
control if (control as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", control as u32)),
other => out.push(other),
}
}
out.push('"');
}