Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.1.1
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-12
Size
576.6 KB
Downloads
4
Checksum
8e0181e42f4abdc9f91c1ff48b058761bf1940be8d2d4747e5e74bd3014f4322
Dependencies
None

font.rs

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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
//! フォント 1 つぶんの寸法と字形
//!
//! OpenType のファイルを読み込み、組版に要るものだけを取り出して持つ。
//! 寸法はすべて em に直してあるので、外から見てフォントの単位は出てこない。
//!
//! MATH テーブルがあればその値を使う。ない書体(Times New Roman など)では、
//! x の高さと線の太さから TeX の既定値に相当するものを組み立てる。

use crate::cff::{self, Cff};
use crate::error::Error;
use crate::glyf::{self, Glyf};
use crate::math::{self, Constant, Construction, Table};
use crate::opentype::{self, require};
use crate::outline::{self, Outline};
use std::collections::HashMap;


/// 字形をどちらの形で持っているか
#[derive(Debug, Clone)]
enum Kind {
    Cff(Cff),
    Glyf(Glyf),
}


/// 読み込んだフォント
#[derive(Debug, Clone)]
pub struct Font {
    data: Vec<u8>,
    family: String,
    units_per_em: f64,
    kind: Kind,
    map: HashMap<char, u16>,
    /// 字形ごとの送り幅(em)
    advance: Vec<f64>,
    /// 字形ごとの、字が実際に占める上下(em)。(高さ, 深さ)
    extent: Vec<(f64, f64)>,
    /// 字形ごとの左右の余白(em)。墨と送り幅のあいだの空き
    bearing: Vec<(f64, f64)>,
    table: Table,
    has_math: bool,
}


/// フォントのファイルを読み込む
pub fn load(data: Vec<u8>) -> Result<Font, Error> {
    let (head_at, _) = require(&data, "head")?;
    let shape = opentype::head(&data, head_at)?;
    let units_per_em = shape.units_per_em;
    let (maxp_at, _) = require(&data, "maxp")?;
    let count = opentype::glyph_count(&data, maxp_at)?;
    let (hmtx_at, _) = require(&data, "hmtx")?;
    let (hhea_at, _) = require(&data, "hhea")?;
    let (cmap_at, _) = require(&data, "cmap")?;
    let (name_at, _) = require(&data, "name")?;

    let kind = match opentype::find(&data, "CFF ")? {
        Some((cff_at, _)) => Kind::Cff(cff::parse(&data, cff_at, units_per_em)?),
        None => {
            let (glyf_at, _) = require(&data, "glyf")?;
            let (loca_at, _) = require(&data, "loca")?;

            Kind::Glyf(glyf::new(glyf_at, loca_at, shape.long_loca, count))
        }
    };

    let advance = opentype::advances(&data, hmtx_at, hhea_at, count)?
        .into_iter()
        .map(|width| width / units_per_em)
        .collect();
    let mut font = Font {
        family: opentype::family_name(&data, name_at)?,
        map: opentype::character_map(&data, cmap_at)?,
        units_per_em,
        kind,
        advance,
        extent: Vec::new(),
        bearing: Vec::new(),
        table: Table::default(),
        has_math: false,
        data,
    };

    let (extent, bearing) = measure_all(&font, count)?;
    font.extent = extent;
    font.bearing = bearing;

    match opentype::find(&font.data, "MATH")? {
        Some((math_at, _)) => {
            font.table = math::parse(&font.data, math_at, units_per_em)?;
            font.has_math = true;
        }
        None => font.table = math::with_constant(derived(&font)),
    }

    Ok(font)
}


/// 書体の名前。CSS の font-family に書くもの
pub fn family(font: &Font) -> &str {
    &font.family
}


/// 書体にない文字を任せる先
///
/// 数式の書体は漢字も仮名も持たない。\text の中にそういう文字が来たときに落ちる先を、
/// HTML と SVG のどちらでも同じにしておく。揃えないと同じ式が別の書体で出る
pub const FALLBACK: &str = "serif";


/// 1em あたりのフォントの座標の単位
pub fn units_per_em(font: &Font) -> f64 {
    font.units_per_em
}


/// MATH テーブルを持っているかどうか
pub fn has_math(font: &Font) -> bool {
    font.has_math
}


/// 組版のための定数
pub fn constant(font: &Font) -> &Constant {
    &font.table.constant
}


/// 文字に当たる字形番号
pub fn glyph_of(font: &Font, character: char) -> Option<u16> {
    font.map.get(&character).copied()
}


