|
| 1 | +package com.demcha.testing.visual; |
| 2 | + |
| 3 | +import com.demcha.compose.GraphCompose; |
| 4 | +import com.demcha.compose.document.api.DocumentSession; |
| 5 | +import com.demcha.compose.document.dsl.ShapeBuilder; |
| 6 | +import com.demcha.compose.document.dsl.ShapeContainerBuilder; |
| 7 | +import com.demcha.compose.document.style.ClipPolicy; |
| 8 | +import com.demcha.compose.document.style.DocumentColor; |
| 9 | +import com.demcha.compose.document.style.DocumentInsets; |
| 10 | +import com.demcha.compose.document.style.DocumentPathSegment; |
| 11 | +import com.demcha.compose.testing.visual.PdfVisualRegression; |
| 12 | +import org.junit.jupiter.api.Test; |
| 13 | + |
| 14 | +import java.awt.image.BufferedImage; |
| 15 | +import java.util.List; |
| 16 | + |
| 17 | +import static org.assertj.core.api.Assertions.assertThat; |
| 18 | + |
| 19 | +/** |
| 20 | + * Deterministic visual check that a {@code ShapeOutline.Path} clip actually |
| 21 | + * shapes the rendered pixels — the gap the {@code %PDF-} smoke tests cannot |
| 22 | + * cover. A full-box red layer clipped to an upward triangle must paint roughly |
| 23 | + * half the box (a triangle is half its bounding box) with the wide base at the |
| 24 | + * BOTTOM. So a dropped clip (≈100% red), an empty clip (≈0% red), a wrong scale, |
| 25 | + * and a y-flip (apex/base swapped) all fail this test. |
| 26 | + * |
| 27 | + * <p>Self-contained on purpose: it samples the in-memory render instead of |
| 28 | + * diffing a committed PNG baseline, so it stays deterministic and |
| 29 | + * anti-aliasing-tolerant across platforms without a blessed pixel reference.</p> |
| 30 | + */ |
| 31 | +class ShapeClipPathVisualTest { |
| 32 | + |
| 33 | + private static final int BOX = 120; |
| 34 | + |
| 35 | + @Test |
| 36 | + void pathClipShapesTheRenderedPixelsRightSideUp() throws Exception { |
| 37 | + BufferedImage page = PdfVisualRegression.standard() |
| 38 | + .renderPages(renderTriangleClip()) |
| 39 | + .get(0); |
| 40 | + |
| 41 | + int w = page.getWidth(); |
| 42 | + int h = page.getHeight(); |
| 43 | + long red = 0; |
| 44 | + long redTop = 0; |
| 45 | + long redBottom = 0; |
| 46 | + for (int y = 0; y < h; y++) { |
| 47 | + for (int x = 0; x < w; x++) { |
| 48 | + if (isRed(page.getRGB(x, y))) { |
| 49 | + red++; |
| 50 | + if (y < h / 2) { |
| 51 | + redTop++; |
| 52 | + } else { |
| 53 | + redBottom++; |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + double fraction = (double) red / ((long) w * h); |
| 60 | + assertThat(fraction) |
| 61 | + .as("clipped red triangle should cover ~half the box " |
| 62 | + + "(clip applied, right scale) — got %.3f", fraction) |
| 63 | + .isBetween(0.30, 0.70); |
| 64 | + assertThat(redBottom) |
| 65 | + .as("wide base at the bottom, narrow apex at the top — guards a y-flip") |
| 66 | + .isGreaterThan(redTop * 2); |
| 67 | + } |
| 68 | + |
| 69 | + /** A 120x120 container clipping a full-box red layer to an upward triangle. */ |
| 70 | + private static byte[] renderTriangleClip() throws Exception { |
| 71 | + try (DocumentSession session = GraphCompose.document() |
| 72 | + .pageSize(BOX, BOX) |
| 73 | + .margin(DocumentInsets.zero()) |
| 74 | + .create()) { |
| 75 | + session.add(new ShapeContainerBuilder() |
| 76 | + .name("TriangleClip") |
| 77 | + .path(BOX, BOX, List.of( |
| 78 | + DocumentPathSegment.moveTo(0.5, 1.0), // apex, top (y grows up) |
| 79 | + DocumentPathSegment.lineTo(1.0, 0.0), // base, bottom-right |
| 80 | + DocumentPathSegment.lineTo(0.0, 0.0), // base, bottom-left |
| 81 | + DocumentPathSegment.close())) |
| 82 | + .clipPolicy(ClipPolicy.CLIP_PATH) |
| 83 | + .layer(new ShapeBuilder() |
| 84 | + .size(BOX, BOX) |
| 85 | + .fillColor(DocumentColor.rgb(220, 20, 20)) |
| 86 | + .build()) |
| 87 | + .build()); |
| 88 | + return session.toPdfBytes(); |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + private static boolean isRed(int rgb) { |
| 93 | + int r = (rgb >> 16) & 0xFF; |
| 94 | + int g = (rgb >> 8) & 0xFF; |
| 95 | + int b = rgb & 0xFF; |
| 96 | + return r > 140 && g < 110 && b < 110; |
| 97 | + } |
| 98 | +} |
0 commit comments