diff --git a/crates/plate-test/src/structure.rs b/crates/plate-test/src/structure.rs index 9b63aa892..aeba70e00 100644 --- a/crates/plate-test/src/structure.rs +++ b/crates/plate-test/src/structure.rs @@ -69,6 +69,9 @@ pub struct ExtGState { pub smask_tr: bool, } +/// The most decoded content bytes read per page (the former `page.operations()` decode limit). +const MAX_PAGE_CONTENT: usize = 256 * 1024 * 1024; + /// Summarize `pdf`. Malformed objects are skipped rather than reported. pub fn summarize(pdf: &[u8]) -> Result { let pdf = Pdf::new(Arc::new(pdf.to_vec())).map_err(|e| Error::Read(format!("{e:?}")))?; @@ -85,8 +88,11 @@ pub fn summarize(pdf: &[u8]) -> Result { if let Some(group) = raw.get::>(b"Group".as_slice()) { walk.group(&format!("page {}", index + 1), &group); } - let mut ops = page.operations(); - walk.operators(&mut ops); + // Read and dropped page by page: `page.operations()` keeps every page's decoded content + // for as long as the document, so pages sharing one stream would each hold a copy. + let mut content = Vec::new(); + page.content_reader().read_into(&mut content, MAX_PAGE_CONTENT); + walk.operators(&mut UntypedIter::new(&content)); let res = page.resources(); walk.resources(&Resources { ext_g_states: res.ext_g_states.clone(), diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs index 46d3fc467..caa1c2bdd 100644 --- a/vendor/hayro-interpret/src/context.rs +++ b/vendor/hayro-interpret/src/context.rs @@ -193,7 +193,8 @@ const MAX_SAVED_STATE_BYTES: usize = 64 << 20; /// Roughly what a saved copy of `state` holds. /// PdfCraft patch (51): the sums saturate, so a huge dash array or clip list counts as over the -/// bound rather than wrapping on a 32-bit target. +/// bound rather than wrapping on a 32-bit target. The pending text-clip outlines (text render +/// modes 4–7) are copied with the state too, so they count. fn state_bytes(state: &State<'_>) -> usize { size_of::>() .saturating_add( @@ -205,6 +206,7 @@ fn state_bytes(state: &State<'_>) -> usize { .saturating_mul(size_of::()), ) .saturating_add(state.clips.len().saturating_mul(size_of::())) + .saturating_add(state.text_state.clip_paths.elements().len().saturating_mul(size_of::())) } impl<'a> Context<'a> { @@ -690,6 +692,23 @@ mod saved_state_tests { assert_eq!(ctx.saved_bytes, 0); } + #[test] + fn pending_text_clip_outlines_count_toward_the_saved_bytes() { + let pdf = pdf(); + let cache = InterpreterCache::new(); + let mut ctx = Context::new(Affine::IDENTITY, Rect::new(0.0, 0.0, 100.0, 100.0), &cache, pdf.xref(), InterpreterSettings::default()); + let empty = state_bytes(ctx.get()); + ctx.get_mut().text_state.clip_paths.move_to((0.0, 0.0)); + for i in 0..100_000 { + ctx.get_mut().text_state.clip_paths.line_to((f64::from(i), 1.0)); + } + assert!(state_bytes(ctx.get()) >= empty + 100_000 * size_of::()); + for _ in 0..20_000 { + ctx.save_state(); + } + assert!(ctx.saved_bytes <= MAX_SAVED_STATE_BYTES); + } + #[test] fn an_overlong_dash_array_strokes_solid() { let max = crate::types::MAX_DASH_ENTRIES;