pdfrum_font/glyphs/cache.rs
1//! The glyph outline cache.
2//!
3//! Owned by a render session, never global. The key is the interesting part:
4//! the obvious `(font_id, gid,
5//! hint_flags)` is **insufficient**, because `dest_width` alone changes the
6//! outline of a Multiple-Master face — and the Multiple-Master faces are the
7//! terminal rung of the substitution ladder, so they are what draws every font
8//! the system cannot supply. The corrected key is the one PDFium's own path
9//! cache uses.
10
11use super::GlyphParams;
12use crate::{Font, FontId, Gid};
13use pdfrum_common::kurbo::BezPath;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17/// What identifies one cached outline.
18///
19/// `hint_flags` is deliberately absent, and stayed absent when wave 7b brought
20/// hinting in. This cache holds the outlines the *path* side of text fills,
21/// and that side is unhinted at every size, for every face.
22///
23/// The hinted outline belongs to the glyph-*bitmap* side, which is a different
24/// cache in a different crate (`pdfrum_render::glyph::BitmapCache`) because it
25/// holds a different thing: a rasterization, which depends on the device
26/// matrix, where an outline does not. That separation is why this key did not
27/// need to grow a size — the key that does have one is over there.
28///
29/// The five fields below all genuinely vary an outline for at least one face
30/// kind.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct GlyphKey {
33 /// Which font — cache entries from two fonts must never be confused even
34 /// when they share a face.
35 pub font: FontId,
36 /// Which glyph.
37 pub gid: Gid,
38 /// The PDF's declared width for this character code, which solves a
39 /// Multiple-Master face's width axis.
40 pub dest_width: i32,
41 /// The substitution weight, which drives a Multiple-Master face's weight
42 /// axis. Zero means the face's own.
43 pub weight: i32,
44 /// The synthetic italic angle, which shears the outline.
45 pub italic_angle: i32,
46 /// Whether this is a vertical-writing form.
47 pub vertical: bool,
48}
49
50impl GlyphKey {
51 /// A key for a glyph drawn with no substitution adjustments at all.
52 #[must_use]
53 pub fn plain(font: FontId, gid: Gid) -> Self {
54 Self {
55 font,
56 gid,
57 dest_width: 0,
58 weight: 0,
59 italic_angle: 0,
60 vertical: false,
61 }
62 }
63
64 /// What this key asks the face for, resolved against the font it names.
65 ///
66 /// The first two fields go straight through; the italic angle does not,
67 /// because the shear it stands for is a *table* lookup that the CJK/CID
68 /// arm can override (`GetEffectiveSkew`), and the embolden level is not in
69 /// the key at all — it is a function of the weight that already is.
70 /// Carrying the two derived numbers rather than the raw inputs is what
71 /// keeps this the only place that knows the derivation.
72 ///
73 /// `font` must be the font `self.font` identifies, which is the same
74 /// precondition [`GlyphCache::path`] states.
75 fn params(self, font: &Font) -> GlyphParams {
76 let mut params = GlyphParams {
77 dest_width: self.dest_width,
78 weight: self.weight,
79 ..GlyphParams::default()
80 };
81 let Some(subst) = font.subst() else {
82 return params;
83 };
84 params.vertical = font.is_vertical();
85 // The *path* side takes the plain skew, not the effective one — the
86 // CJK arm is the render side's alone (`cfx_face.cpp:870` calls
87 // `GetSkew()` where `:769` calls `GetEffectiveSkew()`).
88 params.skew = subst.skew();
89 // The load-path level is in the 26.6 units of a 64-ppem instance —
90 // 64*64 per em, PDFium's own `kCoordUnit` (`cfx_face.cpp:67`) — so it
91 // reaches this crate's 1000/em outlines scaled by 1000/4096.
92 params.embolden = f64::from(subst.embolden_level_for_load()) * 1000.0 / 4096.0;
93 params
94 }
95}
96
97fn fallback_params(
98 fallback: &crate::GlyphFallback,
99 dest_width: i32,
100 vertical: bool,
101) -> GlyphParams {
102 GlyphParams {
103 dest_width,
104 weight: fallback.subst.raw_weight(),
105 skew: fallback.subst.skew(),
106 vertical,
107 embolden: f64::from(fallback.subst.embolden_level_for_load()) * 1000.0 / 4096.0,
108 }
109}
110
111/// Memoized glyph outlines in 1000/em text space.
112///
113/// A miss is cached too: a glyph that produced no outline is stored as `None`
114/// so it is not recomputed, which is what PDFium's own null-result memoization
115/// does.
116///
117/// The outlines are held behind an `Arc` so a caller that wants one *for longer
118/// than the borrow* — the renderer's per-glyph placement record, which cannot
119/// hold a borrow because placing the next glyph needs the cache mutably again —
120/// takes a refcount rather than a copy of the path. Copying instead was 2891
121/// `BezPath` clones and 3.3 MiB per render of one corpus page, on a document
122/// where the copies were then never read.
123#[derive(Debug, Default)]
124pub struct GlyphCache {
125 entries: HashMap<GlyphKey, Option<Arc<BezPath>>>,
126}
127
128impl GlyphCache {
129 /// An empty cache.
130 #[must_use]
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 /// The outline for `key`, drawing it if this is the first request.
136 ///
137 /// `font` must be the font `key.font` identifies; passing a different one
138 /// returns that font's glyph under the wrong key, which is why the key
139 /// carries the id at all.
140 #[cfg(test)]
141 pub(crate) fn path(&mut self, font: &Font, key: GlyphKey) -> Option<&BezPath> {
142 self.entry(font, key).map(AsRef::as_ref)
143 }
144
145 /// The same outline, as a handle that outlives the borrow.
146 ///
147 /// For a caller that needs the outline *after* asking the cache for the next
148 /// glyph. Cloning the returned `Arc` is a refcount bump; cloning a borrowed
149 /// path copies every element.
150 pub fn shared(&mut self, font: &Font, key: GlyphKey) -> Option<Arc<BezPath>> {
151 self.entry(font, key).map(Arc::clone)
152 }
153
154 /// The outline of a glyph drawn from the Arial `ShouldUseFont` stand-in.
155 ///
156 /// The key must name [`crate::GlyphFallback::id`]; the host font's
157 /// identity would collide with Arial on any shared glyph index.
158 pub fn shared_fallback(
159 &mut self,
160 fallback: &crate::GlyphFallback,
161 vertical: bool,
162 key: GlyphKey,
163 ) -> Option<Arc<BezPath>> {
164 self.entries
165 .entry(key)
166 .or_insert_with(|| {
167 let params = fallback_params(fallback, key.dest_width, vertical);
168 fallback.glyphs.outline(key.gid, params).map(Arc::new)
169 })
170 .clone()
171 }
172
173 /// The stored entry, drawn on first request.
174 fn entry(&mut self, font: &Font, key: GlyphKey) -> Option<&Arc<BezPath>> {
175 self.entries
176 .entry(key)
177 .or_insert_with(|| {
178 font.glyphs()
179 .outline(key.gid, key.params(font))
180 .map(Arc::new)
181 })
182 .as_ref()
183 }
184
185 /// How many outlines — hits and misses alike — are memoized.
186 #[cfg(test)]
187 #[must_use]
188 pub(crate) fn len(&self) -> usize {
189 self.entries.len()
190 }
191
192 /// Whether anything has been drawn yet.
193 #[cfg(test)]
194 #[must_use]
195 pub(crate) fn is_empty(&self) -> bool {
196 self.entries.is_empty()
197 }
198
199 /// Forget everything.
200 #[cfg(test)]
201 pub(crate) fn clear(&mut self) {
202 self.entries.clear();
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::{FontCache, SubstFont, subst};
210 use pdfrum_common::kurbo::Shape;
211 use pdfrum_common::{Diagnostics, Limits};
212 use pdfrum_object::{Dict, Name, NoResolve, Object};
213
214 fn helvetica() -> Font {
215 named("Helvetica")
216 }
217
218 /// A non-embedded Type 1 by name. An unrecognised name falls all the way
219 /// to the built-in Multiple-Master generic, which is the face the width
220 /// solve applies to; a base-14 name resolves to a Foxit blob instead.
221 fn named(base_font: &str) -> Font {
222 let dict = Dict::from_pairs([
223 (
224 crate::names::SUBTYPE.clone(),
225 Object::Name(Name::from("Type1")),
226 ),
227 (
228 crate::names::BASE_FONT.clone(),
229 Object::Name(Name::from(base_font)),
230 ),
231 ]);
232 crate::load(
233 &dict,
234 &NoResolve,
235 &FontCache::new(),
236 &Limits::default(),
237 &mut Diagnostics::default(),
238 )
239 .expect("a simple font always constructs")
240 }
241
242 #[test]
243 fn the_key_separates_every_field_it_declares() {
244 let base = GlyphKey::plain(FontId(1), Gid(5));
245 let variants = [
246 GlyphKey {
247 font: FontId(2),
248 ..base
249 },
250 GlyphKey {
251 gid: Gid(6),
252 ..base
253 },
254 GlyphKey {
255 dest_width: 700,
256 ..base
257 },
258 GlyphKey {
259 weight: 700,
260 ..base
261 },
262 GlyphKey {
263 italic_angle: -12,
264 ..base
265 },
266 GlyphKey {
267 vertical: true,
268 ..base
269 },
270 ];
271 for v in variants {
272 assert_ne!(base, v, "these must be distinct cache entries");
273 }
274 }
275
276 #[test]
277 fn a_repeated_request_is_served_from_the_cache() {
278 let font = helvetica();
279 let mut cache = GlyphCache::new();
280 let gid = font.glyphs().name_index(b"A");
281 let key = GlyphKey::plain(font.id(), Gid(gid));
282
283 assert!(cache.is_empty());
284 let first = cache.path(&font, key).cloned();
285 assert_eq!(cache.len(), 1);
286 let second = cache.path(&font, key).cloned();
287 assert_eq!(cache.len(), 1, "no second entry was created");
288 assert_eq!(first, second);
289 }
290
291 #[test]
292 fn a_glyph_with_no_outline_is_memoized_as_a_miss() {
293 let font = helvetica();
294 let mut cache = GlyphCache::new();
295 // A glyph index far past the face's own count.
296 let key = GlyphKey::plain(font.id(), Gid(60_000));
297 assert!(cache.path(&font, key).is_none());
298 assert_eq!(cache.len(), 1, "the miss itself is cached");
299 assert!(cache.path(&font, key).is_none());
300 assert_eq!(cache.len(), 1);
301 }
302
303 #[test]
304 fn a_dest_width_solves_the_multiple_master_width_axis() {
305 // Burn-down wave 5's font defect, in the form that outlives the file
306 // that exposed it. `AGaramond` is not embedded and is not a base-14
307 // name, so it substitutes onto the built-in generic — a
308 // Multiple-Master Type 1 face — and PDFium then solves that face's
309 // width axis until the glyph's own advance equals the PDF's declared
310 // `/Widths` value
311 // (`AdjustVariationParams`, `cfx_face.cpp:1561-1605`, reached from
312 // `cpdf_font.cpp:440-444`).
313 //
314 // Leaving `dest_width` at zero draws the axis default instead. On
315 // `5.5_simple_font.pdf`, whose `/Widths` say `a = 800` against a face
316 // whose own is 452, the glyphs overran their advances and piled into
317 // each other — which read as dropped characters on the `/Type_1_F`
318 // band and as a "too narrow" `/Type_1_MM_F` band. Both were this.
319 let font = named("AGaramond");
320 assert!(
321 font.subst().is_some_and(|s| s.is_builtin_generic),
322 "the fixture must actually reach the Multiple-Master generic"
323 );
324 let gid = Gid(font.glyphs().name_index(b"a"));
325 let params = |w: i32| GlyphParams {
326 dest_width: w,
327 weight: 0,
328 ..GlyphParams::default()
329 };
330 let at = |w: i32| font.glyphs().advance(gid, params(w));
331
332 let default = at(0);
333 let narrow = at(300);
334 let wide = at(900);
335 assert!(default > 0, "the substitute face has a real glyph");
336 assert!(
337 narrow < default && default < wide,
338 "the axis solve tracks dest_width: {narrow} < {default} < {wide}"
339 );
340 // The solve is an interpolation onto the requested advance, so it
341 // lands on it rather than merely moving toward it.
342 // Inside the axis the solve is an *interpolation onto the requested
343 // advance*, so it lands on it exactly rather than merely moving
344 // toward it. This is the assertion the defect would have failed:
345 // before the fix every one of these returned the default, 556.
346 for want in [300, 400, 500, 600, 700] {
347 assert_eq!(at(want), want, "dest_width {want} must be solved for");
348 }
349 // Outside it the advance saturates, because the interpolated design
350 // *coordinate* is unclamped (`AdjustVariationParams` deliberately
351 // extrapolates) but the blend then clamps it to the axis range, which
352 // is what `FT_Set_MM_Design_Coordinates` does. So an extreme
353 // `/Widths` gets the widest or narrowest the face can draw, not a
354 // degenerate outline.
355 assert_eq!(at(900), at(1500), "the wide end saturates");
356 assert_eq!(at(50), at(1), "and so does the narrow end");
357 assert!(at(50) < at(300) && at(700) < at(900));
358 }
359
360 #[test]
361 fn a_dest_width_and_the_default_are_separate_cache_entries() {
362 // The whole reason `dest_width` is in the key: the two draw different
363 // outlines from the same face and glyph.
364 let font = named("AGaramond");
365 let mut cache = GlyphCache::new();
366 let gid = Gid(font.glyphs().name_index(b"a"));
367 let plain = GlyphKey::plain(font.id(), gid);
368 let sized = GlyphKey {
369 dest_width: 300,
370 ..plain
371 };
372 let a = cache.path(&font, plain).cloned();
373 let b = cache.path(&font, sized).cloned();
374 assert_eq!(cache.len(), 2, "two entries, not one");
375 assert_ne!(a, b, "a solved width draws a different outline");
376 }
377
378 #[test]
379 fn clearing_empties_the_cache() {
380 let font = helvetica();
381 let mut cache = GlyphCache::new();
382 cache.path(&font, GlyphKey::plain(font.id(), Gid(1)));
383 assert!(!cache.is_empty());
384 cache.clear();
385 assert!(cache.is_empty());
386 }
387
388 #[test]
389 fn dest_width_changes_a_multiple_master_outline() {
390 // The reason the SPEC key had to grow. The two generic fallback faces
391 // are Multiple Master, and the width axis is solved from `dest_width`
392 // — so two requests differing only in that field must not share an
393 // entry, and must not produce the same outline either.
394 let (source, _) = subst::builtin_generic(false);
395 let gid = Gid(source.name_index(b"A"));
396 assert_ne!(gid.0, 0, "the fallback face has an `A`");
397
398 let narrow = source.outline(
399 gid,
400 GlyphParams {
401 dest_width: 200,
402 weight: 400,
403 ..GlyphParams::default()
404 },
405 );
406 let wide = source.outline(
407 gid,
408 GlyphParams {
409 dest_width: 900,
410 weight: 400,
411 ..GlyphParams::default()
412 },
413 );
414 let (Some(narrow), Some(wide)) = (narrow, wide) else {
415 panic!("both instantiations must draw");
416 };
417 assert_ne!(
418 format!("{narrow:?}"),
419 format!("{wide:?}"),
420 "the width axis must actually move the outline"
421 );
422 }
423
424 #[test]
425 fn weight_changes_a_multiple_master_outline() {
426 let (source, _) = subst::builtin_generic(false);
427 let gid = Gid(source.name_index(b"A"));
428 let light = source.outline(
429 gid,
430 GlyphParams {
431 dest_width: 0,
432 weight: 100,
433 ..GlyphParams::default()
434 },
435 );
436 let heavy = source.outline(
437 gid,
438 GlyphParams {
439 dest_width: 0,
440 weight: 900,
441 ..GlyphParams::default()
442 },
443 );
444 let (Some(light), Some(heavy)) = (light, heavy) else {
445 panic!("both instantiations must draw");
446 };
447 assert_ne!(format!("{light:?}"), format!("{heavy:?}"));
448 }
449
450 #[test]
451 fn a_base14_face_ignores_the_variation_fields() {
452 // A bare CFF has no design space, so the extra key fields are inert
453 // for it — which is exactly why they were easy to leave out and wrong
454 // to leave out.
455 let font = helvetica();
456 let gid = Gid(font.glyphs().name_index(b"A"));
457 let a = font.glyphs().outline(
458 gid,
459 GlyphParams {
460 dest_width: 100,
461 weight: 100,
462 ..GlyphParams::default()
463 },
464 );
465 let b = font.glyphs().outline(
466 gid,
467 GlyphParams {
468 dest_width: 900,
469 weight: 900,
470 ..GlyphParams::default()
471 },
472 );
473 assert_eq!(format!("{a:?}"), format!("{b:?}"));
474 }
475
476 /// A non-embedded font with a descriptor complete enough to earn
477 /// `USE_EXTERN_ATTR` — without that flag substitution throws the caller's
478 /// weight and slant away, and the synthetic adjustments never arise.
479 fn synthesized(base_font: &str, italic_angle: i64, weight: i64) -> Font {
480 let desc = Dict::from_pairs([
481 (
482 crate::names::ITALIC_ANGLE.clone(),
483 Object::Int(italic_angle),
484 ),
485 (crate::names::FONT_WEIGHT.clone(), Object::Int(weight)),
486 (crate::names::ASCENT.clone(), Object::Int(700)),
487 (crate::names::DESCENT.clone(), Object::Int(-200)),
488 (crate::names::CAP_HEIGHT.clone(), Object::Int(700)),
489 (crate::names::STEM_V.clone(), Object::Int(80)),
490 ]);
491 let dict = Dict::from_pairs([
492 (
493 crate::names::SUBTYPE.clone(),
494 Object::Name(Name::from("TrueType")),
495 ),
496 (
497 crate::names::BASE_FONT.clone(),
498 Object::Name(Name::from(base_font)),
499 ),
500 (crate::names::FONT_DESCRIPTOR.clone(), Object::Dict(desc)),
501 ]);
502 crate::load(
503 &dict,
504 &NoResolve,
505 &FontCache::new(),
506 &Limits::default(),
507 &mut Diagnostics::default(),
508 )
509 .expect("a simple font always constructs")
510 }
511
512 /// The defect this cache had for its whole life: `GlyphKey` carried the
513 /// italic angle, `params` dropped it on the floor, and nothing anywhere
514 /// sheared an outline — so a document asking for an italic it did not
515 /// embed got upright glyphs (`CFX_Face::LoadGlyphPath`,
516 /// `cfx_face.cpp:869-876`).
517 #[test]
518 fn a_synthetic_italic_leans_the_outline_the_document_asked_for() {
519 let upright = synthesized("SomeFontNobodyHas", 0, 400);
520 let italic = synthesized("SomeFontNobodyHas", -20, 400);
521 assert_eq!(upright.subst().map(|s| s.italic_angle), Some(0));
522 assert_eq!(
523 italic.subst().map(|s| s.italic_angle),
524 Some(-20),
525 "the fixture must actually reach a synthetic slant"
526 );
527
528 let gid = Gid(italic.glyphs().name_index(b"A"));
529 let mut cache = GlyphCache::new();
530 let straight = cache
531 .path(&upright, GlyphKey::plain(upright.id(), gid))
532 .cloned()
533 .expect("the fallback face draws an A");
534 let leaning = cache
535 .path(
536 &italic,
537 GlyphKey {
538 italic_angle: -20,
539 ..GlyphKey::plain(italic.id(), gid)
540 },
541 )
542 .cloned()
543 .expect("and draws it slanted too");
544 assert_ne!(
545 format!("{straight:?}"),
546 format!("{leaning:?}"),
547 "the shear must reach the outline"
548 );
549 // A shear about the baseline leaves the bottom where it was and pushes
550 // the top to the right, so the slanted glyph reaches further right
551 // without reaching further left.
552 let (a, b) = (straight.bounding_box(), leaning.bounding_box());
553 assert!(b.x1 > a.x1, "the top must lean right: {a:?} vs {b:?}");
554 assert!(b.y0 >= a.y0 - 1.0 && b.y1 <= a.y1 + 1.0, "y is untouched");
555 }
556
557 /// The other half, and the one that reads a different table on each of the
558 /// oracle's two call sites: a weight the face cannot supply is dilated
559 /// into the outline (`FT_Outline_Embolden`, `cfx_face.cpp:886-892`).
560 /// The other half, and the one that reads a different table on each of the
561 /// oracle's two call sites: a weight the face cannot supply is dilated into
562 /// the outline (`FT_Outline_Embolden`, `cfx_face.cpp:886-892`).
563 ///
564 /// Driven through [`GlyphParams`] rather than through a substitution,
565 /// because the only face a test can reach without a system font database
566 /// is the Multiple-Master generic — and that one deliberately suppresses
567 /// the dilation, solving weight on its own axis instead. The derivation
568 /// from a weight to a level is [`SubstFont::embolden_level_for_load`]'s,
569 /// and is pinned beside it; what is proven here is that the level reaches
570 /// the outline at all, which is what it never used to do.
571 #[test]
572 fn a_synthetic_bold_dilates_the_outline_the_document_asked_for() {
573 let font = helvetica();
574 let gid = Gid(font.glyphs().name_index(b"o"));
575 let at = |embolden: f64| {
576 font.glyphs()
577 .outline(
578 gid,
579 GlyphParams {
580 embolden,
581 ..GlyphParams::default()
582 },
583 )
584 .expect("Helvetica draws an o")
585 };
586 let thin = at(0.0);
587 // Weight 700 is level 70 on the load table, which is 70/4096 of an em.
588 let fat = at(f64::from(70) * 1000.0 / 4096.0);
589 assert_ne!(format!("{thin:?}"), format!("{fat:?}"));
590 assert!(
591 fat.area().abs() > thin.area().abs(),
592 "the dilation must add ink: {} vs {}",
593 fat.area().abs(),
594 thin.area().abs()
595 );
596 // ...and the level really is the one a 700-weight substitution asks
597 // for, so the number above is not a magic constant.
598 let bold = SubstFont {
599 weight: Some(700),
600 ..SubstFont::default()
601 };
602 assert_eq!(bold.embolden_level_for_load(), 70);
603 }
604
605 /// The bitmap side's own resolution, which is not the path side's: its
606 /// embolden level scales with the device matrix (`GetEmboldenLevelForRender`,
607 /// `cfx_face.cpp:806-816`) where the path side's does not, and it reads the
608 /// effective skew rather than the plain one (`:769` against `:870`).
609 #[test]
610 fn the_bitmap_side_scales_its_dilation_with_the_device_matrix() {
611 let italic = synthesized("SomeFontNobodyHas", -20, 400);
612 // 12 pt at 1:1, in the oracle's 16.16 `matrix.a / 64 * 65536`.
613 let ft = |em_per_px: f64| (em_per_px / 64.0 * 65536.0) as i32;
614 let small = italic.render_synth(ft(12.0), 0).expect("12 pt draws");
615 let large = italic.render_synth(ft(48.0), 0).expect("48 pt draws too");
616 // The generic suppresses the dilation, so what the size must move here
617 // is nothing — but the skew is a pure table lookup and must not move
618 // with the size either way.
619 assert_eq!(small.skew, large.skew);
620 // `kAngleSkew[20]`, the value `GetSkewFromAngle` returns for -20°.
621 assert_eq!(small.skew, -36, "the render side reads the same table");
622 assert!(!small.vertical, "a simple font is never vertical");
623
624 // A face that does dilate: the level is proportional to the matrix, so
625 // four times the size is four times the strength.
626 let bold = SubstFont {
627 weight: Some(700),
628 ..SubstFont::default()
629 };
630 let level = |px: f64| {
631 bold.embolden_level_for_render(false, ft(px), 0)
632 .expect("700 is inside the table")
633 };
634 assert!(
635 level(48.0) > level(12.0) * 3,
636 "{} {}",
637 level(12.0),
638 level(48.0)
639 );
640 }
641
642 /// The oracle abandons the glyph outright when the level comes back
643 /// negative — a substitution weight of 1400 or more is past the render
644 /// table, and `RenderGlyph` returns a null bitmap (`cfx_face.cpp:809-811`).
645 #[test]
646 fn a_weight_past_the_render_table_abandons_the_glyph() {
647 let heavy = SubstFont {
648 weight: Some(1400),
649 ..SubstFont::default()
650 };
651 assert_eq!(heavy.embolden_level_for_render(false, 1024, 0), None);
652 // ...and the load side clamps instead of failing, which is why the two
653 // sides cannot share one answer.
654 assert!(heavy.embolden_level_for_load() > 0);
655 }
656
657 /// An embedded font is the document's own program, so neither adjustment
658 /// applies — and the outline must come back byte-for-byte as the face drew
659 /// it, not through a shear of zero that a floating-point matrix would
660 /// perturb.
661 #[test]
662 fn a_font_with_no_substitution_takes_neither_adjustment() {
663 let font = helvetica();
664 assert!(font.subst().is_none_or(|s| s.italic_angle == 0));
665 let gid = Gid(font.glyphs().name_index(b"A"));
666 let key = GlyphKey::plain(font.id(), gid);
667 let mut cache = GlyphCache::new();
668 let cached = cache.path(&font, key).cloned().expect("Helvetica draws");
669 let raw = font
670 .glyphs()
671 .outline(gid, GlyphParams::default())
672 .expect("and draws unadjusted");
673 assert_eq!(format!("{cached:?}"), format!("{raw:?}"));
674 }
675}