diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index b2127894f..b502d55ad 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -4298,6 +4298,54 @@ trailer << /Root 1 0 R >> assert!(grey[0] == grey[1] && grey[1] == grey[2] && grey[0] < 255, "the image covers the page: {grey:?}"); } + /// A five-point open polyline is not a rectangle. `path_as_rect` took any five-element path + /// whose points covered the four corners of its bounding box for that box, and a `MoveTo` with + /// four `LineTo` is five elements, so an open zigzag stroked with `S` was painted as its + /// bounding rectangle: the peak between its ends vanished and the open side came out closed. + #[test] + fn a_five_point_polyline_is_not_painted_as_its_bounding_box() { + // The ends and the points beside them sit on the four corners of the bounding box; the + // peak at (20, 20) lies between them and is on no corner. + let content = b"0 0 0 RG 2 w 30 30 m 30 10 l 20 20 l 10 10 l 10 30 l S".to_vec(); + let pdf = one_page_pdf(&content, ""); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false })); + }); + let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("the polyline must render"); + assert!(page.error.is_none(), "{:?}", page.error); + // Device y counts down the 40 pt page, so the two ends at y = 30 are on row 10. + let dark = |x: u32, y: u32| { + let offset = ((y * page.width + x) * 4) as usize; + page.rgba[offset] < 128 + }; + assert!((17..=23).any(|x| (17..=23).any(|y| dark(x, y))), "the peak between the ends draws"); + assert!(!(14..=26).any(|x| dark(x, 10)), "the side the path leaves open stays open"); + } + + /// A rectangle still takes the rectangle route, written either way: `m l l l` and closed, or + /// `m l l l l` back to its first corner. + #[test] + fn a_rectangle_is_still_painted_as_a_rectangle() { + for content in [&b"0 0 0 rg 10 10 m 30 10 l 30 30 l 10 30 l f"[..], &b"0 0 0 rg 10 10 m 30 10 l 30 30 l 10 30 l 10 10 l f"[..]] { + let pdf = one_page_pdf(content, ""); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); + let _ = tx.send(r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false })); + }); + let page = rx.recv_timeout(std::time::Duration::from_secs(60)).expect("the rectangle must render"); + assert!(page.error.is_none(), "{:?}", page.error); + let pixel = |x: u32, y: u32| { + let offset = ((y * page.width + x) * 4) as usize; + page.rgba[offset] + }; + assert!(pixel(20, 20) < 128, "the rectangle is filled"); + assert_eq!(pixel(5, 5), 255, "and nothing outside it is"); + } + } + /// A one-page PDF whose content is the stream `stream`, with the entries `dict` in its /// dictionary. fn one_page_pdf(stream: &[u8], dict: &str) -> Vec { diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs index d6772baa1..3a06a8873 100644 --- a/vendor/hayro-interpret/src/context.rs +++ b/vendor/hayro-interpret/src/context.rs @@ -451,11 +451,25 @@ pub(crate) fn path_as_rect(path: &BezPath) -> Option { let (min_x, min_y, max_x, max_y) = (bbox.min_x(), bbox.min_y(), bbox.max_x(), bbox.max_y()); let mut corners = [false; 4]; + // PdfCraft patch: a point that is not a corner means this is not a rectangle. The length + // check above admits one MoveTo and four LineTo as well as the intended one MoveTo, three + // LineTo and one ClosePath, and covering the four corners says nothing about the points + // between them, so a five-point polyline whose ends reach every corner of its own bounding + // box was painted as that box (and a clip of that shape was discarded). + let mut off_corner = false; + let mut check_point = |p: Point| { - corners[0] |= p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(min_y); - corners[1] |= p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(max_y); - corners[2] |= p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(min_y); - corners[3] |= p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(max_y); + let hits = [ + p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(min_y), + p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(max_y), + p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(min_y), + p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(max_y), + ]; + off_corner |= !hits.iter().any(|hit| *hit); + + for (corner, hit) in corners.iter_mut().zip(hits) { + *corner |= hit; + } }; for (idx, el) in path.elements().iter().enumerate() { @@ -474,7 +488,7 @@ pub(crate) fn path_as_rect(path: &BezPath) -> Option { } } - if corners[0] && corners[1] && corners[2] && corners[3] { + if !off_corner && corners[0] && corners[1] && corners[2] && corners[3] { Some(bbox) } else { None