diff --git a/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/visualtest/TestSupport.java b/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/visualtest/TestSupport.java
index 9a30b5af0..f572cdff1 100644
--- a/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/visualtest/TestSupport.java
+++ b/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/visualtest/TestSupport.java
@@ -1,21 +1,35 @@
package com.openhtmltopdf.visualtest;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Polygon;
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
+import java.util.HashMap;
import java.util.Locale;
+import java.util.Map;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.w3c.dom.Element;
+
import com.openhtmltopdf.bidi.support.ICUBidiReorderer;
import com.openhtmltopdf.bidi.support.ICUBidiSplitter;
import com.openhtmltopdf.bidi.support.ICUBreakers;
+import com.openhtmltopdf.extend.FSObjectDrawer;
+import com.openhtmltopdf.extend.FSObjectDrawerFactory;
import com.openhtmltopdf.extend.FSTextBreaker;
+import com.openhtmltopdf.extend.OutputDevice;
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.TextDirection;
+import com.openhtmltopdf.render.RenderingContext;
import com.openhtmltopdf.svgsupport.BatikSVGDrawer;
import com.openhtmltopdf.util.XRLogger;
import com.openhtmltopdf.visualtest.Java2DVisualTester.Java2DBuilderConfig;
@@ -158,4 +172,56 @@ public void setText(String newText) {
* Configures the builder to use SVG drawer but not font.
*/
public static final BuilderConfig WITH_SVG = (builder) -> builder.useSVGDrawer(new BatikSVGDrawer());
+
+ public static class ShapesObjectDrawer implements FSObjectDrawer {
+ public Map drawObject(Element e, double x, double y, double width, double height,
+ OutputDevice outputDevice, RenderingContext ctx, int dotsPerPixel) {
+
+ Map shapes = new HashMap<>();
+
+ outputDevice.drawWithGraphics((float) x, (float) y, (float) width / dotsPerPixel,
+ (float) height / dotsPerPixel, (Graphics2D g2d) -> {
+
+ double realWidth = width / dotsPerPixel;
+ double realHeight = height / dotsPerPixel;
+
+ Rectangle2D rectUpperLeft = new Rectangle2D.Double(0, 0, realWidth / 4d, realHeight / 4d);
+ Rectangle2D rectLowerRight = new Rectangle2D.Double(realWidth * (3d/4d), realHeight * (3d/4d), realWidth / 4d, realHeight / 4d);
+
+ int[] xpoints = new int[] { (int) (realWidth / 2d), (int) (realWidth * (1d/4d)), (int) (realWidth * (3d/4d)), (int) (realWidth / 2d) };
+ int[] ypoints = new int[] { (int) (realHeight * (1d/4d)), (int) (realHeight * (3d/4d)), (int) (realHeight * (3d/4d)), (int) (realHeight * (1d/4d)) };
+ Polygon centreTriangle = new Polygon(xpoints, ypoints, xpoints.length);
+
+ g2d.setColor(Color.CYAN);
+
+ g2d.draw(rectUpperLeft);
+ g2d.draw(rectLowerRight);
+ g2d.draw(centreTriangle);
+
+ AffineTransform scale = AffineTransform.getScaleInstance(dotsPerPixel, dotsPerPixel);
+
+ shapes.put(scale.createTransformedShape(rectUpperLeft), "http://example.com/1");
+ shapes.put(scale.createTransformedShape(rectLowerRight), "http://example.com/2");
+ shapes.put(scale.createTransformedShape(centreTriangle), "http://example.com/3");
+ });
+
+ return shapes;
+ }
+ }
+
+ public static class ShapesObjectDrawerFactory implements FSObjectDrawerFactory {
+ public FSObjectDrawer createDrawer(Element e) {
+ if (!isReplacedObject(e)) {
+ return null;
+ }
+
+ return new ShapesObjectDrawer();
+ }
+
+ public boolean isReplacedObject(Element e) {
+ return e.getAttribute("type").equals("shapes");
+ }
+ }
+
+ public static final BuilderConfig WITH_SHAPES_DRAWER = (builder) -> { builder.useObjectDrawerFactory(new ShapesObjectDrawerFactory()); };
}
diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/pr-480-link-shapes.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/pr-480-link-shapes.html
new file mode 100644
index 000000000..bdf547df5
--- /dev/null
+++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/pr-480-link-shapes.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java
index 0703b2c71..e827c60b6 100644
--- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java
+++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java
@@ -8,6 +8,8 @@
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.io.IOUtils;
@@ -26,11 +28,13 @@
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
import org.apache.pdfbox.util.Charsets;
import org.hamcrest.CustomTypeSafeMatcher;
+import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import com.openhtmltopdf.testcases.TestcaseRunner;
+import com.openhtmltopdf.visualtest.TestSupport;
import com.openhtmltopdf.visualtest.VisualTester.BuilderConfig;
public class NonVisualRegressionTest {
@@ -717,7 +721,106 @@ public void testInputWithoutNameAttribute() throws IOException {
assertEquals(0, form.getFields().size());
remove("input-without-name-attribute", doc);
}
-
+
+ private static float[] getQuadPoints(PDDocument doc, int pg, int linkIndex) throws IOException {
+ return ((PDAnnotationLink) doc.getPage(pg).getAnnotations().get(linkIndex)).getQuadPoints();
+ }
+
+ private static String print(float[] floats) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("new float[] { ");
+ for (float floater : floats) {
+ sb.append(floater);
+ sb.append("f, ");
+ }
+
+ sb.deleteCharAt(sb.length() - 2);
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ private static final float QUAD_DELTA = 0.5f;
+ private static boolean qAssert(List expectedList, float[] actual, StringBuilder sb, int pg, int linkIndex) {
+ sb.append("PAGE: " + pg + ", LINK: " + linkIndex + "\n");
+ sb.append(" ACT(" + actual.length + "): " + print(actual) + "\n");
+
+ // NOTE: The shapes are returned as a map and are therefore placed on
+ // the page in a non-determined order. So we just searh the expected
+ // list for a match.
+ ALL_EXPECTED:
+ for (float[] expected : expectedList) {
+ if (expected.length != actual.length) {
+ continue;
+ }
+
+ for (int i = 0; i < expected.length; i++) {
+ float diff = Math.abs(expected[i] - actual[i]);
+
+ if (diff > QUAD_DELTA) {
+ continue ALL_EXPECTED;
+ }
+ }
+
+ return false;
+ }
+
+ sb.append(" !FAILED!");
+ sb.append("\n\n");
+ return true;
+ }
+
+ /**
+ * Tests the shaped links support for custom object drawers
+ * in the main document area and in the page margin on multiple
+ * pages.
+ */
+ @Test
+ public void testPR480LinkShapes() throws IOException {
+ try (PDDocument doc = run("pr-480-link-shapes", TestSupport.WITH_SHAPES_DRAWER)) {
+ StringBuilder sb = new StringBuilder();
+ List page0 = new ArrayList<>();
+ List page1 = new ArrayList<>();
+ boolean failure = false;
+
+ page0.add(new float[] { 486.75f, 251.25f, 468.0f, 213.75f, 486.75f, 213.75f, 505.5f, 213.75f, 486.75f, 251.25f, 505.5f, 213.75f, 496.125f, 232.5f, 486.75f, 251.25f });
+ page0.add(new float[] { 449.25f, 270.0f, 449.25f, 251.25f, 458.625f, 251.25f, 468.0f, 251.25f, 449.25f, 270.0f, 468.0f, 251.25f, 468.0f, 260.625f, 468.0f, 270.0f });
+ page0.add(new float[] { 505.5f, 213.75f, 505.5f, 195.0f, 514.875f, 195.0f, 524.25f, 195.0f, 505.5f, 213.75f, 524.25f, 195.0f, 524.25f, 204.375f, 524.25f, 213.75f });
+ page0.add(new float[] { 243.0f, 203.25f, 243.0f, 128.25f, 280.5f, 128.25f, 318.0f, 128.25f, 243.0f, 203.25f, 318.0f, 128.25f, 318.0f, 165.75f, 318.0f, 203.25f });
+ page0.add(new float[] { 168.0f, 353.25f, 93.0f, 203.25f, 168.0f, 203.25f, 243.0f, 203.25f, 168.0f, 353.25f, 243.0f, 203.25f, 205.5f, 278.25f, 168.0f, 353.25f });
+ page0.add(new float[] { 18.0f, 428.25f, 18.0f, 353.25f, 55.5f, 353.25f, 93.0f, 353.25f, 18.0f, 428.25f, 93.0f, 353.25f, 93.0f, 390.75f, 93.0f, 428.25f });
+
+ failure |= qAssert(page0, getQuadPoints(doc, 0, 0), sb, 0, 0);
+ failure |= qAssert(page0, getQuadPoints(doc, 0, 1), sb, 0, 1);
+ failure |= qAssert(page0, getQuadPoints(doc, 0, 2), sb, 0, 2);
+ failure |= qAssert(page0, getQuadPoints(doc, 0, 3), sb, 0, 3);
+ failure |= qAssert(page0, getQuadPoints(doc, 0, 4), sb, 0, 4);
+ failure |= qAssert(page0, getQuadPoints(doc, 0, 5), sb, 0, 5);
+
+ page1.add(new float[] { 486.75f, 251.25f, 468.0f, 213.75f, 486.75f, 213.75f, 505.5f, 213.75f, 486.75f, 251.25f, 505.5f, 213.75f, 496.125f, 232.5f, 486.75f, 251.25f });
+ page1.add(new float[] { 449.25f, 270.0f, 449.25f, 251.25f, 458.625f, 251.25f, 468.0f, 251.25f, 449.25f, 270.0f, 468.0f, 251.25f, 468.0f, 260.625f, 468.0f, 270.0f });
+ page1.add(new float[] { 505.5f, 213.75f, 505.5f, 195.0f, 514.875f, 195.0f, 524.25f, 195.0f, 505.5f, 213.75f, 524.25f, 195.0f, 524.25f, 204.375f, 524.25f, 213.75f });
+ page1.add(new float[] { 243.0f, 209.25f, 243.0f, 134.25f, 280.5f, 134.25f, 318.0f, 134.25f, 243.0f, 209.25f, 318.0f, 134.25f, 318.0f, 171.75f, 318.0f, 209.25f });
+ page1.add(new float[] { 168.0f, 359.25f, 93.0f, 209.25f, 168.0f, 209.25f, 243.0f, 209.25f, 168.0f, 359.25f, 243.0f, 209.25f, 205.5f, 284.25f, 168.0f, 359.25f });
+ page1.add(new float[] { 18.0f, 434.25f, 18.0f, 359.25f, 55.5f, 359.25f, 93.0f, 359.25f, 18.0f, 434.25f, 93.0f, 359.25f, 93.0f, 396.75f, 93.0f, 434.25f });
+
+ failure |= qAssert(page1, getQuadPoints(doc, 1, 0), sb, 1, 0);
+ failure |= qAssert(page1, getQuadPoints(doc, 1, 1), sb, 1, 1);
+ failure |= qAssert(page1, getQuadPoints(doc, 1, 2), sb, 1, 2);
+ failure |= qAssert(page1, getQuadPoints(doc, 1, 3), sb, 1, 3);
+ failure |= qAssert(page1, getQuadPoints(doc, 1, 4), sb, 1, 4);
+ failure |= qAssert(page1, getQuadPoints(doc, 1, 5), sb, 1, 5);
+
+ if (failure) {
+ System.out.print(sb.toString());
+ Assert.fail("Quad points were not correct");
+ }
+
+ remove("pr-480-link-shapes", doc);
+ }
+ }
+
// TODO:
// + More form controls.
// + Custom meta info.