mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 04:51:37 +00:00
fix(measure): list a repeated measurement once and cap what a listing keeps
The listing read a measurement for every /Annots entry, so one annotation referenced many times, or many small annotations sharing one indirect 2048-point vertex array, multiplied its points and text in memory. The listing skips annotation references it has already read and stops, marked truncated, once the kept points and text pass 32 MiB.
This commit is contained in:
1 parent
8da1164f7d
commit
a16661ac10
2 files changed
+47
-2
No files matched your search
@@ -448,10 +448,20 @@ pub struct Unsupported {
|
||||
pub struct Listing {
|
||||
pub measurements: Vec<Measurement>,
|
||||
pub unsupported: Vec<Unsupported>,
|
||||
/// The annotation limit stopped enumeration early.
|
||||
/// The annotation or retained-size limit stopped enumeration early.
|
||||
pub truncated: bool,
|
||||
}
|
||||
const MAX_ANNOTATIONS: usize = 100_000;
|
||||
/// Points and text one listing keeps, as counted by [`retained`]: about a thousand
|
||||
/// measurements of [`MAX_POINTS`] points each.
|
||||
const MAX_LISTED_BYTES: usize = 32 << 20;
|
||||
/// The bytes a listed measurement keeps in points and text.
|
||||
fn retained(m: &Measurement) -> usize {
|
||||
let s = &m.scale;
|
||||
[m.id.len(), m.author.len(), m.label.len(), s.unit.len(), s.area_unit.len(), s.ratio.len()]
|
||||
.into_iter()
|
||||
.fold(m.points.len().saturating_mul(std::mem::size_of::<Point>()), usize::saturating_add)
|
||||
}
|
||||
fn read_measurement(doc: &Document, d: &Dict, kind: Kind, page: usize, index: usize) -> Result<Measurement> {
|
||||
let scale = Scale::read(doc, d.get(b"Measure").ok_or_else(|| invalid("measurement has no scale"))?)?;
|
||||
let key = if kind == Kind::Distance { &b"L"[..] } else { b"Vertices" };
|
||||
@@ -471,6 +481,8 @@ fn read_measurement(doc: &Document, d: &Dict, kind: Kind, page: usize, index: us
|
||||
pub fn list(doc: &Document) -> Listing {
|
||||
let mut out = Listing::default();
|
||||
let mut count = 0usize;
|
||||
let mut kept = 0usize;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for (page, p) in pdfcraft_model::pages(doc).iter().enumerate() {
|
||||
let Some(o) = p.dict.get(b"Annots") else { continue };
|
||||
let o = doc.resolve(o);
|
||||
@@ -481,6 +493,12 @@ pub fn list(doc: &Document) -> Listing {
|
||||
out.truncated = true;
|
||||
return out;
|
||||
}
|
||||
// One annotation object under several entries or pages is one measurement.
|
||||
if let Some(r) = a.as_ref()
|
||||
&& !seen.insert(r)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let o = doc.resolve(a);
|
||||
let Some(d) = o.as_dict() else { continue };
|
||||
let kind = match d.name(b"IT") {
|
||||
@@ -490,7 +508,14 @@ pub fn list(doc: &Document) -> Listing {
|
||||
_ => continue,
|
||||
};
|
||||
match read_measurement(doc, d, kind, page, index) {
|
||||
Ok(m) => out.measurements.push(m),
|
||||
Ok(m) => {
|
||||
kept = kept.saturating_add(retained(&m));
|
||||
if kept > MAX_LISTED_BYTES {
|
||||
out.truncated = true;
|
||||
return out;
|
||||
}
|
||||
out.measurements.push(m);
|
||||
}
|
||||
Err(e) => out.unsupported.push(Unsupported { page, index, reason: e.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,3 +403,23 @@ 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}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn listing_skips_repeated_refs_and_caps_what_it_keeps() {
|
||||
let mut d = fixture("", "", &[]);
|
||||
let points: Vec<Point> = (0..MAX_POINTS).map(|i| [i as f64 * 0.1, 0.0]).collect();
|
||||
add(&mut d, &new(Kind::Perimeter, points), &Meta::default()).unwrap();
|
||||
let (r, mut annotation) = annot(&d, 0, 0).unwrap();
|
||||
let page = pdfcraft_model::pages(&d)[0].obj;
|
||||
let set_annots = |d: &mut Document, list: Vec<Object>| d.update_dict(page, |p| p.set(b"Annots".to_vec(), Object::Array(list))).unwrap();
|
||||
// One annotation referenced 1000 times is one measurement.
|
||||
set_annots(&mut d, vec![Object::Ref(r); 1000]);
|
||||
assert_eq!(list(&d).measurements.len(), 1);
|
||||
// Many small annotations sharing one 2048-point vertex array stop at the retained budget.
|
||||
let vertices = d.add(annotation.get(b"Vertices").unwrap().clone());
|
||||
annotation.set(b"Vertices".to_vec(), Object::Ref(vertices));
|
||||
set_annots(&mut d, vec![Object::Dict(annotation); 1200]);
|
||||
let listing = list(&d);
|
||||
assert!(listing.truncated);
|
||||
assert!(listing.measurements.iter().map(retained).sum::<usize>() <= MAX_LISTED_BYTES);
|
||||
assert!(!listing.measurements.is_empty());
|
||||
}
|
||||
Reference in new issue
Block a user