From be223888f77bf47f5931cf717fc9f6c8f794dfb4 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:20:31 +0200 Subject: [PATCH] Join /Contents pieces with a space in measure snap Fixes #3570 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/measure/src/snap.rs | 3 +- crates/measure/tests/snap_property.rs | 41 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/measure/src/snap.rs b/crates/measure/src/snap.rs index 71ba78f4a..3cf6bbd17 100644 --- a/crates/measure/src/snap.rs +++ b/crates/measure/src/snap.rs @@ -218,7 +218,8 @@ impl Walker<'_> { match s.decoded_within(remaining) { Ok(data) => { joined.extend(data); - joined.push(b'\n'); + // A space, as the renderer joins pieces: a trailing comment runs on. + joined.push(b' '); } Err(_) => self.geometry.unreadable = self.geometry.unreadable.saturating_add(1), } diff --git a/crates/measure/tests/snap_property.rs b/crates/measure/tests/snap_property.rs index a05b507a7..a38bb80aa 100644 --- a/crates/measure/tests/snap_property.rs +++ b/crates/measure/tests/snap_property.rs @@ -175,3 +175,44 @@ fn high_curvature_geometry_hits_segment_cap() { let geometry = check_snap_invariants(&stress_curve); assert!(geometry.truncated, "high-curvature geometry did not report its extraction cap ({} segments)", geometry.segments.len()); } + +fn two_piece_pdf(first: &[u8], second: &[u8]) -> Vec { + let mut objects = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 200] >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /Contents [4 0 R 5 0 R] /Resources << >> >>".to_vec(), + ]; + for content in [first, second] { + let mut object = format!("<< /Length {} >>\nstream\n", content.len()).into_bytes(); + object.extend_from_slice(content); + object.extend_from_slice(b"\nendstream"); + objects.push(object); + } + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (index, object) in objects.iter().enumerate() { + offsets.push(bytes.len()); + bytes.extend_from_slice(format!("{} 0 obj\n", index + 1).as_bytes()); + bytes.extend_from_slice(object); + bytes.extend_from_slice(b"\nendobj\n"); + } + let xref = bytes.len(); + bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes()); + for offset in offsets { + bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes()); + bytes +} + +fn snap_at_box_corner(first: &[u8]) -> Option { + let pdf = two_piece_pdf(first, b"0 0 1 RG 15 95 150 30 re S\n"); + let doc = Document::open(Arc::new(pdf)).unwrap(); + snap::geometry(&doc, 0).unwrap().snap([15.0, 105.0], 4.0, SnapOptions::default()).unwrap() +} + +#[test] +fn trailing_comment_in_previous_piece_hides_next_piece_first_line() { + assert!(snap_at_box_corner(b"q % tail comment, no end of line").is_none(), "snapped to a rectangle the renderer comments out"); + assert!(snap_at_box_corner(b"q % tail comment\n").is_some(), "control: the rectangle is drawn"); +}