/// 字形の送り幅(em)
pub fn advance(font: &Font, glyph: u16) -> f64 {
    font.advance.get(glyph as usize).copied().unwrap_or(0.0)
}


/// フォントが持たない文字を置くときの、おおよその送り幅と高さと深さ(em)
///
/// \text の中に書体の外の文字(日本語など)が来ることがある。輪郭は描けないので
/// 文字として置くしかなく、そのぶんの場所だけはここで空けておく
pub fn fallback_metric(character: char) -> (f64, f64, f64) {
    if character as u32 >= 0x2e80 {
        (1.0, 0.88, 0.12)
    } else {
        (0.5, 0.7, 0.0)
    }
}


/// 字形が占める高さと深さ(em)。深さは下向きが正
pub fn extent(font: &Font, glyph: u16) -> (f64, f64) {
    font.extent.get(glyph as usize).copied().unwrap_or((0.0, 0.0))
}


/// 字形の左右の余白(em)。墨の左端までと、墨の右端から送り幅までの空き
pub fn bearing(font: &Font, glyph: u16) -> (f64, f64) {
    font.bearing.get(glyph as usize).copied().unwrap_or((0.0, 0.0))
}


/// 斜体の字形が右へはみ出す量(em)
pub fn italic_correction(font: &Font, glyph: u16) -> f64 {
    math::italic_correction(&font.table, glyph)
}


/// 上に付ける印をそろえる横位置(em)。フォントが指していなければ字形の真ん中
pub fn accent_center(font: &Font, glyph: u16) -> f64 {
    match math::top_accent(&font.table, glyph) {
        Some(found) => found,
        None => advance(font, glyph) / 2.0,
    }
}


/// 縦に伸ばす方法
pub fn vertical_growth(font: &Font, glyph: u16) -> Option<&Construction> {
    math::vertical(&font.table, glyph)
}


/// 横に伸ばす方法
pub fn horizontal_growth(font: &Font, glyph: u16) -> Option<&Construction> {
    math::horizontal(&font.table, glyph)
}


/// 積み木の部品を重ねる長さの下限(em)
pub fn min_overlap(font: &Font) -> f64 {
    font.table.min_overlap
}


/// 字形の輪郭。座標はフォントの単位のままで、y は上向き
pub fn outline_of(font: &Font, glyph: u16) -> Outline {
    let found = match &font.kind {
        Kind::Cff(cff) => cff::outline(&font.data, cff, glyph),
        Kind::Glyf(glyf) => glyf::outline(&font.data, glyf, glyph),
    };

    found.unwrap_or_default()
}


/// すべての字形の上下と左右を測る。読み込みのときに一度だけ走る
type Measured = (Vec<(f64, f64)>, Vec<(f64, f64)>);


fn measure_all(font: &Font, count: usize) -> Result<Measured, Error> {
    let mut extent = Vec::with_capacity(count);
    let mut bearing = Vec::with_capacity(count);

    for glyph in 0..count {
        let drawn = match &font.kind {
            Kind::Cff(cff) => cff::outline(&font.data, cff, glyph as u16)?,
            Kind::Glyf(glyf) => glyf::outline(&font.data, glyf, glyph as u16)?,
        };
        let (left, bottom, right, top) = outline::bounds(&drawn);
        let units = font.units_per_em;
        extent.push((top / units, -bottom / units));

        let advance = font.advance.get(glyph).copied().unwrap_or(0.0);
        bearing.push((left / units, (advance - right / units).max(0.0)));
    }

    Ok((extent, bearing))
}


