mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-13 01:14:54 -06:00
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:
co-authored by
Claude Opus 5.5
parent
ef320b988d
commit
01fe67010d
+20
-1
@@ -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(),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user