fix(measure): read a stream's XObject resources once when snapping

Every Do operator resolved the stream's /XObject resources again, and
resolving a direct dictionary copies it, so a large direct XObject
dictionary and many short Do operators multiplied the copying. The
dictionary is read on the first Do and kept for the rest of the stream.
This commit is contained in:
aiongg
2026-10-22 18:02:33 +00:00
parent 2d5c5054e2
commit b6cb151774
2 changed files with 24 additions and 1 deletions
+13 -1
View File
@@ -250,6 +250,8 @@ impl Walker<'_> {
let mut current = None;
let mut closures = Vec::new();
let mut path = Geometry::default();
// Read on the first `Do` and kept: resolving a direct dictionary copies it.
let mut xobjects = None;
let point = |m: Matrix, x: f64, y: f64| {
let (x, y) = m.apply(x, y);
[x, y]
@@ -384,7 +386,11 @@ impl Walker<'_> {
}
"Do" => {
if let Some(name) = op.name(0) {
let xobjects = resources.get(b"XObject").map(|o| self.doc.resolve(o));
let xobjects = xobjects.get_or_insert_with(|| {
#[cfg(test)]
XOBJECT_DICT_READS.with(|n| n.set(n.get() + 1));
resources.get(b"XObject").map(|o| self.doc.resolve(o))
});
if let Some(o) = xobjects.as_ref().and_then(|o| o.as_dict()).and_then(|d| d.get(name)) {
let r = o.as_ref();
if r.is_some_and(|r| self.seen.contains(&r)) {
@@ -420,6 +426,12 @@ impl Walker<'_> {
Ok(())
}
}
#[cfg(test)]
thread_local! {
/// Times a content stream's `/XObject` resources were read.
pub(crate) static XOBJECT_DICT_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub fn geometry(doc: &Document, page_index: usize) -> Result<Geometry> {
let p = crate::page(doc, page_index)?;
let resources = p.dict.get(b"Resources").map(|o| doc.resolve(o));
+11
View File
@@ -403,3 +403,14 @@ fn a_scale_ratio_converts_its_page_side_to_points() {
assert!(Scale::ratio([0.0, 0.0], [1.0, 0.0], page, PageUnit::Mm, real, "mm", 2).is_err(), "{page} {real}");
}
}
/// A content stream reads its `/XObject` resources once, not again (as a copy of a direct
/// dictionary) for every `Do`.
#[test]
fn xobject_resources_are_read_once_per_stream() {
let d = fixture(&"/Missing Do\n".repeat(10_000), "", &[]);
snap::XOBJECT_DICT_READS.with(|c| c.set(0));
let paths = snap::geometry(&d, 0).unwrap();
assert!(paths.segments.is_empty());
let reads = snap::XOBJECT_DICT_READS.with(std::cell::Cell::get);
assert!(reads <= 1, "{reads} reads of the page's XObject resources");
}