Skip to main content

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}