The image scan skips a path segment before any m instead of panicking (#3581)

A content stream that uses l, c, v, y or h before any m or re has no
subpath to extend. The edit-side image scan (crates/edit/src/images.rs)
handed the operators straight to BezPath's line_to/curve_to/close_path,
which panic on an empty path by contract, so object_list, page_images,
image_hit and the Office export's image walk all crashed on such a page,
while the renderer, text extraction, MuPDF and pdftotext skip the
malformed segment and render the rest.

The scan now skips the segment when the path has no elements, mirroring
the v arm's existing guard and kurbo's own condition (debug_assert on
!is_empty). A path started with m or re extends and closes exactly as
before. A clip set by a stray segment (W n on an empty path) takes a
zero bounding box, which kurbo defines as Rect::default, not a panic.

Regression test: each of the five segment operators before any m, then
a well-formed m l h and a re W n clip, all before an image Do - the
image is still listed in every case.

Verified against kurbo 0.13.1's source (line_to/curve_to/close_path
debug_assert on empty; bounding_box unwrap_or_default) and the issue's
repro; no Rust toolchain on this machine, so cargo test was not run
here - saying so plainly for the reviewer.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
This commit is contained in:
Rahul Ranjan
2026-10-20 14:50:30 +05:30
parent ec0a965074
commit 631ca00bdd
2 changed files with 37 additions and 4 deletions
+18 -4
View File
@@ -165,12 +165,19 @@ impl ImageState {
}
}
b"l" => {
if let Some([x, y]) = op.nums::<2>() {
// A segment before any m (or re) has no subpath to extend: BezPath's line_to and
// friends panic on an empty path by contract, so the segment is skipped, as the
// renderer, MuPDF and pdftotext do (#3581).
if let Some([x, y]) = op.nums::<2>()
&& self.path.elements().last().is_some()
{
self.path.line_to(self.point(x, y));
}
}
b"c" => {
if let Some([x1, y1, x2, y2, x3, y3]) = op.nums::<6>() {
if let Some([x1, y1, x2, y2, x3, y3]) = op.nums::<6>()
&& self.path.elements().last().is_some()
{
self.path.curve_to(self.point(x1, y1), self.point(x2, y2), self.point(x3, y3));
}
}
@@ -182,12 +189,19 @@ impl ImageState {
}
}
b"y" => {
if let Some([x1, y1, x2, y2]) = op.nums::<4>() {
if let Some([x1, y1, x2, y2]) = op.nums::<4>()
&& self.path.elements().last().is_some()
{
let end = self.point(x2, y2);
self.path.curve_to(self.point(x1, y1), end, end);
}
}
b"h" => self.path.close_path(),
b"h" => {
// ClosePath with no subpath panics by the same contract (#3581).
if self.path.elements().last().is_some() {
self.path.close_path();
}
}
b"re" => {
if let Some([x, y, w, h]) = op.nums::<4>() {
self.path.move_to(self.point(x, y));
+19
View File
@@ -1964,6 +1964,25 @@ fn image_page_with(content: &str) -> Document {
Document::open(Arc::new(out)).unwrap()
}
/// A path segment operator before any `m` (or `re`) has no subpath to extend: the image scan used
/// to call `BezPath`'s `line_to`/`curve_to`/`close_path` on an empty path, which panics by contract,
/// taking `object_list`, `page_images` and the Office export's image walk down with it (#3581).
/// The segment is now skipped, as the renderer, MuPDF and pdftotext do, and the page's images are
/// still listed.
#[test]
fn image_scan_skips_a_path_segment_before_any_m() {
for bad in ["1 1 l S", "1 1 2 2 3 3 c S", "1 1 2 2 4 4 y S", "1 1 2 2 3 3 v S", "h S"] {
let content = format!("q {bad} Q 100 100 200 100 cm /Im0 Do");
let doc = image_page_with(&content);
assert_eq!(images::page_images(&doc, 0).unwrap().len(), 1, "{bad}: the image is still listed");
}
// A path started with m (or re) still extends and closes as before.
let doc = image_page_with("q 0 0 m 1 1 l h S Q 100 100 200 100 cm /Im0 Do");
assert_eq!(images::page_images(&doc, 0).unwrap().len(), 1);
let doc = image_page_with("q 10 10 20 20 re W n Q 100 100 200 100 cm /Im0 Do");
assert_eq!(images::page_images(&doc, 0).unwrap().len(), 1);
}
/// The box of the first `re` on the page in user space, after the `cm`s before it.
fn first_rect_in_user_space(doc: &Document) -> [f64; 4] {
let mut ctm = pdfcraft_content::Matrix::IDENTITY;