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, font: &Font) -> GlyphParams {
let mut params = GlyphParams {
dest_width: self.dest_width,
weight: self.weight,
..GlyphParams::default()
};
let Some(subst) = font.subst() else {
return params;
};
params.vertical = font.is_vertical();
params.skew = subst.skew();
params.embolden = f64::from(subst.embolden_level_for_load()) * 1000.0 / 4096.0;
params
}
}
fn fallback_params(
fallback: &crate::GlyphFallback,
dest_width: i32,
vertical: bool,
) -> GlyphParams {
GlyphParams {
dest_width,
weight: fallback.subst.raw_weight(),
skew: fallback.subst.skew(),
vertical,
embolden: f64::from(fallback.subst.embolden_level_for_load()) * 1000.0 / 4096.0,
}
}
#[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)
}
pub fn shared_fallback(
&mut self,
fallback: &crate::GlyphFallback,
vertical: bool,
key: GlyphKey,
) -> Option<Arc<BezPath>> {
self.entries
.entry(key)
.or_insert_with(|| {
let params = fallback_params(fallback, key.dest_width, vertical);
fallback.glyphs.outline(key.gid, params).map(Arc::new)
})
.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(font))
.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, SubstFont, subst};
use pdfrum_common::kurbo::Shape;
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,
..GlyphParams::default()
};
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,
..GlyphParams::default()
},
);
let wide = source.outline(
gid,
GlyphParams {
dest_width: 900,
weight: 400,
..GlyphParams::default()
},
);
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,
..GlyphParams::default()
},
);
let heavy = source.outline(
gid,
GlyphParams {
dest_width: 0,
weight: 900,
..GlyphParams::default()
},
);
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,
..GlyphParams::default()
},
);
let b = font.glyphs().outline(
gid,
GlyphParams {
dest_width: 900,
weight: 900,
..GlyphParams::default()
},
);
assert_eq!(format!("{a:?}"), format!("{b:?}"));
}
fn synthesized(base_font: &str, italic_angle: i64, weight: i64) -> Font {
let desc = Dict::from_pairs([
(
crate::names::ITALIC_ANGLE.clone(),
Object::Int(italic_angle),
),
(crate::names::FONT_WEIGHT.clone(), Object::Int(weight)),
(crate::names::ASCENT.clone(), Object::Int(700)),
(crate::names::DESCENT.clone(), Object::Int(-200)),
(crate::names::CAP_HEIGHT.clone(), Object::Int(700)),
(crate::names::STEM_V.clone(), Object::Int(80)),
]);
let dict = Dict::from_pairs([
(
crate::names::SUBTYPE.clone(),
Object::Name(Name::from("TrueType")),
),
(
crate::names::BASE_FONT.clone(),
Object::Name(Name::from(base_font)),
),
(crate::names::FONT_DESCRIPTOR.clone(), Object::Dict(desc)),
]);
crate::load(
&dict,
&NoResolve,
&FontCache::new(),
&Limits::default(),
&mut Diagnostics::default(),
)
.expect("a simple font always constructs")
}
#[test]
fn a_synthetic_italic_leans_the_outline_the_document_asked_for() {
let upright = synthesized("SomeFontNobodyHas", 0, 400);
let italic = synthesized("SomeFontNobodyHas", -20, 400);
assert_eq!(upright.subst().map(|s| s.italic_angle), Some(0));
assert_eq!(
italic.subst().map(|s| s.italic_angle),
Some(-20),
"the fixture must actually reach a synthetic slant"
);
let gid = Gid(italic.glyphs().name_index(b"A"));
let mut cache = GlyphCache::new();
let straight = cache
.path(&upright, GlyphKey::plain(upright.id(), gid))
.cloned()
.expect("the fallback face draws an A");
let leaning = cache
.path(
&italic,
GlyphKey {
italic_angle: -20,
..GlyphKey::plain(italic.id(), gid)
},
)
.cloned()
.expect("and draws it slanted too");
assert_ne!(
format!("{straight:?}"),
format!("{leaning:?}"),
"the shear must reach the outline"
);
let (a, b) = (straight.bounding_box(), leaning.bounding_box());
assert!(b.x1 > a.x1, "the top must lean right: {a:?} vs {b:?}");
assert!(b.y0 >= a.y0 - 1.0 && b.y1 <= a.y1 + 1.0, "y is untouched");
}
#[test]
fn a_synthetic_bold_dilates_the_outline_the_document_asked_for() {
let font = helvetica();
let gid = Gid(font.glyphs().name_index(b"o"));
let at = |embolden: f64| {
font.glyphs()
.outline(
gid,
GlyphParams {
embolden,
..GlyphParams::default()
},
)
.expect("Helvetica draws an o")
};
let thin = at(0.0);
let fat = at(f64::from(70) * 1000.0 / 4096.0);
assert_ne!(format!("{thin:?}"), format!("{fat:?}"));
assert!(
fat.area().abs() > thin.area().abs(),
"the dilation must add ink: {} vs {}",
fat.area().abs(),
thin.area().abs()
);
let bold = SubstFont {
weight: Some(700),
..SubstFont::default()
};
assert_eq!(bold.embolden_level_for_load(), 70);
}
#[test]
fn the_bitmap_side_scales_its_dilation_with_the_device_matrix() {
let italic = synthesized("SomeFontNobodyHas", -20, 400);
let ft = |em_per_px: f64| (em_per_px / 64.0 * 65536.0) as i32;
let small = italic.render_synth(ft(12.0), 0).expect("12 pt draws");
let large = italic.render_synth(ft(48.0), 0).expect("48 pt draws too");
assert_eq!(small.skew, large.skew);
assert_eq!(small.skew, -36, "the render side reads the same table");
assert!(!small.vertical, "a simple font is never vertical");
let bold = SubstFont {
weight: Some(700),
..SubstFont::default()
};
let level = |px: f64| {
bold.embolden_level_for_render(false, ft(px), 0)
.expect("700 is inside the table")
};
assert!(
level(48.0) > level(12.0) * 3,
"{} {}",
level(12.0),
level(48.0)
);
}
#[test]
fn a_weight_past_the_render_table_abandons_the_glyph() {
let heavy = SubstFont {
weight: Some(1400),
..SubstFont::default()
};
assert_eq!(heavy.embolden_level_for_render(false, 1024, 0), None);
assert!(heavy.embolden_level_for_load() > 0);
}
#[test]
fn a_font_with_no_substitution_takes_neither_adjustment() {
let font = helvetica();
assert!(font.subst().is_none_or(|s| s.italic_angle == 0));
let gid = Gid(font.glyphs().name_index(b"A"));
let key = GlyphKey::plain(font.id(), gid);
let mut cache = GlyphCache::new();
let cached = cache.path(&font, key).cloned().expect("Helvetica draws");
let raw = font
.glyphs()
.outline(gid, GlyphParams::default())
.expect("and draws unadjusted");
assert_eq!(format!("{cached:?}"), format!("{raw:?}"));
}
}