mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
Render: a five-point polyline is no longer painted as its bounding box
`path_as_rect` decided a path was a rectangle from its element count and from the four corners of its bounding box being covered. Its comment names the shape it means to match -- one MoveTo, three LineTo, one ClosePath -- but `len() != 5` also admits one MoveTo and four LineTo, a five-point open polyline, and covering the corners says nothing about the points between them. Both call sites then paint `device.draw_rect(bbox)`, so such a path was drawn as its own bounding box. Rendered through `pdfcraft-cli render`, an open zigzag whose ends reach the corners of its bounding box 80 38 m 80 21.75 l 60.571 29.875 l 40 21.75 l 40 38 l S lost its middle peak and gained a straight edge the file does not contain: no dark pixel near the peak, and 36 along the bounding box's top. A six-point zigzag and a real rectangle both drew correctly, which is why this shows on some art and not the rest -- and `s` escapes it because its ClosePath makes six elements. A point that is not a corner now rejects the path. A rectangle still matches whether written `m l l l` and closed or `m l l l l` back to its first corner, since every one of its points is a corner. The same mistake also discarded a clip of that shape, through `push_clip_path`, which this fixes too. Found from craft-libs' ATTRIBUTION note that the shared reader centralizes the patched Hayro trees from PdfCraft: the same flaw is in craft-libs, sent there as storytold/craft-libs#175.
This commit is contained in:
@@ -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<u8> {
|
||||
|
||||
+19
-5
@@ -451,11 +451,25 @@ pub(crate) fn path_as_rect(path: &BezPath) -> Option<Rect> {
|
||||
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<Rect> {
|
||||
}
|
||||
}
|
||||
|
||||
if corners[0] && corners[1] && corners[2] && corners[3] {
|
||||
if !off_corner && corners[0] && corners[1] && corners[2] && corners[3] {
|
||||
Some(bbox)
|
||||
} else {
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user