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
//! フォント 1 つぶんの寸法と字形
//!
//! OpenType のファイルを読み込み、組版に要るものだけを取り出して持つ。
//! 寸法はすべて em に直してあるので、外から見てフォントの単位は出てこない。
//!
//! 化学式に要るのは字送りと輪郭、それに大文字の高さと x の高さである。
//! 大文字の高さは結合を通す高さを決め、x の高さは下付きと上付きの位置を決める。
//! 数式のような組み方はしないので、MATH テーブルは読まない。
use crate::cff::{self, Cff};
use crate::error::Error;
use crate::glyf::{self, Glyf};
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)>,
cap_height: f64,
x_height: f64,
digit_height: f64,
}
/// 大文字の高さを測れなかったときの値(em)
const DEFAULT_CAP_HEIGHT: f64 = 0.7;
/// x の高さを測れなかったときの値(em)
const DEFAULT_X_HEIGHT: f64 = 0.45;
/// 数字の高さを測れなかったときの値(em)
const DEFAULT_DIGIT_HEIGHT: f64 = 0.66;
/// フォントのファイルを読み込む
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(),
cap_height: DEFAULT_CAP_HEIGHT,
x_height: DEFAULT_X_HEIGHT,
digit_height: DEFAULT_DIGIT_HEIGHT,
data,
};
let (extent, bearing) = measure_all(&font, count)?;
font.extent = extent;
font.bearing = bearing;
font.cap_height = height_of(&font, 'H', DEFAULT_CAP_HEIGHT);
font.x_height = height_of(&font, 'x', DEFAULT_X_HEIGHT);
font.digit_height = height_of(&font, '0', DEFAULT_DIGIT_HEIGHT);
Ok(font)
}
/// 書体の名前
pub fn family(font: &Font) -> &str {
&font.family
}
/// 書体にない文字を任せる先
pub const FALLBACK: &str = "serif";
/// 1em あたりのフォントの座標の単位
pub fn units_per_em(font: &Font) -> f64 {
font.units_per_em
}
/// 大文字の高さ(em)。結合を通す高さはここから決まる
pub fn cap_height(font: &Font) -> f64 {
font.cap_height
}
/// x の高さ(em)
pub fn x_height(font: &Font) -> f64 {
font.x_height
}
/// 数字の高さ(em)
///
/// 数字はベースラインに乗るが、+ や − は数式の軸を中心に描かれる。
/// 電荷として並べるときは、この高さを使って高さをそろえる
pub fn digit_height(font: &Font) -> f64 {
font.digit_height
}
/// 文字に当たる字形番号
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)。深さは下向きが正
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 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)
}
}
/// 字形の輪郭。座標はフォントの単位のままで、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()
}
/// 試験と見本で使うフォント。試験のあいだだけ組み込まれる
#[cfg(test)]
pub fn sample() -> Font {
let path = "font/latinmodern-math.otf";
let data = std::fs::read(path).expect("試験用のフォントを読めません");
load(data).expect("フォントを読み込めません")
}
/// 1 文字の墨の高さ(em)。その文字を持たなければ既定の値
fn height_of(font: &Font, character: char, default: f64) -> f64 {
match font.map.get(&character) {
Some(glyph) => match font.extent.get(*glyph as usize) {
Some((height, _)) if *height > 0.0 => *height,
_ => default,
},
None => 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))
}
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
#[test]
fn measures_letters() {
let font = sample();
let carbon = glyph_of(&font, 'C').unwrap();
let hydrogen = glyph_of(&font, 'H').unwrap();
assert!(advance(&font, carbon) > 0.3 && advance(&font, carbon) < 1.0);
assert!(extent(&font, hydrogen).1.abs() < 1e-9, "H に深さが出ています");
// C は丸い字なので、上下ともわずかにはみ出す
assert!(extent(&font, carbon).1 > 0.0 && extent(&font, carbon).1 < 0.05);
assert!(extent(&font, carbon).0 > extent(&font, hydrogen).0);
assert!(extent(&font, hydrogen).0 > 0.5);
}
#[test]
fn the_heights_come_from_the_font() {
let font = sample();
assert!(cap_height(&font) > 0.5 && cap_height(&font) < 0.9);
assert!(x_height(&font) > 0.3 && x_height(&font) < 0.6);
assert!(x_height(&font) < cap_height(&font));
}
#[test]
fn outlines_come_out_in_font_units() {
let font = sample();
let oxygen = glyph_of(&font, 'O').unwrap();
let drawn = outline_of(&font, oxygen);
assert!(!drawn.segment.is_empty());
let (_, _, _, top) = outline::bounds(&drawn);
assert!((top / units_per_em(&font) - extent(&font, oxygen).0).abs() < 1e-9);
}
#[test]
fn a_font_without_math_still_loads() {
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!(cap_height(&font) > 0.5);
assert!(glyph_of(&font, 'C').is_some());
}
}