diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index d1d77059c0..a5bd4c6eb9 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -5659,6 +5659,26 @@ fn form_bbox_clips_text_from_reading() { assert_eq!(texts, ["INSIDE"]); } +/// #3503: a page's own `re`/`W`/`n` clip (not a form's `/BBox`) hides text outside it the same +/// way, and `Q` lifts it again -- the clip is scoped to its `q`/`Q` like the rest of the graphics +/// state, not a one-way switch. +#[test] +fn page_re_w_n_clip_hides_text_from_reading_until_q() { + let page = stream( + "", + "q 0 0 50 50 re W n BT /F1 12 Tf 5 5 Td (INSIDE) Tj ET BT /F1 12 Tf 100 100 Td (HIDDEN) Tj ET Q BT /F1 12 Tf 100 100 Td (AFTER) Tj ET", + ); + let doc = build(&[ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>", + &page, + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + ]); + let texts: Vec = text::reading_blocks(&doc, 0).unwrap().into_iter().map(|b| b.text).collect(); + assert_eq!(texts, ["INSIDE", "AFTER"]); +} + /// A chain of forms, each drawing the next twenty times, twelve deep: within the depth cap but /// 20^11 visits without a budget. Reading stops at the page's visit budget instead of hanging. #[test] diff --git a/crates/edit/src/text/interp.rs b/crates/edit/src/text/interp.rs index 5114a76263..70992b6d6e 100644 --- a/crates/edit/src/text/interp.rs +++ b/crates/edit/src/text/interp.rs @@ -218,9 +218,20 @@ pub(crate) fn union(a: [f64; 4], b: [f64; 4]) -> [f64; 4] { [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])] } +/// `a` narrowed to its overlap with `b` (nested clips intersect). A pair that shares no area comes +/// back inverted (`x0 > x1` or `y0 > y1`), which fails every later overlap test on its own -- +/// nothing more needs to notice that the clip is now empty. +fn narrow(a: [f64; 4], b: [f64; 4]) -> [f64; 4] { + [a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])] +} + #[derive(Clone)] pub(super) struct Ts { pub(super) ctm: Matrix, + /// The page-space bounding box of the rectangular clip in force, if any (`re` `W`/`W*` `n`; + /// ISO 32000-2 §8.5.4). `q`/`Q` save and restore it along with the rest of `Ts`. A form drawn + /// inside it inherits it, narrowed further by the form's own `/BBox` ([`super::lines`]). + pub(super) clip: Option<[f64; 4]>, /// The operators that set the current fill colour (`g`, `rg`, `k`, or `cs` + `sc`/`scn`). pub(super) fill: Vec, pub(super) font: Option<(Vec, Rc)>, @@ -503,6 +514,7 @@ impl Carry { pub(super) fn new() -> Self { let ts = Ts { ctm: Matrix::IDENTITY, + clip: None, fill: Vec::new(), font: None, face: None, @@ -842,7 +854,28 @@ pub(super) fn interpret( // with no properties dict (a file saved before this was tracked, or by another writer) reads // as 1, the single space a rejoin always used before #2529. let mut soft_gap = 1usize; + // A pending `x y w h re`, and whether a `W`/`W*` has marked it as the path that `n` turns into + // the new clip (ISO 32000-2 §8.5.4) -- the common "set a rectangular clip" idiom. Several + // `re`s before `W n` keep only the last; any other operator between `re` and `n` drops it. + let mut pending_clip: Option<[f64; 4]> = None; + let mut clip_pending = false; for (i, op) in ops.iter().enumerate() { + match op.op.as_slice() { + b"re" => pending_clip = op.nums::<4>().map(|[x, y, w, h]| [x.min(x + w), y.min(y + h), x.max(x + w), y.max(y + h)]), + b"W" | b"W*" => clip_pending = pending_clip.is_some(), + b"n" => { + if clip_pending && let Some(r) = pending_clip { + let page_rect = ts.ctm.bbox(r); + ts.clip = Some(ts.clip.map_or(page_rect, |c| narrow(c, page_rect))); + } + pending_clip = None; + clip_pending = false; + } + _ => { + pending_clip = None; + clip_pending = false; + } + } match op.op.as_slice() { // No depth cap: every `EMC` pops, so a capped push would pair it with the enclosing // sequence (#3573). Both stacks stay bounded by the number of operators. diff --git a/crates/edit/src/text/lines.rs b/crates/edit/src/text/lines.rs index 7b80953772..21009124a8 100644 --- a/crates/edit/src/text/lines.rs +++ b/crates/edit/src/text/lines.rs @@ -580,8 +580,10 @@ pub(super) fn reading_lines(doc: &Document, page: usize) -> Result<(Vec