PDF: look up page images by hash instead of scanning every earlier image

Page::image_rgba compared each new picture with all earlier pictures on
the page, so N distinct pictures cost N(N-1)/2 comparisons. Keep a
page-local hash-to-index map (pixels still compared with ImageData::same)
and add a regression test with 3,000 distinct pictures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-11-08 12:19:46 +01:00
co-authored by Claude Opus 5.5
parent ef320b988d
commit 01fe67010d
2 changed files with 40 additions and 1 deletions
+20 -1
View File
@@ -434,6 +434,11 @@ pub struct Page {
content: String,
fonts: [bool; FONT_SLOTS],
images: Vec<ImageData>,
/// Indexes into `images`, by content hash; a hash match is confirmed with [`ImageData::same`].
image_index: HashMap<u64, Vec<usize>>,
/// Pixel comparisons made while looking up duplicate pictures (test instrumentation).
#[cfg(test)]
image_comparisons: usize,
/// Style operators already in effect at the end of `content`.
style: Style,
/// One entry per accepted [`Page::save`], restored by [`Page::restore`].
@@ -793,9 +798,20 @@ impl Page {
}
let img = ImageData::new(pw, ph, rgb, alpha);
// The same picture drawn again on this page shares one XObject.
let idx = match self.images.iter().position(|i| i.same(&img)) {
let candidates = self.image_index.entry(img.hash).or_default();
#[cfg(test)]
let comparisons = &mut self.image_comparisons;
let known = candidates.iter().copied().find(|&i| {
#[cfg(test)]
{
*comparisons += 1;
}
self.images.get(i).is_some_and(|o| o.same(&img))
});
let idx = match known {
Some(i) => i,
None => {
candidates.push(self.images.len());
self.images.push(img);
self.images.len() - 1
}
@@ -934,6 +950,9 @@ impl PdfDoc {
content: String::new(),
fonts: [false; FONT_SLOTS],
images: Vec::new(),
image_index: HashMap::new(),
#[cfg(test)]
image_comparisons: 0,
links: Vec::new(),
style: Style::default(),
saved: Vec::new(),
+20
View File
@@ -571,6 +571,26 @@ fn different_images_are_not_merged() {
assert_eq!(pdf.windows(15).filter(|w| *w == b"/Subtype /Image").count(), 6, "three distinct pictures, each with a soft mask (alpha 50)");
}
/// O4: many distinct pictures on one page are looked up by hash, not by comparing each new
/// picture with every earlier one (which made dense picture exports quadratic).
#[test]
fn many_distinct_images_on_one_page_are_not_compared_pairwise() {
const N: u32 = 3000;
let mut d = PdfDoc::new();
let p = d.add_page(612.0, 792.0);
for i in 0..N {
let [a, b, c, _] = i.to_le_bytes();
p.image_rgba(Rect::new(0.0, 0.0, 1.0, 1.0), &[a, b, c, 255], [1, 1]);
}
// A repeat of the first picture still shares its XObject.
p.image_rgba(Rect::new(5.0, 5.0, 1.0, 1.0), &[0, 0, 0, 255], [1, 1]);
assert_eq!(p.images.len(), N as usize);
assert!(p.content.ends_with("/Im1 Do Q\n"), "the repeated picture reuses /Im1");
assert!(p.image_comparisons <= 1, "{} pixel comparisons for {N} distinct pictures", p.image_comparisons);
let pdf = d.finish();
assert_eq!(pdf.windows(15).filter(|w| *w == b"/Subtype /Image").count(), N as usize);
}
/// O4: equal hashes alone never merge images; the pixels are compared.
#[test]
fn hash_collisions_do_not_merge_images() {