/// MATH テーブルを持たない書体のための定数。TeX が使ってきた値に相当する
fn derived(font: &Font) -> Constant {
    let x_height = match font.map.get(&'x') {
        Some(glyph) => font.extent.get(*glyph as usize).map_or(0.45, |found| found.0),
        None => 0.45,
    };
    let rule = 0.04;

    Constant {
        script_percent: 0.7,
        script_script_percent: 0.5,
        delimited_min_height: 1.5,
        display_operator_min_height: 1.35,

        axis_height: x_height / 2.0,
        accent_base_height: x_height,

        subscript_shift_down: 0.25,
        subscript_top_max: 0.8 * x_height,
        subscript_baseline_drop_min: 0.05,
        superscript_shift_up: 0.42,
        superscript_bottom_min: 0.25 * x_height,
        superscript_baseline_drop_max: 0.35,
        sub_superscript_gap_min: 4.0 * rule,
        superscript_bottom_max_with_subscript: 0.8 * x_height,
        space_after_script: 0.05,

        upper_limit_gap_min: 0.2,
        upper_limit_baseline_rise_min: 0.3,
        lower_limit_gap_min: 0.2,
        lower_limit_baseline_drop_min: 0.6,

        stack_top_shift_up: 0.45,
        stack_top_display_shift_up: 0.6,
        stack_bottom_shift_down: 0.55,
        stack_bottom_display_shift_down: 0.7,
        stack_gap_min: 3.0 * rule,
        stack_display_gap_min: 7.0 * rule,

        fraction_numerator_shift_up: 0.45,
        fraction_numerator_display_shift_up: 0.6,
        fraction_denominator_shift_down: 0.55,
        fraction_denominator_display_shift_down: 0.7,
        fraction_numerator_gap_min: rule,
        fraction_numerator_display_gap_min: 3.0 * rule,
        fraction_rule_thickness: rule,
        fraction_denominator_gap_min: rule,
        fraction_denominator_display_gap_min: 3.0 * rule,

        overbar_vertical_gap: 3.0 * rule,
        overbar_rule_thickness: rule,
        overbar_extra_ascender: rule,
        underbar_vertical_gap: 3.0 * rule,
        underbar_rule_thickness: rule,
        underbar_extra_descender: rule,

        radical_vertical_gap: 1.25 * rule,
        radical_display_vertical_gap: rule + 0.25 * x_height,
        radical_rule_thickness: rule,
        radical_extra_ascender: rule,
        radical_kern_before_degree: 0.28,
        radical_kern_after_degree: -0.36,
        radical_degree_bottom_raise_percent: 0.6,
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    pub fn sample() -> Font {
        let data = std::fs::read("font/latinmodern-math.otf").expect("試験用のフォントを読めません");

        load(data).expect("フォントを読み込めません")
    }

    #[test]
    fn reads_the_name_and_the_unit() {
        let font = sample();
        assert_eq!(family(&font), "Latin Modern Math");
        assert_eq!(units_per_em(&font), 1000.0);
        assert!(has_math(&font));
    }

    #[test]
    fn measures_letters() {
        let font = sample();
        let x = glyph_of(&font, 'x').unwrap();
        let y = glyph_of(&font, 'y').unwrap();
        assert!(advance(&font, x) > 0.3 && advance(&font, x) < 0.7);
        assert!(extent(&font, x).1.abs() < 0.02, "x に深さが出ています");
        assert!(extent(&font, y).1 > 0.1, "y に深さがありません");
    }

    #[test]
    fn the_constants_come_from_the_font() {
        let font = sample();
        let value = constant(&font);
        assert_eq!(value.axis_height, 0.25);
        assert!(value.fraction_rule_thickness > 0.0 && value.fraction_rule_thickness < 0.1);
        assert!(value.radical_rule_thickness > 0.0);
    }

    #[test]
    fn brackets_know_how_to_grow() {
        let font = sample();
        let paren = glyph_of(&font, '(').unwrap();
        let growth = vertical_growth(&font, paren).unwrap();
        assert!(growth.variant.len() > 3);
        assert!(min_overlap(&font) > 0.0);
    }

    #[test]
    fn brackets_have_little_room_on_the_inside() {
        let font = sample();
        let open = glyph_of(&font, '(').unwrap();
        let (left, right) = bearing(&font, open);
        assert!(left > right, "開き括弧は内側のほうが詰まっている");
        assert!(right < 0.1, "内側の余白: {right}");
    }

    #[test]
    fn outlines_come_out_in_font_units() {
        let font = sample();
        let x = glyph_of(&font, 'x').unwrap();
        let drawn = outline_of(&font, x);
        assert!(!drawn.segment.is_empty());
        let (_, _, _, top) = outline::bounds(&drawn);
        assert!((top / units_per_em(&font) - extent(&font, x).0).abs() < 1e-9);
    }

    #[test]
    fn a_font_without_math_gets_derived_constants() {
        let path = "/System/Library/Fonts/Supplemental/Times New Roman.ttf";
        let Ok(data) = std::fs::read(path) else {
            return;
        };
        let font = load(data).unwrap();
        assert!(!has_math(&font));
        assert!(constant(&font).axis_height > 0.15);
        assert!(constant(&font).axis_height < 0.35);
        assert!(extent(&font, glyph_of(&font, 'y').unwrap()).1 > 0.1);
    }
}