mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(render): mesh triangles that share an edge leave no seam
The mesh sampler took each pixel's colour at its corner, and only when the corner was strictly inside a triangle. A corner on an edge two triangles share belonged to neither, so a Type 4-7 shading drew a white seam along every shared edge that ran through the sample grid, and dropped pixels along the mesh's own sides (a 30 x 30 square cut along its diagonal: 812 of 900 pixels; MuPDF 900). Such a pixel now goes to the triangle that covers its centre, edges included (Triangle::covers), and still takes the colour at its corner, so every pixel that was drawn keeps its colour. Vendored hayro-interpret patch 41. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dab3149110
commit
421a0dd9c9
@@ -5001,6 +5001,87 @@ trailer << /Root 1 0 R >>
|
||||
assert!(p != [255, 255, 255, 255] && p[3] == 255, "the triangles are drawn: {p:?}");
|
||||
}
|
||||
|
||||
/// Two triangles of a mesh that share an edge cover it together. The sampler tested each
|
||||
/// pixel's corner, and only strictly inside a triangle, so the diagonal two triangles share
|
||||
/// was left white wherever it ran through the samples, and so was a pixel row or column
|
||||
/// along the mesh's own sides. Vendored hayro-interpret patch 41.
|
||||
#[test]
|
||||
fn mesh_triangles_sharing_an_edge_leave_no_seam() {
|
||||
// A red square from 10 to 40 on a 50 × 50 page, cut along its diagonal; in device space
|
||||
// the diagonal runs through pixel centres and corners alike.
|
||||
let vertex = |flag: u8, x: u8, y: u8| [flag, x, y, 255, 0, 0];
|
||||
let (lo, hi) = (10, 40);
|
||||
let mut triangles = Vec::new();
|
||||
for [a, b, c] in [[(lo, lo), (hi, lo), (hi, hi)], [(lo, lo), (hi, hi), (lo, hi)]] {
|
||||
for (x, y) in [a, b, c] {
|
||||
triangles.extend(vertex(0, x, y));
|
||||
}
|
||||
}
|
||||
// Each triangle as its own three vertices, and the second one as a strip (flag 1).
|
||||
let mut strip = Vec::new();
|
||||
for (flag, (x, y)) in [(0, (lo, lo)), (0, (hi, lo)), (0, (hi, hi)), (2, (lo, hi))] {
|
||||
strip.extend(vertex(flag, x, y));
|
||||
}
|
||||
for (label, data) in [("separate triangles", triangles), ("a strip", strip)] {
|
||||
let content = b"/Sh0 sh";
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 50 50] /Contents 4 0 R /Resources << /Shading << /Sh0 5 0 R >> >> >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", content.len()).as_bytes());
|
||||
pdf.extend_from_slice(content);
|
||||
pdf.extend_from_slice(format!("\nendstream endobj\n5 0 obj << /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 8 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 255 0 255 0 1 0 1 0 1] /Length {} >> stream\n", data.len()).as_bytes());
|
||||
pdf.extend_from_slice(&data);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
|
||||
assert!(page.error.is_none(), "{label}: {:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (50, 50));
|
||||
let (mut white_inside, mut red_outside) = (Vec::new(), Vec::new());
|
||||
for y in 0..50_usize {
|
||||
for x in 0..50_usize {
|
||||
let p = &page.rgba[(y * 50 + x) * 4..][..4];
|
||||
let inside = (10..40).contains(&x) && (10..40).contains(&y);
|
||||
if inside && p != [255, 0, 0, 255] {
|
||||
white_inside.push((x, y));
|
||||
} else if !inside && p != [255, 255, 255, 255] {
|
||||
red_outside.push((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(white_inside.is_empty(), "{label}: unshaded pixels inside the square: {white_inside:?}");
|
||||
assert!(red_outside.is_empty(), "{label}: shaded pixels outside the square: {red_outside:?}");
|
||||
}
|
||||
|
||||
// One triangle alone, the square's upper left half: a pixel whose corner lies on its
|
||||
// diagonal is taken by its centre, so one wholly across the diagonal (x + y ≥ 50 in
|
||||
// device space) is not shaded, and one wholly inside is.
|
||||
let mut data = Vec::new();
|
||||
for (x, y) in [(lo, lo), (hi, hi), (lo, hi)] {
|
||||
data.extend(vertex(0, x, y));
|
||||
}
|
||||
let content = b"/Sh0 sh";
|
||||
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 50 50] /Contents 4 0 R /Resources << /Shading << /Sh0 5 0 R >> >> >> endobj\n".to_vec();
|
||||
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", content.len()).as_bytes());
|
||||
pdf.extend_from_slice(content);
|
||||
pdf.extend_from_slice(format!("\nendstream endobj\n5 0 obj << /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 8 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 255 0 255 0 1 0 1 0 1] /Length {} >> stream\n", data.len()).as_bytes());
|
||||
pdf.extend_from_slice(&data);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
let (mut spilled, mut missing) = (Vec::new(), Vec::new());
|
||||
for y in 10..40_usize {
|
||||
for x in 10..40_usize {
|
||||
let p = &page.rgba[(y * 50 + x) * 4..][..4];
|
||||
if x + y >= 50 && p == [255, 0, 0, 255] {
|
||||
spilled.push((x, y));
|
||||
} else if x + y + 2 <= 50 && p != [255, 0, 0, 255] {
|
||||
missing.push((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(spilled.is_empty(), "shaded pixels wholly across the diagonal: {spilled:?}");
|
||||
assert!(missing.is_empty(), "unshaded pixels wholly inside the triangle: {missing:?}");
|
||||
}
|
||||
|
||||
/// Review of the mesh-shading patch: every patch was cut into 722 triangles before anything
|
||||
/// limited them (about 150 KB per patch), and a lattice mesh with `/VerticesPerRow 0` pushed
|
||||
/// empty rows for ever. Vendored hayro-interpret patch: patches are triangulated one at a
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+11
-3
@@ -321,9 +321,17 @@ impl TriangleSampler {
|
||||
return false;
|
||||
}
|
||||
self.visits_left -= 1;
|
||||
let point = Point::new(x as f64, y as f64);
|
||||
if t.contains_point(point) {
|
||||
self.map.insert((x, y), t.interpolate(point));
|
||||
let corner = Point::new(x as f64, y as f64);
|
||||
if t.contains_point(corner) {
|
||||
self.map.insert((x, y), t.interpolate(corner));
|
||||
} else if t.covers(Point::new(x as f64 + 0.5, y as f64 + 0.5)) {
|
||||
// PdfCraft patch (36): a pixel whose corner no triangle holds strictly (it
|
||||
// lies on an edge two triangles share, or on the mesh's side) still belongs
|
||||
// to the triangle that covers its centre, edges included. Testing only the
|
||||
// corner left a white seam along every shared edge. Such a pixel takes the
|
||||
// colour at its corner, as every other pixel does (where MuPDF's colours
|
||||
// agree), unless a triangle that holds the corner gives it one.
|
||||
self.map.entry((x, y)).or_insert_with(|| t.interpolate(corner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -375,6 +375,24 @@ impl Triangle {
|
||||
self.kurbo_tri.winding(pos) != 0
|
||||
}
|
||||
|
||||
/// PdfCraft patch (36): whether the point is inside the triangle or on one of its edges.
|
||||
/// Two triangles of a mesh share an edge; a sample on it belongs to both (their colours
|
||||
/// agree there), where `contains_point` can give it to neither and leave a seam. A
|
||||
/// degenerate triangle covers nothing.
|
||||
pub fn covers(&self, pos: Point) -> bool {
|
||||
// Relative to the barycentric coordinates' scale (1), well above f64 rounding.
|
||||
const EDGE: f64 = 1e-9;
|
||||
let (a, b, c) = (self.p0.point, self.p1.point, self.p2.point);
|
||||
let (v0, v1, v2) = (b - a, c - a, pos - a);
|
||||
let denom = self.d00 * self.d11 - self.d01 * self.d01;
|
||||
let (d20, d21) = (v2.dot(v0), v2.dot(v1));
|
||||
let v = (self.d11 * d20 - self.d01 * d21) / denom;
|
||||
let w = (self.d00 * d21 - self.d01 * d20) / denom;
|
||||
// A degenerate triangle divides by zero: NaN fails every comparison, and infinite
|
||||
// coordinates can't sum to at most one.
|
||||
v >= -EDGE && w >= -EDGE && v + w <= 1.0 + EDGE
|
||||
}
|
||||
|
||||
/// Return the bounding box of the triangle.
|
||||
pub fn bounding_box(&self) -> kurbo::Rect {
|
||||
self.kurbo_tri.bounding_box()
|
||||
|
||||
Reference in New Issue
Block a user