use super::GlyphParams;
use crate::{Font, FontId, Gid};
use pdfrum_common::kurbo::BezPath;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub font: FontId,
pub gid: Gid,
pub dest_width: i32,
pub weight: i32,
pub italic_angle: i32,
pub vertical: bool,
}
impl GlyphKey {
#[must_use]
pub fn plain(font: FontId, gid: Gid) -> Self {
Self {
font,
gid,
dest_width: 0,
weight: 0,
italic_angle: 0,
vertical: false,
}
}
fn params(self) -> GlyphParams {
GlyphParams {
dest_width: self.dest_width,
weight: self.weight,
}
}
}
#[derive(Debug, Default)]
pub struct GlyphCache {
entries: HashMap<GlyphKey, Option<Arc<BezPath>>>,
}
impl GlyphCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg(test)]
pub(crate) fn path(&mut self, font: &Font, key: GlyphKey) -> Option<&BezPath> {
self.entry(font, key).map(AsRef::as_ref)
}
pub fn shared(&mut self, font: &Font, key: GlyphKey) -> Option<Arc<BezPath>> {
self.entry(font, key).map(Arc::clone)
}
fn entry(&mut self, font: &Font, key: GlyphKey) -> Option<&Arc<BezPath>> {
self.entries
.entry(key)
.or_insert_with(|| font.glyphs().outline(key.gid, key.params()).map(Arc::new))
.as_ref()
}
#[cfg(test)]
#[must_use]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[cfg(test)]
pub(crate) fn clear(&mut self) {
self.entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FontCache, subst};
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, NoResolve, Object};
fn helvetica() -> Font {
named("Helvetica")
}
fn named(base_font: &str) -> Font {
let dict = Dict::from_pairs([
(
crate::names::SUBTYPE.clone(),
Object::Name(Name::from("Type1")),
),
(
crate::names::BASE_FONT.clone(),
Object::Name(Name::from(base_font)),
),
]);
crate::load(
&dict,
&NoResolve,
&FontCache::new(),
&Limits::default(),
&mut Diagnostics::default(),
)
.expect("a simple font always constructs")
}
#[test]
fn the_key_separates_every_field_it_declares() {
let base = GlyphKey::plain(FontId(1), Gid(5));
let variants = [
GlyphKey {
font: FontId(2),
..base
},
GlyphKey {
gid: Gid(6),
..base
},
GlyphKey {
dest_width: 700,
..base
},
GlyphKey {
weight: 700,
..base
},
GlyphKey {
italic_angle: -12,
..base
},
GlyphKey {
vertical: true,
..base
},
];
for v in variants {
assert_ne!(base, v, "these must be distinct cache entries");
}
}
#[test]
fn a_repeated_request_is_served_from_the_cache() {
let font = helvetica();
let mut cache = GlyphCache::new();
let gid = font.glyphs().name_index(b"A");
let key = GlyphKey::plain(font.id(), Gid(gid));
assert!(cache.is_empty());
let first = cache.path(&font, key).cloned();
assert_eq!(cache.len(), 1);
let second = cache.path(&font, key).cloned();
assert_eq!(cache.len(), 1, "no second entry was created");
assert_eq!(first, second);
}
#[test]
fn a_glyph_with_no_outline_is_memoized_as_a_miss() {
let font = helvetica();
let mut cache = GlyphCache::new();
let key = GlyphKey::plain(font.id(), Gid(60_000));
assert!(cache.path(&font, key).is_none());
assert_eq!(cache.len(), 1, "the miss itself is cached");
assert!(cache.path(&font, key).is_none());
assert_eq!(cache.len(), 1);
}
#[test]
fn a_dest_width_solves_the_multiple_master_width_axis() {
let font = named("AGaramond");
assert!(
font.subst().is_some_and(|s| s.is_builtin_generic),
"the fixture must actually reach the Multiple-Master generic"
);
let gid = Gid(font.glyphs().name_index(b"a"));
let params = |w: i32| GlyphParams {
dest_width: w,
weight: 0,
};
let at = |w: i32| font.glyphs().advance(gid, params(w));
let default = at(0);
let narrow = at(300);
let wide = at(900);
assert!(default > 0, "the substitute face has a real glyph");
assert!(
narrow < default && default < wide,
"the axis solve tracks dest_width: {narrow} < {default} < {wide}"
);
for want in [300, 400, 500, 600, 700] {
assert_eq!(at(want), want, "dest_width {want} must be solved for");
}
assert_eq!(at(900), at(1500), "the wide end saturates");
assert_eq!(at(50), at(1), "and so does the narrow end");
assert!(at(50) < at(300) && at(700) < at(900));
}
#[test]
fn a_dest_width_and_the_default_are_separate_cache_entries() {
let font = named("AGaramond");
let mut cache = GlyphCache::new();
let gid = Gid(font.glyphs().name_index(b"a"));
let plain = GlyphKey::plain(font.id(), gid);
let sized = GlyphKey {
dest_width: 300,
..plain
};
let a = cache.path(&font, plain).cloned();
let b = cache.path(&font, sized).cloned();
assert_eq!(cache.len(), 2, "two entries, not one");
assert_ne!(a, b, "a solved width draws a different outline");
}
#[test]
fn clearing_empties_the_cache() {
let font = helvetica();
let mut cache = GlyphCache::new();
cache.path(&font, GlyphKey::plain(font.id(), Gid(1)));
assert!(!cache.is_empty());
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn dest_width_changes_a_multiple_master_outline() {
let (source, _) = subst::builtin_generic(false);
let gid = Gid(source.name_index(b"A"));
assert_ne!(gid.0, 0, "the fallback face has an `A`");
let narrow = source.outline(
gid,
GlyphParams {
dest_width: 200,
weight: 400,
},
);
let wide = source.outline(
gid,
GlyphParams {
dest_width: 900,
weight: 400,
},
);
let (Some(narrow), Some(wide)) = (narrow, wide) else {
panic!("both instantiations must draw");
};
assert_ne!(
format!("{narrow:?}"),
format!("{wide:?}"),
"the width axis must actually move the outline"
);
}
#[test]
fn weight_changes_a_multiple_master_outline() {
let (source, _) = subst::builtin_generic(false);
let gid = Gid(source.name_index(b"A"));
let light = source.outline(
gid,
GlyphParams {
dest_width: 0,
weight: 100,
},
);
let heavy = source.outline(
gid,
GlyphParams {
dest_width: 0,
weight: 900,
},
);
let (Some(light), Some(heavy)) = (light, heavy) else {
panic!("both instantiations must draw");
};
assert_ne!(format!("{light:?}"), format!("{heavy:?}"));
}
#[test]
fn a_base14_face_ignores_the_variation_fields() {
let font = helvetica();
let gid = Gid(font.glyphs().name_index(b"A"));
let a = font.glyphs().outline(
gid,
GlyphParams {
dest_width: 100,
weight: 100,
},
);
let b = font.glyphs().outline(
gid,
GlyphParams {
dest_width: 900,
weight: 900,
},
);
assert_eq!(format!("{a:?}"), format!("{b:?}"));
}
}