mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(optimize): page content and forms share one decode budget per page
Image measurement and fragment merging decoded every /Contents entry and every form occurrence with a fresh 1 GiB allowance, so repeated references to one compressed stream multiplied memory. Both charge a per-page DecodeBudget. A page whose content does not decode within it is left unmerged, and its images are not resampled, since a use may be missed; fragment merging no longer rewrites a page from content it could not fully decode.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
//! drops them.
|
||||
|
||||
use pdfcraft_content::{Matrix, Op, parse, serialize_ops};
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream};
|
||||
use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object, Stream};
|
||||
|
||||
use crate::images::{components, content_bytes, decodable, page_resources, pixels, resample};
|
||||
use crate::{OptimizeError, Report, Stage};
|
||||
@@ -213,7 +213,9 @@ fn add_xobject(doc: &mut Document, page: ObjRef, name: &[u8], image: ObjRef) ->
|
||||
/// Merge the fragment runs of one page; returns how many fragments were merged.
|
||||
fn merge_page(doc: &mut Document, page: ObjRef, report: &mut Report, progress: &mut dyn FnMut(Stage) -> bool) -> Result<usize, OptimizeError> {
|
||||
let Some(contents) = doc.get(page).as_dict().and_then(|d| d.get(b"Contents").cloned()) else { return Ok(0) };
|
||||
let parsed = parse(&content_bytes(doc, &contents));
|
||||
// A page whose content isn't all known is left alone: rewriting it would drop the rest.
|
||||
let Some(data) = content_bytes(doc, &contents, &mut DecodeBudget::page()) else { return Ok(0) };
|
||||
let parsed = parse(&data);
|
||||
if parsed.skipped > 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use pdfcraft_content::{Matrix, Pieces};
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream};
|
||||
use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object, Stream};
|
||||
|
||||
use crate::{Compression, ImageSettings, OptimizeError, Report, Settings, Stage};
|
||||
|
||||
@@ -23,21 +23,11 @@ pub(crate) fn page_resources(doc: &Document, page: ObjRef) -> Option<Dict> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn content_bytes(doc: &Document, contents: &Object) -> Vec<u8> {
|
||||
match &*doc.resolve(contents) {
|
||||
Object::Stream(s) => s.decoded().unwrap_or_default(),
|
||||
Object::Array(a) => {
|
||||
let pieces: Vec<_> = a
|
||||
.iter()
|
||||
.filter_map(|c| match &*doc.resolve(c) {
|
||||
Object::Stream(s) => Some(s.decoded().unwrap_or_default()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
Pieces::join(&pieces).data().to_vec()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
/// The page content `contents` names, decoded against `budget` and joined. `None` when a stream
|
||||
/// doesn't decode or the budget runs out: the content is not all known.
|
||||
pub(crate) fn content_bytes(doc: &Document, contents: &Object, budget: &mut DecodeBudget) -> Option<Vec<u8>> {
|
||||
let pieces = budget.decode_contents(doc, contents).into_iter().collect::<Result<Vec<_>, _>>().ok()?;
|
||||
Some(Pieces::join(&pieces).data().to_vec())
|
||||
}
|
||||
|
||||
/// Forms nested deeper than this aren't followed.
|
||||
@@ -57,6 +47,8 @@ struct Walk<'a> {
|
||||
/// Forms walked at least once.
|
||||
seen: HashSet<ObjRef>,
|
||||
rewalks: usize,
|
||||
/// Decoded bytes left for the page being walked.
|
||||
budget: DecodeBudget,
|
||||
/// False once a use may have been missed (depth or work limit).
|
||||
complete: bool,
|
||||
out: HashMap<ObjRef, f64>,
|
||||
@@ -135,23 +127,42 @@ impl Walk<'_> {
|
||||
}
|
||||
self.rewalks += 1;
|
||||
}
|
||||
let Ok(data) = self.budget.decode(s) else {
|
||||
self.complete = false;
|
||||
return;
|
||||
};
|
||||
self.path.push(r);
|
||||
self.content(&s.decoded().unwrap_or_default(), own.as_ref().unwrap_or(resources), inner);
|
||||
self.content(&data, own.as_ref().unwrap_or(resources), inner);
|
||||
self.path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// The effective resolution (ppi) of every image drawn by `pages`: the smallest over its uses.
|
||||
///
|
||||
/// If forms nest too deeply or are drawn too many times to measure every use, every image is
|
||||
/// given `INFINITY` (never resampled), since its largest use may be among those missed.
|
||||
/// If forms nest too deeply, are drawn too many times, or a page's content doesn't decode within
|
||||
/// its budget, every image is given `INFINITY` (never resampled), since its largest use may be
|
||||
/// among those missed.
|
||||
pub fn effective_resolutions(doc: &Document, pages: &[ObjRef]) -> HashMap<ObjRef, f64> {
|
||||
let mut walk = Walk { doc, path: Vec::new(), walked: HashSet::new(), seen: HashSet::new(), rewalks: 0, complete: true, out: HashMap::new() };
|
||||
let mut walk = Walk {
|
||||
doc,
|
||||
path: Vec::new(),
|
||||
walked: HashSet::new(),
|
||||
seen: HashSet::new(),
|
||||
rewalks: 0,
|
||||
budget: DecodeBudget::page(),
|
||||
complete: true,
|
||||
out: HashMap::new(),
|
||||
};
|
||||
for p in pages {
|
||||
let Some(d) = doc.get(*p).as_dict().cloned() else { continue };
|
||||
let Some(contents) = d.get(b"Contents") else { continue };
|
||||
let res = page_resources(doc, *p).unwrap_or_default();
|
||||
walk.content(&content_bytes(doc, contents), &res, Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
walk.budget = DecodeBudget::page();
|
||||
let Some(data) = content_bytes(doc, contents, &mut walk.budget) else {
|
||||
walk.complete = false;
|
||||
continue;
|
||||
};
|
||||
walk.content(&data, &res, Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]));
|
||||
}
|
||||
if !walk.complete {
|
||||
walk.out.values_mut().for_each(|v| *v = f64::INFINITY);
|
||||
@@ -433,3 +444,17 @@ mod dimension_tests {
|
||||
assert_eq!(pixels(&stream, 1), Some(vec![1, 2, 3, 4]));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pdfcraft_cos::{DecodeBudget, Dict, Document, MAX_PAGE_DECODED, Object, Stream};
|
||||
|
||||
#[test]
|
||||
fn repeated_content_references_share_one_decode_budget() {
|
||||
let mut doc = Document::new_empty();
|
||||
let stream = doc.add(Stream::flate(Dict::new(), &vec![b' '; 1 << 20]));
|
||||
let contents = Object::Array(vec![Object::Ref(stream); 300]);
|
||||
let data = super::content_bytes(&doc, &contents, &mut DecodeBudget::page());
|
||||
assert!(data.is_none_or(|d| d.len() <= MAX_PAGE_DECODED + 300));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user