use std::collections::HashMap;
use pdfrum_common::FxBuildHasher;
use pdfrum_object::ObjRef;
use crate::color::Argb;
use crate::pixmap::Pixmap;
pub const RENDERED_CACHE_BUDGET: usize = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PixmapRequest {
pub stencil_color: Argb,
pub transfer: Option<u64>,
pub width: u32,
pub height: u32,
}
impl PixmapRequest {
#[must_use]
pub fn for_image(
image: &pdfrum_page::ImageData,
stencil_color: Argb,
transfer: Option<&crate::transfer::TransferFunc<'_>>,
width: u32,
height: u32,
) -> Self {
let is_stencil = image.samples.is_stencil();
Self {
stencil_color: if is_stencil {
stencil_color
} else {
Argb::BLACK
},
transfer: transfer.filter(|t| !t.is_identity()).map(digest_transfer),
width,
height,
}
}
}
fn digest_transfer(t: &crate::transfer::TransferFunc<'_>) -> u64 {
use std::hash::{BuildHasher, Hasher};
let mut h = FxBuildHasher::default().build_hasher();
for channel in t.samples() {
h.write(channel);
}
h.finish()
}
#[derive(Debug)]
pub enum Rendered<'a> {
Hit(&'a Pixmap),
Uncached(Pixmap),
}
impl std::ops::Deref for Rendered<'_> {
type Target = Pixmap;
fn deref(&self) -> &Pixmap {
match self {
Self::Hit(p) => p,
Self::Uncached(p) => p,
}
}
}
#[derive(Debug, Default)]
pub struct RenderedImageCache {
entries: HashMap<(ObjRef, PixmapRequest), Pixmap, FxBuildHasher>,
bytes: usize,
}
impl RenderedImageCache {
pub fn get_or_render(
&mut self,
source: Option<ObjRef>,
request: PixmapRequest,
render: impl FnOnce() -> Pixmap,
) -> Rendered<'_> {
let Some(source) = source else {
return Rendered::Uncached(render());
};
let key = (source, request);
if !self.entries.contains_key(&key) {
let pixmap = render();
let size = pixmap_bytes(&pixmap);
if self.bytes.saturating_add(size) > RENDERED_CACHE_BUDGET && !self.entries.is_empty() {
return Rendered::Uncached(pixmap);
}
self.bytes = self.bytes.saturating_add(size);
self.entries.insert(key, pixmap);
}
match self.entries.get(&key) {
Some(pixmap) => Rendered::Hit(pixmap),
None => Rendered::Uncached(render_empty()),
}
}
}
fn pixmap_bytes(p: &Pixmap) -> usize {
p.data().len()
}
fn render_empty() -> Pixmap {
Pixmap::new(0, 0)
}
#[cfg(test)]
mod tests {
use super::*;
use pdfrum_page::{ImageData, Pixels, Samples};
fn gray(width: u32, height: u32) -> ImageData {
ImageData {
width,
height,
samples: Samples::Whole(Pixels::Gray8(
vec![0u8; (width as usize) * (height as usize)].into(),
)),
mask: None,
matte: None,
interpolate: false,
}
}
fn stencil(width: u32, height: u32) -> ImageData {
let row_bytes = (width as usize).div_ceil(8);
ImageData {
width,
height,
samples: Samples::Whole(Pixels::Stencil(pdfrum_page::BitImage {
width,
height,
row_bytes,
bits: vec![0u8; row_bytes * (height as usize)],
})),
mask: None,
matte: None,
interpolate: false,
}
}
fn filled(width: u32, height: u32) -> Pixmap {
Pixmap::filled(width, height, peniko::Color::WHITE)
}
#[test]
fn a_second_draw_of_the_same_image_does_not_re_render_it() {
let mut cache = RenderedImageCache::default();
let key = PixmapRequest::for_image(&gray(4, 4), Argb::BLACK, None, 4, 4);
let mut renders = 0;
for _ in 0..5 {
let p = cache.get_or_render(Some(ObjRef::new(1, 0)), key, || {
renders += 1;
filled(4, 4)
});
assert_eq!(p.width(), 4);
}
assert_eq!(renders, 1, "four of the five draws should be hits");
assert_eq!(cache.entries.len(), 1);
}
#[test]
fn two_sizes_of_one_image_are_two_entries() {
let img = gray(8, 8);
let mut cache = RenderedImageCache::default();
let full = PixmapRequest::for_image(&img, Argb::BLACK, None, 8, 8);
let half = PixmapRequest::for_image(&img, Argb::BLACK, None, 4, 4);
assert_ne!(full, half);
let _ = cache.get_or_render(Some(ObjRef::new(1, 0)), full, || filled(8, 8));
let _ = cache.get_or_render(Some(ObjRef::new(1, 0)), half, || filled(4, 4));
assert_eq!(cache.entries.len(), 2, "a reduction is a different pixmap");
}
#[test]
fn a_stencils_ink_is_in_the_key_and_a_photographs_is_not() {
let bits = stencil(8, 8);
let red = PixmapRequest::for_image(&bits, Argb::opaque(255, 0, 0), None, 8, 8);
let blue = PixmapRequest::for_image(&bits, Argb::opaque(0, 0, 255), None, 8, 8);
assert_ne!(red, blue, "a stencil takes its ink from the fill colour");
let photo = gray(8, 8);
let red = PixmapRequest::for_image(&photo, Argb::opaque(255, 0, 0), None, 8, 8);
let blue = PixmapRequest::for_image(&photo, Argb::opaque(0, 0, 255), None, 8, 8);
assert_eq!(
red, blue,
"a non-stencil never reads the fill colour, so it must not key on it"
);
}
#[test]
fn an_image_with_no_reference_is_never_cached() {
let mut cache = RenderedImageCache::default();
let key = PixmapRequest::for_image(&gray(4, 4), Argb::BLACK, None, 4, 4);
let mut renders = 0;
for _ in 0..3 {
let p = cache.get_or_render(None, key, || {
renders += 1;
filled(4, 4)
});
assert_eq!(p.width(), 4, "and the caller still gets its pixels");
}
assert_eq!(renders, 3, "an inline image has no key");
assert!(cache.entries.is_empty());
}
#[test]
fn a_full_cache_stops_inserting_and_still_answers() {
let mut cache = RenderedImageCache::default();
let side = 4097;
let key = PixmapRequest::for_image(&gray(side, side), Argb::BLACK, None, side, side);
let _ = cache.get_or_render(Some(ObjRef::new(1, 0)), key, || filled(side, side));
assert_eq!(cache.entries.len(), 1);
assert!(cache.bytes > RENDERED_CACHE_BUDGET);
let key2 = PixmapRequest::for_image(&gray(side, side), Argb::BLACK, None, side, side);
let p = cache.get_or_render(Some(ObjRef::new(2, 0)), key2, || filled(8, 8));
assert_eq!(p.width(), 8, "the caller gets its pixmap regardless");
assert_eq!(cache.entries.len(), 1, "and the cache did not grow");
}
#[test]
fn one_image_larger_than_the_whole_budget_is_still_cached() {
let mut cache = RenderedImageCache::default();
let side = 5000;
let key = PixmapRequest::for_image(&gray(side, side), Argb::BLACK, None, side, side);
let mut renders = 0;
for _ in 0..2 {
let _ = cache.get_or_render(Some(ObjRef::new(1, 0)), key, || {
renders += 1;
filled(side, side)
});
}
assert_eq!(renders, 1, "the empty-cache case admits an oversized image");
}
#[test]
fn clearing_empties_the_cache() {
let mut cache = RenderedImageCache::default();
let key = PixmapRequest::for_image(&gray(4, 4), Argb::BLACK, None, 4, 4);
let _ = cache.get_or_render(Some(ObjRef::new(1, 0)), key, || filled(4, 4));
assert!(!cache.entries.is_empty());
cache.entries.clear();
cache.bytes = 0;
assert!(cache.entries.is_empty());
assert_eq!(cache.bytes, 0);
}
#[test]
fn two_generations_of_one_object_number_are_two_entries() {
let mut cache = RenderedImageCache::default();
let key = PixmapRequest::for_image(&gray(4, 4), Argb::BLACK, None, 4, 4);
let _ = cache.get_or_render(Some(ObjRef::new(7, 0)), key, || filled(4, 4));
let _ = cache.get_or_render(Some(ObjRef::new(7, 1)), key, || filled(4, 4));
assert_eq!(cache.entries.len(), 2);
}
}