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
//! Font cache manager and glyph path adaptor.
//!
//! Port of the C++ `font_cache_manager` concept. Caches glyph outlines and
//! metrics, and provides a `GlyphPathAdaptor` that implements `VertexSource`
//! for rendering cached glyphs through the AGG pipeline.
//!
//! Copyright (c) 2025. BSD-3-Clause License.
use crate::basics::{VertexSource, PATH_CMD_STOP};
use crate::font_engine::{FontEngine, GlyphData};
use std::collections::HashMap;
// ============================================================================
// GlyphPathAdaptor — VertexSource for a cached glyph
// ============================================================================
/// Replays a cached glyph outline as an AGG vertex source.
///
/// Equivalent of C++ `serialized_integer_path_adaptor` — stores pre-computed
/// outline vertices and replays them at a given (x, y) offset.
pub struct GlyphPathAdaptor {
/// Pre-computed outline vertices at the origin: (x, y, cmd).
vertices: Vec<(f64, f64, u32)>,
/// Current replay index.
vertex_idx: usize,
/// Translation offset applied to all vertices.
offset_x: f64,
offset_y: f64,
}
impl GlyphPathAdaptor {
/// Create an empty path adaptor.
pub fn new() -> Self {
Self {
vertices: Vec::new(),
vertex_idx: 0,
offset_x: 0.0,
offset_y: 0.0,
}
}
/// Initialize the adaptor with a glyph's outline at position (x, y).
///
/// This is the Rust equivalent of C++ `init_embedded_adaptors(glyph, x, y)`.
pub fn init(&mut self, outline: &[(f64, f64, u32)], x: f64, y: f64) {
self.vertices.clear();
self.vertices.extend_from_slice(outline);
self.offset_x = x;
self.offset_y = y;
self.vertex_idx = 0;
}
}
impl Default for GlyphPathAdaptor {
fn default() -> Self {
Self::new()
}
}
impl VertexSource for GlyphPathAdaptor {
fn rewind(&mut self, _path_id: u32) {
self.vertex_idx = 0;
}
fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
if self.vertex_idx < self.vertices.len() {
let (vx, vy, cmd) = self.vertices[self.vertex_idx];
self.vertex_idx += 1;
// Only offset vertex commands (move_to, line_to, curve3, curve4),
// not end_poly/close/stop commands.
if crate::basics::is_vertex(cmd) {
*x = vx + self.offset_x;
*y = vy + self.offset_y;
} else {
*x = 0.0;
*y = 0.0;
}
cmd
} else {
PATH_CMD_STOP
}
}
}
// ============================================================================
// FontCacheManager
// ============================================================================
/// Font cache manager — caches glyph outlines and provides rendering adaptors.
///
/// Simplified port of C++ `font_cache_manager<FontEngine>`. Caches glyph
/// outlines and metrics in a HashMap, and provides a `GlyphPathAdaptor`
/// for rendering glyphs through the AGG vertex pipeline.
pub struct FontCacheManager {
engine: FontEngine,
cache: HashMap<u32, GlyphData>,
/// Glyph index used as the left-side glyph for the next kerning lookup.
prev_glyph_index: Option<u16>,
/// Glyph index of the most recently requested glyph via `glyph()`.
///
/// C++ `font_cache_manager` keeps "last glyph" separate from "previous glyph":
/// callers do `glyph(ch)` first, then `add_kerning(...)`.
last_glyph_index: Option<u16>,
/// Path adaptor for the current glyph.
path_adaptor: GlyphPathAdaptor,
}
impl FontCacheManager {
/// Create a font cache manager from raw TTF/OTF data.
pub fn from_data(data: Vec<u8>) -> Result<Self, String> {
let engine = FontEngine::from_data(data, 0)?;
Ok(Self {
engine,
cache: HashMap::new(),
prev_glyph_index: None,
last_glyph_index: None,
path_adaptor: GlyphPathAdaptor::new(),
})
}
/// Get mutable access to the font engine (for setting height, flip_y, etc.).
pub fn engine_mut(&mut self) -> &mut FontEngine {
// Changing engine settings invalidates the cache
&mut self.engine
}
/// Get access to the font engine.
pub fn engine(&self) -> &FontEngine {
&self.engine
}
/// Clear the glyph cache (call after changing engine settings like height).
pub fn reset_cache(&mut self) {
self.cache.clear();
self.prev_glyph_index = None;
self.last_glyph_index = None;
}
/// Reset the kerning state (call at the start of a new text run).
pub fn reset_last_glyph(&mut self) {
self.prev_glyph_index = None;
self.last_glyph_index = None;
}
/// Get a cached glyph, preparing it if not already cached.
///
/// Returns `None` if the character has no glyph in this font.
/// Updates the "current glyph" state for a subsequent `add_kerning()` call.
pub fn glyph(&mut self, char_code: u32) -> Option<&GlyphData> {
// Ensure the glyph is in the cache
if !self.cache.contains_key(&char_code) {
let data = self.engine.prepare_glyph(char_code)?;
self.cache.insert(char_code, data);
}
let glyph = self.cache.get(&char_code)?;
// Track "last glyph" (current glyph), not "previous glyph".
// Kerning is applied later in add_kerning(), matching C++ call order.
self.last_glyph_index = Some(glyph.glyph_index);
Some(glyph)
}
/// Apply kerning between the previous and current glyphs.
///
/// C++ call order is: `glyph(current)` then `add_kerning(...)`.
/// This method follows that behavior by using the most recent `glyph()`
/// result as the right-side glyph and the previously committed glyph as
/// the left-side glyph.
///
/// Returns `true` if kerning was applied.
pub fn add_kerning(&mut self, char_code: u32, x: &mut f64, _y: &mut f64) -> bool {
// Keep char_code in the API for compatibility with existing call sites
// and to mirror C++ usage where the current glyph is identified by code.
let _ = char_code;
let mut applied = false;
if let (Some(prev_idx), Some(last_idx)) = (self.prev_glyph_index, self.last_glyph_index) {
let kern = self.engine.kerning(prev_idx, last_idx);
if kern.abs() > 1e-10 {
*x += kern;
applied = true;
}
}
// Advance kerning chain for the next glyph pair.
self.prev_glyph_index = self.last_glyph_index;
applied
}
/// Initialize the path adaptor for a glyph at position (x, y).
///
/// After calling this, `path_adaptor()` returns a VertexSource that
/// replays the glyph outline offset by (x, y).
pub fn init_embedded_adaptors(&mut self, char_code: u32, x: f64, y: f64) {
if let Some(glyph) = self.cache.get(&char_code) {
self.path_adaptor.init(&glyph.outline, x, y);
}
}
/// Get an immutable reference to the path adaptor.
pub fn path_adaptor(&self) -> &GlyphPathAdaptor {
&self.path_adaptor
}
/// Get a mutable reference to the path adaptor.
///
/// Needed because `ConvCurve` etc. require `&mut VertexSource`.
pub fn path_adaptor_mut(&mut self) -> &mut GlyphPathAdaptor {
&mut self.path_adaptor
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::basics::{is_stop, is_vertex, PATH_CMD_MOVE_TO};
#[test]
fn test_glyph_path_adaptor_empty() {
let mut adaptor = GlyphPathAdaptor::new();
adaptor.rewind(0);
let (mut x, mut y) = (0.0, 0.0);
let cmd = adaptor.vertex(&mut x, &mut y);
assert!(is_stop(cmd));
}
#[test]
fn test_glyph_path_adaptor_offset() {
let mut adaptor = GlyphPathAdaptor::new();
let vertices = vec![
(10.0, 20.0, PATH_CMD_MOVE_TO),
(30.0, 40.0, crate::basics::PATH_CMD_LINE_TO),
];
adaptor.init(&vertices, 100.0, 200.0);
let (mut x, mut y) = (0.0, 0.0);
let cmd = adaptor.vertex(&mut x, &mut y);
assert!(is_vertex(cmd));
assert!((x - 110.0).abs() < 1e-10);
assert!((y - 220.0).abs() < 1e-10);
let cmd = adaptor.vertex(&mut x, &mut y);
assert!(is_vertex(cmd));
assert!((x - 130.0).abs() < 1e-10);
assert!((y - 240.0).abs() < 1e-10);
let cmd = adaptor.vertex(&mut x, &mut y);
assert!(is_stop(cmd));
}
#[test]
fn test_glyph_path_adaptor_rewind() {
let mut adaptor = GlyphPathAdaptor::new();
let vertices = vec![(5.0, 10.0, PATH_CMD_MOVE_TO)];
adaptor.init(&vertices, 0.0, 0.0);
// Read first vertex
let (mut x, mut y) = (0.0, 0.0);
adaptor.vertex(&mut x, &mut y);
// Rewind and read again
adaptor.rewind(0);
let cmd = adaptor.vertex(&mut x, &mut y);
assert!(is_vertex(cmd));
assert!((x - 5.0).abs() < 1e-10);
}
#[test]
fn test_add_kerning_uses_previous_and_current_glyph() {
// Use the same embedded font as the demo code.
let mut fman = FontCacheManager::from_data(
include_bytes!("../demo/wasm/fonts/LiberationSerif-Regular.ttf").to_vec(),
)
.expect("font should load");
fman.engine_mut().set_height(24.0);
fman.reset_cache();
// Find a pair with non-zero kerning in this font to ensure the test
// actually verifies pairwise kerning semantics.
let candidates = [
('A', 'V'),
('A', 'W'),
('T', 'o'),
('T', 'a'),
('Y', 'o'),
('L', 'T'),
];
let mut chosen: Option<(u32, u32, f64)> = None;
for (left, right) in candidates {
let left_code = left as u32;
let right_code = right as u32;
let left_idx = fman.glyph(left_code).expect("left glyph").glyph_index;
let right_idx = fman.glyph(right_code).expect("right glyph").glyph_index;
let k = fman.engine().kerning(left_idx, right_idx);
if k.abs() > 1e-10 {
chosen = Some((left_code, right_code, k));
break;
}
}
let (left_code, right_code, expected_kern) =
chosen.expect("expected at least one non-zero kerning pair");
// Reproduce C++/demo call order:
// glyph(left); add_kerning(left) -> no kerning (first glyph)
// glyph(right); add_kerning(right)-> applies kerning(left,right)
fman.reset_last_glyph();
let left_advance = fman.glyph(left_code).expect("left glyph").advance_x;
let mut x = 0.0;
let mut y = 0.0;
assert!(
!fman.add_kerning(left_code, &mut x, &mut y),
"first glyph should not apply kerning",
);
x += left_advance;
fman.glyph(right_code).expect("right glyph");
let applied = fman.add_kerning(right_code, &mut x, &mut y);
assert!(applied, "expected kerning to be applied for the pair");
assert!(
(x - (left_advance + expected_kern)).abs() < 1e-8,
"x={} expected={}",
x,
left_advance + expected_kern
);
}
}